From: Aleksey Filippov Date: Thu, 11 Dec 2025 21:15:08 +0000 (+0300) Subject: Доработка документации добавление схемы БД X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=18980eb38a5552829be6e3cfd5859743f6cf4637;p=erp24_rep%2Fyii-erp24%2F.git Доработка документации добавление схемы БД --- diff --git a/erp24/docs/database/DATABASE_OVERVIEW.md b/erp24/docs/database/DATABASE_OVERVIEW.md index 6c479815..bb23f091 100644 --- a/erp24/docs/database/DATABASE_OVERVIEW.md +++ b/erp24/docs/database/DATABASE_OVERVIEW.md @@ -1,5 +1,64 @@ # Обзор базы данных ERP24 +## Mindmap: Обзор базы данных + +```mermaid +mindmap + root((ERP24 DATABASE)) + Технологии + PostgreSQL 15.6 + Схема erp24 + UTF-8 + 309 таблиц + 281 миграция + 387 моделей + Интеграция 1С + GUID первичный ключ + VARCHAR 36 + sales + products_1c + balances + export_import_table + синхронизация + Двусторонний обмен + Архитектурные паттерны + Soft delete + active флаг + deleted_at + History таблицы + sales_history + admin_payroll_history + Log таблицы + api_logs + task_logs + JSONB хранение + payments + store_arr + config_json + Домены данных + Ядро HR + admin сотрудники + admin_group должности + admin_payroll зарплата + Ядро продаж + sales чеки + sales_products товары + users клиенты + Ядро товаров + products_1c номенклатура + prices цены + balances остатки + Ядро магазинов + city_store точки + cluster кластеры + store_orders заказы + RBAC система + auth_item роли + auth_assignment назначения + auth_item_child иерархия + auth_rule правила +``` + ## Общая информация **СУБД:** PostgreSQL diff --git a/erp24/docs/database/RELATIONS.md b/erp24/docs/database/RELATIONS.md new file mode 100644 index 00000000..7dd70675 --- /dev/null +++ b/erp24/docs/database/RELATIONS.md @@ -0,0 +1,1784 @@ +# Полная схема связей базы данных ERP24 + +## Mindmap: Связи таблиц + +```mermaid +mindmap + root((RELATIONS ERP24)) + Центральные таблицы + admin 79 связей + created_by updated_by admin_id + controller_id mentor_id + city_store 24 связи + store_id warehouse_guid + products_1c 20 связей + product_id guid store_id + Типы связей 375+ + hasOne 280+ + FK → PK reference + hasMany 95+ + PK ← FK collection + Домены связей + HR admin→admin_group + Sales sales→admin sales→city_store + Products products_1c→prices + Clients users→users_bonus + Tasks task→task_users + FK constraints + 28 миграций с addForeignKey + Основные timetable_fact rnp_data +``` + +## Метаинформация + +**Общее количество связей в моделях:** 375+ +**Таблиц в БД:** 309 +**Таблиц в каталоге:** 305 (из моделей ActiveRecord) +**ActiveRecord моделей:** 393 +**FK constraints в миграциях:** 28+ +**Доменов:** 19 +**Дата актуализации:** 2025-12-12 + +--- + +## Центральные таблицы-хабы + +Эти таблицы имеют наибольшее количество входящих связей: + +| Таблица | Входящих связей | Типичные FK поля | +|---------|-----------------|------------------| +| `admin` | 79 | admin_id, created_by, updated_by, controller_id, mentor_id, florist_id, courier_id | +| `city_store` | 24 | store_id | +| `products_1c` | 20 | product_id, guid, store_id | +| `admin_group` | 14 | group_id, admin_group_id, d_id | +| `files` | 14 | entity_id (полиморфная) | +| `task` | 5 | task_id, entity_id | +| `lessons` | 5 | entity_id | + +--- + +## Полная карта связей по доменам + +### 1. HR и Сотрудники + +```mermaid +erDiagram + ADMIN { + bigint id PK + varchar guid UK + integer group_id FK + integer store_id FK + integer employee_position_id FK + } + + ADMIN_GROUP { + integer id PK + integer parent_id FK + varchar name + } + + CITY_STORE { + integer id PK + integer city_id FK + integer administrator_id FK + } + + EMPLOYEE_POSITION { + integer id PK + integer group_id FK + varchar name + } + + ADMIN_PAYROLL { + integer id PK + integer admin_id FK + integer store_id FK + integer year + integer month + } + + ADMIN_PAYROLL_DAYS { + integer id PK + integer admin_payroll_id FK + integer admin_id FK + date date + } + + ADMIN_PAYROLL_VALUES { + integer id PK + integer admin_payroll_id FK + integer dict_id FK + } + + ADMIN_PAYROLL_VALUES_DICT { + integer id PK + varchar name + } + + GRADE { + integer id PK + integer admin_id FK + integer grade_id FK + } + + GRADE_GROUP { + integer id PK + integer grade_id FK + integer group_id FK + } + + TIMETABLE { + integer id PK + integer admin_id FK + integer store_id FK + integer shift_id FK + integer plan_id FK + } + + TIMETABLE_FACT { + integer id PK + integer admin_id FK + integer store_id FK + integer plan_id FK + integer checkin_start_id FK + integer checkin_end_id FK + } + + SHIFT { + integer id PK + varchar name + } + + ADMIN_CHECKIN { + integer id PK + integer admin_id FK + integer plan_id FK + } + + EMPLOYEE_ON_SHIFT { + integer id PK + integer admin_id FK + integer shift_id FK + integer store_id FK + } + + EMPLOYEE_SKILL { + integer id PK + varchar name + } + + EMPLOYEE_POSITION_SKILL { + integer id PK + integer position_id FK + integer skill_id FK + } + + ADMIN ||--|| ADMIN_GROUP : "group_id" + ADMIN ||--|| CITY_STORE : "store_id" + ADMIN ||--o| EMPLOYEE_POSITION : "employee_position_id" + ADMIN ||--o{ ADMIN_PAYROLL : "admin_id" + ADMIN ||--o{ TIMETABLE : "admin_id" + ADMIN ||--o{ TIMETABLE_FACT : "admin_id" + ADMIN ||--o{ GRADE : "admin_id" + ADMIN ||--o{ ADMIN_CHECKIN : "admin_id" + ADMIN ||--o{ EMPLOYEE_ON_SHIFT : "admin_id" + + ADMIN_GROUP ||--o| ADMIN_GROUP : "parent_id" + ADMIN_GROUP ||--o{ GRADE_GROUP : "group_id" + + CITY_STORE ||--o| ADMIN : "administrator_id" + + EMPLOYEE_POSITION ||--o{ EMPLOYEE_POSITION_SKILL : "position_id" + EMPLOYEE_POSITION_SKILL ||--|| EMPLOYEE_SKILL : "skill_id" + + ADMIN_PAYROLL ||--|| CITY_STORE : "store_id" + ADMIN_PAYROLL ||--o{ ADMIN_PAYROLL_DAYS : "admin_payroll_id" + ADMIN_PAYROLL ||--o{ ADMIN_PAYROLL_VALUES : "admin_payroll_id" + ADMIN_PAYROLL_VALUES ||--|| ADMIN_PAYROLL_VALUES_DICT : "dict_id" + + GRADE ||--o| GRADE_GROUP : "grade_id" + + TIMETABLE ||--|| SHIFT : "shift_id" + TIMETABLE ||--|| CITY_STORE : "store_id" + TIMETABLE ||--o{ TIMETABLE_FACT : "plan_id" + + TIMETABLE_FACT ||--|| CITY_STORE : "store_id" + TIMETABLE_FACT ||--o| ADMIN_CHECKIN : "checkin_start_id" + TIMETABLE_FACT ||--o| ADMIN_CHECKIN : "checkin_end_id" + + EMPLOYEE_ON_SHIFT ||--|| SHIFT : "shift_id" + EMPLOYEE_ON_SHIFT ||--|| CITY_STORE : "store_id" +``` + +#### Связи HR (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| admin | hasOne | AdminGroup | group_id | Должность сотрудника | +| admin | hasOne | CityStore | store_id | Основной магазин | +| admin | hasOne | EmployeePosition | employee_position_id | Позиция в штатке | +| admin | hasMany | AdminPayroll | admin_id | Зарплатные ведомости | +| admin | hasMany | Timetable | admin_id | Расписания | +| admin | hasMany | TimetableFact | admin_id | Факт выходов | +| admin | hasMany | Grade | admin_id | История грейдов | +| admin | hasMany | AdminCheckin | admin_id | Чекины | +| admin_group | hasOne | AdminGroup | parent_id | Родительская группа | +| admin_group | hasMany | AdminGroupShift | admin_group_id | Связь со сменами | +| admin_payroll | hasOne | Admin | admin_id | Сотрудник | +| admin_payroll | hasOne | CityStore | store_id | Магазин | +| admin_payroll | hasMany | AdminPayrollDays | admin_payroll_id | Дни | +| admin_payroll | hasMany | AdminPayrollValues | admin_payroll_id | Компоненты | +| timetable | hasOne | Admin | admin_id | Сотрудник | +| timetable | hasOne | CityStore | store_id | Магазин | +| timetable | hasOne | Shift | shift_id | Смена | +| timetable_fact | hasOne | TimetablePlan | plan_id | План | +| timetable_fact | hasOne | AdminCheckin | checkin_start_id | Чекин начала | +| timetable_fact | hasOne | AdminCheckin | checkin_end_id | Чекин конца | +| grade | hasOne | GradeGroup | grade_id | Группа грейда | +| grade | hasOne | Admin | admin_id | Сотрудник | + +--- + +### 2. Продажи и чеки + +```mermaid +erDiagram + SALES { + varchar id PK "GUID из 1С" + bigint admin_id FK + integer store_id FK + bigint phone + varchar order_id + varchar sales_check FK + } + + SALES_PRODUCTS { + varchar check_id PK_FK + varchar product_id PK_FK + integer quantity + numeric price + } + + PRODUCTS_1C { + varchar id PK "GUID" + varchar name + varchar articul + } + + ADMIN { + bigint id PK + varchar name + } + + CITY_STORE { + integer id PK + varchar name + } + + USERS { + bigint id PK + varchar phone + } + + CREATE_CHECKS { + integer id PK + varchar guid UK + varchar check_id + varchar store_id FK + varchar seller_id FK + varchar marketplace_order_id FK + } + + USERS_BONUS { + integer id PK + varchar check_id FK + bigint user_id FK + varchar phone + } + + MARKETPLACE_ORDERS { + integer id PK + varchar marketplace_order_id UK + varchar check_guid FK + } + + SALES ||--o{ SALES_PRODUCTS : "check_id→id" + SALES }o--|| ADMIN : "admin_id" + SALES }o--|| CITY_STORE : "store_id" + SALES }o--o| USERS : "phone" + SALES }o--o| SALES : "sales_check (возврат)" + SALES ||--o{ USERS_BONUS : "check_id" + + SALES_PRODUCTS }o--|| PRODUCTS_1C : "product_id" + + CREATE_CHECKS }o--|| CITY_STORE : "store_id" + CREATE_CHECKS }o--o| MARKETPLACE_ORDERS : "marketplace_order_id" + CREATE_CHECKS }o--o| SALES : "guid" + + USERS_BONUS }o--|| USERS : "user_id" +``` + +#### Связи продаж (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| sales | hasMany | SalesProducts | check_id→id | Товары в чеке | +| sales | hasOne | Admin | admin_id | Продавец | +| sales | hasOne | CityStore | store_id | Магазин | +| sales | hasOne | Sales | sales_check | Чек возврата | +| sales | hasMany | UsersBonus | check_id | Начисленные бонусы | +| sales_products | hasOne | Products1c | product_id | Товар | +| sales_products | hasOne | Sales | check_id | Чек | +| create_checks | hasOne | CityStore | store_id | Магазин | +| create_checks | hasOne | Sales | guid | Созданный чек | +| create_checks | hasOne | MarketplaceOrders | marketplace_order_id | Заказ МП | + +--- + +### 3. Товары и номенклатура + +```mermaid +erDiagram + PRODUCTS_1C { + varchar id PK "GUID" + varchar name + varchar articul + varchar barcode + varchar parent_id FK + } + + PRODUCTS_CLASS { + integer id PK + integer parent_id FK + varchar name + } + + PRICES { + integer id PK + varchar product_id FK + integer store_id FK + numeric price_retail + } + + PRICES_DYNAMIC { + integer id PK + varchar product_id FK + date date + numeric price + } + + BALANCES { + integer id PK + varchar product_id FK + integer store_id FK + numeric quantity + } + + MATRIX_ERP { + integer id PK + varchar guid UK + integer type_id FK + } + + MATRIX_ERP_MEDIA { + integer id PK + varchar guid FK + varchar url + } + + MATRIX_ERP_PROPERTY { + integer id PK + varchar guid FK + varchar name + } + + MATRIX_TYPE { + integer id PK + varchar name + } + + BOUQUET_COMPOSITION { + integer id PK + varchar name + } + + BOUQUET_COMPOSITION_PRODUCTS { + integer id PK + integer bouquet_id FK + varchar product_guid FK + } + + SELF_COST_PRODUCT_DYNAMIC { + integer id PK + varchar product_guid FK + numeric cost + } + + PRODUCTS_1C }o--o| PRODUCTS_CLASS : "parent_id→category_id" + PRODUCTS_1C ||--o{ PRICES : "product_id" + PRODUCTS_1C ||--o{ PRICES_DYNAMIC : "product_id" + PRODUCTS_1C ||--o{ BALANCES : "product_id" + PRODUCTS_1C ||--o{ SELF_COST_PRODUCT_DYNAMIC : "product_guid" + PRODUCTS_1C ||--o{ BOUQUET_COMPOSITION_PRODUCTS : "product_guid" + + MATRIX_ERP ||--o{ MATRIX_ERP_MEDIA : "guid" + MATRIX_ERP ||--o{ MATRIX_ERP_PROPERTY : "guid" + MATRIX_ERP }o--|| MATRIX_TYPE : "type_id" + + BOUQUET_COMPOSITION ||--o{ BOUQUET_COMPOSITION_PRODUCTS : "bouquet_id" + + PRICES }o--|| CITY_STORE : "store_id" + BALANCES }o--|| CITY_STORE : "store_id" +``` + +#### Связи товаров (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| products_1c | hasOne | ProductsClass | parent_id | Категория | +| products_1c | hasMany | Prices | product_id | Цены | +| products_1c | hasMany | Balances | product_id | Остатки | +| products_1c | hasOne | Products1cNomenclature | product_id | Номенклатура | +| products_1c | hasMany | SelfCostProductDynamic | product_guid | Себестоимость | +| prices | hasOne | Products1c | product_id | Товар | +| prices | hasOne | CityStore | store_id | Магазин | +| matrix_erp | hasMany | MatrixErpMedia | guid | Медиа | +| matrix_erp | hasMany | MatrixErpProperty | guid | Свойства | +| matrix_erp | hasOne | MatrixType | type_id | Тип | +| bouquet_composition | hasMany | BouquetCompositionProducts | bouquet_id | Состав | +| bouquet_composition_products | hasOne | Products1c | product_guid | Товар | + +--- + +### 4. Клиенты и лояльность + +```mermaid +erDiagram + USERS { + bigint id PK + varchar phone UK + varchar name + numeric balans + varchar bonus_level + integer referral_id FK + } + + USERS_BONUS { + integer id PK + bigint user_id FK + varchar phone + varchar check_id FK + varchar tip + numeric bonus + } + + USERS_EVENTS { + integer id PK + varchar phone FK + bigint user_id FK + varchar event_type + date event_date + } + + USERS_TELEGRAM { + integer id PK + varchar phone FK + bigint telegram_id + } + + BONUS_LEVELS { + integer id PK + varchar name + numeric percent + } + + REFERRAL_STATUS { + integer id PK + bigint user_id FK + bigint referrer_id FK + } + + SENT_KOGORT { + integer id PK + varchar phone FK + integer kogort_id + } + + KOGORT_STOP_LIST { + integer id PK + varchar phone + } + + USERS ||--o{ USERS_BONUS : "user_id" + USERS ||--o{ USERS_EVENTS : "user_id" + USERS ||--o| USERS_TELEGRAM : "phone" + USERS }o--o| BONUS_LEVELS : "bonus_level" + USERS ||--o{ REFERRAL_STATUS : "user_id" + USERS ||--o{ SENT_KOGORT : "phone" + USERS }o--o| USERS : "referral_id (реферер)" + + USERS_BONUS }o--o| SALES : "check_id" +``` + +#### Связи клиентов (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| users | hasMany | UsersBonus | user_id | Бонусные транзакции | +| users | hasMany | UsersEvents | user_id | События | +| users | hasOne | UsersTelegram | phone | Telegram | +| users | hasOne | Users | referral_id | Реферер | +| users | hasMany | ReferralStatus | user_id | Статусы реферала | +| users_bonus | hasOne | Users | user_id | Клиент | +| users_bonus | hasOne | Sales | check_id | Чек | +| users_events | hasOne | Users | user_id | Клиент | + +--- + +### 5. Магазины и локации + +```mermaid +erDiagram + CITY_STORE { + integer id PK + integer f_id + integer city_id FK + integer administrator_id FK + integer firma_id FK + varchar name + varchar gps + } + + CITY { + integer id_city PK + varchar name + } + + CITY_STORE_PARAMS { + integer id PK + integer store_id FK + integer store_type FK + varchar address_city FK + varchar address_region FK + } + + STORE_TYPE { + integer id PK + varchar name + } + + STORE_CITY_LIST { + integer id PK + integer parent_id FK + varchar name + } + + CLUSTER { + integer id PK + varchar name + } + + CLUSTER_ADMIN { + integer id PK + integer cluster_id FK + integer admin_id FK + } + + EXPORT_IMPORT_TABLE { + integer id PK + varchar entity + integer entity_id FK + integer export_id + varchar export_val "GUID" + } + + MARKETPLACE_STORE { + integer id PK + integer store_id FK + varchar warehouse_guid + } + + CITY_STORE }o--|| CITY : "city_id" + CITY_STORE }o--o| ADMIN : "administrator_id" + CITY_STORE ||--o| CITY_STORE_PARAMS : "store_id" + CITY_STORE ||--o{ EXPORT_IMPORT_TABLE : "entity_id" + CITY_STORE ||--o{ MARKETPLACE_STORE : "store_id" + + CITY_STORE_PARAMS }o--o| STORE_TYPE : "store_type" + CITY_STORE_PARAMS }o--o| STORE_CITY_LIST : "address_city" + CITY_STORE_PARAMS }o--o| STORE_CITY_LIST : "address_region" + + STORE_CITY_LIST }o--o| STORE_CITY_LIST : "parent_id" + + CLUSTER ||--o{ CLUSTER_ADMIN : "cluster_id" + CLUSTER_ADMIN }o--|| ADMIN : "admin_id" +``` + +#### Связи магазинов (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| city_store | hasOne | City | city_id | Город | +| city_store | hasOne | Admin | administrator_id | Администратор | +| city_store | hasOne | ExportImportTable | entity_id | GUID для 1С | +| city_store | hasOne | CityStoreParams | store_id | Параметры | +| city_store | hasMany | MarketplaceStore | store_id | Маркетплейсы | +| city_store_params | hasOne | CityStore | store_id | Магазин | +| city_store_params | hasOne | StoreType | store_type | Тип | +| city_store_params | hasOne | StoreCityList | address_city | Город | +| city_store_params | hasOne | StoreCityList | address_region | Регион | +| cluster | hasMany | ClusterAdmin | cluster_id | Привязки кустовых | +| cluster_admin | hasOne | Cluster | cluster_id | Куст | +| cluster_admin | hasOne | Admin | admin_id | Сотрудник | + +--- + +### 6. Заказы и маркетплейсы + +```mermaid +erDiagram + MARKETPLACE_ORDERS { + integer id PK + varchar marketplace_order_id UK + integer store_id FK + integer order_status_id FK + integer order_substatus_id FK + varchar check_guid FK + } + + MARKETPLACE_ORDER_ITEMS { + integer id PK + integer order_id FK + varchar product_id FK + integer quantity + } + + MARKETPLACE_ORDER_STATUS_TYPES { + integer id PK + varchar name + } + + MARKETPLACE_ORDER_STATUS_HISTORY { + integer id PK + integer order_id FK + integer status_id FK + } + + MARKETPLACE_STORE { + integer id PK + integer store_id FK + varchar warehouse_guid + } + + MARKETPLACE_PRICES { + integer id PK + varchar product_id FK + integer marketplace_store_id FK + numeric price + } + + STORE_ORDERS { + integer id PK + integer store_id FK + integer admin_id FK + varchar status + } + + STORE_ORDERS_ITEM { + integer id PK + integer order_id FK + varchar product_id FK + } + + ORDERS_AMO { + integer id PK + integer store_id FK + varchar status + } + + MARKETPLACE_ORDERS ||--o{ MARKETPLACE_ORDER_ITEMS : "order_id" + MARKETPLACE_ORDERS }o--|| MARKETPLACE_ORDER_STATUS_TYPES : "order_status_id" + MARKETPLACE_ORDERS }o--o| MARKETPLACE_ORDER_STATUS_TYPES : "order_substatus_id" + MARKETPLACE_ORDERS ||--o{ MARKETPLACE_ORDER_STATUS_HISTORY : "order_id" + MARKETPLACE_ORDERS }o--o| SALES : "check_guid" + MARKETPLACE_ORDERS }o--o| MARKETPLACE_STORE : "store_id" + + MARKETPLACE_ORDER_ITEMS }o--|| PRODUCTS_1C : "product_id" + MARKETPLACE_ORDER_STATUS_HISTORY }o--|| MARKETPLACE_ORDER_STATUS_TYPES : "status_id" + + MARKETPLACE_STORE }o--|| CITY_STORE : "store_id" + MARKETPLACE_PRICES }o--|| PRODUCTS_1C : "product_id" + MARKETPLACE_PRICES }o--|| MARKETPLACE_STORE : "marketplace_store_id" + + STORE_ORDERS }o--|| CITY_STORE : "store_id" + STORE_ORDERS }o--|| ADMIN : "admin_id" + STORE_ORDERS ||--o{ STORE_ORDERS_ITEM : "order_id" + STORE_ORDERS_ITEM }o--|| PRODUCTS_1C : "product_id" + + ORDERS_AMO }o--|| CITY_STORE : "store_id" +``` + +#### Связи заказов (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| marketplace_orders | hasMany | MarketplaceOrderItems | order_id | Товары | +| marketplace_orders | hasOne | MarketplaceOrderStatusTypes | order_status_id | Статус | +| marketplace_orders | hasOne | MarketplaceOrderStatusTypes | order_substatus_id | Подстатус | +| marketplace_orders | hasMany | MarketplaceOrderStatusHistory | order_id | История | +| marketplace_orders | hasOne | Sales | check_guid | Чек | +| marketplace_order_items | hasOne | Products1c | product_id | Товар | +| marketplace_store | hasOne | CityStore | store_id | Магазин | +| marketplace_prices | hasOne | Products1c | product_id | Товар | +| marketplace_prices | hasOne | MarketplaceStore | marketplace_store_id | МП-магазин | +| store_orders | hasOne | CityStore | store_id | Магазин | +| store_orders | hasOne | Admin | admin_id | Ответственный | +| store_orders | hasMany | StoreOrdersItem | order_id | Товары | + +--- + +### 7. Задачи и управление + +```mermaid +erDiagram + TASK { + integer id PK + integer created_by FK + integer updated_by FK + integer controller_id FK + integer task_type_id FK + integer status FK + integer parent_id FK + integer company_function_id FK + integer task_template_id FK + } + + TASK_USERS { + integer id PK + integer task_id FK + integer admin_id FK + } + + TASK_LOGS { + integer id PK + integer task_id FK + varchar field + text value_before + text value_after + } + + TASK_VIEWERS { + integer id PK + integer task_id FK + integer admin_id FK + } + + TASKS_TYPE { + integer id PK + varchar name + } + + TASK_STATUS { + integer id PK + varchar name + } + + TASK_TEMPLATES { + integer id PK + integer parent_id FK + integer company_function_id FK + } + + TASK_MOTIVATION { + integer id PK + integer task_id FK + integer motivation_id FK + } + + TASK_ENTITY { + integer id PK + varchar name + } + + TASK_ALERT_LEVEL { + integer id PK + varchar name + } + + COMPANY_FUNCTIONS { + integer id PK + varchar name + } + + TASK }o--|| ADMIN : "created_by" + TASK }o--o| ADMIN : "updated_by" + TASK }o--o| ADMIN : "controller_id" + TASK }o--|| TASKS_TYPE : "task_type_id" + TASK }o--|| TASK_STATUS : "status" + TASK }o--o| TASK : "parent_id" + TASK }o--o| COMPANY_FUNCTIONS : "company_function_id" + TASK }o--o| TASK_TEMPLATES : "task_template_id" + TASK ||--o{ TASK_USERS : "task_id" + TASK ||--o{ TASK_LOGS : "task_id" + TASK ||--o{ TASK_VIEWERS : "task_id" + TASK ||--o{ TASK_MOTIVATION : "task_id" + + TASK_USERS }o--|| ADMIN : "admin_id" + TASK_VIEWERS }o--|| ADMIN : "admin_id" + + TASK_TEMPLATES }o--o| TASK_TEMPLATES : "parent_id" + TASK_TEMPLATES }o--o| COMPANY_FUNCTIONS : "company_function_id" +``` + +#### Связи задач (детализация) + +| Источник | Связь | Цель | FK поле | Описание | +|----------|-------|------|---------|----------| +| task | hasOne | Admin | created_by | Создатель | +| task | hasOne | Admin | updated_by | Редактор | +| task | hasOne | Admin | controller_id | Контролёр | +| task | hasOne | TasksType | task_type_id | Тип | +| task | hasOne | TaskStatus | status | Статус | +| task | hasOne | Task | parent_id | Родительская | +| task | hasOne | CompanyFunctions | company_function_id | Функция | +| task | hasOne | TaskTemplates | task_template_id | Шаблон | +| task | hasMany | TaskUsers | task_id | Исполнители | +| task | hasMany | TaskLogs | task_id | Логи | +| task | hasMany | TaskViewers | task_id | Наблюдатели | +| task | hasMany | TaskMotivation | task_id | Мотивация | +| task | hasMany | Messager | task_id | Сообщения | +| task_users | hasOne | Admin | admin_id | Исполнитель | +| task_users | hasOne | Task | task_id | Задача | +| task_templates | hasOne | TaskTemplates | parent_id | Родительский | +| task_templates | hasMany | TaskTriggerConditions | task_template_id | Условия | + +--- + +### 8. Списания и движение товаров + +```mermaid +erDiagram + WRITE_OFFS_ERP { + integer id PK + varchar guid UK + integer store_id FK + integer created_admin_id FK + integer confirm_admin_id FK + varchar status + } + + WRITE_OFFS_PRODUCTS_ERP { + integer id PK + integer write_offs_erp_id FK + varchar product_id FK + integer cause_id FK + numeric quantity + } + + WRITE_OFFS_ERP_CAUSE_DICT { + integer id PK + varchar name + } + + WAYBILL_INCOMING { + integer id PK + integer store_id FK + varchar number + } + + WAYBILL_INCOMING_PRODUCTS { + integer id PK + integer waybill_incoming_id FK + varchar product_id FK + } + + WAYBILL_WRITE_OFFS { + integer id PK + integer store_id FK + } + + WAYBILL_WRITE_OFFS_PRODUCTS { + integer id PK + integer waybill_write_offs_id FK + varchar product_id FK + } + + SHIFT_TRANSFER { + integer id PK + integer store_id FK + integer admin_id FK + } + + SHIFT_REMAINS { + integer id PK + integer shift_transfer_id FK + } + + EQUALIZATION_REMAINS { + integer id PK + integer shift_transfer_id FK + } + + WRITE_OFFS_ERP }o--|| CITY_STORE : "store_id" + WRITE_OFFS_ERP }o--|| ADMIN : "created_admin_id" + WRITE_OFFS_ERP }o--o| ADMIN : "confirm_admin_id" + WRITE_OFFS_ERP ||--o{ WRITE_OFFS_PRODUCTS_ERP : "write_offs_erp_id" + + WRITE_OFFS_PRODUCTS_ERP }o--|| PRODUCTS_1C : "product_id" + WRITE_OFFS_PRODUCTS_ERP }o--o| WRITE_OFFS_ERP_CAUSE_DICT : "cause_id" + + WAYBILL_INCOMING }o--|| CITY_STORE : "store_id" + WAYBILL_INCOMING ||--o{ WAYBILL_INCOMING_PRODUCTS : "waybill_incoming_id" + WAYBILL_INCOMING_PRODUCTS }o--|| PRODUCTS_1C : "product_id" + + WAYBILL_WRITE_OFFS }o--|| CITY_STORE : "store_id" + WAYBILL_WRITE_OFFS ||--o{ WAYBILL_WRITE_OFFS_PRODUCTS : "waybill_write_offs_id" + WAYBILL_WRITE_OFFS_PRODUCTS }o--|| PRODUCTS_1C : "product_id" + + SHIFT_TRANSFER }o--|| CITY_STORE : "store_id" + SHIFT_TRANSFER }o--|| ADMIN : "admin_id" + SHIFT_TRANSFER ||--o{ SHIFT_REMAINS : "shift_transfer_id" + SHIFT_TRANSFER ||--o{ EQUALIZATION_REMAINS : "shift_transfer_id" +``` + +--- + +### 9. Обучение и регламенты + +```mermaid +erDiagram + LESSONS { + integer id PK + integer group_id FK + integer created_by FK + varchar name + } + + LESSONS_GROUP { + integer id PK + integer parent_id FK + varchar name + } + + LESSONS_PASSED { + integer id PK + integer lesson_id FK + integer admin_id FK + timestamp finished_at + } + + LESSONS_POLL { + integer id PK + integer lesson_id FK + varchar question + } + + LESSON_POLL_ANSWERS { + integer id PK + integer poll_id FK + integer admin_id FK + } + + REGULATIONS { + integer id PK + integer group_id FK + integer created_by FK + } + + REGULATION_GROUP { + integer id PK + varchar name + } + + REGULATIONS_PASSED { + integer id PK + integer regulation_id FK + integer admin_id FK + } + + REGULATIONS_POLL { + integer id PK + integer regulation_id FK + } + + REGULATIONS_POLL_ANSWERS { + integer id PK + integer poll_id FK + integer admin_id FK + } + + WIKI_CATEGORY { + integer id PK + integer parent_id FK + varchar name + } + + WIKI_ARTICLE { + integer id PK + integer category_id FK + integer author_id FK + } + + LESSONS }o--|| LESSONS_GROUP : "group_id" + LESSONS }o--|| ADMIN : "created_by" + LESSONS ||--o{ LESSONS_PASSED : "lesson_id" + LESSONS ||--o{ LESSONS_POLL : "lesson_id" + + LESSONS_GROUP }o--o| LESSONS_GROUP : "parent_id" + LESSONS_GROUP ||--o{ LESSONS : "group_id" + + LESSONS_PASSED }o--|| ADMIN : "admin_id" + LESSONS_POLL ||--o{ LESSON_POLL_ANSWERS : "poll_id" + LESSON_POLL_ANSWERS }o--|| ADMIN : "admin_id" + + REGULATIONS }o--|| REGULATION_GROUP : "group_id" + REGULATIONS }o--|| ADMIN : "created_by" + REGULATIONS ||--o{ REGULATIONS_PASSED : "regulation_id" + REGULATIONS ||--o{ REGULATIONS_POLL : "regulation_id" + + REGULATIONS_PASSED }o--|| ADMIN : "admin_id" + REGULATIONS_POLL ||--o{ REGULATIONS_POLL_ANSWERS : "poll_id" + + WIKI_CATEGORY }o--o| WIKI_CATEGORY : "parent_id" + WIKI_CATEGORY ||--o{ WIKI_ARTICLE : "category_id" + WIKI_ARTICLE }o--|| ADMIN : "author_id" +``` + +--- + +### 10. RBAC и авторизация + +```mermaid +erDiagram + AUTH_ITEM { + varchar name PK + integer type "1=role, 2=permission" + varchar description + varchar rule_name FK + } + + AUTH_ITEM_CHILD { + varchar parent PK_FK + varchar child PK_FK + } + + AUTH_ASSIGNMENT { + varchar item_name PK_FK + varchar user_id PK + } + + AUTH_RULE { + varchar name PK + text data + } + + CRM_MENU { + integer id PK + varchar name + integer parent_id FK + } + + CRM_MENU_PERMISSION { + integer id PK + integer menu_id FK + varchar alias + } + + ADMIN_GROUP_RBAC_CONFIG { + integer id PK + integer group_id FK + varchar permission + } + + AUTH_ITEM }o--o| AUTH_RULE : "rule_name" + AUTH_ITEM ||--o{ AUTH_ITEM_CHILD : "parent" + AUTH_ITEM ||--o{ AUTH_ITEM_CHILD : "child" + AUTH_ITEM ||--o{ AUTH_ASSIGNMENT : "item_name" + + AUTH_ITEM_CHILD }o--|| AUTH_ITEM : "parent" + AUTH_ITEM_CHILD }o--|| AUTH_ITEM : "child" + + AUTH_ASSIGNMENT }o--|| AUTH_ITEM : "item_name" + AUTH_ASSIGNMENT }o--|| ADMIN : "user_id" + + CRM_MENU }o--o| CRM_MENU : "parent_id" + CRM_MENU ||--o{ CRM_MENU_PERMISSION : "menu_id" + + ADMIN_GROUP_RBAC_CONFIG }o--|| ADMIN_GROUP : "group_id" +``` + +--- + +### 11. RNP (Аналитика) + +```mermaid +erDiagram + RNP_INDEX { + integer id PK + varchar name + } + + RNP_ALIAS { + integer id PK + varchar name + } + + RNP_DATA { + integer id PK + integer index_id FK + integer alias_id FK + numeric value + date date + } + + RNP_DATA }o--|| RNP_INDEX : "index_id" + RNP_DATA }o--|| RNP_ALIAS : "alias_id" + + RNP_INDEX ||--o{ RNP_DATA : "index_id" + RNP_ALIAS ||--o{ RNP_DATA : "alias_id" +``` + +--- + +## FK Constraints в миграциях + +Явные внешние ключи, определённые в миграциях: + +| Миграция | Таблица | FK поле | Ссылается на | +|----------|---------|---------|--------------| +| m231030_065754 | rnp_data | alias_id | rnp_alias.id | +| m231030_065754 | rnp_data | index_id | rnp_index.id | +| m240603_082541 | timetable_fact | admin_id | admin.id | +| m240603_082541 | timetable_fact | store_id | city_store.id | +| m240603_082541 | timetable_fact | admin_group_id | admin_group.id | +| m240603_082541 | timetable_fact | d_id | admin_group.id | +| m240603_082541 | timetable_fact | plan_id | timetable.id | +| m240603_082541 | timetable_fact | admin_id_add | admin.id | +| m240611_095133 | timetable_fact | checkin_start_id | admin_checkin.id | +| m240611_095133 | timetable_fact | checkin_end_id | admin_checkin.id | +| m240611_095133 | timetable_fact | store_id | city_store.id | +| m240703_094403 | timetable_fact | plan_id | timetable.id | +| m241023_064710 | marketplace_store | store_id | city_store.id | +| m241102_124956 | products_1c_additional_characteristics | property_id | products_1c_prop_type.id | +| m241114_085841 | product_1c_replacement_log | replacement_id | product_1c_replacement.id | +| m241210_090957 | waybill_write_offs | store_id | city_store.id | +| m241210_091021 | waybill_write_offs_products | waybill_write_offs_id | waybill_write_offs.id | +| m241211_073430 | waybill_incoming_products | waybill_incoming_id | waybill_incoming.id | +| m241212_122541 | waybill_incoming_products | product_id | products_1c.id | +| m241212_122541 | waybill_write_offs_products | product_id | products_1c.id | +| m250116_112743 | store_city_list | parent_id | store_city_list.id | +| m250210_120558 | wiki_article | category_id | wiki_category.id | +| m250210_120558 | wiki_category | parent_id | wiki_category.id | +| m251028_141001 | employee_position | salary_group_id | admin_group.id | +| m251201_120001 | employee_payment | employee_position_id | employee_position.id | + +--- + +## Полиморфные связи + +Некоторые таблицы используют полиморфные связи через поля `entity` и `entity_id`: + +### Files (Файлы) + +```php +// Файлы привязываются к разным сущностям через entity/entity_id +Files::find()->where([ + 'entity' => 'task_proof', + 'entity_id' => $taskId +]); + +Files::find()->where([ + 'entity' => 'lesson_picture', + 'entity_id' => $lessonId +]); + +Files::find()->where([ + 'entity' => 'notification', + 'entity_id' => $notificationId +]); +``` + +| entity значение | Связь с таблицей | +|-----------------|------------------| +| task_proof | task | +| lesson_picture | lessons | +| lesson_group_picture | lessons_group | +| lesson_poll_answer_picture | lesson_poll_answers | +| notification | notification | +| bouquet_presentation | bouquet_composition | +| bouquet_build_process | bouquet_composition | +| matrix_erp_media | matrix_erp | + +### ExportImportTable (Синхронизация с 1С) + +```php +// Маппинг ID ↔ GUID для любой сущности +ExportImportTable::find()->where([ + 'entity' => 'city_store', + 'entity_id' => $storeId, + 'export_id' => 1 // 1 = 1С +])->select('export_val'); +``` + +| entity значение | Связь с таблицей | +|-----------------|------------------| +| city_store | city_store | +| admin | admin | +| products_1c | products_1c | + +### Comment (Комментарии) + +```php +// Комментарии к любым сущностям +Comment::find()->where([ + 'entity_type' => 'task', + 'entity_id' => $taskId +]); +``` + +--- + +## Статистика связей + +### По типу связи + +| Тип связи | Количество | Описание | +|-----------|------------|----------| +| hasOne | 280+ | FK → PK (один-к-одному) | +| hasMany | 95+ | PK ← FK (один-ко-многим) | +| belongsTo | редко | Синоним hasOne | +| viaTable | 5+ | Many-to-Many через junction | + +### По целевой таблице (TOP-20) + +| Таблица-цель | Входящих связей | Типичные FK | +|--------------|-----------------|-------------| +| admin | 79 | admin_id, created_by, updated_by | +| city_store | 24 | store_id | +| products_1c | 20 | product_id, guid | +| admin_group | 14 | group_id, admin_group_id | +| files | 14 | entity_id | +| task | 5 | task_id, entity_id | +| lessons | 5 | entity_id, lesson_id | +| shift | 4 | shift_id | +| sales | 4 | check_id, guid | +| marketplace_order_status_types | 6 | status_id, substatus_id | +| store_city_list | 5 | address_city, address_region | +| employee_position | 5 | employee_position_id | +| prices | 4 | product_id | +| bouquet_composition | 4 | bouquet_id | +| task_templates | 4 | task_template_id | + +--- + +## Глобальная ERD (упрощённая) + +```mermaid +erDiagram + ADMIN ||--|| ADMIN_GROUP : belongs_to + ADMIN ||--|| CITY_STORE : works_at + ADMIN ||--o{ ADMIN_PAYROLL : receives + ADMIN ||--o{ TIMETABLE : scheduled + ADMIN ||--o{ TASK : creates + ADMIN ||--o{ TASK_USERS : assigned_to + ADMIN ||--o{ SALES : sells + ADMIN ||--o{ LESSONS_PASSED : completed + ADMIN ||--o{ GRADE : has_grade + + CITY_STORE }o--|| CITY : located_in + CITY_STORE ||--o{ SALES : records + CITY_STORE ||--o{ BALANCES : has_inventory + CITY_STORE ||--o{ MARKETPLACE_STORE : on_marketplace + CITY_STORE ||--o{ TIMETABLE : schedules + CITY_STORE ||--o{ WRITE_OFFS_ERP : has_writeoffs + + USERS ||--o{ USERS_BONUS : earns + USERS ||--o{ SALES : purchases + USERS ||--o{ USERS_EVENTS : has_events + USERS }o--|| BONUS_LEVELS : has_level + + SALES ||--o{ SALES_PRODUCTS : contains + SALES ||--o{ USERS_BONUS : generates + SALES }o--|| ADMIN : sold_by + SALES }o--|| CITY_STORE : at_store + + SALES_PRODUCTS }o--|| PRODUCTS_1C : references + PRODUCTS_1C ||--o{ BALANCES : has_balance + PRODUCTS_1C ||--o{ PRICES : has_price + PRODUCTS_1C ||--o{ MATRIX_ERP : in_matrix + + TASK ||--o{ TASK_USERS : assigned_to + TASK }o--|| TASKS_TYPE : type + TASK ||--o{ TASK_LOGS : logged + + MARKETPLACE_ORDERS ||--o{ MARKETPLACE_ORDER_ITEMS : contains + MARKETPLACE_ORDERS }o--|| MARKETPLACE_STORE : from_store + + WRITE_OFFS_ERP ||--o{ WRITE_OFFS_PRODUCTS_ERP : contains + + LESSONS ||--o{ LESSONS_PASSED : completed + LESSONS }o--|| LESSONS_GROUP : in_group +``` + +--- + +## Полный каталог таблиц по доменам (305 таблиц) + +### Домен: HR и Сотрудники (41 таблица) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `admin` | Сотрудники | group_id→admin_group, store_id→city_store, employee_position_id→employee_position | +| `admin_bonus_conversion` | Конвертация бонусов | admin_id→admin | +| `admin_chats` | Чаты сотрудников | admin_id→admin | +| `admin_checkin` | Чекины сотрудников | admin_id→admin, plan_id→timetable | +| `admin_desktop` | Рабочий стол | admin_id→admin | +| `admin_device` | Устройства сотрудников | admin_id→admin | +| `admin_dynamic` | Динамические поля | admin_id→admin, category_id→admin_dynamic_category_dict | +| `admin_dynamic_category_dict` | Справочник категорий | - | +| `admin_grade_history` | История грейдов | admin_id→admin, grade_id→grade | +| `admin_group` | Должности/группы | parent_id→admin_group | +| `admin_group_company_function_visibility` | Видимость функций | admin_group_id→admin_group, company_function_id→company_functions | +| `admin_group_dynamic` | Динамика групп | admin_group_id→admin_group | +| `admin_group_rbac_config` | RBAC конфигурация | group_id→admin_group | +| `admin_group_regulation` | Регламенты групп | admin_group_id→admin_group, regulation_id→regulations | +| `admin_payroll` | Зарплатные ведомости | admin_id→admin, store_id→city_store | +| `admin_payroll_days` | Дневные расчёты | admin_payroll_id→admin_payroll, admin_id→admin | +| `admin_payroll_history` | История зарплат | admin_payroll_id→admin_payroll | +| `admin_payroll_month_info` | Сводка по месяцу | admin_payroll_id→admin_payroll | +| `admin_payroll_stat` | Статистика зарплат | admin_id→admin, store_id→city_store | +| `admin_payroll_values` | Компоненты выплат | admin_payroll_id→admin_payroll, dict_id→admin_payroll_values_dict | +| `admin_payroll_values_dict` | Справочник типов выплат | - | +| `admin_person_bonuses` | Персональные бонусы | admin_id→admin | +| `admin_rating` | Рейтинги сотрудников | admin_id→admin | +| `admin_stores` | Доступные магазины | admin_id→admin, store_guid→city_store | +| `employee_balance` | Баланс сотрудника | admin_id→admin | +| `employee_on_shift` | Сотрудники на смене | admin_id→admin, shift_id→shift, store_id→city_store | +| `employee_payment` | Выплаты сотрудникам | employee_position_id→employee_position | +| `employee_position` | Должности | group_id→admin_group | +| `employee_position_skill` | Навыки должности | position_id→employee_position, skill_id→employee_skill | +| `employee_position_status` | Статусы должностей | admin_id→admin | +| `employee_skill` | Навыки | - | +| `employee_skill_need` | Требования к навыкам | skill_id→employee_skill | +| `employee_skill_status` | Статусы навыков | admin_id→admin | +| `employee_skill_type` | Типы навыков | - | +| `grade` | Грейды | admin_id→admin | +| `grade_group` | Группы грейдов | grade_id→grade, group_id→admin_group | +| `grade_price` | Цены грейдов | grade_id→grade | +| `shift_transfer` | Передача смены | store_id→city_store, admin_id→admin | +| `shift_remains` | Остатки смены | shift_transfer_id→shift_transfer | +| `equalization_remains` | Выравнивание остатков | shift_transfer_id→shift_transfer | +| `teambonus_settings` | Настройки тимбонуса | - | + +### Домен: Расписание и смены (9 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `timetable` | Расписание | admin_id→admin, store_id→city_store, shift_id→shift, plan_id→timetable | +| `timetable_fact` | Факт расписания | admin_id→admin, store_id→city_store, plan_id→timetable, checkin_start_id→admin_checkin | +| `timetable_shift` | Смены расписания | admin_id→admin, shift_id→shift | +| `timetable_workbot` | Бот расписания | admin_id→admin | +| `shift` | Смены | - | +| `holiday` | Праздники | - | +| `production_calendar` | Производственный календарь | - | +| `calendar_admin_link` | Связь календаря с админами | admin_id→admin | +| `cluster_calendar` | Календарь кустов | cluster_id→cluster, category_id→cluster_calendar_category_dict | + +### Домен: Продажи (18 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `sales` | Чеки продаж | admin_id→admin, store_id→city_store, sales_check→sales | +| `sales_products` | Товары в чеках | check_id→sales, product_id→products_1c | +| `sales_history` | История продаж | - | +| `sales_items` | Элементы продаж | check_id→sales | +| `sales_products_history` | История товаров в чеках | check_id→sales | +| `sales_products_update` | Обновления товаров | check_id→sales | +| `sales_update` | Очередь обновлений | - | +| `sales_write_offs_plan` | План списаний | store_id→city_store | +| `create_checks` | Черновики чеков | store_id→city_store, seller_id→admin, marketplace_order_id→marketplace_orders | +| `create_checks2` | Черновики чеков v2 | store_id→city_store | +| `create_checks_bags` | Пакеты чеков | check_id→create_checks | +| `check_conduct` | Проверка чеков | store_id→city_store, created_by→admin | +| `check_conduct_item` | Элементы проверки | check_conduct_id→check_conduct, check_criteria_id→check_criteria | +| `check_criteria` | Критерии проверки | company_function_id→company_functions, check_group_id→check_group | +| `check_criteria_item` | Элементы критериев | check_criteria_id→check_criteria | +| `check_group` | Группы проверок | - | +| `check_type` | Типы проверок | - | +| `cashes` | Кассы | store_id→city_store | + +### Домен: Товары и номенклатура (26 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `products_1c` | Номенклатура из 1С | parent_id→products_class | +| `products_1c_additional_characteristics` | Доп характеристики | product_id→products_1c, property_id→products_1c_prop_type | +| `products_1c_nomenclature` | Номенклатура | product_id→products_1c | +| `products_1c_nomenclature_actuality` | Актуальность | product_id→products_1c | +| `products_1c_options` | Опции товаров | product_id→products_1c | +| `products_1c_prop_type` | Типы свойств | - | +| `products_cat_property` | Свойства категорий | category_id→products_class | +| `products_class` | Классы товаров | parent_id→products_class | +| `products_property_value` | Значения свойств | product_id→products_1c | +| `products_varieties` | Разновидности | product_id→products_1c | +| `prices` | Цены | product_id→products_1c, store_id→city_store | +| `prices_dynamic` | Динамические цены | product_id→products_1c | +| `prices_region` | Региональные цены | product_id→products_1c | +| `prices_zakup` | Закупочные цены | product_id→products_1c | +| `balances` | Остатки | product_id→products_1c, store_id→city_store | +| `self_cost_product` | Себестоимость | product_guid→products_1c | +| `self_cost_product_dynamic` | Динамика себестоимости | product_guid→products_1c | +| `product_1c_replacement` | Замены товаров | product_id→products_1c, replacement_id→products_1c | +| `product_1c_replacement_log` | Лог замен | replacement_id→product_1c_replacement | +| `cat_property` | Свойства категорий | - | +| `category_plan` | План категорий | store_id→city_store | +| `incoming` | Поступления | store_id→city_store | +| `incoming_items` | Товары поступлений | incoming_id→incoming, product_id→products_1c | +| `assemblies` | Сборки букетов | store_id→city_store, admin_id→admin | +| `autoplannogramma` | Автопланограмма | store_id→city_store | +| `shipment_providers` | Поставщики | - | + +### Домен: Матрица и букеты (14 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `matrix_erp` | Матрица товаров | type_id→matrix_type | +| `matrix_erp_media` | Медиа матрицы | guid→matrix_erp | +| `matrix_erp_property` | Свойства матрицы | guid→matrix_erp | +| `matrix_erp_property_dynamic` | Динамика свойств | guid→matrix_erp | +| `erp24.matrix_type` | Типы матрицы | - | +| `matrix_bouquet_actuality` | Актуальность букетов | bouquet_id→bouquet_composition | +| `matrix_bouquet_forecast` | Прогноз букетов | bouquet_id→bouquet_composition | +| `erp24.bouquet_composition` | Состав букетов | - | +| `erp24.bouquet_composition_products` | Товары букетов | bouquet_id→bouquet_composition, product_guid→products_1c | +| `erp24.bouquet_composition_matrix_type_history` | История типов | bouquet_id→bouquet_composition | +| `erp24.bouquet_forecast` | Прогноз букетов | bouquet_id→bouquet_composition | +| `bouquet_composition_price` | Цены букетов | bouquet_id→bouquet_composition | +| `replacement_invoice` | Накладные замен | store_id→city_store | +| `replacement_invoice_products` | Товары накладных | replacement_invoice_id→replacement_invoice | + +### Домен: Клиенты и лояльность (21 таблица) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `users` | Клиенты | referral_id→users | +| `users_bonus` | Бонусы клиентов | user_id→users, check_id→sales | +| `users_bonus_levels` | Уровни бонусов | user_id→users | +| `users_events` | События клиентов | user_id→users | +| `users_phones` | Телефоны клиентов | user_id→users | +| `users_stop_list` | Стоп-лист | phone→users | +| `users_telegram` | Telegram клиентов | phone→users | +| `users_telegram_log` | Лог Telegram | phone→users | +| `users_telegram_message` | Сообщения Telegram | phone→users | +| `users_whatsapp_message` | Сообщения WhatsApp | phone→users | +| `users_auth_call_log` | Лог авторизации | phone→users | +| `users_message_management` | Управление сообщениями | user_id→users | +| `users_message_management_logs` | Лог сообщений | user_id→users | +| `user_bonus_send_to_tg_logs` | Лог отправки бонусов | user_id→users | +| `user_reviews` | Отзывы клиентов | user_id→users, check_id→sales | +| `bonus_levels` | Уровни лояльности | - | +| `referral_status` | Статусы рефералов | user_id→users, referrer_id→users | +| `sent_kogort` | Когорты рассылок | phone→users | +| `kogort_stop_list` | Стоп-лист когорт | - | +| `promocode` | Промокоды | parent_id→promocode | +| `phone_change_history` | История смены телефонов | user_id→users | + +### Домен: Магазины и локации (28 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `city_store` | Магазины | city_id→city, administrator_id→admin | +| `city_store_params` | Параметры магазинов | store_id→city_store, store_type→store_type | +| `city` | Города | - | +| `our_cities` | Наши города | - | +| `erp24.store_type` | Типы магазинов | - | +| `store_city_list` | Список городов | parent_id→store_city_list | +| `stores_type_list` | Список типов | - | +| `cluster` | Кусты | - | +| `cluster_admin` | Кустовые админы | cluster_id→cluster, admin_id→admin | +| `cluster_calendar_category_dict` | Справочник категорий | - | +| `store_plan` | Планы магазинов | store_id→city_store | +| `store_plan_increase_holidays` | Праздничные надбавки | store_plan_id→store_plan | +| `store_balance` | Балансы магазинов | store_id→city_store | +| `store_visitors` | Посетители | store_id→city_store | +| `store_dynamic` | Динамика магазинов | store_id→city_store | +| `store_products_fact` | Факт товаров | store_id→city_store, product_id→products_1c | +| `store_staffing` | Штатное расписание | store_id→city_store | +| `store_staffing_log` | Лог штатки | store_staffing_id→store_staffing | +| `store_planogram` | Планограммы | store_id→city_store | +| `store_planogram_colors_sort` | Сортировка цветов | store_planogram_id→store_planogram | +| `store_planogram_logi` | Лог планограмм | store_planogram_id→store_planogram | +| `store_guid_buh` | GUID бухгалтерии | store_id→city_store | +| `firms` | Фирмы | - | +| `firms_group_prefix` | Префиксы групп | - | +| `company` | Компании | - | +| `companies` | Компании (alt) | - | +| `company_stores` | Магазины компаний | company_id→company, store_id→city_store | +| `terminals` | Терминалы | store_id→city_store | + +### Домен: Заказы магазинов (15 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `store_orders` | Заказы магазинов | store_id→city_store, admin_id→admin | +| `store_orders_item` | Товары заказов | order_id→store_orders, product_id→products_1c | +| `store_orders_fields` | Поля заказов | - | +| `store_orders_fields_data` | Данные полей | order_id→store_orders, field_id→store_orders_fields | +| `store_orders_fields_data_logi` | Лог данных | - | +| `store_orders_fields_property` | Свойства полей | field_id→store_orders_fields | +| `store_orders_colors` | Цвета заказов | order_id→store_orders | +| `store_orders_prices` | Цены заказов | order_id→store_orders | +| `store_orders_statuses` | Статусы заказов | - | +| `store_order_status` | Статус заказа | order_id→store_orders | +| `store_order_status_log` | Лог статусов | order_id→store_orders | +| `order_store_sort` | Сортировка заказов | store_id→city_store | +| `orders_amo` | Заказы AmoCRM | store_id→city_store | +| `orders_status` | Статусы заказов | - | +| `payment_types` | Типы оплат | - | + +### Домен: Маркетплейсы (15 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `marketplace_orders` | Заказы МП | store_id→marketplace_store, status_id→marketplace_order_status_types | +| `marketplace_order_items` | Товары заказов | order_id→marketplace_orders, product_id→products_1c | +| `marketplace_order_status_types` | Типы статусов | - | +| `marketplace_order_status_history` | История статусов | order_id→marketplace_orders, status_id→marketplace_order_status_types | +| `marketplace_order_1c_statuses` | Статусы 1С | - | +| `marketplace_order_1c_statuses_relations` | Связи статусов | status_id→marketplace_order_status_types | +| `marketplace_order_delivery` | Доставка заказов | order_id→marketplace_orders | +| `marketplace_store` | МП-магазины | store_id→city_store | +| `marketplace_prices` | Цены МП | product_id→products_1c, marketplace_store_id→marketplace_store | +| `marketplace_prices_log` | Лог цен | marketplace_prices_id→marketplace_prices | +| `marketplace_priority` | Приоритеты | marketplace_store_id→marketplace_store | +| `marketplace_status` | Статусы МП | - | +| `marketplace_flowwow_emails` | Email Flowwow | - | + +### Домен: Задачи (20 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `task` | Задачи | created_by→admin, task_type_id→tasks_type, status→task_status | +| `task_users` | Исполнители | task_id→task, admin_id→admin | +| `task_logs` | Логи задач | task_id→task | +| `task_viewers` | Наблюдатели | task_id→task, admin_id→admin | +| `task_status` | Статусы задач | - | +| `tasks_type` | Типы задач | - | +| `task_templates` | Шаблоны задач | parent_id→task_templates, company_function_id→company_functions | +| `task_trigger_conditions` | Условия триггеров | task_template_id→task_templates | +| `task_trigger_time_conditions` | Временные условия | task_template_id→task_templates | +| `task_entity` | Сущности задач | - | +| `task_alert_level` | Уровни алертов | - | +| `task_alert_level_data` | Данные алертов | alert_level_id→task_alert_level | +| `task_alert_log` | Лог алертов | task_id→task | +| `task_motivation` | Мотивация | task_id→task, motivation_id→motivation | +| `task_receiver_type` | Типы получателей | - | +| `templates_receiver_type` | Типы получателей шаблонов | - | +| `technical_request_type` | Типы технических заявок | - | +| `messager` | Сообщения | task_id→task | +| `messager_accepted` | Принятые сообщения | messager_id→messager | +| `messager_user` | Пользователи мессенджера | admin_id→admin | + +### Домен: Обучение (14 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `lessons` | Уроки | group_id→lessons_group, created_by→admin | +| `lessons_group` | Группы уроков | parent_id→lessons_group | +| `lessons_passed` | Пройденные уроки | lesson_id→lessons, admin_id→admin | +| `lessons_poll` | Опросы уроков | lesson_id→lessons | +| `lesson_poll_answers` | Ответы на опросы | poll_id→lessons_poll, admin_id→admin | +| `regulations` | Регламенты | group_id→regulation_group, created_by→admin | +| `regulation_group` | Группы регламентов | - | +| `regulations_passed` | Пройденные регламенты | regulation_id→regulations, admin_id→admin | +| `regulations_poll` | Опросы регламентов | regulation_id→regulations | +| `regulations_poll_answers` | Ответы на опросы | poll_id→regulations_poll, admin_id→admin | +| `function_regulations` | Регламенты функций | function_id→company_functions | +| `wiki_article` | Статьи Wiki | category_id→wiki_category, author_id→admin | +| `wiki_category` | Категории Wiki | parent_id→wiki_category | +| `company_functions` | Функции компании | - | + +### Домен: Списания и накладные (12 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `write_offs` | Списания (старые) | store_id→city_store | +| `write_offs_erp` | Списания ERP | store_id→city_store, created_admin_id→admin | +| `write_offs_products` | Товары списаний | write_off_id→write_offs, product_id→products_1c | +| `write_offs_products_erp` | Товары списаний ERP | write_offs_erp_id→write_offs_erp, product_id→products_1c | +| `write_offs_erp_cause_dict` | Причины списаний | - | +| `waybill_incoming` | Входящие накладные | store_id→city_store | +| `waybill_incoming_products` | Товары накладных | waybill_incoming_id→waybill_incoming, product_id→products_1c | +| `waybill_write_offs` | Накладные списаний | store_id→city_store | +| `waybill_write_offs_products` | Товары накладных | waybill_write_offs_id→waybill_write_offs, product_id→products_1c | + +### Домен: Мотивация (7 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `motivation` | Мотивация | - | +| `motivation_value` | Значения мотивации | motivation_id→motivation | +| `motivation_value_group` | Группы значений | - | +| `motivation_buh` | Мотивация бухгалтерия | - | +| `motivation_buh_value` | Значения бухгалтерии | - | +| `motivation_costs_items` | Статьи расходов | - | +| `quality_rating` | Рейтинги качества | admin_id→admin | + +### Домен: RBAC и авторизация (8 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `auth_item` | Роли и права | rule_name→auth_rule | +| `auth_item_child` | Иерархия ролей | parent→auth_item, child→auth_item | +| `auth_assignment` | Назначение ролей | item_name→auth_item, user_id→admin | +| `auth_rule` | Правила авторизации | - | +| `crm_menu` | Меню CRM | parent_id→crm_menu | +| `crm_menu_permission` | Права меню | menu_id→crm_menu | +| `company_function_admins` | Админы функций | company_function_id→company_functions, admin_id→admin | + +### Домен: Аналитика RNP (3 таблицы) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `rnp_index` | Индексы RNP | - | +| `rnp_alias` | Алиасы RNP | - | +| `rnp_data` | Данные RNP | index_id→rnp_index, alias_id→rnp_alias | + +### Домен: Отчёты и дашборды (13 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `dashboard` | Дашборды | - | +| `dashboard_fields` | Поля дашбордов | - | +| `dashboard_fields_links` | Связи полей | dashboard_id→dashboard, field_id→dashboard_fields | +| `dashboard_fields_property` | Свойства полей | field_id→dashboard_fields | +| `dashboard_sales` | Продажи дашбордов | store_id→city_store | +| `report` | Отчёты | created_by→admin | +| `reports` | Отчёты (alt) | - | +| `reports_fields` | Поля отчётов | report_id→reports | +| `reports_groups` | Группы отчётов | parent_id→reports_groups | +| `rate_category_admin_group` | Категории рейтингов | admin_group_id→admin_group | +| `rate_dict` | Справочник рейтингов | - | +| `rate_store_category` | Категории магазинов | store_id→city_store | +| `analysts_business_operations` | Бизнес-операции | type_id→analysts_business_operations_types | + +### Домен: Уведомления и коммуникации (11 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `notification` | Уведомления | admin_id→admin | +| `notification_status` | Статусы уведомлений | notification_id→notification | +| `notifiable_user` | Уведомляемые | admin_id→admin | +| `news_letter_delivery_status` | Статусы рассылок | - | +| `communication_type` | Типы коммуникаций | - | +| `tg_subscription` | Подписки Telegram | admin_id→admin | +| `alert_receiver_type` | Типы получателей | - | +| `chatbot_action` | Действия чатбота | - | +| `track_event` | События трекинга | admin_id→admin | +| `meeting` | Встречи | created_by→admin | +| `meeting_admin_link` | Связь встреч с админами | meeting_id→meeting, admin_id→admin | + +### Домен: Системные и логи (20 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `api_logs` | Логи API | - | +| `api_error_log` | Логи ошибок API | - | +| `api_integration_logs` | Логи интеграций | - | +| `api_cron` | CRON задачи | - | +| `api_cron_buh` | CRON бухгалтерия | - | +| `api_cron_test` | Тестовый CRON | - | +| `error_log` | Логи ошибок | - | +| `error_info_erp` | Информация об ошибках | - | +| `info_log` | Информационные логи | - | +| `info_items_table_shop_0` | Информация о товарах | - | +| `script_launcher_log` | Логи скриптов | - | +| `scheduler_task` | Задачи планировщика | - | +| `scheduler_task_counter` | Счётчик задач | - | +| `scheduler_task_log` | Лог планировщика | scheduler_task_id→scheduler_task | +| `page_statistics` | Статистика страниц | - | +| `quality_rating_log` | Лог рейтингов | rating_id→quality_rating | + +### Домен: Интеграции и справочники (15 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `export_import` | Экспорт/импорт | - | +| `export_import_table` | Таблица экспорта | entity_id→(полиморфная) | +| `export_import_integrations` | Интеграции | - | +| `files` | Файлы | entity_id→(полиморфная) | +| `images` | Изображения | - | +| `image_document_link` | Связь изображений | image_id→images | +| `comment` | Комментарии | entity_id→(полиморфная), admin_id→admin | +| `universal_catalog` | Универсальные каталоги | - | +| `universal_catalog_item` | Элементы каталогов | catalog_id→universal_catalog | +| `modules_uni_fields` | Поля модулей | - | +| `entity_type` | Типы сущностей | - | +| `contest001` | Конкурсы | - | + +### Домен: KIK Фидбек (5 таблиц) + +| Таблица | Описание | FK связи | +|---------|----------|----------| +| `kik_feedback_request` | Запросы фидбека | category→kik_feedback_category, source→kik_feedback_source | +| `kik_feedback_category` | Категории | - | +| `kik_feedback_subcategory` | Подкатегории | category_id→kik_feedback_category | +| `kik_feedback_source` | Источники | - | +| `kik_feedback_verdict` | Вердикты | - | + +--- + +## Рекомендации + +### 1. Недостающие FK constraints + +Многие связи определены только на уровне приложения (Yii2 ActiveRecord). Рекомендуется добавить явные FK в БД для: + +- `sales.admin_id` → `admin.id` +- `sales.store_id` → `city_store.id` +- `task.created_by` → `admin.id` +- `task.task_type_id` → `tasks_type.id` +- `admin.group_id` → `admin_group.id` +- `admin.store_id` → `city_store.id` + +### 2. Индексы для FK + +Проверить наличие индексов на всех FK полях: + +```sql +-- Проверка индексов +SELECT + tablename, + indexname, + indexdef +FROM pg_indexes +WHERE indexdef LIKE '%admin_id%' + OR indexdef LIKE '%store_id%' + OR indexdef LIKE '%product_id%'; +``` + +### 3. Нормализация полиморфных связей + +Рассмотреть создание отдельных junction-таблиц вместо полиморфных связей для: +- `files` → отдельные `task_files`, `lesson_files` и т.д. +- `comment` → отдельные `task_comments`, `order_comments` и т.д. + +--- + +## Связанные документы + +- [SCHEMA.md](./SCHEMA.md) - Полная схема БД с описанием таблиц +- [TABLES.md](./TABLES.md) - Детальный справочник таблиц +- [../models/README.md](../models/README.md) - Документация ActiveRecord моделей +- [../models/ERD_DIAGRAMS.md](../models/ERD_DIAGRAMS.md) - ERD диаграммы по доменам + +--- + +**Версия:** 1.1 +**Дата:** 2025-12-12 + +**Источники данных:** + +- PHP модели (erp24/records/*.php) — 305 таблиц +- Миграции (erp24/migrations/*.php) — 283 миграции +- pg_dump схемы БД — 309 таблиц diff --git a/erp24/docs/database/SCHEMA.md b/erp24/docs/database/SCHEMA.md index 5e87551b..589a8547 100644 --- a/erp24/docs/database/SCHEMA.md +++ b/erp24/docs/database/SCHEMA.md @@ -1,5 +1,59 @@ # Полная схема базы данных ERP24 +## Mindmap: Схема базы данных + +```mermaid +mindmap + root((SCHEMA ERP24)) + PostgreSQL 15.6 + Схема erp24 + 307 таблиц + Схема public + 2 таблицы + UTF-8 + ENUM типы 28шт + Статусы + admin_active + messanger_status + notification_type + Операции + create_checks_type + money_types_type_pay + products_logi_action + Типы полей + orders_fields_type + products_fields_type + api_fields_type + Персонал + admin_pol + admin_tip_ustroen + admin_vcompany + Архитектура + GUID из 1С + VARCHAR 36 + Таблицы sales products_1c + Soft delete + active флаг + deleted_at timestamp + JSONB поля + payments + store_arr + config_json + История + _history таблицы + _log таблицы + Домены 12 групп + HR 24 таблицы + Продажи 11 таблиц + Товары 15 таблиц + Магазины 27 таблиц + Клиенты 13 таблиц + Маркетплейсы 13 таблиц + Задачи 15 таблиц + Обучение 10 таблиц + Системные 15+ таблиц +``` + ## Метаинформация **Система управления БД:** PostgreSQL 15.6 (Debian 15.6-0+deb12u1) diff --git a/erp24/docs/database/TABLES.md b/erp24/docs/database/TABLES.md index b7d8c366..3bfd4718 100644 --- a/erp24/docs/database/TABLES.md +++ b/erp24/docs/database/TABLES.md @@ -4,6 +4,117 @@ **СУБД:** PostgreSQL 15.6 (Debian 15.6-0+deb12u1) **Дата актуализации:** 2025-12-11 (на основе актуального pg_dump) +## Mindmap: Структура таблиц + +```mermaid +mindmap + root((309 ТАБЛИЦ)) + HR и персонал + admin + центральная + BIGINT PK + guid синхр 1С + admin_group + должности + admin_payroll + зарплата + admin_payroll_days + admin_payroll_values + grade + грейды + employee_position + employee_skill + Продажи + sales + чеки GUID PK + синхр 1С + sales_products + товары в чеке + sales_history + аудит + create_checks + черновики + check_conduct + Товары + products_1c + GUID PK + номенклатура 1С + products_class + категории + prices + цены + prices_dynamic + balances + остатки + Магазины + city_store + центральная + city + города + cluster + кластеры + store_plan + store_staffing + store_visitors + Клиенты + users + центральная + users_bonus + транзакции + users_events + памятные даты + users_telegram + promocode + Заказы + store_orders + заказы магазинов + marketplace_orders + маркетплейсы + marketplace_order_items + marketplace_prices + Задачи + task + центральная + task_users + исполнители + task_logs + история + task_templates + task_motivation + Расписание + timetable + табель + timetable_shift + смены + employee_on_shift + shift_transfer + holiday + Списания + write_offs_erp + write_offs_products_erp + waybill_incoming + waybill_write_offs + incoming + Обучение + lessons + уроки + lessons_group + lessons_passed + regulations + регламенты + wiki_article + Системные + api_logs + api_error_log + auth_assignment + RBAC + auth_item + роли + migration + Yii2 + scheduler_task +``` + --- ## Оглавление diff --git a/erp24/docs/database/schema-overview.md b/erp24/docs/database/schema-overview.md index 988084a2..b2aaab64 100644 --- a/erp24/docs/database/schema-overview.md +++ b/erp24/docs/database/schema-overview.md @@ -2,6 +2,62 @@ > **Comprehensive database documentation for ERP24 PostgreSQL schema** +## Mindmap: Schema Overview + +```mermaid +mindmap + root((ERP24 Schema)) + PostgreSQL 12+ + Schema erp24 + Schema public + 389+ tables + 278 migrations + Primary Keys + Integer auto-increment + admin + city_store + users + UUID GUID + sales + products_1c + balances + Domains + Admin HR 80+ + Employee management + Payroll + Timetable + Sales Orders 60+ + Sales checks + Marketplace + Products + Customers 40+ + Users + Bonus program + Events + Inventory 35+ + Products + Stores + Write-offs + Tasks 25+ + Task management + Templates + Analytics 20+ + Dashboard + Reports + Metrics + Training 15+ + Lessons + Regulations + System 30+ + Auth RBAC + Logs + Config + Soft References + Application layer FK + No DB constraints + Flexible structure +``` + ## Table of Contents - [Overview](#overview) diff --git a/erp24/docs/models/AdminBonusConversion.md b/erp24/docs/models/AdminBonusConversion.md new file mode 100644 index 00000000..dc847bc5 --- /dev/null +++ b/erp24/docs/models/AdminBonusConversion.md @@ -0,0 +1,236 @@ +# Модель AdminBonusConversion + + +## Mindmap + +```mermaid +mindmap + root((AdminBonusConversion)) + Таблица БД + admin_bonus_conversion + Свойства + id + int + date + string + year + int + month + int + base + int + cost + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminBonusConversion` представляет справочник коэффициентов конвертации бонусов сотрудников. Хранит помесячные данные о базовой сумме и стоимости для расчёта конвертации бонусных баллов в денежный эквивалент. Используется в системе мотивации при расчёте заработной платы. + +**Файл модели:** `erp24/records/AdminBonusConversion.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_bonus_conversion` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `date` | VARCHAR(100) | Строковое представление даты периода (уникальное) | +| `year` | INTEGER | Год периода | +| `month` | INTEGER | Месяц периода (1-12) | +| `base` | INTEGER | Базовая сумма для расчёта | +| `cost` | INTEGER | Стоимость конвертации | +| `date_time` | TIMESTAMP | Дата и время создания/обновления записи | + +--- + +## Описание полей + +### `date` — Строковый идентификатор периода + +Уникальный строковый идентификатор периода. Обычно представлен в формате "YYYY-MM" или аналогичном. + +**Особенности:** +- Поле уникально (unique constraint) +- Используется для быстрого поиска нужного периода + +**Примеры:** `"2025-01"`, `"2025-12"` + +### `year` — Год периода + +Год, к которому относится данный коэффициент конвертации. + +**Примеры:** `2024`, `2025` + +### `month` — Месяц периода + +Номер месяца от 1 до 12. + +**Примеры:** `1` (январь), `12` (декабрь) + +### `base` — Базовая сумма + +Базовое значение для расчёта конвертации бонусов. Определяет "точку отсчёта" для пересчёта. + +### `cost` — Стоимость конвертации + +Стоимость одного бонусного балла в рублях или других единицах. + +--- + +## Методы модели + +### Геттеры и сеттеры + +Модель предоставляет полный набор геттеров и сеттеров для всех полей: + +| Метод | Описание | +|-------|----------| +| `getId(): int` | Возвращает идентификатор записи | +| `getDate(): string` | Возвращает строковую дату периода | +| `setDate(string $date): void` | Устанавливает строковую дату | +| `getYear(): int` | Возвращает год | +| `setYear(int $year): void` | Устанавливает год | +| `getMonth(): int` | Возвращает месяц | +| `setMonth(int $month): void` | Устанавливает месяц | +| `getBase(): int` | Возвращает базовую сумму | +| `setBase(int $base): void` | Устанавливает базовую сумму | +| `getCost(): int` | Возвращает стоимость конвертации | +| `setCost(int $cost): void` | Устанавливает стоимость | +| `getDateTime(): string` | Возвращает дату-время записи | +| `setDateTime(string $date_time): void` | Устанавливает дату-время | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_bonus_conversion { + int id PK + string date UK + int year + int month + int base + int cost + timestamp date_time + } + + admin_bonus_conversion ||--o{ admin_payroll : "used_for_calculation" + + admin_payroll { + int id PK + int admin_id FK + float summ + int year + int month + } +``` + +--- + +## Примеры использования + +### Получение коэффициента конвертации за период + +```php +$conversion = AdminBonusConversion::findOne([ + 'year' => 2025, + 'month' => 1 +]); + +if ($conversion) { + echo "Базовая сумма: {$conversion->getBase()}"; + echo "Стоимость: {$conversion->getCost()}"; +} +``` + +### Получение по строковой дате + +```php +$conversion = AdminBonusConversion::findOne(['date' => '2025-01']); +``` + +### Создание нового коэффициента + +```php +$conversion = new AdminBonusConversion(); +$conversion->setDate('2025-02'); +$conversion->setYear(2025); +$conversion->setMonth(2); +$conversion->setBase(1000); +$conversion->setCost(50); +$conversion->setDateTime(date('Y-m-d H:i:s')); +$conversion->save(); +``` + +### Получение всех коэффициентов за год + +```php +$conversions = AdminBonusConversion::find() + ->where(['year' => 2025]) + ->orderBy(['month' => SORT_ASC]) + ->all(); + +foreach ($conversions as $conv) { + echo "Месяц {$conv->getMonth()}: base={$conv->getBase()}, cost={$conv->getCost()}\n"; +} +``` + +### Расчёт конвертации бонусов + +```php +$bonusPoints = 150; +$conversion = AdminBonusConversion::findOne([ + 'year' => date('Y'), + 'month' => date('n') +]); + +if ($conversion) { + $moneyValue = $bonusPoints * $conversion->getCost(); + echo "Бонусы {$bonusPoints} = {$moneyValue} руб."; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `date` | Обязательное, макс. 100 символов, уникальное | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `base` | Обязательное, целое число | +| `cost` | Обязательное, целое число | +| `date_time` | Обязательное | + +--- + +## Связанные модели + +- **[AdminPayroll](./AdminPayroll.md)** — расчёт заработной платы (использует коэффициенты) +- **[Motivation](./Motivation.md)** — система мотивации +- **[Admin](./Admin.md)** — сотрудники + +--- + +## Бизнес-логика + +Коэффициенты конвертации используются для: +1. Пересчёта накопленных бонусных баллов в денежный эквивалент +2. Расчёта премиальной части заработной платы +3. Формирования отчётов по системе мотивации + +Коэффициенты устанавливаются ежемесячно и могут изменяться в зависимости от финансовых показателей компании. + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminChats.md b/erp24/docs/models/AdminChats.md new file mode 100644 index 00000000..ed847d88 --- /dev/null +++ b/erp24/docs/models/AdminChats.md @@ -0,0 +1,200 @@ +# Модель AdminChats + + +## Mindmap + +```mermaid +mindmap + root((AdminChats)) + Таблица БД + admin_chats + Свойства + id + int + chat_id + int + admin_id + int + state + int + last_message_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminChats` представляет связь между чатами мессенджера и сотрудниками системы. Хранит информацию о привязке Telegram-чатов к аккаунтам сотрудников, состоянии чата и последнем прочитанном сообщении. Используется для управления коммуникациями и отслеживания активности в чатах. + +**Файл модели:** `erp24/records/AdminChats.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_chats` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `chat_id` | INTEGER | Идентификатор чата в мессенджере | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `state` | INTEGER | Состояние чата | +| `last_message_id` | INTEGER | ID последнего прочитанного сообщения | + +--- + +## Описание полей + +### `chat_id` — Идентификатор чата + +Уникальный идентификатор чата в Telegram или другом мессенджере. Может быть как положительным (личный чат), так и отрицательным (групповой чат). + +**Особенности:** +- Обязательное поле +- Для Telegram: положительный ID — личный чат, отрицательный — группа + +### `admin_id` — Сотрудник + +ID сотрудника, которому принадлежит данный чат. + +**Связь:** `admin_chats.admin_id` → `admins.id` + +### `state` — Состояние чата + +Числовое значение, определяющее текущее состояние взаимодействия с чатом (например, активен, неактивен, заблокирован). + +**Примеры значений:** +- `0` — неактивен +- `1` — активен +- `2` — заблокирован + +### `last_message_id` — Последнее сообщение + +ID последнего прочитанного или обработанного сообщения. Используется для отслеживания новых сообщений. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_chats }o--|| admins : "belongs_to" + admin_chats ||--o{ messages : "tracks" + + admin_chats { + int id PK + int chat_id + int admin_id FK + int state + int last_message_id + } + + admins { + int id PK + string name + string login + } +``` + +--- + +## Примеры использования + +### Получение чата сотрудника + +```php +$adminChat = AdminChats::findOne(['admin_id' => $adminId]); +if ($adminChat) { + echo "Chat ID: {$adminChat->chat_id}"; + echo "State: {$adminChat->state}"; +} +``` + +### Поиск по идентификатору чата + +```php +$adminChat = AdminChats::findOne(['chat_id' => $chatId]); +if ($adminChat) { + $admin = Admin::findOne($adminChat->admin_id); + echo "Чат принадлежит: {$admin->name}"; +} +``` + +### Создание привязки чата к сотруднику + +```php +$adminChat = new AdminChats(); +$adminChat->chat_id = $telegramChatId; +$adminChat->admin_id = $employeeId; +$adminChat->state = 1; // активен +$adminChat->save(); +``` + +### Обновление последнего сообщения + +```php +$adminChat = AdminChats::findOne(['chat_id' => $chatId]); +if ($adminChat) { + $adminChat->last_message_id = $newMessageId; + $adminChat->save(); +} +``` + +### Получение активных чатов + +```php +$activeChats = AdminChats::find() + ->where(['state' => 1]) + ->all(); + +foreach ($activeChats as $chat) { + echo "Admin {$chat->admin_id}: chat {$chat->chat_id}\n"; +} +``` + +### Проверка новых сообщений + +```php +$adminChat = AdminChats::findOne(['admin_id' => $adminId]); +if ($adminChat && $currentMessageId > $adminChat->last_message_id) { + $unreadCount = $currentMessageId - $adminChat->last_message_id; + echo "Непрочитанных сообщений: {$unreadCount}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `chat_id` | Обязательное, число | +| `state` | Обязательное, число | +| `admin_id` | Число (необязательное) | +| `last_message_id` | Число (необязательное) | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[UsersTelegram](./UsersTelegram.md)** — Telegram-аккаунты пользователей +- **[Messager](./Messager.md)** — система сообщений + +--- + +## Бизнес-логика + +Модель используется для: +1. Привязки Telegram-чатов к сотрудникам системы +2. Отслеживания состояния коммуникации (активен/неактивен) +3. Определения последнего прочитанного сообщения для показа уведомлений +4. Управления ботами и автоматизированными рассылками + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminCheckin.md b/erp24/docs/models/AdminCheckin.md new file mode 100644 index 00000000..aee71011 --- /dev/null +++ b/erp24/docs/models/AdminCheckin.md @@ -0,0 +1,373 @@ +# Модель AdminCheckin + + +## Mindmap + +```mermaid +mindmap + root((AdminCheckin)) + Таблица БД + admin_checkin + Свойства + id + int + admin_id + int + type_id + int + date + string + time + string + store_id + int + Связи + Position + 1:1 AdminGroup + User + 1:1 Admin + Store + 1:1 CityStore + Plan + 1:1 TimetablePlan + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `AdminCheckin` представляет записи отметок сотрудников о начале и окончании работы (чекины). Хранит информацию о времени прихода/ухода, геолокации, фото подтверждения, оценке состояния и привязке к магазину и смене. Используется для контроля рабочего времени и табелирования. + +**Файл модели:** `erp24/records/AdminCheckin.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_checkin` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `type_id` | INTEGER | Тип чекина (1-открытие, 2-закрытие, 3-появление) | +| `date` | DATE | Дата чекина | +| `time` | TIMESTAMP | Время чекина | +| `store_id` | INTEGER | ID магазина (FK → city_store.id) | +| `ball` | INTEGER | Оценка состояния (1-5) | +| `comment` | TEXT | Комментарий к чекину | +| `photo` | TEXT | URL фотографии подтверждения | +| `d_id` | INTEGER | ID должности (FK → admin_group.id) | +| `replaced_admin_id` | INTEGER | ID замещаемого сотрудника | +| `plan_id` | INTEGER | ID планового расписания (FK → timetable_plan.id) | +| `device_id` | INTEGER | ID устройства | +| `lat` | FLOAT | Широта (геолокация) | +| `lon` | FLOAT | Долгота (геолокация) | +| `status` | INTEGER | Статус верификации (0-ожидает, 1-подтверждён) | + +--- + +## Константы + +### Типы чекинов (TYPE_*) + +```php +const TYPE_START = 1; // Открытие смены +const TYPE_END = 2; // Закрытие смены +const TYPE_APPEAR = 3; // Появление (промежуточная отметка) +``` + +### Статусы верификации (STATUS_*) + +```php +const STATUS_PENDING = 0; // Ожидает проверки +const STATUS_VERIFIED = 1; // Подтверждён +``` + +--- + +## Описание полей + +### `type_id` — Тип чекина + +Определяет тип отметки сотрудника: +- `1` (TYPE_START) — Открытие смены (приход на работу) +- `2` (TYPE_END) — Закрытие смены (уход с работы) +- `3` (TYPE_APPEAR) — Промежуточное появление + +### `ball` — Оценка состояния + +Самооценка состояния сотрудника по шкале от 1 до 5. Используется для мониторинга well-being персонала. + +### `lat` / `lon` — Геолокация + +Координаты GPS в момент чекина. Позволяет проверить, находился ли сотрудник в магазине. + +### `photo` — Фото подтверждение + +URL фотографии, сделанной при чекине. Дополнительное подтверждение присутствия. + +### `replaced_admin_id` — Замещение + +ID сотрудника, которого замещает текущий работник (при работе за другого). + +--- + +## Методы модели + +### `isStart(): bool` + +Проверяет, является ли чекин открытием смены. + +```php +if ($checkin->isStart()) { + echo "Это открытие смены"; +} +``` + +### `isEnd(): bool` + +Проверяет, является ли чекин закрытием смены. + +```php +if ($checkin->isEnd()) { + echo "Это закрытие смены"; +} +``` + +### `checkinType(): array` + +Возвращает массив типов чекинов с названиями. + +```php +$types = $checkin->checkinType(); +// ['1' => 'Открытие смены', '2' => 'Закрытие смены', '3' => 'Появление'] +``` + +### `getStatus($code): ?string` + +Статический метод для получения названия типа по коду. + +```php +$typeName = AdminCheckin::getStatus(1); // "Открытие смены" +``` + +### `getRating(): mixed` + +Возвращает текстовое описание оценки состояния. + +```php +$rating = $checkin->getRating(); +``` + +### `dateTime(): DateTime` + +Возвращает объект DateTime для времени чекина. + +```php +$dt = $checkin->dateTime(); +echo $dt->format('H:i:s'); +``` + +--- + +## Связи (Relations) + +### `getPosition(): ActiveQuery` + +Возвращает должность сотрудника на момент чекина. + +```php +$checkin = AdminCheckin::findOne($id); +$position = $checkin->position; // AdminGroup +echo "Должность: {$position->name}"; +``` + +### `getUser(): ActiveQuery` + +Возвращает сотрудника, сделавшего чекин. + +```php +$admin = $checkin->user; // Admin +echo "Сотрудник: {$admin->name}"; +``` + +### `getStore(): ActiveQuery` + +Возвращает магазин, в котором сделан чекин. + +```php +$store = $checkin->store; // CityStore +echo "Магазин: {$store->name}"; +``` + +### `getPlan(): ActiveQuery` + +Возвращает плановое расписание. + +```php +$plan = $checkin->plan; // TimetablePlan +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_checkin }o--|| admins : "made_by" + admin_checkin }o--|| city_store : "at_store" + admin_checkin }o--|| admin_group : "position" + admin_checkin }o--o| timetable_plan : "linked_to_plan" + admin_checkin }o--o| admins : "replaces" + + admin_checkin { + int id PK + int admin_id FK + int type_id + date date + timestamp time + int store_id FK + int ball + text comment + text photo + int d_id FK + int replaced_admin_id FK + int plan_id FK + int device_id + float lat + float lon + int status + } + + admins { + int id PK + string name + } + + city_store { + int id PK + string name + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание чекина открытия смены + +```php +$checkin = new AdminCheckin(); +$checkin->admin_id = $adminId; +$checkin->type_id = AdminCheckin::TYPE_START; +$checkin->date = date('Y-m-d'); +$checkin->time = date('Y-m-d H:i:s'); +$checkin->store_id = $storeId; +$checkin->device_id = $deviceId; +$checkin->lat = $latitude; +$checkin->lon = $longitude; +$checkin->ball = 4; +$checkin->status = AdminCheckin::STATUS_PENDING; +$checkin->save(); +``` + +### Получение чекинов сотрудника за день + +```php +$checkins = AdminCheckin::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['date' => '2025-01-15']) + ->orderBy(['time' => SORT_ASC]) + ->all(); + +foreach ($checkins as $c) { + echo "{$c->checkinType()[$c->type_id]}: {$c->time}\n"; +} +``` + +### Расчёт отработанного времени + +```php +$start = AdminCheckin::find() + ->where(['admin_id' => $adminId, 'date' => $date, 'type_id' => AdminCheckin::TYPE_START]) + ->one(); + +$end = AdminCheckin::find() + ->where(['admin_id' => $adminId, 'date' => $date, 'type_id' => AdminCheckin::TYPE_END]) + ->one(); + +if ($start && $end) { + $startTime = new DateTime($start->time); + $endTime = new DateTime($end->time); + $diff = $startTime->diff($endTime); + echo "Отработано: {$diff->h} ч. {$diff->i} мин."; +} +``` + +### Проверка геолокации + +```php +$checkin = AdminCheckin::findOne($id); +$store = $checkin->store; + +// Расчёт расстояния до магазина +$distance = calculateDistance( + $checkin->lat, $checkin->lon, + $store->lat, $store->lon +); + +if ($distance > 100) { // более 100 метров + echo "Чекин вне магазина! Расстояние: {$distance} м"; +} +``` + +### Статистика оценок состояния + +```php +$stats = AdminCheckin::find() + ->select(['ball', 'COUNT(*) as cnt']) + ->where(['type_id' => AdminCheckin::TYPE_START]) + ->andWhere(['>=', 'date', '2025-01-01']) + ->groupBy('ball') + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `type_id` | Обязательное, целое число | +| `status` | Обязательное, целое число | +| `date` | Обязательное, формат yyyy-M-d | +| `time` | Обязательное | +| `device_id` | Обязательное, целое число | +| `store_id` | Целое число | +| `ball` | Целое число | +| `lat`, `lon` | Числа с плавающей точкой | +| `comment`, `photo` | Строки | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[AdminGroup](./AdminGroup.md)** — должности +- **[TimetablePlan](./TimetablePlan.md)** — плановое расписание +- **[AdminDesktop](./AdminDesktop.md)** — устройства + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminDesktop.md b/erp24/docs/models/AdminDesktop.md new file mode 100644 index 00000000..99f33ad2 --- /dev/null +++ b/erp24/docs/models/AdminDesktop.md @@ -0,0 +1,318 @@ +# Модель AdminDesktop + + +## Mindmap + +```mermaid +mindmap + root((AdminDesktop)) + Таблица БД + admin_desktop + Свойства + id + int + name + int + store_id + int + date_add + int + type_id + string + device_info + string + Связи + Store + 1:1 CityStore + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `AdminDesktop` представляет привязанные устройства сотрудников (рабочие станции). Хранит информацию о компьютерах, планшетах и телефонах, с которых сотрудники авторизуются в системе. Используется для контроля доступа, безопасности и определения рабочего места. + +**Файл модели:** `erp24/records/AdminDesktop.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_desktop` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR | Название устройства | +| `store_id` | INTEGER | ID магазина привязки (FK → city_store.id) | +| `date_add` | TIMESTAMP | Дата добавления устройства | +| `type_id` | INTEGER | Тип устройства | +| `device_info` | TEXT | Информация об устройстве (User-Agent и др.) | +| `lasttime` | TIMESTAMP | Время последней активности | +| `keygen` | VARCHAR | Токен авторизации устройства | +| `ip` | VARCHAR | IP-адрес | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | + +--- + +## Константы + +### Типы устройств (DEVICE_TYPE_*) + +```php +const DEVICE_TYPE_STORE_PC = 1; // Стационарный PC в магазине +const DEVICE_TYPE_COMPANY_PHONE = 2; // Служебный смартфон +const DEVICE_TYPE_PERSONAL_PHONE = 4; // Личный смартфон +const DEVICE_TYPE_COMPANY_TABLET = 5; // Служебный планшет +const DEVICE_TYPE_HOME_PC = 6; // Домашний компьютер +const DEVICE_TYPE_COMPANY_LAPTOP = 7; // Служебный ноутбук +``` + +--- + +## Описание полей + +### `type_id` — Тип устройства + +Определяет категорию устройства: + +| Код | Константа | Описание | +|-----|-----------|----------| +| 1 | DEVICE_TYPE_STORE_PC | Стационарный PC в магазине | +| 2 | DEVICE_TYPE_COMPANY_PHONE | Служебный смартфон | +| 4 | DEVICE_TYPE_PERSONAL_PHONE | Личный смартфон | +| 5 | DEVICE_TYPE_COMPANY_TABLET | Мобильный планшет служебный | +| 6 | DEVICE_TYPE_HOME_PC | Домашний компьютер | +| 7 | DEVICE_TYPE_COMPANY_LAPTOP | Служебный ноутбук | + +### `keygen` — Токен авторизации + +Уникальный ключ для идентификации устройства. Генерируется при первом входе и используется для автоматической авторизации. + +### `store_id` — Привязка к магазину + +ID магазина, к которому привязано устройство. Для стационарных ПК и ноутбуков определяет рабочее место. + +### `device_info` — Информация об устройстве + +Техническая информация: User-Agent браузера, характеристики устройства, разрешение экрана и т.д. + +--- + +## Методы модели + +### `getByToken($token): ?AdminDesktop` + +Статический метод поиска устройства по токену авторизации. + +**Параметры:** +- `$token` (string) — Токен устройства + +**Возвращает:** `AdminDesktop|null` + +```php +$device = AdminDesktop::getByToken($authToken); +if ($device) { + echo "Устройство найдено: {$device->name}"; +} +``` + +### `deviceTypes(): array` + +Статический метод, возвращающий массив типов устройств с названиями. + +```php +$types = AdminDesktop::deviceTypes(); +// [1 => 'Стационарный PC в магазине', 2 => 'Служебный смартфон', ...] +``` + +### `getDeviceTypeName(): string` + +Возвращает текстовое название типа устройства. + +```php +$device = AdminDesktop::findOne($id); +echo $device->getDeviceTypeName(); // "Служебный смартфон" +``` + +### `isFixedStore(): bool` + +Проверяет, является ли устройство привязанным к фиксированному рабочему месту (PC или ноутбук в магазине). + +```php +if ($device->isFixedStore()) { + echo "Устройство с фиксированным рабочим местом"; +} +``` + +--- + +## Связи (Relations) + +### `getStore(): ActiveQuery` + +Возвращает магазин, к которому привязано устройство. + +```php +$device = AdminDesktop::findOne($id); +$store = $device->store; // CityStore +echo "Магазин: {$store->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_desktop }o--|| city_store : "located_at" + admin_desktop }o--|| admins : "belongs_to" + admin_desktop ||--o{ admin_checkin : "used_for" + + admin_desktop { + int id PK + string name + int store_id FK + timestamp date_add + int type_id + text device_info + timestamp lasttime + string keygen + string ip + int admin_id FK + } + + city_store { + int id PK + string name + } + + admins { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Регистрация нового устройства + +```php +$device = new AdminDesktop(); +$device->name = 'iPhone сотрудника'; +$device->type_id = AdminDesktop::DEVICE_TYPE_COMPANY_PHONE; +$device->admin_id = $adminId; +$device->store_id = $storeId; +$device->keygen = Yii::$app->security->generateRandomString(64); +$device->device_info = $_SERVER['HTTP_USER_AGENT']; +$device->ip = $_SERVER['REMOTE_ADDR']; +$device->date_add = date('Y-m-d H:i:s'); +$device->save(); +``` + +### Авторизация по токену + +```php +$token = Yii::$app->request->getHeaders()->get('X-Device-Token'); +$device = AdminDesktop::getByToken($token); + +if ($device) { + // Обновляем время последней активности + $device->lasttime = date('Y-m-d H:i:s'); + $device->ip = $_SERVER['REMOTE_ADDR']; + $device->save(); + + // Получаем сотрудника + $admin = Admin::findOne($device->admin_id); +} +``` + +### Получение всех устройств сотрудника + +```php +$devices = AdminDesktop::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['lasttime' => SORT_DESC]) + ->all(); + +foreach ($devices as $device) { + echo "{$device->getDeviceTypeName()}: последняя активность {$device->lasttime}\n"; +} +``` + +### Поиск стационарных устройств магазина + +```php +$storeDevices = AdminDesktop::find() + ->where(['store_id' => $storeId]) + ->andWhere(['type_id' => AdminDesktop::DEVICE_TYPE_STORE_PC]) + ->all(); +``` + +### Проверка типа устройства для ограничения функций + +```php +$device = AdminDesktop::getByToken($token); + +if ($device->isFixedStore()) { + // Разрешаем кассовые операции только с рабочих мест + $canProcessSales = true; +} else { + $canProcessSales = false; +} +``` + +### Отчёт по типам устройств + +```php +$stats = AdminDesktop::find() + ->select(['type_id', 'COUNT(*) as cnt']) + ->groupBy('type_id') + ->asArray() + ->all(); + +$types = AdminDesktop::deviceTypes(); +foreach ($stats as $row) { + echo "{$types[$row['type_id']]}: {$row['cnt']} устройств\n"; +} +``` + +--- + +## Валидация + +Правила валидации не определены явно в модели. Рекомендуемые ограничения: + +| Поле | Рекомендуемое правило | +|------|----------------------| +| `name` | Строка, макс. 255 символов | +| `type_id` | Целое число, одно из DEVICE_TYPE_* | +| `keygen` | Строка, уникальная | +| `store_id` | Целое число, существующий магазин | +| `admin_id` | Целое число, существующий сотрудник | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[AdminCheckin](./AdminCheckin.md)** — чекины сотрудников +- **[AdminDevice](./AdminDevice.md)** — дополнительная информация об устройствах + +--- + +## Безопасность + +1. **Токен (keygen)** должен быть криптографически стойким +2. **IP-адрес** используется для аудита и выявления подозрительной активности +3. **Тип устройства** влияет на доступные функции +4. **Фиксированные рабочие места** имеют расширенные права + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminDevice.md b/erp24/docs/models/AdminDevice.md new file mode 100644 index 00000000..9b4f7ccb --- /dev/null +++ b/erp24/docs/models/AdminDevice.md @@ -0,0 +1,254 @@ +# Модель AdminDevice + + +## Mindmap + +```mermaid +mindmap + root((AdminDevice)) + Таблица БД + admin_device + Свойства + id + int + name + int + is_mobile + int + admin_id + int + info + string + date_add + string + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `AdminDevice` представляет устройства, с которых сотрудники заходят в систему. Хранит расширенную техническую информацию об устройствах: мобильность, характеристики экрана, поддержка GPS, телефон. Используется для сбора статистики и аналитики по устройствам. + +**Файл модели:** `erp24/records/AdminDevice.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_device` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR | Название/модель устройства | +| `is_mobile` | INTEGER | Признак мобильного устройства (0/1) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `info` | TEXT | Техническая информация (User-Agent и др.) | +| `date_add` | TIMESTAMP | Дата первого входа с устройства | +| `date_end` | TIMESTAMP | Дата последнего использования | +| `ip` | VARCHAR | IP-адрес | +| `ip_arr` | TEXT | Массив используемых IP-адресов | +| `screen` | VARCHAR | Разрешение экрана | +| `phone` | VARCHAR | Номер телефона устройства | +| `gps` | INTEGER | Поддержка GPS (0/1) | +| `location_store_id` | INTEGER | ID магазина по геолокации | +| `status` | INTEGER | Статус устройства | + +--- + +## Описание полей + +### `is_mobile` — Мобильность + +Флаг, определяющий тип устройства: +- `0` — настольный компьютер +- `1` — мобильное устройство (телефон, планшет) + +### `info` — Техническая информация + +User-Agent строка и другие технические данные браузера/приложения. + +**Пример:** +``` +Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 +``` + +### `screen` — Разрешение экрана + +Размеры экрана устройства в пикселях. + +**Примеры:** `"1920x1080"`, `"390x844"` + +### `gps` — Поддержка GPS + +Флаг наличия GPS-модуля: +- `0` — GPS отсутствует или отключён +- `1` — GPS доступен + +### `ip_arr` — История IP-адресов + +Массив (JSON или сериализованный) всех IP-адресов, с которых использовалось устройство. + +### `location_store_id` — Определённый магазин + +ID магазина, определённый по геолокации устройства. Используется для автоматического определения рабочего места. + +### `status` — Статус + +Текущий статус устройства (активно, заблокировано и т.д.). + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_device }o--|| admins : "owned_by" + admin_device }o--o| city_store : "located_at" + + admin_device { + int id PK + string name + int is_mobile + int admin_id FK + text info + timestamp date_add + timestamp date_end + string ip + text ip_arr + string screen + string phone + int gps + int location_store_id FK + int status + } + + admins { + int id PK + string name + } + + city_store { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Регистрация устройства при входе + +```php +$device = new AdminDevice(); +$device->name = $this->detectDeviceName($_SERVER['HTTP_USER_AGENT']); +$device->is_mobile = $this->isMobileDevice() ? 1 : 0; +$device->admin_id = $adminId; +$device->info = $_SERVER['HTTP_USER_AGENT']; +$device->date_add = date('Y-m-d H:i:s'); +$device->ip = $_SERVER['REMOTE_ADDR']; +$device->screen = $screenResolution; +$device->gps = $hasGps ? 1 : 0; +$device->status = 1; +$device->save(); +``` + +### Получение устройств сотрудника + +```php +$devices = AdminDevice::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date_end' => SORT_DESC]) + ->all(); + +foreach ($devices as $device) { + $type = $device->is_mobile ? 'Мобильное' : 'Десктоп'; + echo "{$device->name} ({$type}): {$device->screen}\n"; +} +``` + +### Поиск мобильных устройств с GPS + +```php +$gpsDevices = AdminDevice::find() + ->where(['is_mobile' => 1, 'gps' => 1]) + ->all(); +``` + +### Статистика по устройствам + +```php +$stats = AdminDevice::find() + ->select([ + 'is_mobile', + 'COUNT(*) as cnt' + ]) + ->groupBy('is_mobile') + ->asArray() + ->all(); + +// Результат: [['is_mobile' => 0, 'cnt' => 150], ['is_mobile' => 1, 'cnt' => 280]] +``` + +### Обновление последней активности + +```php +$device = AdminDevice::findOne(['admin_id' => $adminId, 'name' => $deviceName]); +if ($device) { + $device->date_end = date('Y-m-d H:i:s'); + $device->ip = $_SERVER['REMOTE_ADDR']; + $device->save(); +} +``` + +### Определение магазина по устройству + +```php +$device = AdminDevice::findOne($deviceId); +if ($device->location_store_id) { + $store = CityStore::findOne($device->location_store_id); + echo "Устройство в магазине: {$store->name}"; +} +``` + +--- + +## Валидация + +Правила валидации не определены в модели. Рекомендуемые ограничения: + +| Поле | Рекомендуемое правило | +|------|----------------------| +| `name` | Строка, макс. 255 символов | +| `is_mobile` | 0 или 1 | +| `admin_id` | Целое число | +| `gps` | 0 или 1 | +| `status` | Целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[AdminDesktop](./AdminDesktop.md)** — авторизованные рабочие станции +- **[AdminCheckin](./AdminCheckin.md)** — чекины с устройств + +--- + +## Отличие от AdminDesktop + +| Характеристика | AdminDevice | AdminDesktop | +|----------------|-------------|--------------| +| Назначение | Техническая информация | Авторизация | +| Токен | Нет | Есть (keygen) | +| Детали устройства | Расширенные (screen, gps) | Базовые | +| Типизация | is_mobile (0/1) | type_id (7 типов) | +| Использование | Статистика | Безопасность | + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminDynamic.md b/erp24/docs/models/AdminDynamic.md new file mode 100644 index 00000000..d9873a88 --- /dev/null +++ b/erp24/docs/models/AdminDynamic.md @@ -0,0 +1,296 @@ +# Модель AdminDynamic + + +## Mindmap + +```mermaid +mindmap + root((AdminDynamic)) + Таблица БД + admin_dynamic + Свойства + id + int + admin_id + int + value_type + string + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminDynamic` представляет динамические атрибуты сотрудников с историей изменений. Хранит изменяющиеся во времени значения (должность, магазин, ставка и др.) с датами действия. Используется для отслеживания истории изменений параметров сотрудника и расчётов за прошлые периоды. + +**Файл модели:** `erp24/records/AdminDynamic.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_dynamic` +**Родительский класс:** `yii\db\ActiveRecord` +**Трейты:** `HistoryModelTrait` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `value_type` | VARCHAR(100) | Тип значения (int, string) | +| `value_int` | INTEGER | Целочисленное значение | +| `value_string` | VARCHAR(255) | Строковое значение | +| `date_from` | DATE | Дата начала действия | +| `date_to` | DATE | Дата окончания действия (NULL = действует) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `active` | INTEGER | Признак активности (0/1) | +| `category_id` | INTEGER | ID категории значения (FK) | + +--- + +## Описание полей + +### `value_type` — Тип значения + +Определяет тип хранимого значения: +- `"int"` — целочисленное (хранится в value_int) +- `"string"` — строковое (хранится в value_string) + +### `value_int` / `value_string` — Значение + +Фактическое значение атрибута. В зависимости от типа используется одно из полей: +- Для ID должности, магазина: value_int +- Для текстовых значений: value_string + +### `date_from` / `date_to` — Период действия + +Диапазон дат, в течение которого действует данное значение: +- `date_from` — начало периода +- `date_to` — конец периода (NULL означает "по настоящее время") + +### `category_id` — Категория + +ID категории из справочника AdminDynamicCategoryDict. Определяет, какой атрибут хранится: +- `1` — магазин (store_id) +- `2` — должность и т.д. + +--- + +## Методы модели + +### `getCategory(): ActiveQuery` + +Возвращает категорию значения. + +```php +$dynamic = AdminDynamic::findOne($id); +$category = $dynamic->category; // AdminDynamicCategoryDict +echo "Категория: {$category->alias}"; +``` + +### `getAdminGroup(): ActiveQuery` + +Возвращает должность, если value_int содержит ID должности. + +```php +$dynamic = AdminDynamic::findOne($id); +$group = $dynamic->adminGroup; // AdminGroup +``` + +### `setStoreId(int $storeId): object` + +Устанавливает значение магазина (категория 1). + +```php +$dynamic = new AdminDynamic(); +$dynamic->setAdminId($adminId); +$dynamic->setStoreId($storeId); +$dynamic->date_from = date('Y-m-d'); +$dynamic->save(); +``` + +### `getGroupByDate($employeeId, $dateFrom, $dateTo, $category = 1): ?int` + +Статический метод получения значения группы/должности за указанный период. + +**Параметры:** +- `$employeeId` (int) — ID сотрудника +- `$dateFrom` (string) — начало периода +- `$dateTo` (string) — конец периода +- `$category` (int) — категория значения (по умолчанию 1) + +**Возвращает:** ID группы/должности + +```php +$groupId = AdminDynamic::getGroupByDate($adminId, '2025-01-01', '2025-01-31'); +``` + +--- + +## Геттеры и сеттеры + +| Метод | Описание | +|-------|----------| +| `getId(): int` | Возвращает ID записи | +| `getAdminId(): int` | Возвращает ID сотрудника | +| `setAdminId(int): object` | Устанавливает ID сотрудника (fluent) | +| `getCategoryId(): ?int` | Возвращает ID категории | +| `setCategoryId(?int): object` | Устанавливает категорию (fluent) | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_dynamic }o--|| admins : "belongs_to" + admin_dynamic }o--|| admin_dynamic_category_dict : "has_category" + admin_dynamic }o--o| admin_group : "may_reference" + + admin_dynamic { + int id PK + int admin_id FK + string value_type + int value_int + string value_string + date date_from + date date_to + timestamp created_at + timestamp updated_at + int active + int category_id FK + } + + admins { + int id PK + string name + } + + admin_dynamic_category_dict { + int id PK + string alias + string value_type + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание записи о смене магазина + +```php +// Закрываем текущую запись +$current = AdminDynamic::find() + ->where(['admin_id' => $adminId, 'category_id' => 1, 'active' => 1]) + ->andWhere(['is', 'date_to', null]) + ->one(); + +if ($current) { + $current->date_to = date('Y-m-d', strtotime('-1 day')); + $current->save(); +} + +// Создаём новую запись +$new = new AdminDynamic(); +$new->setAdminId($adminId); +$new->setStoreId($newStoreId); +$new->date_from = date('Y-m-d'); +$new->active = 1; +$new->save(); +``` + +### Получение истории изменений + +```php +$history = AdminDynamic::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $record) { + $category = $record->category; + echo "{$record->date_from} - {$record->date_to}: "; + echo "{$category->alias} = {$record->value_int}\n"; +} +``` + +### Получение значения на конкретную дату + +```php +$value = AdminDynamic::find() + ->where(['admin_id' => $adminId, 'category_id' => $categoryId]) + ->andWhere(['<=', 'date_from', $targetDate]) + ->andWhere(['or', + ['>=', 'date_to', $targetDate], + ['is', 'date_to', null] + ]) + ->one(); +``` + +### Получение текущей должности сотрудника + +```php +$currentGroup = AdminDynamic::find() + ->where([ + 'admin_id' => $adminId, + 'category_id' => 2, // должность + 'active' => 1 + ]) + ->andWhere(['is', 'date_to', null]) + ->one(); + +if ($currentGroup) { + $group = $currentGroup->adminGroup; + echo "Текущая должность: {$group->name}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `value_type` | Обязательное, макс. 100 символов | +| `value_int` | Целое число | +| `value_string` | Строка, макс. 255 символов | +| `date_from` | safe | +| `date_to` | Строка, макс. 255 символов | +| `active` | Целое число | +| `category_id` | Целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[AdminDynamicCategoryDict](./AdminDynamicCategoryDict.md)** — справочник категорий +- **[AdminGroup](./AdminGroup.md)** — должности +- **[AdminGroupDynamic](./AdminGroupDynamic.md)** — динамика должностей + +--- + +## Бизнес-логика + +Модель реализует паттерн **temporal data** (версионирование по времени): +1. При изменении значения старая запись закрывается (date_to = вчера) +2. Создаётся новая запись с date_from = сегодня +3. Это позволяет получить значение на любую дату в прошлом +4. Используется для корректного расчёта зарплаты за периоды с изменениями + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminDynamicCategoryDict.md b/erp24/docs/models/AdminDynamicCategoryDict.md new file mode 100644 index 00000000..0cc1ed64 --- /dev/null +++ b/erp24/docs/models/AdminDynamicCategoryDict.md @@ -0,0 +1,203 @@ +# Модель AdminDynamicCategoryDict + + +## Mindmap + +```mermaid +mindmap + root((AdminDynamicCategoryDict)) + Таблица БД + admin_dynamic_category_dict + Свойства + id + int + name + int + alias + string + value_type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminDynamicCategoryDict` представляет справочник категорий динамических атрибутов сотрудников. Определяет типы данных, которые могут храниться в AdminDynamic: магазин, должность, ставка и другие изменяемые параметры. Используется для типизации и валидации динамических значений. + +**Файл модели:** `erp24/records/AdminDynamicCategoryDict.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_dynamic_category_dict` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `name` | INTEGER | Числовой код названия | +| `alias` | VARCHAR(255) | Текстовый алиас категории | +| `value_type` | VARCHAR(255) | Тип значения (int, string) | + +--- + +## Описание полей + +### `alias` — Алиас категории + +Уникальный текстовый идентификатор категории для программного доступа. + +**Примеры:** +- `"store"` — магазин +- `"position"` — должность +- `"rate"` — ставка + +### `value_type` — Тип значения + +Определяет, в каком поле AdminDynamic хранится значение: +- `"int"` — целочисленное (value_int) +- `"string"` — строковое (value_string) + +### `name` — Числовой код + +Целочисленный идентификатор для связи с внешними системами. + +--- + +## Методы модели + +### `getCategory($categoryAlias): ?object` + +Статический метод поиска категории по алиасу. + +**Параметры:** +- `$categoryAlias` (string) — Алиас категории + +**Возвращает:** Объект с полями id и value_type или null + +```php +$category = AdminDynamicCategoryDict::getCategory('store'); +if ($category) { + echo "ID: {$category->id}, Тип: {$category->value_type}"; +} +``` + +### Геттеры и сеттеры + +| Метод | Описание | +|-------|----------| +| `getId(): int` | Возвращает ID категории | +| `getValueType(): string` | Возвращает тип значения | +| `getName(): int` | Возвращает числовой код | +| `setName(int): void` | Устанавливает числовой код | +| `getAlias(): string` | Возвращает алиас | +| `setAlias(string): void` | Устанавливает алиас | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_dynamic_category_dict ||--o{ admin_dynamic : "categorizes" + + admin_dynamic_category_dict { + int id PK + int name + string alias UK + string value_type + } + + admin_dynamic { + int id PK + int admin_id FK + int category_id FK + string value_type + int value_int + string value_string + } +``` + +--- + +## Примеры использования + +### Получение категории по алиасу + +```php +$storeCategory = AdminDynamicCategoryDict::getCategory('store'); +if ($storeCategory) { + // Используем для создания AdminDynamic + $dynamic = new AdminDynamic(); + $dynamic->category_id = $storeCategory->getId(); + $dynamic->value_type = $storeCategory->getValueType(); +} +``` + +### Получение всех категорий + +```php +$categories = AdminDynamicCategoryDict::find()->all(); + +foreach ($categories as $cat) { + echo "{$cat->getAlias()}: тип {$cat->getValueType()}\n"; +} +``` + +### Проверка типа значения + +```php +$category = AdminDynamicCategoryDict::findOne($categoryId); +if ($category->getValueType() === 'int') { + $value = (int) $inputValue; +} else { + $value = (string) $inputValue; +} +``` + +### Создание новой категории + +```php +$category = new AdminDynamicCategoryDict(); +$category->id = 5; +$category->name = 5; +$category->alias = 'salary_rate'; +$category->value_type = 'int'; +$category->save(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `id` | Обязательное, целое число | +| `name` | Обязательное, целое число | +| `alias` | Обязательное, макс. 255 символов | +| `value_type` | Строка, макс. 255 символов | + +--- + +## Типичные категории + +| ID | Alias | Описание | Тип значения | +|----|-------|----------|--------------| +| 1 | store | Магазин сотрудника | int (store_id) | +| 2 | position | Должность | int (group_id) | +| 3 | rate | Ставка | int | +| 4 | cluster | Кластер | int (cluster_id) | + +--- + +## Связанные модели + +- **[AdminDynamic](./AdminDynamic.md)** — динамические атрибуты сотрудников +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGradeHistory.md b/erp24/docs/models/AdminGradeHistory.md new file mode 100644 index 00000000..667adc73 --- /dev/null +++ b/erp24/docs/models/AdminGradeHistory.md @@ -0,0 +1,263 @@ +# Модель AdminGradeHistory + + +## Mindmap + +```mermaid +mindmap + root((AdminGradeHistory)) + Таблица БД + admin_grade_history + Свойства + id + int + admin_id + int + created_at + string + created_by + int + closed_at + string + grade_id + int + Связи + Admin + 1:1 Admin + CreatedBy + 1:1 Admin + Grade + 1:1 Grade + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminGradeHistory` представляет историю грейдов (уровней квалификации) сотрудников. Хранит записи о присвоении и изменении грейдов с указанием периода действия и автора изменения. Используется для отслеживания карьерного роста и расчёта заработной платы на основе грейда. + +**Файл модели:** `erp24/records/AdminGradeHistory.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_grade_history` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `created_at` | TIMESTAMP | Дата создания записи (присвоения грейда) | +| `created_by` | INTEGER | ID сотрудника, создавшего запись | +| `closed_at` | TIMESTAMP | Дата окончания грейда (макс. 2100-01-01) | +| `grade_id` | INTEGER | ID грейда (FK → grade.id) | + +--- + +## Описание полей + +### `admin_id` — Сотрудник + +ID сотрудника, которому присвоен грейд. + +**Связь:** `admin_grade_history.admin_id` → `admins.id` + +### `grade_id` — Грейд + +ID уровня квалификации из справочника Grade. + +**Связь:** `admin_grade_history.grade_id` → `grade.id` + +### `created_at` — Дата присвоения + +Момент времени, когда сотруднику был присвоен данный грейд. + +### `closed_at` — Дата окончания + +Момент времени, когда данный грейд перестал действовать: +- `2100-01-01 00:00:00` — грейд ещё активен +- Конкретная дата — грейд закрыт, действует новый + +### `created_by` — Автор изменения + +ID сотрудника (обычно HR или руководитель), который присвоил грейд. + +--- + +## Связи (Relations) + +### `getAdmin(): ActiveQuery` + +Возвращает сотрудника, которому присвоен грейд. + +```php +$history = AdminGradeHistory::findOne($id); +$admin = $history->admin; // Admin +echo "Сотрудник: {$admin->name}"; +``` + +### `getCreatedBy(): ActiveQuery` + +Возвращает сотрудника, создавшего запись о грейде. + +```php +$author = $history->createdBy; // Admin +echo "Присвоил: {$author->name}"; +``` + +### `getGrade(): ActiveQuery` + +Возвращает объект грейда. + +```php +$grade = $history->grade; // Grade +echo "Грейд: {$grade->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_grade_history }o--|| admins : "assigned_to" + admin_grade_history }o--|| admins : "created_by" + admin_grade_history }o--|| grade : "has_grade" + + admin_grade_history { + int id PK + int admin_id FK + timestamp created_at + int created_by FK + timestamp closed_at + int grade_id FK + } + + admins { + int id PK + string name + } + + grade { + int id PK + string name + int level + } +``` + +--- + +## Примеры использования + +### Присвоение нового грейда + +```php +// Закрываем текущий грейд +$current = AdminGradeHistory::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['>=', 'closed_at', date('Y-m-d')]) + ->one(); + +if ($current) { + $current->closed_at = date('Y-m-d H:i:s'); + $current->save(); +} + +// Создаём новую запись +$new = new AdminGradeHistory(); +$new->admin_id = $adminId; +$new->grade_id = $newGradeId; +$new->created_at = date('Y-m-d H:i:s'); +$new->created_by = Yii::$app->user->id; +$new->closed_at = '2100-01-01 00:00:00'; +$new->save(); +``` + +### Получение текущего грейда сотрудника + +```php +$currentGrade = AdminGradeHistory::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['>=', 'closed_at', date('Y-m-d H:i:s')]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($currentGrade) { + $grade = $currentGrade->grade; + echo "Текущий грейд: {$grade->name}"; +} +``` + +### История грейдов сотрудника + +```php +$history = AdminGradeHistory::find() + ->with(['grade', 'createdBy']) + ->where(['admin_id' => $adminId]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +foreach ($history as $record) { + echo "{$record->created_at}: {$record->grade->name}"; + echo " (присвоил: {$record->createdBy->name})\n"; +} +``` + +### Получение грейда на конкретную дату + +```php +$targetDate = '2024-06-15'; +$gradeAtDate = AdminGradeHistory::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['<=', 'created_at', $targetDate]) + ->andWhere(['>=', 'closed_at', $targetDate]) + ->one(); +``` + +### Статистика по грейдам + +```php +$stats = AdminGradeHistory::find() + ->select(['grade_id', 'COUNT(DISTINCT admin_id) as cnt']) + ->where(['>=', 'closed_at', date('Y-m-d')]) + ->groupBy('grade_id') + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `created_at` | Обязательное | +| `created_by` | Обязательное, целое число | +| `closed_at` | Обязательное | +| `grade_id` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[Grade](./Grade.md)** — справочник грейдов +- **[GradePrice](./GradePrice.md)** — ставки по грейдам +- **[AdminPayroll](./AdminPayroll.md)** — расчёт зарплаты + +--- + +## Бизнес-логика + +1. **Грейд определяет базовую ставку** — от грейда зависит почасовая оплата +2. **История для корректных расчётов** — при расчёте зарплаты за прошлые периоды используется грейд, действовавший в тот момент +3. **Аудит изменений** — фиксируется кто и когда присвоил грейд +4. **Паттерн temporal data** — закрытие старой записи + создание новой + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroup.md b/erp24/docs/models/AdminGroup.md new file mode 100644 index 00000000..9cad979e --- /dev/null +++ b/erp24/docs/models/AdminGroup.md @@ -0,0 +1,308 @@ +# Модель AdminGroup + + +## Mindmap + +```mermaid +mindmap + root((AdminGroup)) + Таблица БД + admin_group + Свойства + id + int + name + string + admin_group_add_arr + string + adminGroupShift + AdminGroupShift + Связи + Shift + 1:N Shift + AdminGroupShift + 1:N AdminGroupShift + ChildGroups + 1:N AdminGroup + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `AdminGroup` представляет должности и группы сотрудников компании. Является ключевой сущностью для организационной структуры HR-модуля и системы разграничения прав доступа (RBAC). + +**Файл модели:** `erp24/records/AdminGroup.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(200) | Название группы/должности (уникальное) | +| `parent_id` | INTEGER | ID родительской группы (иерархия) | +| `message` | TEXT | Сообщение/описание группы | +| `dostup` | TEXT | Уровни доступа | +| `dostup_array` | TEXT | Массив доступов | +| `status_dostup_arr` | TEXT | Массив доступных статусов | +| `status_arr` | TEXT | Массив статусов | +| `istochnik_dostup_all` | INTEGER | Доступ ко всем источникам (0/1) | +| `istochnik_dostup_arr` | TEXT | Массив доступных источников | +| `istochnik_id_default` | INTEGER | ID источника по умолчанию | +| `info_block` | TEXT | Информационный блок | +| `posit` | INTEGER | Позиция для сортировки | +| `orders_dostup` | VARCHAR(1) | Доступ к заказам | +| `admin_group_add_arr` | TEXT | Дополнительный массив групп | + +--- + +## Константы групп + +Модель содержит константы для ключевых должностей: + +```php +const GROUP_FIRED = -1; // Уволенные +const NOT_INITIALIZED_GROUP = 1000; // Неинициализированная группа + +// Руководство +const DIRECTOR = 1; // Директор +const GROUP_HR = 20; // HR-менеджер +const GROUP_HR_DIRECTOR = 8; // HR-директор +const GROUP_RS_DIRECTOR = 10; // Директор розничной сети +const GROUP_BUSH_DIRECTOR = 7; // Куст-директор +const GROUP_OPERATIONAL_DIRECTOR = 51; // Операционный директор +const GROUP_FINANCE_DIRECTOR = 9; // Финансовый директор + +// Логистика +const GROUP_LOGIST_TRANSPORT = 15; // Транспортный логист + +// Флористы +const GROUP_FLORIST = 89; // Флорист (общий) +const GROUP_FLORIST_DAY = 30; // Флорист дневной +const GROUP_FLORIST_NIGHT = 35; // Флорист ночной +const GROUP_FLORIST_SUPPORT_DAY = 40; // Помощник флориста дневной +const GROUP_FLORIST_SUPPORT_NIGHT = 72; // Помощник флориста ночной +const GROUP_BUSH_CHEF_FLORIST = 18; // Шеф-флорист куста + +// Прочие +const GROUP_WORKERS = 45; // Работники +const GROUP_WORKERS_ARCHIVE = 90; // Архив работников +const GROUP_ADMINISTRATORS = 50; // Администраторы +const GROUP_IT = 81; // IT-отдел +``` + +--- + +## Методы модели + +### Статические методы получения групп + +#### `getWorkersGroups(): array` + +Возвращает массив ID групп работников (флористы, администраторы). Используется для логики грейдов и JavaScript. + +```php +$workerGroups = AdminGroup::getWorkersGroups(); +// [30, 35, 40, 45, 50, 72, 89] +``` + +#### `getGroupsForEmployeeController(): array` + +Возвращает группы для контроллера сотрудников. + +#### `getGroupsForEmployeeOnCashbox(): array` + +Возвращает группы сотрудников, работающих на кассе. + +#### `GROUP_DAY(): array` + +Возвращает группы дневных сотрудников. + +```php +$dayGroups = AdminGroup::GROUP_DAY(); +// [50, 30, 40, 89] — администраторы, флористы дневные +``` + +#### `GROUP_NIGHT(): array` + +Возвращает группы ночных сотрудников. + +```php +$nightGroups = AdminGroup::GROUP_NIGHT(); +// [35, 72, 45] — флористы ночные, работники +``` + +### Методы выборки данных + +#### `all(): array` + +Возвращает все активные группы с загруженными сменами (исключая ID 1, 2, -1). + +```php +$groups = AdminGroup::all(); +``` + +#### `groupsWithShift(): array` + +Возвращает только группы, у которых настроены смены. + +```php +$groupsWithShift = AdminGroup::groupsWithShift(); +``` + +#### `getNames($orderBy = null): array` + +Возвращает ассоциативный массив `[id => name]` всех групп. + +```php +$names = AdminGroup::getNames('orderByNameASC'); +// [1 => 'Директор', 7 => 'Куст-директор', ...] +``` + +#### `getAllIdName(): array` + +Возвращает все группы в формате `[id => name]`. + +--- + +## Связи (Relations) + +### `getShift()` + +Связь many-to-many со сменами через промежуточную таблицу `AdminGroupShift`. + +```php +$group = AdminGroup::findOne(30); +$shifts = $group->shift; // Shift[] +``` + +**Тип:** hasMany через via +**Связанная модель:** `Shift` +**Промежуточная модель:** `AdminGroupShift` + +### `getAdminGroupShift()` + +Прямая связь с настройками смен группы. + +```php +$groupShifts = $group->adminGroupShift; // AdminGroupShift[] +``` + +**Тип:** hasMany +**Связанная модель:** `AdminGroupShift` +**FK:** `admin_group_id` → `id` + +### `getChildGroups()` + +Дочерние группы (иерархия должностей). + +```php +$childGroups = $group->childGroups; // AdminGroup[] +``` + +**Тип:** hasMany (self-reference) +**FK:** `parent_id` → `id` + +--- + +## Вспомогательные методы + +### `isRoaming(): bool` + +Проверяет, является ли группа "разъездной" (куст-директор или администратор). + +```php +$group = AdminGroup::findOne(7); +$isRoaming = $group->isRoaming(); // true для GROUP_BUSH_DIRECTOR +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group ||--o{ admin : "has_many" + admin_group ||--o{ admin_group : "child_groups" + admin_group ||--o{ admin_group_shift : "has_many" + admin_group_shift }o--|| shift : "belongs_to" + admin_group ||--o{ admin_group_regulation : "has_many" + admin_group ||--o{ admin_group_rbac_config : "has_many" + + admin_group { + int id PK + string name UK + int parent_id FK + string message + string dostup + int posit + } +``` + +--- + +## Примеры использования + +### Получение группы сотрудника + +```php +$admin = Admin::findOne($adminId); +$group = AdminGroup::findOne($admin->group_id); +echo $group->name; // "Флорист дневной" +``` + +### Проверка принадлежности к рабочей группе + +```php +$admin = Admin::findOne($adminId); +$isWorker = in_array($admin->group_id, AdminGroup::getWorkersGroups()); +``` + +### Построение иерархии групп + +```php +$topGroups = AdminGroup::find() + ->where(['parent_id' => null]) + ->with(['childGroups']) + ->all(); +``` + +### Получение смен для группы + +```php +$group = AdminGroup::findOne(AdminGroup::GROUP_FLORIST_DAY); +foreach ($group->shift as $shift) { + echo $shift->name . ': ' . $shift->start_time . ' - ' . $shift->end_time; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, уникальное, макс. 200 символов | +| `parent_id` | Целое число | +| `posit` | Целое число | +| `orders_dostup` | Макс. 1 символ | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники, принадлежащие группе +- **AdminGroupShift** — настройки смен для группы +- **Shift** — смены работы +- **AdminGroupRegulation** — регламенты группы +- **AdminGroupRbacConfig** — RBAC-конфигурация группы + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroupCompanyFunctionVisibility.md b/erp24/docs/models/AdminGroupCompanyFunctionVisibility.md new file mode 100644 index 00000000..7a2acb8a --- /dev/null +++ b/erp24/docs/models/AdminGroupCompanyFunctionVisibility.md @@ -0,0 +1,209 @@ +# Модель AdminGroupCompanyFunctionVisibility + + +## Mindmap + +```mermaid +mindmap + root((AdminGroupCompanyFunctionVisibility)) + Таблица БД + admin_group_company_function_visibility + Свойства + id + int + company_function_id + int + admin_group_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminGroupCompanyFunctionVisibility` управляет видимостью элементов интерфейса функций компании для разных должностей. Определяет, какие компоненты функции (узлы, администраторы, исполнители, регламенты, системные задачи, метки) должны быть скрыты для конкретной группы сотрудников. Используется для тонкой настройки доступа к интерфейсу. + +**Файл модели:** `erp24/records/AdminGroupCompanyFunctionVisibility.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group_company_function_visibility` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `company_function_id` | INTEGER | ID функции компании (FK) | +| `admin_group_id` | INTEGER | ID должности (FK → admin_group.id) | +| `hide_node` | INTEGER | Скрыть узел функции (0/1) | +| `hide_administrator` | INTEGER | Скрыть администратора (0/1) | +| `hide_executors` | INTEGER | Скрыть исполнителей (0/1) | +| `hide_regulations` | INTEGER | Скрыть регламенты (0/1) | +| `hide_system_tasks` | INTEGER | Скрыть системные задачи (0/1) | +| `hide_labels` | INTEGER | Скрыть метки (0/1) | + +--- + +## Описание полей + +### `company_function_id` — Функция компании + +ID функции из справочника CompanyFunctions, для которой настраивается видимость. + +### `admin_group_id` — Должность + +ID должности из AdminGroup, для которой применяются настройки. + +### Флаги скрытия (hide_*) + +Каждый флаг определяет видимость соответствующего элемента: +- `0` или `NULL` — элемент отображается +- `1` — элемент скрыт + +| Флаг | Описание элемента | +|------|-------------------| +| `hide_node` | Весь узел функции в дереве | +| `hide_administrator` | Информация об администраторе функции | +| `hide_executors` | Список исполнителей | +| `hide_regulations` | Связанные регламенты | +| `hide_system_tasks` | Системные задачи функции | +| `hide_labels` | Метки и теги | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group_company_function_visibility }o--|| company_functions : "configures" + admin_group_company_function_visibility }o--|| admin_group : "applies_to" + + admin_group_company_function_visibility { + int id PK + int company_function_id FK + int admin_group_id FK + int hide_node + int hide_administrator + int hide_executors + int hide_regulations + int hide_system_tasks + int hide_labels + } + + company_functions { + int id PK + string name + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание настройки видимости + +```php +$visibility = new AdminGroupCompanyFunctionVisibility(); +$visibility->company_function_id = $functionId; +$visibility->admin_group_id = $groupId; +$visibility->hide_node = 0; +$visibility->hide_administrator = 1; +$visibility->hide_executors = 0; +$visibility->hide_regulations = 1; +$visibility->hide_system_tasks = 1; +$visibility->hide_labels = 0; +$visibility->save(); +``` + +### Получение настроек для должности + +```php +$settings = AdminGroupCompanyFunctionVisibility::find() + ->where(['admin_group_id' => $groupId]) + ->indexBy('company_function_id') + ->all(); + +foreach ($settings as $functionId => $setting) { + echo "Функция {$functionId}:\n"; + if ($setting->hide_node) echo " - узел скрыт\n"; + if ($setting->hide_regulations) echo " - регламенты скрыты\n"; +} +``` + +### Проверка видимости элемента + +```php +$visibility = AdminGroupCompanyFunctionVisibility::findOne([ + 'company_function_id' => $functionId, + 'admin_group_id' => $userGroupId +]); + +$showRegulations = !$visibility || !$visibility->hide_regulations; +if ($showRegulations) { + // Отображаем регламенты +} +``` + +### Получение скрытых функций + +```php +$hiddenFunctions = AdminGroupCompanyFunctionVisibility::find() + ->select('company_function_id') + ->where(['admin_group_id' => $groupId, 'hide_node' => 1]) + ->column(); + +// Исключаем скрытые функции из запроса +$visibleFunctions = CompanyFunctions::find() + ->where(['NOT IN', 'id', $hiddenFunctions]) + ->all(); +``` + +### Массовое обновление настроек + +```php +AdminGroupCompanyFunctionVisibility::updateAll( + ['hide_system_tasks' => 1], + ['admin_group_id' => $groupId] +); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `company_function_id` | Обязательное, целое число | +| `admin_group_id` | Обязательное, целое число | +| `hide_*` | Целые числа (0 или 1) | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности +- **[CompanyFunctions](./CompanyFunctions.md)** — функции компании +- **[Regulations](./Regulations.md)** — регламенты +- **[Task](./Task.md)** — задачи + +--- + +## Бизнес-логика + +Модель реализует **ролевую модель видимости**: +1. По умолчанию все элементы видны (NULL или 0) +2. Администратор может скрыть отдельные элементы для должности +3. Позволяет упростить интерфейс для определённых ролей +4. Не влияет на права доступа — только на отображение + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroupDynamic.md b/erp24/docs/models/AdminGroupDynamic.md new file mode 100644 index 00000000..7cc21815 --- /dev/null +++ b/erp24/docs/models/AdminGroupDynamic.md @@ -0,0 +1,294 @@ +# Модель AdminGroupDynamic + + +## Mindmap + +```mermaid +mindmap + root((AdminGroupDynamic)) + Таблица БД + admin_group_dynamic + Свойства + id + int + admin_id + int + group_id + int + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminGroupDynamic` представляет историю изменений должностей сотрудников. Хранит записи о переходах между должностями с указанием периода действия. Используется для корректного расчёта заработной платы и отслеживания карьерного пути сотрудника. + +**Файл модели:** `erp24/records/AdminGroupDynamic.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group_dynamic` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `group_id` | INTEGER | ID должности (FK → admin_group.id) | +| `date_from` | DATE | Дата начала работы в должности | +| `date_to` | DATE | Дата окончания (NULL = текущая) | +| `active` | INTEGER | Признак активности записи (0/1) | + +--- + +## Описание полей + +### `admin_id` — Сотрудник + +ID сотрудника, для которого фиксируется изменение должности. + +### `group_id` — Должность + +ID должности из справочника AdminGroup. Значение `-1` может использоваться для обозначения отсутствия должности (увольнение). + +### `date_from` — Дата начала + +Дата, с которой сотрудник начал работать в данной должности. + +### `date_to` — Дата окончания + +Дата, когда сотрудник покинул данную должность: +- `NULL` — должность является текущей +- Конкретная дата — сотрудник перешёл на другую должность + +### `active` — Активность + +Флаг активности записи для быстрой фильтрации. + +--- + +## Методы модели + +### `getGroupByDate($employeeId, $dateFrom, $dateTo): ?int` + +Статический метод получения ID должности сотрудника за указанный период. + +**Параметры:** +- `$employeeId` (int) — ID сотрудника +- `$dateFrom` (string) — начало периода +- `$dateTo` (string) — конец периода + +**Возвращает:** ID должности (из admin.group_id) + +```php +$groupId = AdminGroupDynamic::getGroupByDate($adminId, '2025-01-01', '2025-01-31'); +``` + +### `isGroupChangeInMonth($employeeId, $dateFrom, $dateTo): bool` + +Проверяет, было ли изменение должности в течение месяца. + +**Параметры:** +- `$employeeId` (int) — ID сотрудника +- `$dateFrom` (string) — начало периода +- `$dateTo` (string) — конец периода + +**Возвращает:** true если была смена должности + +```php +if (AdminGroupDynamic::isGroupChangeInMonth($adminId, '2025-01-01', '2025-01-31')) { + echo "В январе была смена должности"; +} +``` + +### `getLastWorkGroupInMonth($employeeId, $dateFrom, $dateTo): string` + +Получает последнюю рабочую должность в месяце (исключая -1). + +**Параметры:** +- `$employeeId` (int) — ID сотрудника +- `$dateFrom` (string) — начало периода +- `$dateTo` (string) — конец периода + +**Возвращает:** ID последней рабочей должности или '-1' + +```php +$lastGroupId = AdminGroupDynamic::getLastWorkGroupInMonth($adminId, '2025-01-01', '2025-01-31'); +``` + +### Геттеры и сеттеры + +| Метод | Описание | +|-------|----------| +| `getAdminId(): int` | Возвращает ID сотрудника | +| `setAdminId(int): void` | Устанавливает ID сотрудника | +| `getGroupId(): int` | Возвращает ID должности | +| `setGroupId(int): void` | Устанавливает ID должности | +| `getDateFrom(): string` | Возвращает дату начала | +| `setDateFrom(string): void` | Устанавливает дату начала | +| `getDateTo(): ?string` | Возвращает дату окончания | +| `setDateTo(?string): void` | Устанавливает дату окончания | +| `getActive(): int` | Возвращает флаг активности | +| `setActive(int): void` | Устанавливает флаг активности | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group_dynamic }o--|| admins : "tracks" + admin_group_dynamic }o--|| admin_group : "references" + + admin_group_dynamic { + int id PK + int admin_id FK + int group_id FK + date date_from + date date_to + int active + } + + admins { + int id PK + string name + int group_id + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Фиксация смены должности + +```php +// Закрываем текущую запись +$current = AdminGroupDynamic::find() + ->where(['admin_id' => $adminId, 'active' => 1]) + ->andWhere(['is', 'date_to', null]) + ->one(); + +if ($current) { + $current->setDateTo(date('Y-m-d', strtotime('-1 day'))); + $current->save(); +} + +// Создаём новую запись +$new = new AdminGroupDynamic(); +$new->setAdminId($adminId); +$new->setGroupId($newGroupId); +$new->setDateFrom(date('Y-m-d')); +$new->setActive(1); +$new->save(); +``` + +### Получение истории должностей + +```php +$history = AdminGroupDynamic::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $record) { + $group = AdminGroup::findOne($record->getGroupId()); + echo "{$record->getDateFrom()} - {$record->getDateTo()}: {$group->name}\n"; +} +``` + +### Расчёт зарплаты с учётом смены должности + +```php +$dateFrom = '2025-01-01'; +$dateTo = '2025-01-31'; + +if (AdminGroupDynamic::isGroupChangeInMonth($adminId, $dateFrom, $dateTo)) { + // Получаем все должности за месяц + $periods = AdminGroupDynamic::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['<=', 'date_from', $dateTo]) + ->andWhere(['or', + ['>=', 'date_to', $dateFrom], + ['is', 'date_to', null] + ]) + ->all(); + + foreach ($periods as $period) { + // Рассчитываем зарплату отдельно для каждого периода + $periodStart = max($dateFrom, $period->getDateFrom()); + $periodEnd = min($dateTo, $period->getDateTo() ?? $dateTo); + + calculateSalaryForPeriod($adminId, $period->getGroupId(), $periodStart, $periodEnd); + } +} else { + // Стандартный расчёт за весь месяц + $groupId = AdminGroupDynamic::getGroupByDate($adminId, $dateFrom, $dateTo); + calculateSalaryForPeriod($adminId, $groupId, $dateFrom, $dateTo); +} +``` + +### Поиск сотрудников с конкретной должностью на дату + +```php +$targetDate = '2025-01-15'; +$targetGroupId = 5; + +$adminIds = AdminGroupDynamic::find() + ->select('admin_id') + ->where(['group_id' => $targetGroupId]) + ->andWhere(['<=', 'date_from', $targetDate]) + ->andWhere(['or', + ['>=', 'date_to', $targetDate], + ['is', 'date_to', null] + ]) + ->column(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `group_id` | Обязательное, целое число | +| `date_from` | Обязательное | +| `date_to` | safe | +| `active` | Целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[AdminGroup](./AdminGroup.md)** — должности +- **[AdminDynamic](./AdminDynamic.md)** — другие динамические атрибуты +- **[AdminPayroll](./AdminPayroll.md)** — расчёт зарплаты + +--- + +## Отличие от AdminDynamic + +| Характеристика | AdminGroupDynamic | AdminDynamic | +|----------------|-------------------|--------------| +| Назначение | Только должности | Любые атрибуты | +| Поля значения | group_id | value_int/value_string | +| Категории | Нет | category_id | +| Специализация | Узкая | Универсальная | + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroupRbacConfig.md b/erp24/docs/models/AdminGroupRbacConfig.md new file mode 100644 index 00000000..efb54140 --- /dev/null +++ b/erp24/docs/models/AdminGroupRbacConfig.md @@ -0,0 +1,158 @@ +# Модель AdminGroupRbacConfig + + +## Mindmap + +```mermaid +mindmap + root((AdminGroupRbacConfig)) + Таблица БД + admin_group_rbac_config + Свойства + id + int + admin_group_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminGroupRbacConfig` хранит конфигурации RBAC (Role-Based Access Control) для должностей. Позволяет определять расширенные настройки прав доступа для каждой группы сотрудников в формате JSON. Используется для гибкой настройки разрешений, выходящих за рамки стандартной системы Yii2 RBAC. + +**Файл модели:** `erp24/records/AdminGroupRbacConfig.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group_rbac_config` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_group_id` | INTEGER | ID должности (FK → admin_group.id) | +| `config` | TEXT | JSON-конфигурация прав доступа | + +--- + +## Описание полей + +### `admin_group_id` — Должность + +ID должности из справочника AdminGroup, для которой определяется конфигурация. + +### `config` — Конфигурация + +JSON-строка с расширенными настройками прав доступа. Структура зависит от бизнес-требований. + +**Пример структуры:** +```json +{ + "canEditOwnProfile": true, + "canViewAllStores": false, + "maxDiscountPercent": 10, + "allowedModules": ["sales", "inventory"], + "restrictedActions": ["delete_order", "refund"] +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group_rbac_config }o--|| admin_group : "configures" + + admin_group_rbac_config { + int id PK + int admin_group_id FK + text config + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение конфигурации для должности + +```php +$config = AdminGroupRbacConfig::findOne(['admin_group_id' => $groupId]); +if ($config) { + $settings = json_decode($config->config, true); + if ($settings['canEditOwnProfile']) { + // Разрешаем редактирование профиля + } +} +``` + +### Создание конфигурации + +```php +$rbacConfig = new AdminGroupRbacConfig(); +$rbacConfig->admin_group_id = $groupId; +$rbacConfig->config = json_encode([ + 'canViewReports' => true, + 'canExportData' => false, + 'maxOrderAmount' => 100000 +]); +$rbacConfig->save(); +``` + +### Обновление конфигурации + +```php +$config = AdminGroupRbacConfig::findOne(['admin_group_id' => $groupId]); +$settings = json_decode($config->config, true); +$settings['canExportData'] = true; +$config->config = json_encode($settings); +$config->save(); +``` + +### Проверка разрешения + +```php +function hasPermission($adminGroupId, $permission) { + $config = AdminGroupRbacConfig::findOne(['admin_group_id' => $adminGroupId]); + if (!$config) return false; + + $settings = json_decode($config->config, true); + return $settings[$permission] ?? false; +} + +// Использование +if (hasPermission($user->group_id, 'canApproveRefunds')) { + // Показываем кнопку возврата +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_group_id` | Обязательное, целое число | +| `config` | Строка (JSON) | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности +- **[AuthItem](./AuthItem.md)** — стандартные роли RBAC +- **[AuthAssignment](./AuthAssignment.md)** — назначение ролей + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroupRegulation.md b/erp24/docs/models/AdminGroupRegulation.md new file mode 100644 index 00000000..8192388f --- /dev/null +++ b/erp24/docs/models/AdminGroupRegulation.md @@ -0,0 +1,200 @@ +# Модель AdminGroupRegulation + + +## Mindmap + +```mermaid +mindmap + root((AdminGroupRegulation)) + Таблица БД + admin_group_regulation + Свойства + admin_group_id + int + regulation_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminGroupRegulation` представляет связь между должностями и регламентами. Определяет, какие регламенты обязательны для изучения сотрудниками конкретной должности. Используется для автоматического назначения обучающих материалов при приёме на должность. + +**Файл модели:** `erp24/records/AdminGroupRegulation.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group_regulation` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `admin_group_id` | INTEGER | ID должности (FK → admin_group.id) | +| `regulation_id` | INTEGER | ID регламента (FK → regulations.id) | + +--- + +## Описание полей + +### `admin_group_id` — Должность + +ID должности из справочника AdminGroup. + +### `regulation_id` — Регламент + +ID регламента из таблицы Regulations, обязательного для данной должности. + +--- + +## Особенности + +- Таблица реализует связь **many-to-many** между должностями и регламентами +- Первичный ключ может быть составным (`admin_group_id`, `regulation_id`) +- Одной должности может соответствовать несколько регламентов +- Один регламент может быть назначен нескольким должностям + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group ||--o{ admin_group_regulation : "has" + regulations ||--o{ admin_group_regulation : "assigned_to" + + admin_group_regulation { + int admin_group_id PK,FK + int regulation_id PK,FK + } + + admin_group { + int id PK + string name + } + + regulations { + int id PK + string name + text content + } +``` + +--- + +## Примеры использования + +### Назначение регламента должности + +```php +$link = new AdminGroupRegulation(); +$link->admin_group_id = $groupId; +$link->regulation_id = $regulationId; +$link->save(); +``` + +### Получение регламентов для должности + +```php +$regulationIds = AdminGroupRegulation::find() + ->select('regulation_id') + ->where(['admin_group_id' => $groupId]) + ->column(); + +$regulations = Regulations::find() + ->where(['id' => $regulationIds]) + ->all(); +``` + +### Получение должностей для регламента + +```php +$groupIds = AdminGroupRegulation::find() + ->select('admin_group_id') + ->where(['regulation_id' => $regulationId]) + ->column(); + +$groups = AdminGroup::find() + ->where(['id' => $groupIds]) + ->all(); +``` + +### Проверка обязательности регламента + +```php +$isRequired = AdminGroupRegulation::find() + ->where([ + 'admin_group_id' => $userGroupId, + 'regulation_id' => $regulationId + ]) + ->exists(); + +if ($isRequired) { + echo "Этот регламент обязателен для вашей должности"; +} +``` + +### Удаление связи + +```php +AdminGroupRegulation::deleteAll([ + 'admin_group_id' => $groupId, + 'regulation_id' => $regulationId +]); +``` + +### Получение непройденных регламентов сотрудника + +```php +// Получаем обязательные регламенты для должности +$requiredIds = AdminGroupRegulation::find() + ->select('regulation_id') + ->where(['admin_group_id' => $employee->group_id]) + ->column(); + +// Получаем пройденные регламенты +$passedIds = RegulationsPassed::find() + ->select('regulation_id') + ->where(['admin_id' => $employee->id]) + ->column(); + +// Непройденные регламенты +$notPassedIds = array_diff($requiredIds, $passedIds); +$notPassed = Regulations::find() + ->where(['id' => $notPassedIds]) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_group_id` | Обязательное, целое число | +| `regulation_id` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности +- **[Regulations](./Regulations.md)** — регламенты +- **[RegulationsPassed](./RegulationsPassed.md)** — пройденные регламенты +- **[Admin](./Admin.md)** — сотрудники + +--- + +## Бизнес-логика + +1. При приёме сотрудника на должность автоматически определяются обязательные регламенты +2. Система отслеживает прогресс изучения регламентов +3. Непройденные регламенты могут влиять на допуск к работе или премии +4. При смене должности список обязательных регламентов пересчитывается + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminGroupShift.md b/erp24/docs/models/AdminGroupShift.md new file mode 100644 index 00000000..7d51db6c --- /dev/null +++ b/erp24/docs/models/AdminGroupShift.md @@ -0,0 +1,238 @@ +# Модель AdminGroupShift + + +## Mindmap + +```mermaid +mindmap + root((AdminGroupShift)) + Таблица БД + ActiveRecord + Свойства + id + string + admin_group_id + string + shift_id + string + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `AdminGroupShift` представляет связь между должностями (группами сотрудников) и доступными рабочими сменами. Определяет, какие смены могут быть назначены сотрудникам определённой должности. Используется для ограничения выбора смен при планировании графика работы. + +**Файл модели:** `erp24/records/AdminGroupShift.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_group_shift` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_group_id` | INTEGER | ID группы/должности (FK → admin_group) | +| `shift_id` | INTEGER | ID смены (FK → timetable_shift) | + +--- + +## Описание полей + +### `admin_group_id` — Должность + +Идентификатор группы сотрудников (должности). + +**Связь:** `admin_group.id` + +**Примеры:** флорист дневной, флорист ночной, кассир, управляющий + +### `shift_id` — Смена + +Идентификатор рабочей смены. + +**Связь:** `timetable_shift.id` + +**Примеры:** дневная 10-22, ночная 22-10, утренняя 8-16 + +--- + +## Уникальность + +Комбинация `[admin_group_id, shift_id]` должна быть уникальной — одна должность не может иметь одну и ту же смену дважды: + +```php +public function rules() +{ + return [ + [['admin_group_id'], 'unique', 'targetAttribute' => ['admin_group_id', 'shift_id']] + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_group_shift }o--|| admin_group : "group" + admin_group_shift }o--|| timetable_shift : "shift" + + admin_group_shift { + int id PK + int admin_group_id FK + int shift_id FK + } + + admin_group { + int id PK + string name + string short_name + } + + timetable_shift { + int id PK + string name + string short_name + time start_time + float duration + } +``` + +--- + +## Примеры использования + +### Получение доступных смен для должности + +```php +$groupId = AdminGroup::GROUP_FLORIST_DAY; + +$shiftIds = AdminGroupShift::find() + ->select('shift_id') + ->where(['admin_group_id' => $groupId]) + ->column(); + +$shifts = TimetableShift::find() + ->where(['id' => $shiftIds]) + ->all(); + +foreach ($shifts as $shift) { + echo "{$shift->name} ({$shift->start_time})\n"; +} +``` + +### Проверка доступности смены для должности + +```php +$canUseShift = AdminGroupShift::find() + ->where([ + 'admin_group_id' => $employee->admin_group_id, + 'shift_id' => $shiftId + ]) + ->exists(); + +if (!$canUseShift) { + throw new Exception('Эта смена недоступна для данной должности'); +} +``` + +### Добавление смены для должности + +```php +$groupShift = new AdminGroupShift(); +$groupShift->admin_group_id = AdminGroup::GROUP_FLORIST_DAY; +$groupShift->shift_id = $dayShiftId; +$groupShift->save(); +``` + +### Удаление связи + +```php +AdminGroupShift::deleteAll([ + 'admin_group_id' => $groupId, + 'shift_id' => $shiftId +]); +``` + +### Получение должностей для смены + +```php +$shiftId = 1; // Дневная смена + +$groupIds = AdminGroupShift::find() + ->select('admin_group_id') + ->where(['shift_id' => $shiftId]) + ->column(); + +$groups = AdminGroup::find() + ->where(['id' => $groupIds]) + ->all(); +``` + +### Использование в форме планирования + +```php +use yii\helpers\ArrayHelper; + +// Получаем доступные смены для сотрудника +$employee = Admin::findOne($adminId); +$availableShiftIds = AdminGroupShift::find() + ->select('shift_id') + ->where(['admin_group_id' => $employee->admin_group_id]) + ->column(); + +$shifts = TimetableShift::find() + ->where(['id' => $availableShiftIds]) + ->all(); + +$shiftList = ArrayHelper::map($shifts, 'id', 'name'); + +echo Html::dropDownList('shift_id', null, $shiftList, [ + 'prompt' => 'Выберите смену' +]); +``` + +### Массовая настройка смен для должности + +```php +$groupId = AdminGroup::GROUP_CASHIER; +$shiftIds = [1, 2, 3]; // ID доступных смен + +// Удаляем старые привязки +AdminGroupShift::deleteAll(['admin_group_id' => $groupId]); + +// Создаём новые +foreach ($shiftIds as $shiftId) { + $gs = new AdminGroupShift(); + $gs->admin_group_id = $groupId; + $gs->shift_id = $shiftId; + $gs->save(); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_group_id` | Уникальность в паре с shift_id | +| `shift_id` | Уникальность в паре с admin_group_id | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности/группы сотрудников +- **[TimetableShift](./TimetableShift.md)** — справочник рабочих смен +- **[Timetable](./Timetable.md)** — табель учёта рабочего времени + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollDays.md b/erp24/docs/models/AdminPayrollDays.md new file mode 100644 index 00000000..6f6bfad2 --- /dev/null +++ b/erp24/docs/models/AdminPayrollDays.md @@ -0,0 +1,1041 @@ +# Модель AdminPayrollDays + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollDays)) + Таблица БД + admin_payroll_days + Свойства + id + int + admin_id + int + group_id + int + store_id + int + date + string + date_time + string + Связи + Store + 1:1 CityStore + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollDays` представляет дневные расчеты заработной платы сотрудников. Хранит детальную информацию о начислениях за каждый рабочий день, включая продажи, постоянную и переменную части зарплаты, премии и другие показатели. Используется для формирования итоговой месячной ведомости в `AdminPayroll`. + +**Файл модели:** `erp24/records/AdminPayrollDays.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_days` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → `admin.id`) | +| `group_id` | INTEGER | ID группы сотрудника (FK → `admin_group.id`) | +| `store_id` | INTEGER | ID магазина (FK → `city_store.id`) | +| `date` | VARCHAR(100) | Дата смены в формате "YYYY-MM-DD" | +| `date_time` | VARCHAR(100) | Дата и время последнего обновления | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `day` | INTEGER | День месяца (1-31) | +| `smena_type` | INTEGER | Тип смены (1 - дневная, 2 - ночная) | +| `plan_by_day_from_rate_info` | INTEGER | План продаж на день из расчетной информации | +| `timetable_person_count` | INTEGER | Количество сотрудников в смене | +| `payroll_sum` | FLOAT | Общая сумма оплаты за день | +| `day_payroll` | FLOAT | Дневная зарплата (постоянная + переменная + командная премия) | +| `payroll_constant` | FLOAT | Постоянная часть зарплаты за день | +| `payroll_variable` | FLOAT | Переменная часть зарплаты за день | +| `payroll_constant_and_variable` | FLOAT | Сумма постоянной и переменной части | +| `sales_sum` | FLOAT | Сумма продаж за день | +| `matrix_sum` | FLOAT | Сумма продаж по матрице | +| `wrap_sum` | FLOAT | Сумма за упаковку | +| `potted_sum` | FLOAT | Сумма за горшечные растения | +| `related_sum` | FLOAT | Сумма за сопутствующие товары | +| `services_sum` | FLOAT | Сумма за услуги | +| `salut_sum` | FLOAT | Сумма за салюты | +| `other_items_sum` | FLOAT | Сумма за прочие товары | +| `team_bonus_sum` | FLOAT | Командная премия | +| `quality_bonus_sum` | FLOAT | Премия за качество | +| `created_at` | VARCHAR(100) | Дата создания записи | +| `update_at` | VARCHAR(100) | Дата последнего обновления | + +**Уникальный индекс:** `[admin_id, year, month, day]` — гарантирует, что у сотрудника может быть только одна запись за каждый день. + +--- + +## Методы модели + +### Геттеры и сеттеры + +#### `getId(): int` + +Возвращает ID записи дневного расчета. + +**Возвращает:** `int` — первичный ключ записи + +```php +$payrollDay = AdminPayrollDays::findOne(1); +$id = $payrollDay->getId(); // 1 +``` + +--- + +#### `getAdminId(): int` + +Возвращает ID сотрудника. + +**Возвращает:** `int` — ID сотрудника + +```php +$adminId = $payrollDay->getAdminId(); // 15 +``` + +--- + +#### `setAdminId(int $admin_id): object` + +Устанавливает ID сотрудника. + +**Параметры:** +- `$admin_id` (int) — ID сотрудника + +**Возвращает:** `object` — текущий экземпляр для цепочки вызовов + +**Пример:** +```php +$payrollDay = new AdminPayrollDays(); +$payrollDay->setAdminId(15); +``` + +--- + +#### `getGroupId(): int` + +Возвращает ID группы сотрудника. + +**Возвращает:** `int` — ID группы + +**Пример:** +```php +$groupId = $payrollDay->getGroupId(); // 30 (GROUP_FLORIST_DAY) +``` + +--- + +#### `setGroupId(int $group_id): object` + +Устанавливает ID группы сотрудника. + +**Параметры:** +- `$group_id` (int) — ID группы + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getStoreId(): int` + +Возвращает ID магазина. + +**Возвращает:** `int` — ID магазина + +--- + +#### `setStoreId(int $store_id): object` + +Устанавливает ID магазина. + +**Параметры:** +- `$store_id` (int) — ID магазина + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getDate(): string` + +Возвращает дату смены в формате "YYYY-MM-DD". + +**Возвращает:** `string` — дата смены + +```php +$date = $payrollDay->getDate(); // "2025-12-11" +``` + +--- + +#### `setDate(string $date): object` + +Устанавливает дату смены. + +**Параметры:** +- `$date` (string) — дата в формате "YYYY-MM-DD" + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getDateTime(): string` + +Возвращает дату и время последнего обновления. + +**Возвращает:** `string` — datetime в формате "Y-m-d H:i:s" + +--- + +#### `setDateTime(string $date_time): object` + +Устанавливает дату и время обновления. + +**Параметры:** +- `$date_time` (string) — datetime в формате "Y-m-d H:i:s" + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getYear(): int` + +Возвращает год. + +**Возвращает:** `int` — год (например, 2025) + +--- + +#### `setYear(int $year): object` + +Устанавливает год. + +**Параметры:** +- `$year` (int) — год + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getMonth(): int` + +Возвращает месяц. + +**Возвращает:** `int` — месяц (1-12) + +--- + +#### `setMonth(int $month): object` + +Устанавливает месяц. + +**Параметры:** +- `$month` (int) — месяц (1-12) + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getDay(): int` + +Возвращает день месяца. + +**Возвращает:** `int` — день (1-31) + +--- + +#### `setDay(int $day): object` + +Устанавливает день месяца. + +**Параметры:** +- `$day` (int) — день (1-31) + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getSmenaType(): int` + +Возвращает тип смены. + +**Возвращает:** `int` — тип смены (1 - дневная, 2 - ночная) + +```php +$smenaType = $payrollDay->getSmenaType(); // 1 (дневная) +``` + +--- + +#### `setSmenaType(int $smena_type): object` + +Устанавливает тип смены. + +**Параметры:** +- `$smena_type` (int) — тип смены (1 - дневная, 2 - ночная) + +**Возвращает:** `object` — текущий экземпляр + +--- + +### Методы работы с зарплатой + +#### `getDayPayroll(): float` + +Возвращает дневную зарплату (сумма постоянной, переменной части и командной премии). + +**Возвращает:** `float` — дневная зарплата + +```php +$dayPayroll = $payrollDay->getDayPayroll(); // 2500.00 +``` + +--- + +#### `setDayPayroll(float $day_payroll): object` + +Устанавливает дневную зарплату. + +**Параметры:** +- `$day_payroll` (float) — дневная зарплата + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getPayrollSum(): float` + +Возвращает общую сумму оплаты за день. + +**Возвращает:** `float` — общая сумма оплаты + +```php +$payrollSum = $payrollDay->getPayrollSum(); // 3000.00 +``` + +--- + +#### `setPayrollSum(float $payroll_sum): object` + +Устанавливает общую сумму оплаты за день. + +**Параметры:** +- `$payroll_sum` (float) — общая сумма оплаты + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getPayrolConstant(): float` + +Возвращает постоянную часть зарплаты. + +**Возвращает:** `float` — постоянная часть + +```php +$constant = $payrollDay->getPayrolConstant(); // 1500.00 +``` + +--- + +#### `setPayrollConstant(float $payroll_constant): object` + +Устанавливает постоянную часть зарплаты. + +**Параметры:** +- `$payroll_constant` (float) — постоянная часть + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getPayrollVariable(): float` + +Возвращает переменную часть зарплаты. + +**Возвращает:** `float` — переменная часть + +```php +$variable = $payrollDay->getPayrollVariable(); // 800.00 +``` + +--- + +#### `setPayrollVariable(float $payroll_variable): object` + +Устанавливает переменную часть зарплаты. + +**Параметры:** +- `$payroll_variable` (float) — переменная часть + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getPayrollConstantAndVariable(): float` + +Возвращает сумму постоянной и переменной части. + +**Возвращает:** `float` — сумма постоянной и переменной части + +```php +$sum = $payrollDay->getPayrollConstantAndVariable(); // 2300.00 +``` + +--- + +#### `setPayrollConstantAndVariable(float $payroll_constant_and_variable): object` + +Устанавливает сумму постоянной и переменной части. + +**Параметры:** +- `$payroll_constant_and_variable` (float) — сумма + +**Возвращает:** `object` — текущий экземпляр + +--- + +### Методы работы с продажами + +#### `getSalesSum(): float` + +Возвращает общую сумму продаж за день. + +**Возвращает:** `float` — сумма продаж + +```php +$sales = $payrollDay->getSalesSum(); // 45000.00 +``` + +--- + +#### `setSalesSum(float $sales_sum): object` + +Устанавливает общую сумму продаж. + +**Параметры:** +- `$sales_sum` (float) — сумма продаж + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getMatrixSum(): float` + +Возвращает сумму продаж по матрице. + +**Возвращает:** `float` — сумма по матрице + +--- + +#### `setMatrixSum(float $matrix_sum): object` + +Устанавливает сумму продаж по матрице. + +**Параметры:** +- `$matrix_sum` (float) — сумма по матрице + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getWrapSum(): float` + +Возвращает сумму за упаковку. + +**Возвращает:** `float` — сумма за упаковку + +--- + +#### `setWrapSum(float $wrap_sum): object` + +Устанавливает сумму за упаковку. + +**Параметры:** +- `$wrap_sum` (float) — сумма за упаковку + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getPottedSum(): float` + +Возвращает сумму за горшечные растения. + +**Возвращает:** `float` — сумма за горшечные + +--- + +#### `setPottedSum(float $potted_sum): object` + +Устанавливает сумму за горшечные растения. + +**Параметры:** +- `$potted_sum` (float) — сумма за горшечные + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getRelatedSum(): float` + +Возвращает сумму за сопутствующие товары. + +**Возвращает:** `float` — сумма за сопутствующие + +--- + +#### `setRelatedSum(float $related_sum): object` + +Устанавливает сумму за сопутствующие товары. + +**Параметры:** +- `$related_sum` (float) — сумма за сопутствующие + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getServicesSum(): float` + +Возвращает сумму за услуги. + +**Возвращает:** `float` — сумма за услуги + +--- + +#### `setServicesSum(float $services_sum): object` + +Устанавливает сумму за услуги. + +**Параметры:** +- `$services_sum` (float) — сумма за услуги + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getSalutSum(): float` + +Возвращает сумму за салюты. + +**Возвращает:** `float` — сумма за салюты + +--- + +#### `setSalutSum(float $salut_sum): object` + +Устанавливает сумму за салюты. + +**Параметры:** +- `$salut_sum` (float) — сумма за салюты + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getOtherItemsSum(): float` + +Возвращает сумму за прочие товары. + +**Возвращает:** `float` — сумма за прочие товары + +--- + +#### `setOtherItemsSum(float $other_items_sum): object` + +Устанавливает сумму за прочие товары. + +**Параметры:** +- `$other_items_sum` (float) — сумма за прочие товары + +**Возвращает:** `object` — текущий экземпляр + +--- + +### Методы работы с премиями + +#### `getTeamBonusSum(): float` + +Возвращает командную премию. + +**Возвращает:** `float` — командная премия + +```php +$teamBonus = $payrollDay->getTeamBonusSum(); // 200.00 +``` + +--- + +#### `setTeamBonusSum(float $team_bonus_sum): object` + +Устанавливает командную премию. + +**Параметры:** +- `$team_bonus_sum` (float) — командная премия + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getQualityBonusSum(): float` + +Возвращает премию за качество. + +**Возвращает:** `float` — премия за качество + +--- + +#### `setQualityBonusSum(float $quality_bonus_sum): object` + +Устанавливает премию за качество. + +**Параметры:** +- `$quality_bonus_sum` (float) — премия за качество + +**Возвращает:** `object` — текущий экземпляр + +--- + +### Методы работы с планом и расписанием + +#### `getPlanByDayFromRateInfo(): int` + +Возвращает план продаж на день из расчетной информации. + +**Возвращает:** `int` — план продаж + +```php +$plan = $payrollDay->getPlanByDayFromRateInfo(); // 50000 +``` + +--- + +#### `setPlanByDayFromRateInfo(int $plan_by_day_from_rate_info): object` + +Устанавливает план продаж на день. + +**Параметры:** +- `$plan_by_day_from_rate_info` (int) — план продаж + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getTimetablePersonCount(): int` + +Возвращает количество сотрудников в смене. + +**Возвращает:** `int` — количество сотрудников + +```php +$count = $payrollDay->getTimetablePersonCount(); // 3 +``` + +--- + +#### `setTimetablePersonCount($timetable_person_count): object` + +Устанавливает количество сотрудников в смене. + +**Параметры:** +- `$timetable_person_count` (int) — количество сотрудников + +**Возвращает:** `object` — текущий экземпляр + +--- + +### Методы работы с временными метками + +#### `getCreatedAt(): string` + +Возвращает дату создания записи. + +**Возвращает:** `string` — дата создания в формате "Y-m-d H:i:s" + +--- + +#### `setCreatedAt(): object` + +Устанавливает текущую дату и время как дату создания. + +**Логика:** +1. Получает текущее время через `date("Y-m-d H:i:s")` +2. Присваивает его полю `created_at` + +**Возвращает:** `object` — текущий экземпляр + +--- + +#### `getUpdateAt(): ?string` + +Возвращает дату последнего обновления. + +**Возвращает:** `string|null` — дата обновления или null + +--- + +#### `setUpdateAt(): object` + +Устанавливает текущую дату и время как дату обновления. + +**Логика:** +1. Проверяет, есть ли дата создания +2. Если нет — устанавливает дату создания +3. Получает текущее время через `date("Y-m-d H:i:s")` +4. Присваивает его полю `update_at` + +**Возвращает:** `object` — текущий экземпляр + +--- + +## Статические методы + +### `setValues(?array $payrollValues, array $payrollValuesInterval, $dateToInterval): void` + +Создает или обновляет запись дневного расчета зарплаты сотрудника. + +**Параметры:** +- `$payrollValues` (array|null) — данные о зарплате за конкретный день +- `$payrollValuesInterval` (array) — данные о зарплате за интервал (месяц) +- `$dateToInterval` — дата окончания интервала + +**Структура $payrollValues:** +```php +[ + 'employeeId' => 15, + 'employeeGroupId' => 30, + 'employeeSelectStoreId' => 5, + 'yearSelect' => 2025, + 'monthSelect' => 12, + 'monthWithZeroSelect' => '12', + 'day' => 11, + 'timetableAdminTypeFirstShift' => 1, + 'planByDayFromRateInfo' => 50000, + 'timetableAdminPersonCount' => 3, + 'bonusConstantSum' => 1500.00, + 'bonusVariableSum' => 800.00, + 'bonusVariableByMonthSum' => 2000.00, + 'teamPremium' => 500.00, + 'storeIdsInShift' => [5 => 1], + 'timetableAdmin' => [...], + 'matrixPrime' => 300.00, + 'userSalaryServicesPremium' => 100.00, + 'userSalaryRelatedPremium' => 50.00, + 'userSalaryPottedPremium' => 75.00, + 'userSalaryWrapPremium' => 25.00, + 'userSalarySalutPremium' => 30.00, + 'userSalaryOtherItemsPremium' => 20.00, +] +``` + +**Структура $payrollValuesInterval:** +```php +[ + 'timetableAdminPersonCount' => 3, + 'bonusVariableSum' => 15000.00, + 'dailyPayment' => 3000.00, +] +``` + +**Логика работы:** +1. Извлекает основные параметры из входных массивов +2. Рассчитывает переменную часть зарплаты: + - Берет переменную часть за день (`bonusVariableSumByDay`) + - Добавляет долю месячной переменной части (`bonusVariableByMonthSum / dayCountDivision`) +3. Рассчитывает командную премию (делит `teamPremium` на количество дней со сменами) +4. Определяет магазин и тип смены из `storeIdsInShift` +5. Проверяет, является ли сотрудник администратором: + - Если да — использует `dailyPayment` из интервальных данных + - Если нет — использует `allWagesAdmin` или `dailyPayment` из дневных данных +6. Рассчитывает итоговую дневную зарплату: + - `dayPayrollConstantAndVariable` = `bonusConstantSum` + `bonusVariableSum` + - `dayPayroll` = `dayPayrollConstantAndVariable` + `teamPremiumSum` +7. Извлекает сумму продаж из `timetableAdmin` или `salaryByAdmin` +8. Формирует дату в формате "Y-m-d" +9. Ищет существующую запись по `admin_id`, `year`, `month`, `day` +10. Если запись не найдена — создает новую с установкой `created_at` +11. Обновляет все поля записи +12. Валидирует и сохраняет запись +13. При ошибках валидации — логирует их через `ErrorInfoErp` + +**Используемые методы:** +- `Admin::isAdministrator()` — проверка, является ли сотрудник администратором +- `AdminPayrollDays::find()` — поиск существующей записи +- `ErrorInfoErp::save()` — логирование ошибок + +**Пример использования:** +```php +$payrollValues = [ + 'employeeId' => 15, + 'employeeGroupId' => 30, + 'yearSelect' => 2025, + 'monthSelect' => 12, + 'monthWithZeroSelect' => '12', + 'day' => 11, + 'bonusConstantSum' => 1500.00, + 'bonusVariableSumByDay' => 800.00, + // ... остальные поля +]; + +$payrollValuesInterval = [ + 'timetableAdminPersonCount' => 22, + 'dailyPayment' => 3000.00, +]; + +AdminPayrollDays::setValues($payrollValues, $payrollValuesInterval, '2025-12-31'); +``` + +--- + +## Связи (Relations) + +### `getStore(): ActiveQueryInterface` + +Связь с магазином. + +**Тип:** hasOne (один к одному) +**Связанная модель:** `CityStore` +**Связь:** `city_store.id` → `admin_payroll_days.store_id` + +**Пример использования:** +```php +$payrollDay = AdminPayrollDays::findOne(1); +$store = $payrollDay->store; // CityStore +echo $store->name; // "Магазин на Невском" +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `admin_id` | Обязательное, целое число, уникальное в комбинации с year, month, day | +| `group_id` | Целое число | +| `store_id` | Обязательное, целое число | +| `date` | Обязательное, строка, максимум 100 символов | +| `date_time` | Строка, максимум 100 символов | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `day` | Обязательное, целое число | +| `smena_type` | Обязательное, целое число | +| `timetable_person_count` | Безопасный тип | +| `payroll_sum` | Обязательное, число (float) | +| `day_payroll` | Число (float) | +| `payroll_constant` | Обязательное, число (float) | +| `payroll_variable` | Обязательное, число (float) | +| `payroll_constant_and_variable` | Число (float) | +| `plan_by_day_from_rate_info` | Число (integer) | +| `sales_sum` | Обязательное, число (float) | +| `matrix_sum` | Число (float) | +| `wrap_sum` | Число (float) | +| `potted_sum` | Число (float) | +| `related_sum` | Число (float) | +| `services_sum` | Число (float) | +| `salut_sum` | Число (float) | +| `other_items_sum` | Число (float) | +| `team_bonus_sum` | Число (float) | +| `quality_bonus_sum` | Число (float) | +| `created_at` | Обязательное, строка, максимум 100 символов | +| `update_at` | Строка, максимум 100 символов | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_days }o--|| admin : "belongs_to" + admin_payroll_days }o--|| city_store : "belongs_to" + admin_payroll_days }o--|| admin_group : "belongs_to" + + admin_payroll_days { + int id PK + int admin_id FK + int group_id FK + int store_id FK + string date + int year + int month + int day + int smena_type + float payroll_sum + float day_payroll + float payroll_constant + float payroll_variable + float sales_sum + float matrix_sum + float team_bonus_sum + string created_at + string update_at + } + + admin { + int id PK + string name + int group_id + } + + city_store { + int id PK + string name + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание дневного расчета + +```php +$payrollValues = [ + 'employeeId' => 15, + 'employeeGroupId' => 30, + 'employeeSelectStoreId' => 5, + 'yearSelect' => 2025, + 'monthSelect' => 12, + 'monthWithZeroSelect' => '12', + 'day' => 11, + 'timetableAdminTypeFirstShift' => 1, + 'bonusConstantSum' => 1500.00, + 'bonusVariableSum' => 800.00, + 'bonusVariableByMonthSum' => 5000.00, + 'teamPremium' => 1000.00, + 'matrixPrime' => 300.00, + 'planByDayFromRateInfo' => 50000, + 'timetableAdminPersonCount' => 3, +]; + +$payrollValuesInterval = [ + 'timetableAdminPersonCount' => 22, + 'dailyPayment' => 3000.00, +]; + +AdminPayrollDays::setValues($payrollValues, $payrollValuesInterval, '2025-12-31'); +``` + +--- + +### Получение дневных расчетов сотрудника за месяц + +```php +$adminId = 15; +$year = 2025; +$month = 12; + +$payrollDays = AdminPayrollDays::find() + ->with(['store']) + ->andWhere(['admin_id' => $adminId]) + ->andWhere(['year' => $year, 'month' => $month]) + ->orderBy(['day' => SORT_ASC]) + ->all(); + +foreach ($payrollDays as $day) { + echo "День {$day->day}: продажи {$day->sales_sum}, зарплата {$day->day_payroll}\n"; +} +``` + +--- + +### Расчет итоговой зарплаты за месяц + +```php +$adminId = 15; +$year = 2025; +$month = 12; + +$total = AdminPayrollDays::find() + ->andWhere(['admin_id' => $adminId]) + ->andWhere(['year' => $year, 'month' => $month]) + ->sum('day_payroll'); + +echo "Итого за месяц: {$total} руб."; +``` + +--- + +### Получение статистики по продажам + +```php +$adminId = 15; +$year = 2025; +$month = 12; + +$stats = AdminPayrollDays::find() + ->select([ + 'SUM(sales_sum) as total_sales', + 'SUM(matrix_sum) as matrix_total', + 'SUM(wrap_sum) as wrap_total', + 'SUM(services_sum) as services_total', + 'AVG(sales_sum) as avg_sales', + 'COUNT(*) as work_days', + ]) + ->andWhere(['admin_id' => $adminId]) + ->andWhere(['year' => $year, 'month' => $month]) + ->asArray() + ->one(); + +echo "Всего продаж: {$stats['total_sales']}\n"; +echo "Средние продажи: {$stats['avg_sales']}\n"; +echo "Рабочих дней: {$stats['work_days']}\n"; +``` + +--- + +### Получение лучших дней по продажам + +```php +$adminId = 15; +$year = 2025; +$month = 12; + +$bestDays = AdminPayrollDays::find() + ->with(['store']) + ->andWhere(['admin_id' => $adminId]) + ->andWhere(['year' => $year, 'month' => $month]) + ->orderBy(['sales_sum' => SORT_DESC]) + ->limit(5) + ->all(); + +foreach ($bestDays as $day) { + echo "{$day->date}: {$day->sales_sum} руб. в {$day->store->name}\n"; +} +``` + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники компании +- **[AdminGroup](./AdminGroup.md)** — группы/должности сотрудников +- **[CityStore](./CityStore.md)** — магазины +- **[AdminPayroll](./AdminPayroll.md)** — месячные ведомости +- **[Timetable](./Timetable.md)** — расписание смен + +--- + +## Связанные сервисы + +- **ErrorInfoErp** — логирование ошибок при сохранении дневных расчетов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollDaysSearch.md b/erp24/docs/models/AdminPayrollDaysSearch.md new file mode 100644 index 00000000..5804cc51 --- /dev/null +++ b/erp24/docs/models/AdminPayrollDaysSearch.md @@ -0,0 +1,202 @@ +# Класс: AdminPayrollDaysSearch + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollDaysSearch)) + Таблица БД + ActiveRecord + Наследование + extends AdminPayrollDays +``` + +## Назначение +Search-модель для поиска и аналитики дневных начислений ФОТ сотрудников в ERP24. Предоставляет методы для агрегированного поиска данных по зарплате за период с группировкой по кластерам, магазинам и типам смен. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`AdminPayrollDays` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$date_start_str` | string | Начало периода поиска (default: первый день текущего месяца) | +| `$date_end_str` | string | Конец периода поиска (default: последний день текущего месяца) | +| `$mode_group` | int | Режим группировки данных | + +## Методы + +### rules() +**Описание:** Правила валидации для параметров поиска. + +**Возвращает:** `array` — массив правил валидации + +**Правила:** +- `date_start_str`, `date_end_str` — safe +- `mode_group` — integer +- `date_start_str` по умолчанию: первый день текущего месяца +- `date_end_str` по умолчанию: последний день текущего месяца + +### search(): ActiveQuery +**Описание:** Основной метод поиска с агрегацией данных ФОТ за текущий период. + +**Возвращает:** `ActiveQuery` — запрос с агрегированными данными + +**Логика:** +1. Создаёт запрос к AdminPayrollDays +2. Проверяет валидацию параметров +3. Присоединяет store_dynamic для получения cluster_id +4. Присоединяет admin для фильтрации по текущему пользователю (через store_arr) +5. Присоединяет city_store для получения названия магазина +6. Фильтрует по типам смен (1 и 2) +7. Группирует по кластеру, магазину, типу смены и дате +8. Агрегирует: SUM(sales_sum), SUM(day_payroll), COUNT(admin_id) +9. Фильтрует по периоду date_start_str — date_end_str +10. Сортирует по дате + +**Выборка возвращает:** +- `cluster_id` — ID кластера +- `store_id` — ID магазина +- `store_name` — название магазина +- `shift_type` — тип смены (1 или 2) +- `sales_sum` — сумма продаж смены +- `day_payroll` — ФОТ смены +- `admin_count` — количество сотрудников +- `date` — дата в формате Y-m-d + +### searchPreviousYear(): ActiveQuery +**Описание:** Поиск данных за аналогичный период прошлого года для сравнительного анализа. + +**Возвращает:** `ActiveQuery` — запрос с данными прошлого года + +**Логика:** +1. Аналогично методу search() +2. Дополнительно фильтрует store_dynamic по category=1 и active=1 +3. Сдвигает период на год назад (`strtotime('previous year')`) +4. Используется для year-over-year сравнения + +**Выборка идентична методу search()** + +## Диаграмма процесса поиска + +```mermaid +flowchart TD + A[AdminPayrollDaysSearch] --> B{validate()} + B -->|Ошибка| C[where 0=1] + B -->|OK| D[JOIN store_dynamic] + + D --> E[JOIN admin
фильтр по store_arr] + E --> F[JOIN city_store] + + F --> G[WHERE smena_type IN 1,2] + G --> H[WHERE date >= date_start_str] + H --> I[WHERE date <= date_end_str] + + I --> J[GROUP BY cluster, store,
shift_type, date] + J --> K[SELECT с агрегацией
SUM, COUNT] + K --> L[ORDER BY date ASC] + + L --> M[ActiveQuery] +``` + +## Диаграмма сравнения периодов + +```mermaid +sequenceDiagram + participant C as Controller + participant S as AdminPayrollDaysSearch + participant DB as Database + + C->>S: search() + S->>DB: SELECT текущий период + DB-->>S: Данные 2024 + + C->>S: searchPreviousYear() + S->>DB: SELECT год назад + DB-->>S: Данные 2023 + + C->>C: Сравнение YoY +``` + +## Примеры использования + +### Поиск ФОТ за текущий месяц +```php +$searchModel = new AdminPayrollDaysSearch(); +$searchModel->date_start_str = date('Y-m-01'); +$searchModel->date_end_str = date('Y-m-t'); + +$query = $searchModel->search(); +$data = $query->asArray()->all(); + +foreach ($data as $row) { + echo "{$row['store_name']}: ФОТ {$row['day_payroll']}, Продажи {$row['sales_sum']}\n"; +} +``` + +### Поиск за произвольный период +```php +$searchModel = new AdminPayrollDaysSearch(); +$searchModel->date_start_str = '2024-01-01'; +$searchModel->date_end_str = '2024-03-31'; + +$query = $searchModel->search(); +``` + +### Сравнение с прошлым годом +```php +$searchModel = new AdminPayrollDaysSearch(); +$searchModel->date_start_str = '2024-06-01'; +$searchModel->date_end_str = '2024-06-30'; + +$currentYear = $searchModel->search()->asArray()->all(); +$previousYear = $searchModel->searchPreviousYear()->asArray()->all(); + +// Сравнение YoY +foreach ($currentYear as $i => $current) { + $prev = $previousYear[$i] ?? null; + if ($prev) { + $growth = ($current['sales_sum'] - $prev['sales_sum']) / $prev['sales_sum'] * 100; + echo "Рост продаж: {$growth}%\n"; + } +} +``` + +### Группировка по кластерам +```php +$searchModel = new AdminPayrollDaysSearch(); +$query = $searchModel->search(); + +$byCluster = []; +foreach ($query->asArray()->all() as $row) { + $clusterId = $row['cluster_id']; + if (!isset($byCluster[$clusterId])) { + $byCluster[$clusterId] = ['sales' => 0, 'payroll' => 0]; + } + $byCluster[$clusterId]['sales'] += $row['sales_sum']; + $byCluster[$clusterId]['payroll'] += $row['day_payroll']; +} +``` + +## Связанные модели + +- [AdminPayrollDays](./AdminPayrollDays.md) — базовая модель дневных начислений +- [Admin](./Admin.md) — сотрудники (фильтрация по store_arr) +- [CityStore](./CityStore.md) — магазины (получение названия) +- [StoreDynamic](./StoreDynamic.md) — динамические параметры магазина (cluster_id) + +## Особенности реализации + +1. **Наследование от ActiveRecord**: Расширяет AdminPayrollDays, а не Model +2. **Возврат ActiveQuery**: Методы возвращают query, а не DataProvider +3. **Права доступа**: Фильтрация по магазинам текущего пользователя через admin.store_arr +4. **Типы смен**: Фильтр только по smena_type 1 и 2 +5. **Агрегация**: SUM для сумм, COUNT для количества сотрудников +6. **Формат даты**: Конкатенация year, month, day через DATE_FORMAT +7. **YoY сравнение**: Метод searchPreviousYear для year-over-year анализа +8. **Default значения**: Автоматически текущий месяц при отсутствии параметров diff --git a/erp24/docs/models/AdminPayrollHistory.md b/erp24/docs/models/AdminPayrollHistory.md new file mode 100644 index 00000000..1d0cae67 --- /dev/null +++ b/erp24/docs/models/AdminPayrollHistory.md @@ -0,0 +1,288 @@ +# Модель AdminPayrollHistory + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollHistory)) + Таблица БД + admin_payroll_history + Свойства + id + int + admin_id + int + store_id + int + year + int + month + int + type_value + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollHistory` хранит историю расчётов заработной платы сотрудников. Фиксирует промежуточные и итоговые значения расчёта: общую сумму, командный бонус, детализацию. Используется для аудита, версионирования расчётов и восстановления данных. + +**Файл модели:** `erp24/records/AdminPayrollHistory.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_history` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `store_id` | INTEGER | ID магазина (FK → city_store.id) | +| `year` | INTEGER | Год расчёта | +| `month` | INTEGER | Месяц расчёта (1-12) | +| `group_number` | INTEGER | Группа/тип значения | +| `value_number` | FLOAT | Числовое значение | +| `value_string` | TEXT | Строковое значение (JSON детализация) | +| `value_type` | VARCHAR | Тип значения (number/string) | +| `created_at` | TIMESTAMP | Дата-время создания записи | +| `created_date` | DATE | Дата создания | +| `packet_num` | INTEGER | Номер пакета расчёта | + +--- + +## Константы + +### Группы значений (GROUP_VALUE_*) + +```php +const GROUP_VALUE_ALL_TOTAL_PAYROLL = 1; // Общая сумма ЗП +const GROUP_VALUE_TEAM_BONUS_VALUE = 2; // Сумма командного бонуса +const GROUP_VALUE_TEAM_BONUS_DETAIL = 3; // Детализация командного бонуса +``` + +### Типы значений (TYPE_VALUE_*) + +```php +const TYPE_VALUE_NUMBER = 'number'; // Числовое значение +const TYPE_VALUE_STRING = 'string'; // Строковое значение (JSON) +``` + +--- + +## Описание полей + +### `group_number` — Группа значения + +Определяет, какой показатель хранится в записи: +- `1` — общая сумма заработной платы +- `2` — сумма командного бонуса +- `3` — детализация командного бонуса (JSON) + +### `value_number` / `value_string` + +В зависимости от value_type используется одно из полей: +- `number` → `value_number` (float) +- `string` → `value_string` (JSON-строка) + +### `packet_num` — Номер пакета + +Идентификатор пакета расчёта. Позволяет группировать записи одного расчётного цикла. + +--- + +## Методы модели + +### `setValues(...): void` + +Статический метод массового сохранения значений расчёта. + +**Параметры:** +- `$payrollValues` (array|null) — массив значений ['allTotalPayroll', 'teamBonusValue', 'teamBonusDetail'] +- `$employeeId` (int) — ID сотрудника +- `$storeId` (int) — ID магазина +- `$yearSelect` (int) — год +- `$monthSelect` (int) — месяц +- `$packetNum` (int) — номер пакета + +**Пример:** +```php +AdminPayrollHistory::setValues( + [ + 'allTotalPayroll' => 45000.50, + 'teamBonusValue' => 5000, + 'teamBonusDetail' => json_encode(['sales' => 3000, 'kpi' => 2000]) + ], + $adminId, + $storeId, + 2025, + 1, + 123 +); +``` + +**Логика метода:** +1. Создаёт 3 записи для каждого типа значения +2. Автоматически определяет value_type и group_number +3. Устанавливает created_at и created_date +4. Валидирует и сохраняет каждую запись +5. При ошибке формирует JSON с описанием + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_history }o--|| admins : "belongs_to" + admin_payroll_history }o--|| city_store : "at_store" + + admin_payroll_history { + int id PK + int admin_id FK + int store_id FK + int year + int month + int group_number + float value_number + text value_string + string value_type + timestamp created_at + date created_date + int packet_num + } + + admins { + int id PK + string name + } + + city_store { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Сохранение результатов расчёта + +```php +$payrollData = [ + 'allTotalPayroll' => $totalSalary, + 'teamBonusValue' => $teamBonus, + 'teamBonusDetail' => json_encode($bonusBreakdown) +]; + +AdminPayrollHistory::setValues( + $payrollData, + $employee->id, + $employee->store_id, + $year, + $month, + $batchNumber +); +``` + +### Получение истории расчётов сотрудника + +```php +$history = AdminPayrollHistory::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['group_number' => AdminPayrollHistory::GROUP_VALUE_ALL_TOTAL_PAYROLL]) + ->orderBy(['year' => SORT_DESC, 'month' => SORT_DESC]) + ->all(); + +foreach ($history as $record) { + echo "{$record->year}-{$record->month}: {$record->value_number} руб.\n"; +} +``` + +### Получение детализации командного бонуса + +```php +$detail = AdminPayrollHistory::findOne([ + 'admin_id' => $adminId, + 'year' => 2025, + 'month' => 1, + 'group_number' => AdminPayrollHistory::GROUP_VALUE_TEAM_BONUS_DETAIL +]); + +if ($detail) { + $breakdown = json_decode($detail->value_string, true); + foreach ($breakdown as $component => $amount) { + echo "{$component}: {$amount} руб.\n"; + } +} +``` + +### Получение всех записей пакета расчёта + +```php +$batchRecords = AdminPayrollHistory::find() + ->where(['packet_num' => $packetNum]) + ->all(); +``` + +### Сравнение расчётов разных пакетов + +```php +$oldTotal = AdminPayrollHistory::find() + ->where([ + 'admin_id' => $adminId, + 'year' => $year, + 'month' => $month, + 'group_number' => AdminPayrollHistory::GROUP_VALUE_ALL_TOTAL_PAYROLL + ]) + ->orderBy(['packet_num' => SORT_ASC]) + ->one(); + +$newTotal = AdminPayrollHistory::find() + ->where([ + 'admin_id' => $adminId, + 'year' => $year, + 'month' => $month, + 'group_number' => AdminPayrollHistory::GROUP_VALUE_ALL_TOTAL_PAYROLL + ]) + ->orderBy(['packet_num' => SORT_DESC]) + ->one(); + +$difference = $newTotal->value_number - $oldTotal->value_number; +echo "Изменение ЗП: {$difference} руб."; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `store_id` | Обязательное, целое число | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `packet_num` | Обязательное, целое число | +| `value_type` | Обязательное | +| `created_at` | Обязательное | +| `value_number` | Число | +| `group_number` | Целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[AdminPayroll](./AdminPayroll.md)** — текущие расчёты ЗП +- **[AdminPayrollStat](./AdminPayrollStat.md)** — статистика по расчётам + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollMonthInfo.md b/erp24/docs/models/AdminPayrollMonthInfo.md new file mode 100644 index 00000000..4e85c27b --- /dev/null +++ b/erp24/docs/models/AdminPayrollMonthInfo.md @@ -0,0 +1,190 @@ +# Модель AdminPayrollMonthInfo + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollMonthInfo)) + Таблица БД + admin_payroll_month_info + Свойства + id + int + admin_id + int + year + int + month + int + payroll_value + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollMonthInfo` хранит итоговую информацию о заработной плате сотрудника за месяц. Представляет собой агрегированное значение ЗП для быстрого доступа без необходимости пересчёта. Используется для отчётности и отображения в интерфейсе. + +**Файл модели:** `erp24/records/AdminPayrollMonthInfo.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_month_info` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `payroll_value` | FLOAT | Итоговая сумма ЗП | +| `date` | VARCHAR(255) | Строковое представление периода | +| `created_at` | INTEGER | Unix timestamp создания | +| `updated_at` | INTEGER | Unix timestamp обновления | + +--- + +## Описание полей + +### `payroll_value` — Сумма ЗП + +Итоговая начисленная заработная плата за месяц в рублях. + +### `date` — Период + +Строковое представление периода (например, "2025-01"). + +### `created_at` / `updated_at` + +Unix timestamp создания и последнего обновления записи. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_month_info }o--|| admins : "belongs_to" + + admin_payroll_month_info { + int id PK + int admin_id FK + int year + int month + float payroll_value + string date + int created_at + int updated_at + } + + admins { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение ЗП за месяц + +```php +$monthInfo = AdminPayrollMonthInfo::findOne([ + 'admin_id' => $adminId, + 'year' => 2025, + 'month' => 1 +]); + +if ($monthInfo) { + echo "ЗП за январь 2025: {$monthInfo->payroll_value} руб."; +} +``` + +### Создание/обновление записи + +```php +$monthInfo = AdminPayrollMonthInfo::findOne([ + 'admin_id' => $adminId, + 'year' => $year, + 'month' => $month +]); + +if (!$monthInfo) { + $monthInfo = new AdminPayrollMonthInfo(); + $monthInfo->admin_id = $adminId; + $monthInfo->year = $year; + $monthInfo->month = $month; + $monthInfo->created_at = time(); +} + +$monthInfo->payroll_value = $calculatedSalary; +$monthInfo->date = sprintf('%d-%02d', $year, $month); +$monthInfo->updated_at = time(); +$monthInfo->save(); +``` + +### Годовой доход сотрудника + +```php +$yearlyIncome = AdminPayrollMonthInfo::find() + ->where(['admin_id' => $adminId, 'year' => 2025]) + ->sum('payroll_value'); + +echo "Доход за 2025 год: {$yearlyIncome} руб."; +``` + +### Средняя ЗП за год + +```php +$avgSalary = AdminPayrollMonthInfo::find() + ->where(['admin_id' => $adminId, 'year' => 2025]) + ->average('payroll_value'); + +echo "Средняя ЗП: " . round($avgSalary, 2) . " руб."; +``` + +### История ЗП сотрудника + +```php +$history = AdminPayrollMonthInfo::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['year' => SORT_DESC, 'month' => SORT_DESC]) + ->limit(12) + ->all(); + +foreach ($history as $record) { + echo "{$record->date}: {$record->payroll_value} руб.\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `payroll_value` | Обязательное, число | +| `date` | Строка, макс. 255 символов | +| `created_at`, `updated_at` | Целые числа | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[AdminPayroll](./AdminPayroll.md)** — детальный расчёт ЗП +- **[AdminPayrollHistory](./AdminPayrollHistory.md)** — история расчётов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollSearch.md b/erp24/docs/models/AdminPayrollSearch.md new file mode 100644 index 00000000..3ae77dc3 --- /dev/null +++ b/erp24/docs/models/AdminPayrollSearch.md @@ -0,0 +1,163 @@ +# Класс: AdminPayrollSearch + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollSearch)) + Таблица БД + ActiveRecord + Наследование + extends AdminPayroll +``` + +## Назначение +Search-модель для поиска и фильтрации записей расчёта зарплаты сотрудников в ERP24. Обеспечивает стандартный GridView-поиск по всем полям модели AdminPayroll с поддержкой ActiveDataProvider. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`AdminPayroll` + +## Методы + +### rules() +**Описание:** Правила валидации для параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `admin_id`, `year`, `month`, `count_shift` — integer +- `date`, `date_time` — safe +- `shift_cost`, `const_salary`, `variable_salary`, `matrix_salary`, `related_salary`, `total_salary`, `advance_funds_cash`, `advance_funds_cashless`, `retention_amount`, `total_amount` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model, минуя родительскую реализацию. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с применённым поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска из формы/запроса + +**Возвращает:** `ActiveDataProvider` — провайдер данных для GridView + +**Логика:** +1. Создаёт базовый запрос AdminPayroll::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры через load($params) +4. При ошибке валидации возвращает DataProvider без фильтрации +5. Применяет фильтры andFilterWhere для числовых полей +6. Применяет like-фильтр для поля date + +**Фильтруемые поля:** +- По точному совпадению: id, admin_id, year, month, все salary-поля, count_shift, date_time +- По LIKE: date + +## Диаграмма процесса поиска + +```mermaid +flowchart TD + A[GET/POST params] --> B[AdminPayrollSearch] + B --> C[load params] + C --> D{validate} + + D -->|Ошибка| E[DataProvider
без фильтров] + D -->|OK| F[andFilterWhere
числовые поля] + + F --> G[andFilterWhere LIKE
date] + G --> H[ActiveDataProvider] + + H --> I[GridView] +``` + +## Примеры использования + +### Стандартный поиск в контроллере +```php +public function actionIndex() +{ + $searchModel = new AdminPayrollSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по сотруднику и периоду +```php +$searchModel = new AdminPayrollSearch(); +$dataProvider = $searchModel->search([ + 'AdminPayrollSearch' => [ + 'admin_id' => 123, + 'year' => 2024, + 'month' => 6, + ] +]); + +$records = $dataProvider->getModels(); +``` + +### Поиск с высокой зарплатой +```php +$searchModel = new AdminPayrollSearch(); +$dataProvider = $searchModel->search([ + 'AdminPayrollSearch' => [ + 'total_salary' => 50000, + ] +]); +``` + +### Использование в GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'admin_id', + 'year', + 'month', + 'total_salary', + 'total_amount', + 'date', + ], +]) ?> +``` + +### Программный поиск +```php +$searchModel = new AdminPayrollSearch(); +$searchModel->year = 2024; +$searchModel->admin_id = 456; + +if ($searchModel->validate()) { + $query = AdminPayroll::find() + ->andFilterWhere(['admin_id' => $searchModel->admin_id]) + ->andFilterWhere(['year' => $searchModel->year]); + + $data = $query->all(); +} +``` + +## Связанные модели + +- [AdminPayroll](./AdminPayroll.md) — базовая модель расчёта зарплаты +- [Admin](./Admin.md) — сотрудники (admin_id) + +## Особенности реализации + +1. **Стандартный Gii-шаблон**: Типичная Search-модель Yii2 +2. **Наследование от ActiveRecord**: Расширяет AdminPayroll +3. **Bypass scenarios**: Использует Model::scenarios() напрямую +4. **ActiveDataProvider**: Возвращает провайдер для GridView +5. **andFilterWhere**: Пропускает пустые значения фильтров +6. **LIKE для date**: Текстовый поиск по дате +7. **Числовые фильтры**: Точное совпадение для salary-полей diff --git a/erp24/docs/models/AdminPayrollStat.md b/erp24/docs/models/AdminPayrollStat.md new file mode 100644 index 00000000..57f5d12a --- /dev/null +++ b/erp24/docs/models/AdminPayrollStat.md @@ -0,0 +1,199 @@ +# Модель AdminPayrollStat + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollStat)) + Таблица БД + admin_payroll_stat + Свойства + id + int + admin_id + int + store_id + int + date + string + year + int + month + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollStat` хранит статистику по расчётам заработной платы. Фиксирует суммы расчётов по дням с привязкой к сотруднику, магазину и номеру пакета. Используется для отслеживания динамики начислений и аудита расчётных процессов. + +**Файл модели:** `erp24/records/AdminPayrollStat.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_stat` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `store_id` | INTEGER | ID магазина (FK → city_store.id) | +| `date` | VARCHAR(100) | Строковая дата | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `date_time` | VARCHAR(100) | Дата-время расчёта | +| `sum` | FLOAT | Сумма расчёта | +| `bath_num` | INTEGER | Номер пакета (batch) | + +--- + +## Описание полей + +### `sum` — Сумма расчёта + +Рассчитанная сумма заработной платы за указанный период. + +### `bath_num` — Номер пакета + +Идентификатор пакетного расчёта. Позволяет отслеживать, в рамках какого цикла расчёта была создана запись. + +### `date` / `date_time` + +- `date` — дата в строковом формате (для группировки) +- `date_time` — точное время расчёта + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_stat }o--|| admins : "belongs_to" + admin_payroll_stat }o--|| city_store : "at_store" + + admin_payroll_stat { + int id PK + int admin_id FK + int store_id FK + string date + int year + int month + string date_time + float sum + int bath_num + } + + admins { + int id PK + string name + } + + city_store { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание записи статистики + +```php +$stat = new AdminPayrollStat(); +$stat->admin_id = $adminId; +$stat->store_id = $storeId; +$stat->date = date('Y-m-d'); +$stat->year = (int) date('Y'); +$stat->month = (int) date('n'); +$stat->date_time = date('Y-m-d H:i:s'); +$stat->sum = $calculatedSum; +$stat->bath_num = $batchNumber; +$stat->save(); +``` + +### Получение статистики за месяц + +```php +$stats = AdminPayrollStat::find() + ->where([ + 'admin_id' => $adminId, + 'year' => 2025, + 'month' => 1 + ]) + ->orderBy(['date_time' => SORT_ASC]) + ->all(); + +foreach ($stats as $stat) { + echo "{$stat->date_time}: {$stat->sum} руб. (batch #{$stat->bath_num})\n"; +} +``` + +### Сумма по магазину за период + +```php +$storeTotal = AdminPayrollStat::find() + ->where(['store_id' => $storeId, 'year' => 2025, 'month' => 1]) + ->sum('sum'); + +echo "Фонд ЗП магазина: {$storeTotal} руб."; +``` + +### Статистика по пакетам + +```php +$batchStats = AdminPayrollStat::find() + ->select(['bath_num', 'SUM(sum) as total', 'COUNT(*) as cnt']) + ->where(['year' => 2025, 'month' => 1]) + ->groupBy('bath_num') + ->asArray() + ->all(); +``` + +### Последний расчёт сотрудника + +```php +$lastStat = AdminPayrollStat::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date_time' => SORT_DESC]) + ->one(); + +if ($lastStat) { + echo "Последний расчёт: {$lastStat->date_time}, сумма: {$lastStat->sum}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `store_id` | Обязательное, целое число | +| `date` | Обязательное, макс. 100 символов | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `date_time` | Обязательное, макс. 100 символов | +| `sum` | Обязательное, число | +| `bath_num` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[AdminPayroll](./AdminPayroll.md)** — расчёт ЗП +- **[AdminPayrollHistory](./AdminPayrollHistory.md)** — история расчётов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollValues.md b/erp24/docs/models/AdminPayrollValues.md new file mode 100644 index 00000000..c0af66e6 --- /dev/null +++ b/erp24/docs/models/AdminPayrollValues.md @@ -0,0 +1,359 @@ +# Модель AdminPayrollValues + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollValues)) + Таблица БД + admin_payroll_values + Свойства + id + int + payroll_id + int + value_id + int + value_type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollValues` представляет значения зарплатных показателей в системе EAV (Entity-Attribute-Value). Хранит конкретные значения параметров зарплаты для каждой ведомости. Поддерживает полиморфное хранение данных разных типов (целые числа, вещественные числа, строки). + +**Файл модели:** `erp24/records/AdminPayrollValues.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_values` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `payroll_id` | INTEGER | ID ведомости (FK → `admin_payroll.id`) | +| `value_id` | INTEGER | ID показателя (FK → `admin_payroll_values_dict.id`) | +| `value_type` | VARCHAR(100) | Тип значения: 'int', 'float', 'string', 'none' | +| `value_int` | INTEGER | Значение целочисленного типа | +| `value_float` | FLOAT | Значение вещественного типа | +| `value_string` | TEXT | Значение строкового типа (может содержать JSON) | + +--- + +## Методы модели + +### Геттеры и сеттеры + +#### `getId(): int` +Возвращает ID записи. + +#### `setId(int $id): void` +Устанавливает ID записи. + +#### `getPayrollId(): int` +Возвращает ID ведомости. + +#### `setPayrollId(int $payroll_id): self` +Устанавливает ID ведомости. Возвращает `$this` для цепочки вызовов. + +#### `getValueId(): int` +Возвращает ID показателя из справочника. + +#### `setValueId(int $value_id): self` +Устанавливает ID показателя. + +#### `getValueType(): string` +Возвращает тип значения ('int', 'float', 'string', 'none'). + +#### `setValueType(string $value_type): self` +Устанавливает тип значения. + +#### `getValueInt(): ?int` +Возвращает целочисленное значение или null. + +#### `setValueInt(?int $value_int): void` +Устанавливает целочисленное значение. + +#### `getValueFloat(): ?float` +Возвращает вещественное значение или null. + +#### `setValueFloat(?float $value_float): void` +Устанавливает вещественное значение. + +#### `getValueString(): ?string` +Возвращает строковое значение или null. + +#### `setValueString(?string $value_string): void` +Устанавливает строковое значение. + +--- + +### Методы работы с полиморфными значениями + +#### `setValueByType($value, $valueType): void` + +Устанавливает значение в соответствующее поле по типу. + +**Параметры:** +- `$value` (mixed) — значение для сохранения +- `$valueType` (string) — тип значения ('int', 'float', 'string') + +**Логика:** +1. Формирует имя метода-сеттера: `setValue` + ucfirst($valueType) +2. Вызывает динамический сеттер: `setValueInt()`, `setValueFloat()` или `setValueString()` +3. Сохраняет значение в соответствующее поле + +**Пример:** +```php +$value = new AdminPayrollValues(); +$value->setValueByType(5000, 'int'); // вызовет setValueInt(5000) +$value->setValueByType(2500.50, 'float'); // вызовет setValueFloat(2500.50) +$value->setValueByType('примечание', 'string'); // вызовет setValueString('примечание') +``` + +--- + +#### `getValueByType(): mixed` + +Получает значение из соответствующего поля по типу. + +**Возвращает:** `mixed` — значение из поля `value_int`, `value_float` или `value_string` в зависимости от `value_type` + +**Логика:** +1. Получает текущий тип значения через `getValueType()` +2. Формирует имя метода-геттера: `getValue` + ucfirst($valueType) +3. Вызывает динамический геттер и возвращает значение + +**Пример:** +```php +$value = AdminPayrollValues::findOne(1); +$actualValue = $value->getValueByType(); +// Если value_type = 'int', вернет value_int +// Если value_type = 'float', вернет value_float +// Если value_type = 'string', вернет value_string +``` + +--- + +#### `isNumeric(): bool` + +Проверяет, является ли значение числовым типом. + +**Возвращает:** `bool` — true, если тип 'int' или 'float', иначе false + +**Логика:** +1. Определяет список числовых типов: `['int', 'float']` +2. Получает текущий тип значения +3. Проверяет наличие типа в списке числовых + +**Пример:** +```php +$value = AdminPayrollValues::findOne(1); +if ($value->isNumeric()) { + $sum += $value->getValueByType(); +} +``` + +--- + +## Статические методы + +### `getSumByAlias($adminPayrollValuesId, $payrollIds): float` + +Вычисляет сумму числовых значений для конкретного показателя по нескольким ведомостям. + +**Параметры:** +- `$adminPayrollValuesId` (int) — ID показателя из справочника +- `$payrollIds` (array) — массив ID ведомостей + +**Возвращает:** `float` — сумма значений + +**Логика:** +1. Формирует запрос для поиска всех значений по указанному показателю и списку ведомостей +2. Получает все записи +3. Инициализирует переменную суммы: `$sum = 0` +4. Для каждой записи: + - Проверяет, является ли значение числовым через `isNumeric()` + - Если да — получает значение через `getValueByType()` + - Добавляет значение к сумме +5. Возвращает итоговую сумму + +**Используется:** для агрегации показателей по нескольким ведомостям + +**Пример:** +```php +// Получить общий оклад для сотрудников за квартал +$bonusConstantId = 5; // ID показателя "Постоянная часть" +$payrollIds = [101, 102, 103]; // ID ведомостей за 3 месяца + +$totalBonus = AdminPayrollValues::getSumByAlias($bonusConstantId, $payrollIds); +echo "Общий оклад за квартал: {$totalBonus} руб."; +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `payroll_id` | Обязательное, целое число | +| `value_id` | Обязательное, целое число | +| `value_type` | Обязательное, строка, максимум 100 символов | +| `value_int` | Целое число | +| `value_float` | Число (float) | +| `value_string` | Безопасный тип (text) | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_values }o--|| admin_payroll : "belongs_to" + admin_payroll_values }o--|| admin_payroll_values_dict : "belongs_to" + + admin_payroll_values { + int id PK + int payroll_id FK + int value_id FK + string value_type + int value_int + float value_float + text value_string + } + + admin_payroll { + int id PK + int admin_id + string date + } + + admin_payroll_values_dict { + int id PK + string name + string alias + int is_active + } +``` + +--- + +## Примеры использования + +### Создание и сохранение значения + +```php +$value = new AdminPayrollValues(); +$value->setPayrollId(123) + ->setValueId(5) // ID показателя "Постоянная часть" + ->setValueType('float') + ->setValueByType(25000.00, 'float'); + +if ($value->validate() && $value->save()) { + echo "Значение сохранено"; +} +``` + +--- + +### Получение значения показателя + +```php +$value = AdminPayrollValues::find() + ->andWhere(['payroll_id' => 123]) + ->andWhere(['value_id' => 5]) + ->one(); + +if ($value) { + $actualValue = $value->getValueByType(); + echo "Постоянная часть: {$actualValue}"; +} +``` + +--- + +### Сохранение различных типов данных + +```php +// Целочисленное значение (количество дней) +$value1 = new AdminPayrollValues(); +$value1->setPayrollId(123)->setValueId(10); +$value1->setValueByType(22, 'int'); +$value1->setValueType('int'); +$value1->save(); + +// Вещественное значение (сумма премии) +$value2 = new AdminPayrollValues(); +$value2->setPayrollId(123)->setValueId(11); +$value2->setValueByType(5500.50, 'float'); +$value2->setValueType('float'); +$value2->save(); + +// Строковое значение (комментарий или JSON) +$value3 = new AdminPayrollValues(); +$value3->setPayrollId(123)->setValueId(12); +$value3->setValueByType(json_encode(['reason' => 'bonus']), 'string'); +$value3->setValueType('string'); +$value3->save(); +``` + +--- + +### Расчет суммы показателя по нескольким ведомостям + +```php +$bonusId = 5; +$payrollIds = [101, 102, 103]; + +$total = AdminPayrollValues::getSumByAlias($bonusId, $payrollIds); +echo "Итого за квартал: {$total} руб."; +``` + +--- + +### Получение всех значений ведомости + +```php +$payrollId = 123; + +$values = AdminPayrollValues::find() + ->with(['dict' => function($query) { + $query->select(['id', 'name', 'alias']); + }]) + ->andWhere(['payroll_id' => $payrollId]) + ->all(); + +foreach ($values as $value) { + $name = $value->dict->name; + $actualValue = $value->getValueByType(); + echo "{$name}: {$actualValue}\n"; +} +``` + +--- + +## Особенности реализации + +1. **EAV-паттерн**: Модель реализует паттерн Entity-Attribute-Value для гибкого хранения различных показателей +2. **Полиморфизм**: Поддержка трех типов данных (int, float, string) в одной таблице +3. **Динамические методы**: Использование динамического вызова методов через `ucfirst()` для работы с разными типами +4. **JSON-поддержка**: Строковые поля могут хранить JSON для сложных структур данных +5. **Агрегация**: Встроенный метод для суммирования числовых показателей + +--- + +## Связанные модели + +- **[AdminPayroll](./AdminPayroll.md)** — месячные ведомости зарплаты +- **[AdminPayrollValuesDict](./AdminPayrollValuesDict.md)** — справочник зарплатных показателей + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollValuesDict.md b/erp24/docs/models/AdminPayrollValuesDict.md new file mode 100644 index 00000000..35e1e653 --- /dev/null +++ b/erp24/docs/models/AdminPayrollValuesDict.md @@ -0,0 +1,329 @@ +# Модель AdminPayrollValuesDict + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollValuesDict)) + Таблица БД + admin_payroll_values_dict + Свойства + id + int + is_active + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPayrollValuesDict` представляет справочник зарплатных показателей. Определяет набор параметров, которые могут быть использованы при расчете заработной платы сотрудников. Является ключевым элементом EAV-структуры зарплатной системы, предоставляя метаданные для значений в таблице `AdminPayrollValues`. + +**Файл модели:** `erp24/records/AdminPayrollValuesDict.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_payroll_values_dict` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `is_active` | INTEGER | Статус активности показателя (0 - неактивен, 1 - активен) | +| `name` | VARCHAR(100) | Название показателя (отображаемое имя) | +| `alias` | VARCHAR(100) | Алиас показателя (системное имя для кода) | +| `first_group_id` | INTEGER | ID первой группы сотрудников (FK → `admin_group.id`) | +| `second_group_id` | INTEGER | ID второй группы сотрудников (FK → `admin_group.id`) | + +**Примеры показателей:** +- `baseSalary` — базовый оклад +- `bonusConstantSum` — постоянная часть премии +- `bonusVariableSum` — переменная часть премии +- `teamPremium` — командная премия +- `workDays` — количество рабочих дней +- `hoursWorked` — отработанные часы +- `salesSum` — сумма продаж + +--- + +## Методы модели + +### `tableName(): string` + +Возвращает имя таблицы базы данных. + +**Возвращает:** `string` — `'admin_payroll_values_dict'` + +--- + +### `rules(): array` + +Определяет правила валидации для модели. + +**Возвращает:** `array` — массив правил валидации + +**Правила:** +- `name`, `alias` — обязательные поля +- `first_group_id`, `second_group_id`, `is_active` — целые числа +- `name`, `alias` — строки, максимум 100 символов + +--- + +### `attributeLabels(): array` + +Возвращает метки атрибутов для форм. + +**Возвращает:** `array` — ассоциативный массив меток + +```php +[ + 'id' => 'ID', + 'name' => 'Name', + 'alias' => 'Alias', + 'is_active' => 'is_active', + 'first_group_id' => 'First Group ID', + 'second_group_id' => 'Second Group ID', +] +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `name` | Обязательное, строка, максимум 100 символов | +| `alias` | Обязательное, строка, максимум 100 символов | +| `is_active` | Целое число (0 или 1) | +| `first_group_id` | Целое число, может быть null | +| `second_group_id` | Целое число, может быть null | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_payroll_values_dict ||--o{ admin_payroll_values : "has_many" + admin_payroll_values_dict }o--o| admin_group : "first_group" + admin_payroll_values_dict }o--o| admin_group : "second_group" + + admin_payroll_values_dict { + int id PK + int is_active + string name + string alias UK + int first_group_id FK + int second_group_id FK + } + + admin_payroll_values { + int id PK + int payroll_id FK + int value_id FK + string value_type + } + + admin_group { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание нового показателя + +```php +$indicator = new AdminPayrollValuesDict(); +$indicator->name = 'Командная премия'; +$indicator->alias = 'teamPremium'; +$indicator->is_active = 1; +$indicator->first_group_id = 30; // GROUP_FLORIST_DAY +$indicator->second_group_id = 35; // GROUP_FLORIST_NIGHT + +if ($indicator->save()) { + echo "Показатель создан с ID: {$indicator->id}"; +} +``` + +--- + +### Получение всех активных показателей + +```php +$activeIndicators = AdminPayrollValuesDict::find() + ->where(['is_active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($activeIndicators as $indicator) { + echo "{$indicator->name} ({$indicator->alias})\n"; +} +``` + +--- + +### Получение показателей с индексацией по ID + +```php +$indicators = AdminPayrollValuesDict::find() + ->select(['id', 'alias', 'name']) + ->where(['is_active' => 1]) + ->indexBy('id') + ->asArray() + ->all(); + +// Результат: [5 => ['alias' => 'baseSalary', 'name' => 'Базовый оклад'], ...] +``` + +--- + +### Получение показателей с индексацией по алиасу + +```php +$indicators = AdminPayrollValuesDict::find() + ->select(['id', 'alias', 'name']) + ->where(['is_active' => 1]) + ->indexBy('alias') + ->asArray() + ->all(); + +// Использование: +if (isset($indicators['teamPremium'])) { + $teamPremiumId = $indicators['teamPremium']['id']; +} +``` + +--- + +### Поиск показателя по алиасу + +```php +$indicator = AdminPayrollValuesDict::find() + ->where(['alias' => 'bonusConstantSum', 'is_active' => 1]) + ->one(); + +if ($indicator) { + echo "ID показателя '{$indicator->name}': {$indicator->id}"; +} +``` + +--- + +### Получение показателей для конкретной группы + +```php +$groupId = 30; // GROUP_FLORIST_DAY + +$indicators = AdminPayrollValuesDict::find() + ->where(['is_active' => 1]) + ->andWhere([ + 'or', + ['first_group_id' => $groupId], + ['second_group_id' => $groupId], + ['and', ['first_group_id' => null], ['second_group_id' => null]] + ]) + ->all(); +``` + +--- + +### Деактивация показателя + +```php +$indicator = AdminPayrollValuesDict::findOne(['alias' => 'oldBonus']); +if ($indicator) { + $indicator->is_active = 0; + $indicator->save(); + echo "Показатель '{$indicator->name}' деактивирован"; +} +``` + +--- + +### Получение статистики использования показателей + +```php +$indicators = AdminPayrollValuesDict::find() + ->select([ + 'admin_payroll_values_dict.*', + 'COUNT(admin_payroll_values.id) as usage_count' + ]) + ->leftJoin('admin_payroll_values', 'admin_payroll_values.value_id = admin_payroll_values_dict.id') + ->where(['admin_payroll_values_dict.is_active' => 1]) + ->groupBy('admin_payroll_values_dict.id') + ->asArray() + ->all(); + +foreach ($indicators as $indicator) { + echo "{$indicator['name']}: использован {$indicator['usage_count']} раз\n"; +} +``` + +--- + +## Типичные показатели системы + +### Базовые показатели +- `baseSalary` — базовый оклад +- `monthlyPayment` — месячная оплата +- `dailyPayment` — дневная оплата +- `hourlyRate` — часовая ставка + +### Премии и надбавки +- `bonusConstantSum` — постоянная часть премии +- `bonusVariableSum` — переменная часть премии +- `teamPremium` — командная премия +- `qualityBonus` — премия за качество +- `performanceBonus` — премия за выполнение плана + +### Рабочее время +- `workDays` — количество рабочих дней +- `hoursWorked` — отработанные часы +- `overtimeHours` — сверхурочные часы +- `nightHours` — ночные часы + +### Продажи +- `salesSum` — общая сумма продаж +- `matrixPrime` — премия за матрицу +- `wrapPremium` — премия за упаковку +- `servicesPremium` — премия за услуги + +### Удержания +- `deductions` — удержания +- `fines` — штрафы +- `advances` — авансы + +--- + +## Особенности реализации + +1. **Справочник метаданных**: Модель не содержит бизнес-логики, только метаданные о показателях +2. **Алиасы**: Поле `alias` используется для программного доступа к показателям +3. **Группы сотрудников**: Поля `first_group_id` и `second_group_id` позволяют связывать показатели с определенными должностями +4. **Активность**: Флаг `is_active` позволяет отключать показатели без удаления данных +5. **Расширяемость**: Новые показатели могут быть добавлены без изменения структуры БД + +--- + +## Связанные модели + +- **[AdminPayrollValues](./AdminPayrollValues.md)** — значения зарплатных показателей +- **[AdminPayroll](./AdminPayroll.md)** — месячные ведомости зарплаты +- **[AdminGroup](./AdminGroup.md)** — группы сотрудников + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPayrollValuesDictSearch.md b/erp24/docs/models/AdminPayrollValuesDictSearch.md new file mode 100644 index 00000000..524b5a04 --- /dev/null +++ b/erp24/docs/models/AdminPayrollValuesDictSearch.md @@ -0,0 +1,143 @@ +# Класс: AdminPayrollValuesDictSearch + + +## Mindmap + +```mermaid +mindmap + root((AdminPayrollValuesDictSearch)) + Таблица БД + ActiveRecord + Наследование + extends AdminPayrollValuesDict +``` + +## Назначение +Search-модель для поиска и фильтрации записей справочника типов начислений ФОТ в ERP24. Обеспечивает поиск по названию, алиасу и группам начислений. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`AdminPayrollValuesDict` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `first_group_id`, `second_group_id` — integer +- `name`, `alias` — safe (текстовый поиск) + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос AdminPayrollValuesDict::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, first_group_id, second_group_id + - LIKE: name, alias + +## Диаграмма структуры справочника + +```mermaid +erDiagram + AdminPayrollValuesDict { + int id PK + varchar name + varchar alias + int first_group_id FK + int second_group_id FK + } + + FirstGroup ||--o{ AdminPayrollValuesDict : "first_group_id" + SecondGroup ||--o{ AdminPayrollValuesDict : "second_group_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new AdminPayrollValuesDictSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new AdminPayrollValuesDictSearch(); +$dataProvider = $searchModel->search([ + 'AdminPayrollValuesDictSearch' => [ + 'name' => 'премия', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new AdminPayrollValuesDictSearch(); +$dataProvider = $searchModel->search([ + 'AdminPayrollValuesDictSearch' => [ + 'first_group_id' => 1, + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new AdminPayrollValuesDictSearch(); +$dataProvider = $searchModel->search([ + 'AdminPayrollValuesDictSearch' => [ + 'alias' => 'bonus', + ] +]); +``` + +### GridView с фильтрацией +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'alias', + 'first_group_id', + 'second_group_id', + ], +]) ?> +``` + +## Связанные модели + +- [AdminPayrollValuesDict](./AdminPayrollValuesDict.md) — базовая модель справочника +- [AdminPayrollValues](./AdminPayrollValues.md) — значения начислений + +## Особенности реализации + +1. **Стандартный Gii-шаблон**: Типичная Search-модель +2. **LIKE-поиск**: Для текстовых полей name и alias +3. **Группировка**: Поиск по first_group_id и second_group_id +4. **Справочник**: Поиск по статическим данным конфигурации diff --git a/erp24/docs/models/AdminPersonBonuses.md b/erp24/docs/models/AdminPersonBonuses.md new file mode 100644 index 00000000..1c80ba84 --- /dev/null +++ b/erp24/docs/models/AdminPersonBonuses.md @@ -0,0 +1,314 @@ +# Модель AdminPersonBonuses + + +## Mindmap + +```mermaid +mindmap + root((AdminPersonBonuses)) + Таблица БД + admin_person_bonuses + Свойства + id + int + admin_id + int + date + string + year + int + month + int + date_time + string + Связи + Admin + 1:1 Admin + Store + 1:1 CityStore + AdminGroup + 1:1 AdminGroup + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminPersonBonuses` хранит персональные корректировки к заработной плате сотрудников: премии, вычеты, авансы, коррекции смен, отпускные и переработки. Позволяет HR вносить индивидуальные изменения к расчёту ЗП за конкретный месяц. + +**Файл модели:** `erp24/records/AdminPersonBonuses.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_person_bonuses` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admins.id) | +| `date` | VARCHAR(100) | Строковый период (YYYY-MM) | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `bonuses` | INTEGER | Премия в рублях | +| `color_ruble_bonuses` | INTEGER | Премия в цвето-рублях | +| `retention` | INTEGER | Вычет/удержание | +| `retention_comment` | TEXT | Комментарий к вычету | +| `prepaid_expense` | INTEGER | Аванс | +| `counting` | INTEGER | Подсчёт (доп. расчёт) | +| `shift_correction` | INTEGER | Коррекция смен (часы) | +| `vacation_day` | INTEGER | Оплаченные дни отпуска | +| `part_time_job_hours` | INTEGER | Часы подработки | +| `overtime` | FLOAT | Переработка (часы) | +| `date_time` | TIMESTAMP | Дата-время создания | +| `created_admin_id` | INTEGER | ID автора записи | + +--- + +## Описание полей + +### `bonuses` — Премия + +Дополнительная премия в рублях, начисляемая сверх стандартного расчёта. + +### `color_ruble_bonuses` — Премия в цвето-рублях + +Премия, выплачиваемая во внутренней валюте компании (цвето-рубли). + +### `retention` — Вычет + +Сумма удержания из заработной платы (штрафы, недостачи и т.д.). + +### `retention_comment` — Комментарий к вычету + +Обязательное обоснование причины удержания. + +### `prepaid_expense` — Аванс + +Сумма выданного аванса, которая учитывается при окончательном расчёте. + +### `shift_correction` — Коррекция смен + +Корректировка количества отработанных смен/часов (положительное или отрицательное значение). + +### `vacation_day` — Отпускные дни + +Количество оплаченных дней отпуска в данном месяце. + +### `part_time_job_hours` — Подработка + +Количество часов дополнительной работы (подработки). + +### `overtime` — Переработка + +Количество часов переработки сверх нормы. + +--- + +## Связи (Relations) + +### `getAdmin(): ActiveQuery` + +Возвращает сотрудника. + +```php +$bonus = AdminPersonBonuses::findOne($id); +$admin = $bonus->admin; // Admin +``` + +### `getStore(): ActiveQuery` + +Возвращает магазин сотрудника (через Admin). + +```php +$store = $bonus->store; // CityStore +``` + +### `getAdminGroup(): ActiveQuery` + +Возвращает должность сотрудника (через Admin). + +```php +$group = $bonus->adminGroup; // AdminGroup +``` + +--- + +## Геттеры и сеттеры + +| Метод | Описание | +|-------|----------| +| `getAdminId(): int` | Возвращает ID сотрудника | +| `setAdminId(int): void` | Устанавливает ID сотрудника | +| `getDate(): string` | Возвращает период | +| `setDate(): void` | Формирует date из year и month | +| `getYear(): int` | Возвращает год | +| `setYear(int): void` | Устанавливает год | +| `getMonth(): int` | Возвращает месяц | +| `setMonth(int): void` | Устанавливает месяц | +| `getBonuses(): ?int` | Возвращает премию | +| `setBonuses(?int): void` | Устанавливает премию | +| `getRetention(): ?int` | Возвращает вычет | +| `setRetention(?int): void` | Устанавливает вычет | +| `getPrepaidExpense(): ?int` | Возвращает аванс | +| `setPrepaidExpense(?int): void` | Устанавливает аванс | +| `getCounting(): ?int` | Возвращает доп. расчёт | +| `setCounting(?int): void` | Устанавливает доп. расчёт | +| `getDateTime(): string` | Возвращает дату-время | +| `setDateTime(): void` | Устанавливает текущее время | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_person_bonuses }o--|| admins : "belongs_to" + admin_person_bonuses }o--|| admins : "created_by" + + admin_person_bonuses { + int id PK + int admin_id FK + string date + int year + int month + int bonuses + int color_ruble_bonuses + int retention + text retention_comment + int prepaid_expense + int counting + int shift_correction + int vacation_day + int part_time_job_hours + float overtime + timestamp date_time + int created_admin_id FK + } + + admins { + int id PK + string name + int group_id + int store_id + } +``` + +--- + +## Примеры использования + +### Создание премии сотруднику + +```php +$bonus = new AdminPersonBonuses(); +$bonus->setAdminId($adminId); +$bonus->setYear(2025); +$bonus->setMonth(1); +$bonus->setDate(); +$bonus->setBonuses(5000); +$bonus->created_admin_id = Yii::$app->user->id; +$bonus->setDateTime(); +$bonus->save(); +``` + +### Создание вычета с комментарием + +```php +$penalty = new AdminPersonBonuses(); +$penalty->setAdminId($adminId); +$penalty->setYear(2025); +$penalty->setMonth(1); +$penalty->setDate(); +$penalty->setRetention(2000); +$penalty->retention_comment = 'Недостача по инвентаризации от 15.01.2025'; +$penalty->created_admin_id = Yii::$app->user->id; +$penalty->setDateTime(); +$penalty->save(); +``` + +### Получение корректировок за месяц + +```php +$adjustments = AdminPersonBonuses::find() + ->where(['admin_id' => $adminId, 'year' => 2025, 'month' => 1]) + ->all(); + +$totalBonus = 0; +$totalRetention = 0; + +foreach ($adjustments as $adj) { + $totalBonus += ($adj->bonuses ?? 0) + ($adj->color_ruble_bonuses ?? 0); + $totalRetention += $adj->retention ?? 0; +} + +echo "Премии: {$totalBonus}, Вычеты: {$totalRetention}"; +``` + +### Отчёт по отпускам + +```php +$vacations = AdminPersonBonuses::find() + ->with('admin') + ->where(['>', 'vacation_day', 0]) + ->andWhere(['year' => 2025, 'month' => 1]) + ->all(); + +foreach ($vacations as $v) { + echo "{$v->admin->name}: {$v->vacation_day} дней отпуска\n"; +} +``` + +### Расчёт переработок по магазину + +```php +$overtimes = AdminPersonBonuses::find() + ->joinWith('admin') + ->where(['admins.store_id' => $storeId]) + ->andWhere(['year' => 2025, 'month' => 1]) + ->sum('overtime'); + +echo "Всего переработок: {$overtimes} часов"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `date` | Обязательное, макс. 100 символов | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `date_time` | Обязательное | +| `created_admin_id` | Обязательное, целое число | +| `bonuses`, `retention`, `prepaid_expense` и др. | Целые числа | +| `overtime` | Число с плавающей точкой | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[AdminGroup](./AdminGroup.md)** — должности +- **[CityStore](./CityStore.md)** — магазины +- **[AdminPayroll](./AdminPayroll.md)** — расчёт ЗП +- **[EmployeeBalance](./EmployeeBalance.md)** — баланс цвето-рублей + +--- + +## Бизнес-логика + +1. HR создаёт записи для корректировки ЗП сотрудника +2. Все корректировки учитываются при финальном расчёте AdminPayroll +3. Обязателен аудит — фиксируется автор каждой записи +4. Вычеты требуют комментария-обоснования +5. Записи версионируются по месяцам + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AdminPersonBonusesSearch.md b/erp24/docs/models/AdminPersonBonusesSearch.md new file mode 100644 index 00000000..dbd0a8ba --- /dev/null +++ b/erp24/docs/models/AdminPersonBonusesSearch.md @@ -0,0 +1,235 @@ +# Класс: AdminPersonBonusesSearch + + +## Mindmap + +```mermaid +mindmap + root((AdminPersonBonusesSearch)) + Таблица БД + ActiveRecord + Наследование + extends AdminPersonBonuses +``` + +## Назначение +Search-модель для поиска и фильтрации персональных бонусов сотрудников в ERP24. Расширенная модель с поддержкой поиска по связанным таблицам (сотрудник, должность, магазин) и кастомной сортировкой. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`AdminPersonBonuses` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$adminName` | string | ФИО сотрудника (поиск по admin.name) | +| `$adminGroup` | string | Название должности (поиск по admin_group.name) | +| `$store` | string | Название магазина (поиск по city_store.name) | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `admin_id`, `year`, `month`, `vacation_day`, `bonuses`, `retention`, `prepaid_expense`, `counting`, `color_ruble_bonuses` — integer +- `adminName`, `adminGroup`, `store` — safe (текстовый поиск по связям) +- `date`, `shift_correction`, `date_time` — safe +- `overtime` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к связанным таблицам. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер с кастомной сортировкой + +**Логика:** +1. Создаёт запрос с joinWith: + - `admin a` — таблица сотрудников + - `adminGroup ag` — таблица должностей + - `store s` — таблица магазинов +2. Настраивает кастомную сортировку по связанным полям +3. Применяет фильтры по всем полям +4. Использует ilike для регистронезависимого поиска + +**Кастомные атрибуты сортировки:** +- `adminName` — сортировка по a.name +- `adminGroup` — сортировка по ag.name +- `store` — сортировка по s.name + +**Сортировка по умолчанию:** id DESC + +## Диаграмма связей + +```mermaid +erDiagram + AdminPersonBonuses { + int id PK + int admin_id FK + int year + int month + int bonuses + int vacation_day + int retention + } + + Admin { + int id PK + varchar name + int group_id FK + int store_id FK + } + + AdminGroup { + int id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + Admin ||--o{ AdminPersonBonuses : "admin_id" + AdminGroup ||--o{ Admin : "group_id" + CityStore ||--o{ Admin : "store_id" +``` + +## Диаграмма процесса поиска + +```mermaid +flowchart TD + A[Параметры поиска] --> B[AdminPersonBonusesSearch] + + B --> C[joinWith admin] + C --> D[joinWith adminGroup] + D --> E[joinWith store] + + E --> F[Настройка сортировки
по связанным полям] + F --> G[load params] + + G --> H{validate} + H -->|OK| I[andFilterWhere
основные поля] + H -->|Error| J[DataProvider без фильтров] + + I --> K[ilike фильтр
по adminName] + K --> L[ilike фильтр
по adminGroup] + L --> M[ilike фильтр
по store] + + M --> N[ActiveDataProvider] +``` + +## Примеры использования + +### Стандартный поиск в контроллере +```php +public function actionIndex() +{ + $searchModel = new AdminPersonBonusesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по имени сотрудника +```php +$searchModel = new AdminPersonBonusesSearch(); +$dataProvider = $searchModel->search([ + 'AdminPersonBonusesSearch' => [ + 'adminName' => 'Иванов', + ] +]); +``` + +### Поиск по должности и периоду +```php +$searchModel = new AdminPersonBonusesSearch(); +$dataProvider = $searchModel->search([ + 'AdminPersonBonusesSearch' => [ + 'adminGroup' => 'Флорист', + 'year' => 2024, + 'month' => 6, + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new AdminPersonBonusesSearch(); +$dataProvider = $searchModel->search([ + 'AdminPersonBonusesSearch' => [ + 'store' => 'Центральный', + ] +]); +``` + +### GridView с кастомной сортировкой +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'adminName', + 'value' => 'admin.name', + ], + [ + 'attribute' => 'adminGroup', + 'value' => 'adminGroup.name', + ], + [ + 'attribute' => 'store', + 'value' => 'store.name', + ], + 'year', + 'month', + 'bonuses', + 'vacation_day', + ], +]) ?> +``` + +### Поиск с несколькими условиями +```php +$searchModel = new AdminPersonBonusesSearch(); +$dataProvider = $searchModel->search([ + 'AdminPersonBonusesSearch' => [ + 'adminName' => 'Петров', + 'year' => 2024, + 'bonuses' => 1000, + ] +]); +``` + +## Связанные модели + +- [AdminPersonBonuses](./AdminPersonBonuses.md) — базовая модель бонусов +- [Admin](./Admin.md) — сотрудники +- [AdminGroup](./AdminGroup.md) — должности +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Расширенный поиск**: Поиск по связанным таблицам через joinWith +2. **Дополнительные свойства**: adminName, adminGroup, store для фильтрации по связям +3. **Кастомная сортировка**: setSort с атрибутами из связанных таблиц +4. **ilike**: Регистронезависимый поиск (PostgreSQL) +5. **Сортировка по умолчанию**: id DESC +6. **Алиасы таблиц**: a, ag, s для краткости diff --git a/erp24/docs/models/AdminRating.md b/erp24/docs/models/AdminRating.md index 7379d01c..e08b6acd 100644 --- a/erp24/docs/models/AdminRating.md +++ b/erp24/docs/models/AdminRating.md @@ -1,5 +1,35 @@ # Class: AdminRating + +## Mindmap + +```mermaid +mindmap + root((AdminRating)) + Таблица БД + admin_rating + Свойства + id + int + admin_id + int + rating_id + int + rating + int + administrators_count + int + date + string + Связи + Admin + 1:1 Admin + Store + 1:1 CityStore + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель AdminRating хранит расчётные данные рейтингов сотрудников (администраторов) за определённый период. Рейтинги используются для оценки эффективности работы, распределения бонусов и построения отчётов по KPI персонала. diff --git a/erp24/docs/models/AdminSearch.md b/erp24/docs/models/AdminSearch.md new file mode 100644 index 00000000..1f90d7d4 --- /dev/null +++ b/erp24/docs/models/AdminSearch.md @@ -0,0 +1,224 @@ +# Класс: AdminSearch + +## Назначение +Search-модель для поиска и фильтрации сотрудников в ERP24. Полнофункциональная модель поиска по всем атрибутам сотрудника: персональные данные, доступы, организационная структура, паспортные данные. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Admin` + +## Методы + +### rules() +**Описание:** Правила валидации для всех атрибутов поиска. + +**Возвращает:** `array` — массив правил + +**Категории полей:** + +**Integer (точное совпадение):** +- Идентификаторы: id, group_id, d_id, parent_admin_id, mentor_id, org_id, filial_id, city_id, store_id +- Доступы: dostup, sites_dostup_all, city_dostup_all, filial_dostup_all, store_dostup_all, kassa_dostup_all, sklad_dostup_all, istochnik_dostup_all +- Умолчания: kassa_default, sklad_id, istochnik_id +- Кадровые: work_status, avans_percent, kol_deti, org_id_ustroen, summa_oklad, summa_oklad_nalog, tabel_number +- Системные: posit, group_id_last, remove_admin_id + +**Safe (LIKE-поиск):** +- Персональные: guid, name, name_full, group_name, mobile, adress, description, adress_fakt, photo, avatarka +- Авторизация: login_user, pass_user +- Массивы доступов: org_arr, sites_arr, city_arr, store_arr, store_arr_guid, kassa_arr, sklad_arr, istochnik_arr, status_dostup_arr +- Паспортные: grazhdanstvo, mesto_r, adress_prozhivaniya, adress_info, passport_nomer, passport_seriya, kem_vidan, passport_kod_podrazdel, passport_mesto_rozhdeniya +- Документы: inn, snils +- Кадровые: pol, vid_zanatosti, active, tip_ustroen, vcompany +- Даты: lasttime, date_add, birthdate, data_passport, passport_srok_begin, passport_end, data_priem, data_uval, remove_date +- Прочее: ignor_post_arr, popular_modules + +**Number:** +- sale_percent — процент скидки + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с полным набором фильтров. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт базовый запрос Admin::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет andFilterWhere для числовых полей (~40 полей) +5. Применяет andFilterWhere с LIKE для текстовых полей (~35 полей) + +## Диаграмма категорий полей поиска + +```mermaid +mindmap + root((AdminSearch)) + Персональные + name + name_full + mobile + birthdate + pol + Организация + group_id + org_id + store_id + city_id + filial_id + Доступы + dostup + sites_dostup_all + store_dostup_all + kassa_dostup_all + Паспорт + passport_nomer + passport_seriya + inn + snils + Кадровые + work_status + data_priem + data_uval + tabel_number + Авторизация + login_user + active +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new AdminSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по ФИО +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'name' => 'Иванов', + ] +]); +``` + +### Поиск по должности и магазину +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'group_id' => 5, // Флорист + 'store_id' => 10, + ] +]); +``` + +### Поиск активных сотрудников +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'active' => '1', + 'work_status' => 1, + ] +]); +``` + +### Поиск по телефону +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'mobile' => '9001234567', + ] +]); +``` + +### Поиск по ИНН +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'inn' => '123456789012', + ] +]); +``` + +### Поиск уволенных сотрудников +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'work_status' => 0, // Уволен + ] +]); +``` + +### GridView с основными полями +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'group_name', + 'mobile', + 'store_id', + [ + 'attribute' => 'active', + 'filter' => ['1' => 'Активен', '0' => 'Неактивен'], + ], + 'data_priem', + ], +]) ?> +``` + +### Комплексный поиск +```php +$searchModel = new AdminSearch(); +$dataProvider = $searchModel->search([ + 'AdminSearch' => [ + 'name' => 'Петров', + 'city_id' => 1, + 'active' => '1', + 'group_id' => 5, + ] +]); +``` + +## Связанные модели + +- [Admin](./Admin.md) — базовая модель сотрудника +- [AdminGroup](./AdminGroup.md) — должности (group_id) +- [CityStore](./CityStore.md) — магазины (store_id) +- [City](./City.md) — города (city_id) +- [Company](./Company.md) — организации (org_id) + +## Особенности реализации + +1. **Полный набор полей**: ~75 атрибутов для поиска +2. **Категоризация правил**: integer для ID, safe для текста, number для дробных +3. **LIKE-поиск**: Для всех текстовых полей +4. **Паспортные данные**: Поиск по номеру, серии, ИНН, СНИЛС +5. **Массивы доступов**: LIKE-поиск по store_arr, city_arr и др. +6. **Стандартный Gii-шаблон**: Автогенерация со всеми полями модели diff --git a/erp24/docs/models/AdminStores.md b/erp24/docs/models/AdminStores.md new file mode 100644 index 00000000..2975b0f7 --- /dev/null +++ b/erp24/docs/models/AdminStores.md @@ -0,0 +1,225 @@ +# Модель AdminStores + + +## Mindmap + +```mermaid +mindmap + root((AdminStores)) + Таблица БД + admin_stores + Свойства + id + int + admin_id + int + store_id + int + store_guid + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AdminStores` представляет связь между сотрудниками и магазинами. Определяет, к каким магазинам прикреплён сотрудник. Используется для ограничения доступа сотрудников к данным только своих магазинов и для формирования отчётов по сотрудникам в разрезе магазинов. + +**Файл модели:** `erp24/records/AdminStores.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `admin_stores` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admin) | +| `store_id` | INTEGER | ID магазина (FK → city_store) | +| `store_guid` | VARCHAR(36) | GUID склада из 1С (FK → products_1c) | + +--- + +## Описание полей + +### `admin_id` — Сотрудник + +Идентификатор сотрудника из таблицы `admin`. + +**Связь:** `admin.id` + +### `store_id` — Магазин + +Идентификатор магазина из таблицы `city_store`. + +**Связь:** `city_store.id` + +### `store_guid` — GUID склада + +GUID склада из системы 1С для интеграции с товарными остатками. + +**Формат:** UUID (36 символов) + +**Пример:** `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + admin_stores }o--|| admin : "belongs_to" + admin_stores }o--|| city_store : "refers_to" + admin_stores }o--o| products_1c : "linked_guid" + + admin_stores { + int id PK + int admin_id FK + int store_id FK + string store_guid FK + } + + admin { + int id PK + string name + int admin_group_id + } + + city_store { + int id PK + string name + int cluster_id + } + + products_1c { + string id PK + string name + string tip + } +``` + +--- + +## Примеры использования + +### Получение магазинов сотрудника + +```php +$adminId = Yii::$app->user->id; +$storeIds = AdminStores::find() + ->select('store_id') + ->where(['admin_id' => $adminId]) + ->column(); + +$stores = CityStore::find() + ->where(['id' => $storeIds]) + ->all(); +``` + +### Проверка доступа сотрудника к магазину + +```php +$hasAccess = AdminStores::find() + ->where([ + 'admin_id' => $adminId, + 'store_id' => $storeId + ]) + ->exists(); + +if (!$hasAccess) { + throw new ForbiddenHttpException('Нет доступа к этому магазину'); +} +``` + +### Привязка сотрудника к магазину + +```php +$adminStore = new AdminStores(); +$adminStore->admin_id = $adminId; +$adminStore->store_id = $storeId; +$adminStore->store_guid = $store->guid; // GUID из 1С +$adminStore->save(); +``` + +### Получение сотрудников магазина + +```php +$storeId = 5; +$adminIds = AdminStores::find() + ->select('admin_id') + ->where(['store_id' => $storeId]) + ->column(); + +$employees = Admin::find() + ->where(['id' => $adminIds]) + ->all(); +``` + +### Удаление привязки + +```php +AdminStores::deleteAll([ + 'admin_id' => $adminId, + 'store_id' => $storeId +]); +``` + +### Массовое назначение магазинов + +```php +$adminId = 10; +$storeIds = [1, 2, 3, 5]; + +// Удаляем старые привязки +AdminStores::deleteAll(['admin_id' => $adminId]); + +// Создаём новые +foreach ($storeIds as $storeId) { + $store = CityStore::findOne($storeId); + $adminStore = new AdminStores(); + $adminStore->admin_id = $adminId; + $adminStore->store_id = $storeId; + $adminStore->store_guid = $store->guid ?? null; + $adminStore->save(); +} +``` + +### Фильтрация данных по доступным магазинам + +```php +// Продажи только по доступным сотруднику магазинам +$userStores = AdminStores::find() + ->select('store_id') + ->where(['admin_id' => Yii::$app->user->id]); + +$sales = Sales::find() + ->where(['store_id' => $userStores]) + ->andWhere(['>=', 'date', $startDate]) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `store_id` | Обязательное, целое число | +| `store_guid` | Макс. 36 символов | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[Products1c](./Products1c.md)** — номенклатура 1С (для GUID склада) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AlertReceiverType.md b/erp24/docs/models/AlertReceiverType.md new file mode 100644 index 00000000..ba533f8f --- /dev/null +++ b/erp24/docs/models/AlertReceiverType.md @@ -0,0 +1,247 @@ +# Модель AlertReceiverType + + +## Mindmap + +```mermaid +mindmap + root((AlertReceiverType)) + Таблица БД + alert_receiver_type + Свойства + id + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AlertReceiverType` представляет справочник типов получателей уведомлений. Определяет категории сотрудников или групп, которые могут получать оповещения системы: руководители, ответственные за процесс, исполнители и т.д. Используется для настройки правил рассылки уведомлений. + +**Файл модели:** `erp24/records/AlertReceiverType.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `alert_receiver_type` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(100) | Название типа получателя | +| `alias` | VARCHAR(100) | Алиас для программного использования (уникальный) | + +--- + +## Описание полей + +### `name` — Название + +Человекочитаемое название типа получателя: +- "Руководитель магазина" +- "Ответственный за процесс" +- "Исполнитель задачи" +- "Все сотрудники отдела" + +### `alias` — Алиас + +Уникальный программный идентификатор для использования в коде: +- `store_manager` +- `process_owner` +- `task_executor` +- `department_all` + +Поле имеет уникальный индекс для предотвращения дублирования. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + alert_receiver_type ||--o{ alert_rule : "defines_receiver" + + alert_receiver_type { + int id PK + string name + string alias UK + } + + alert_rule { + int id PK + int receiver_type_id FK + string event_type + string channel + } +``` + +--- + +## Примеры использования + +### Создание типа получателя + +```php +$type = new AlertReceiverType(); +$type->name = 'Региональный менеджер'; +$type->alias = 'regional_manager'; +$type->save(); +``` + +### Получение всех типов + +```php +$types = AlertReceiverType::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($types as $type) { + echo "{$type->name} ({$type->alias})\n"; +} +``` + +### Поиск по алиасу + +```php +$storeManager = AlertReceiverType::findOne(['alias' => 'store_manager']); + +if ($storeManager) { + echo "ID типа 'Руководитель магазина': {$storeManager->id}"; +} +``` + +### Построение выпадающего списка + +```php +$list = AlertReceiverType::find() + ->select(['name', 'id']) + ->indexBy('id') + ->column(); + +echo Html::dropDownList('receiver_type_id', $selectedId, $list, [ + 'class' => 'form-control', + 'prompt' => 'Выберите тип получателя' +]); +``` + +### Использование в правилах оповещений + +```php +// Создание правила оповещения +$rule = new AlertRule(); +$rule->event_type = 'task_overdue'; +$rule->receiver_type_id = AlertReceiverType::findOne(['alias' => 'process_owner'])->id; +$rule->channel = 'telegram'; +$rule->save(); +``` + +### Проверка существования алиаса + +```php +$alias = 'new_type'; + +if (AlertReceiverType::findOne(['alias' => $alias])) { + echo "Алиас '{$alias}' уже используется"; +} else { + echo "Алиас '{$alias}' свободен"; +} +``` + +### Получение получателей по типу + +```php +function getReceiversByType($alias, $context = []) { + $type = AlertReceiverType::findOne(['alias' => $alias]); + + if (!$type) { + return []; + } + + switch ($alias) { + case 'store_manager': + return Admin::find() + ->where(['store_id' => $context['store_id'], 'is_manager' => 1]) + ->all(); + + case 'task_executor': + return Admin::find() + ->where(['id' => $context['executor_id']]) + ->all(); + + case 'department_all': + return Admin::find() + ->where(['department_id' => $context['department_id']]) + ->all(); + + default: + return []; + } +} + +$managers = getReceiversByType('store_manager', ['store_id' => 15]); +``` + +### Массовое создание типов + +```php +$types = [ + ['name' => 'Создатель задачи', 'alias' => 'task_creator'], + ['name' => 'Наблюдатель', 'alias' => 'observer'], + ['name' => 'Администратор системы', 'alias' => 'system_admin'], +]; + +foreach ($types as $data) { + $type = new AlertReceiverType(); + $type->name = $data['name']; + $type->alias = $data['alias']; + + if (!$type->save()) { + echo "Ошибка при создании {$data['alias']}: " . implode(', ', $type->getFirstErrors()) . "\n"; + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 100 символов | +| `alias` | Обязательное, макс. 100 символов, уникальное | + +--- + +## Типичные типы получателей + +| Алиас | Название | Описание | +|-------|----------|----------| +| `task_creator` | Создатель задачи | Тот, кто создал задачу | +| `task_executor` | Исполнитель задачи | Назначенный исполнитель | +| `store_manager` | Руководитель магазина | Управляющий торговой точкой | +| `process_owner` | Ответственный за процесс | Владелец бизнес-процесса | +| `regional_manager` | Региональный менеджер | Руководитель региона | +| `department_head` | Начальник отдела | Руководитель подразделения | +| `system_admin` | Администратор | Системный администратор | +| `all_participants` | Все участники | Все связанные с сущностью | + +--- + +## Связанные модели + +- **[AlertRule](./AlertRule.md)** — правила оповещений +- **[Notification](./Notification.md)** — уведомления +- **[Admin](./Admin.md)** — сотрудники-получатели +- **[Task](./Task.md)** — задачи (источник событий) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AnalystsBusinessOperations.md b/erp24/docs/models/AnalystsBusinessOperations.md new file mode 100644 index 00000000..9afc99d4 --- /dev/null +++ b/erp24/docs/models/AnalystsBusinessOperations.md @@ -0,0 +1,283 @@ +# Модель AnalystsBusinessOperations + + +## Mindmap + +```mermaid +mindmap + root((AnalystsBusinessOperations)) + Таблица БД + analysts_business_operations + Свойства + id + string + name + string + type + int + show + int + created_at + string + Связи + TypeRel + 1:1 AnalystsBusinessOperationsTypes + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `AnalystsBusinessOperations` представляет справочник аналитик хозяйственных операций, синхронизированных с 1С. Хранит информацию о видах операций: списания, оприходования, пересорт и др. Используется для классификации складских и бухгалтерских операций в системе ERP. + +**Файл модели:** `erp24/records/AnalystsBusinessOperations.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `analysts_business_operations` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(255) | GUID аналитики (первичный ключ из 1С) | +| `name` | VARCHAR(255) | Название аналитики | +| `type` | INTEGER | Вид использования хозяйственной операции | +| `type_id` | INTEGER | ID вида (FK → analysts_business_operations_types.id) | +| `active` | INTEGER | Активность записи (1 — активна, 0 — неактивна) | +| `show` | INTEGER | Видимость типа бизнес-операции в интерфейсе | +| `created_at` | TIMESTAMP | Дата создания записи | + +--- + +## Константы + +### GUID специальных операций + +```php +const TYPE_ID_WAYBILL_WRITE_OFFS = '0ee028a1-a379-11f0-84ee-ac1f6b1b7573'; // Списание при передаче смены +const TYPE_ID_WAYBILL_INCOMING = '16d5f647-a379-11f0-84ee-ac1f6b1b7573'; // Оприходование при передаче смены +const TYPE_ID_REPLACEMENT = '23f57c92-a379-11f0-84ee-ac1f6b1b7573'; // Пересорт при передаче смены +``` + +--- + +## Описание полей + +### `id` — GUID + +Строковый первичный ключ в формате UUID, синхронизированный с 1С. Обеспечивает уникальную идентификацию операций между системами. + +### `type` — Вид использования + +Числовой код, определяющий контекст применения операции: +- В документах списания +- В документах оприходования +- В пересортице +- Универсальные операции + +### `show` — Видимость + +Определяет, отображается ли операция в интерфейсе выбора: +- `1` — отображается в списках выбора +- `0` — скрыта (используется только программно) + +--- + +## Связи (Relations) + +### `getTypeRel(): ActiveQuery` + +Возвращает тип бизнес-операции. + +```php +$operation = AnalystsBusinessOperations::findOne($id); +$type = $operation->typeRel; // AnalystsBusinessOperationsTypes +echo "Тип: {$type->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + analysts_business_operations }o--|| analysts_business_operations_types : "belongs_to" + analysts_business_operations ||--o{ waybill_write_offs : "used_in" + analysts_business_operations ||--o{ waybill_incoming : "used_in" + + analysts_business_operations { + string id PK "GUID" + string name + int type + int type_id FK + int active + int show + timestamp created_at + } + + analysts_business_operations_types { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение операции по GUID + +```php +$writeOff = AnalystsBusinessOperations::findOne( + AnalystsBusinessOperations::TYPE_ID_WAYBILL_WRITE_OFFS +); + +if ($writeOff) { + echo "Операция: {$writeOff->name}"; +} +``` + +### Получение активных и видимых операций + +```php +$operations = AnalystsBusinessOperations::find() + ->where(['active' => 1, 'show' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($operations as $op) { + echo "{$op->name}\n"; +} +``` + +### Построение выпадающего списка + +```php +$list = AnalystsBusinessOperations::find() + ->select(['name', 'id']) + ->where(['active' => 1, 'show' => 1]) + ->indexBy('id') + ->column(); + +echo Html::dropDownList('operation_id', $selectedId, $list, [ + 'class' => 'form-control', + 'prompt' => 'Выберите операцию' +]); +``` + +### Фильтрация по типу + +```php +// Операции определённого типа +$typeOperations = AnalystsBusinessOperations::find() + ->where(['type_id' => $typeId, 'active' => 1]) + ->all(); +``` + +### Использование в документе списания + +```php +$waybill = new WaybillWriteOffs(); +$waybill->analysts_id = AnalystsBusinessOperations::TYPE_ID_WAYBILL_WRITE_OFFS; +$waybill->store_id = $storeId; +$waybill->date = date('Y-m-d'); +$waybill->save(); +``` + +### Проверка типа операции + +```php +$operationId = $document->analysts_id; + +switch ($operationId) { + case AnalystsBusinessOperations::TYPE_ID_WAYBILL_WRITE_OFFS: + echo "Это списание при передаче смены"; + break; + case AnalystsBusinessOperations::TYPE_ID_WAYBILL_INCOMING: + echo "Это оприходование при передаче смены"; + break; + case AnalystsBusinessOperations::TYPE_ID_REPLACEMENT: + echo "Это пересорт"; + break; + default: + $operation = AnalystsBusinessOperations::findOne($operationId); + echo "Операция: {$operation->name}"; +} +``` + +### Синхронизация с 1С + +```php +// Обновление или создание записи из 1С +$data = $response1c['operation']; + +$operation = AnalystsBusinessOperations::findOne($data['guid']); + +if (!$operation) { + $operation = new AnalystsBusinessOperations(); + $operation->id = $data['guid']; +} + +$operation->name = $data['name']; +$operation->type = $data['type']; +$operation->type_id = $data['type_id'] ?? 1; +$operation->active = $data['active'] ?? 1; +$operation->show = $data['show'] ?? 0; +$operation->save(); +``` + +### Статистика использования операций + +```php +// Подсчёт документов по операциям +$stats = WaybillWriteOffs::find() + ->select(['analysts_id', 'COUNT(*) as count']) + ->groupBy('analysts_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $operation = AnalystsBusinessOperations::findOne($row['analysts_id']); + echo "{$operation->name}: {$row['count']} документов\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `id` | Обязательное, макс. 255 символов, уникальное | +| `name` | Обязательное, макс. 255 символов | +| `type` | Обязательное, целое число | +| `type_id` | Целое число, по умолчанию 1 | +| `active` | Целое число, по умолчанию null | +| `show` | Целое число, по умолчанию 0 | +| `created_at` | Безопасное | + +--- + +## Типичные операции + +| GUID | Название | Описание | +|------|----------|----------| +| `0ee028a1-...` | Списание при передаче смены | Списание остатков при закрытии смены | +| `16d5f647-...` | Оприходование при передаче смены | Оприходование новых поступлений | +| `23f57c92-...` | Пересорт при передаче смены | Корректировка пересорта | + +--- + +## Связанные модели + +- **[AnalystsBusinessOperationsTypes](./AnalystsBusinessOperationsTypes.md)** — типы операций +- **[WaybillWriteOffs](./WaybillWriteOffs.md)** — документы списания +- **[WaybillIncoming](./WaybillIncoming.md)** — документы оприходования +- **[Products1c](./Products1c.md)** — товары + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/AnalystsBusinessOperationsTypes.md b/erp24/docs/models/AnalystsBusinessOperationsTypes.md new file mode 100644 index 00000000..5566b653 --- /dev/null +++ b/erp24/docs/models/AnalystsBusinessOperationsTypes.md @@ -0,0 +1,156 @@ +# Класс: AnalystsBusinessOperationsTypes + + +## Mindmap + +```mermaid +mindmap + root((AnalystsBusinessOperationsTypes)) + Таблица БД + analysts_business_operations_types + Свойства + id + int + code + int + created_at + string + Связи + TypeRel + 1:1 AnalystsBusinessOperationsTypes + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов бизнес-операций для аналитической системы ERP24. Хранит классификацию операций, используемых при анализе бизнес-процессов, с поддержкой уникальных кодов и алиасов для идентификации типов операций в системе отчётности и аналитики. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`analysts_business_operations_types` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `code` | int | Уникальный числовой код типа операции (0, 1, 2, ...) | +| `alias` | string(255) / null | Текстовый алиас типа операции для использования в коде | +| `name` | string(255) / null | Человекочитаемое название типа операции | +| `created_by` | int / null | ID сотрудника, создавшего запись | +| `updated_by` | int / null | ID сотрудника, обновившего запись | +| `created_at` | timestamp | Дата и время создания записи | +| `updated_at` | timestamp / null | Дата и время последнего обновления | + +## Индексы и ограничения + +- **PRIMARY KEY**: `id` +- **UNIQUE**: `code` — каждый код типа операции уникален + +## Связи (Relations) + +### getTypeRel() +Возвращает связь на саму себя (возможно, для иерархических типов операций). + +```php +public function getTypeRel(): \yii\db\ActiveQuery +``` + +**Возвращает**: `hasOne(AnalystsBusinessOperationsTypes::class, ['id' => 'type_id'])` + +## Диаграмма связей + +```mermaid +erDiagram + AnalystsBusinessOperationsTypes { + int id PK + int code UK + varchar alias + varchar name + int created_by FK + int updated_by FK + timestamp created_at + timestamp updated_at + } + + AnalystsBusinessOperations { + int id PK + int type_id FK + } + + Admin { + int id PK + } + + AnalystsBusinessOperations }o--|| AnalystsBusinessOperationsTypes : "type_id" + AnalystsBusinessOperationsTypes }o--o| Admin : "created_by" + AnalystsBusinessOperationsTypes }o--o| Admin : "updated_by" +``` + +## Примеры использования + +### Получение типа операции по коду +```php +$type = AnalystsBusinessOperationsTypes::find() + ->where(['code' => 1]) + ->one(); + +echo $type->name; // Название операции +``` + +### Создание нового типа операции +```php +$type = new AnalystsBusinessOperationsTypes(); +$type->code = 3; +$type->alias = 'inventory_adjustment'; +$type->name = 'Корректировка инвентаризации'; +$type->save(); +``` + +### Получение всех типов операций +```php +$types = AnalystsBusinessOperationsTypes::find() + ->indexBy('code') + ->all(); + +// Доступ по коду +$typeName = $types[1]->name; +``` + +### Получение списка для выпадающего списка +```php +$typesList = ArrayHelper::map( + AnalystsBusinessOperationsTypes::find()->all(), + 'code', + 'name' +); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `code` | required, integer, unique | +| `alias` | string (max 255), nullable | +| `name` | string (max 255), nullable | +| `created_by` | integer, nullable | +| `updated_by` | integer, nullable | +| `created_at` | safe (автоматически) | +| `updated_at` | safe (автоматически) | + +## Связанные модели + +- [AnalystsBusinessOperations](./AnalystsBusinessOperations.md) — бизнес-операции с данным типом +- [Admin](./Admin.md) — сотрудники, создающие/редактирующие типы + +## Особенности реализации + +1. **Уникальность кода**: Каждый тип операции имеет уникальный числовой код, что позволяет использовать его как идентификатор в интеграциях +2. **Алиас для кода**: Поле `alias` позволяет использовать осмысленные строковые идентификаторы вместо числовых кодов +3. **Аудит изменений**: Поддерживается tracking создания и обновления записей (created_by/updated_by, created_at/updated_at) +4. **Справочные данные**: Модель предназначена для хранения относительно статичных справочных данных diff --git a/erp24/docs/models/ApiCron.md b/erp24/docs/models/ApiCron.md new file mode 100644 index 00000000..ded6f930 --- /dev/null +++ b/erp24/docs/models/ApiCron.md @@ -0,0 +1,570 @@ +# Class: ApiCron + +## 🧠 Mindmap: Модель ApiCron + +```mermaid +mindmap + root((ApiCron)) + Идентификация + id PK автоинкремент + request_id GUID запроса + Данные задачи + json_post JSON данные + date дата создания + date_up дата обновления из 1С + Статус выполнения + status состояние + 0 Ожидает выполнения + 1 Выполнено + Константы + AWAITING_COMPLETION + COMPLETE +``` + +--- + +## Назначение + +Модель задач Cron для API в системе ERP24. Хранит очередь задач для отложенного выполнения через планировщик Cron. Используется для асинхронной обработки данных, поступающих из внешних систем (например, 1С). + +Задачи создаются со статусом "Ожидает выполнения" и обрабатываются фоновыми процессами. После успешной обработки статус меняется на "Выполнено". Поддерживает хранение произвольных JSON данных для передачи параметров задачи. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`api_cron` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ, автоинкремент | +| `request_id` | string(36) | **Уникальный ID запроса** (GUID, обязательное) | + +### Данные задачи + +| Имя | Тип | Описание | +|-----|-----|----------| +| `json_post` | text | **JSON данные задачи** (параметры для обработки, обязательное) | +| `date` | datetime | **Дата создания задачи** (обязательное) | +| `date_up` | datetime | **Дата обновления из 1С** (когда данные синхронизированы) | + +### Статус выполнения + +| Имя | Тип | Описание | +|-----|-----|----------| +| `status` | int | **Статус задачи**: 0 - ожидает, 1 - выполнено | + +--- + +## Константы + +```php +const AWAITING_COMPLETION = 0; // Ожидает выполнения +const COMPLETE = 1; // Выполнено +``` + +### getStatus() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `array` — массив статусов +**Описание:** Возвращает массив с описаниями статусов на русском языке + +**Логика работы:** +Статический метод, возвращающий ассоциативный массив, где ключи - числовые константы статусов, а значения - их текстовые описания на русском языке. Используется для отображения статусов в интерфейсе. + +**Пример:** +```php +$statuses = ApiCron::getStatus(); +echo $statuses[0]; // "Ожидает выполнения" +echo $statuses[1]; // "Выполнено" + +// Использование в форме +$task = new ApiCron(); +echo $statuses[$task->status]; // Отобразить текущий статус +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'date', // дата создания + 'json_post', // JSON данные + 'request_id' // уникальный ID +] +``` + +### Типы данных +```php +['date', 'date_up'] // safe (datetime) +['status'] // integer +['json_post'] // text +['request_id'] // string max:36 (GUID) +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'api_cron' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = ApiCron::tableName(); // 'api_cron' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательные поля: date, json_post, request_id +2. Безопасные поля для временных меток (datetime) +3. Тип данных integer для status +4. Тип данных text для json_post +5. Ограничение длины request_id (36 символов для GUID) + +**Пример:** +```php +$task = new ApiCron(); +$task->date = date('Y-m-d H:i:s'); +$task->json_post = json_encode(['action' => 'sync', 'entity' => 'orders']); +$task->request_id = \yii\helpers\StringHelper::uuid(); +$task->status = ApiCron::AWAITING_COMPLETION; +if ($task->validate()) { + $task->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив с названиями атрибутов на английском языке. Используется в формах и сообщениях об ошибках. Поле date_up имеет комментарий на русском в PHPDoc, указывающий на его связь с синхронизацией из 1С. + +**Пример:** +```php +$labels = (new ApiCron())->attributeLabels(); +echo $labels['date']; // "Date" +echo $labels['status']; // "Status" +``` + +--- + +## Примеры использования + +### Создание новой задачи + +```php +use yii_app\records\ApiCron; + +// Создание задачи для обработки +$task = new ApiCron(); +$task->date = date('Y-m-d H:i:s'); +$task->json_post = json_encode([ + 'action' => 'import_orders', + 'store_id' => 'uuid-store-123', + 'date_from' => '2025-01-01', + 'date_to' => '2025-01-10' +]); +$task->request_id = \yii\helpers\StringHelper::uuid(); +$task->status = ApiCron::AWAITING_COMPLETION; +$task->save(); + +echo "Task created with ID: {$task->id}\n"; +``` + +### Получение задач для обработки + +```php +// Выбираем все необработанные задачи +$pendingTasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->orderBy(['date' => SORT_ASC]) // FIFO: первым пришел - первым обработан + ->all(); + +echo "Found " . count($pendingTasks) . " pending tasks\n"; + +foreach ($pendingTasks as $task) { + echo "Task ID: {$task->id}, Created: {$task->date}\n"; +} +``` + +### Обработка задачи + +```php +// Cron скрипт +$tasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->limit(10) // обрабатываем по 10 задач за раз + ->all(); + +foreach ($tasks as $task) { + try { + // Декодируем JSON данные + $data = json_decode($task->json_post, true); + + // Выполняем действие + switch ($data['action']) { + case 'import_orders': + $this->importOrders($data); + break; + case 'sync_products': + $this->syncProducts($data); + break; + default: + throw new \Exception("Unknown action: {$data['action']}"); + } + + // Помечаем как выполненную + $task->status = ApiCron::COMPLETE; + $task->date_up = date('Y-m-d H:i:s'); // фиксируем время обработки + $task->save(); + + echo "Task {$task->id} completed\n"; + + } catch (\Exception $e) { + echo "Task {$task->id} failed: " . $e->getMessage() . "\n"; + // Задача остается со статусом AWAITING_COMPLETION для повторной попытки + } +} +``` + +### Поиск задач по request_id + +```php +$requestId = 'specific-request-uuid'; +$task = ApiCron::findOne(['request_id' => $requestId]); + +if ($task) { + $statuses = ApiCron::getStatus(); + echo "Task status: {$statuses[$task->status]}\n"; + echo "Created: {$task->date}\n"; + if ($task->date_up) { + echo "Completed: {$task->date_up}\n"; + } + echo "Data: {$task->json_post}\n"; +} else { + echo "Task not found\n"; +} +``` + +### Статистика задач + +```php +// Количество ожидающих задач +$awaitingCount = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->count(); + +// Количество выполненных задач +$completedCount = ApiCron::find() + ->where(['status' => ApiCron::COMPLETE]) + ->count(); + +// Всего задач +$totalCount = ApiCron::find()->count(); + +echo "Task statistics:\n"; +echo "Awaiting: {$awaitingCount}\n"; +echo "Completed: {$completedCount}\n"; +echo "Total: {$totalCount}\n"; +echo "Completion rate: " . round(($completedCount / $totalCount) * 100, 2) . "%\n"; +``` + +### Удаление старых выполненных задач + +```php +// Удаляем выполненные задачи старше 30 дней +$date = date('Y-m-d', strtotime('-30 days')); +$deleted = ApiCron::deleteAll([ + 'and', + ['status' => ApiCron::COMPLETE], + ['<', 'date', $date] +]); + +echo "Deleted {$deleted} old completed tasks\n"; +``` + +### Повторная обработка зависших задач + +```php +// Задачи, которые ждут обработки более 1 часа +$stuckTasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->andWhere(['<', 'date', date('Y-m-d H:i:s', strtotime('-1 hour'))]) + ->all(); + +echo "Found " . count($stuckTasks) . " stuck tasks\n"; + +foreach ($stuckTasks as $task) { + echo "Stuck task ID: {$task->id}, Created: {$task->date}\n"; + // Можно добавить логику повторной обработки или уведомления +} +``` + +### Группировка задач по типу действия + +```php +$tasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->all(); + +$grouped = []; +foreach ($tasks as $task) { + $data = json_decode($task->json_post, true); + $action = $data['action'] ?? 'unknown'; + + if (!isset($grouped[$action])) { + $grouped[$action] = []; + } + $grouped[$action][] = $task; +} + +echo "Tasks grouped by action:\n"; +foreach ($grouped as $action => $actionTasks) { + echo "{$action}: " . count($actionTasks) . " tasks\n"; +} +``` + +### Мониторинг времени обработки + +```php +$completedTasks = ApiCron::find() + ->where(['status' => ApiCron::COMPLETE]) + ->andWhere(['not', ['date_up' => null]]) + ->limit(100) + ->all(); + +$totalTime = 0; +$count = 0; + +foreach ($completedTasks as $task) { + $created = strtotime($task->date); + $completed = strtotime($task->date_up); + $processingTime = $completed - $created; + + $totalTime += $processingTime; + $count++; +} + +if ($count > 0) { + $avgTime = $totalTime / $count; + echo "Average processing time: " . round($avgTime, 2) . " seconds\n"; + echo "Total tasks analyzed: {$count}\n"; +} +``` + +### Создание задачи из 1С + +```php +// API endpoint для получения данных из 1С +public function actionReceiveFrom1C() +{ + $data = Yii::$app->request->post(); + + // Создаем задачу для отложенной обработки + $task = new ApiCron(); + $task->date = date('Y-m-d H:i:s'); + $task->json_post = json_encode($data); + $task->request_id = $data['request_id'] ?? \yii\helpers\StringHelper::uuid(); + $task->status = ApiCron::AWAITING_COMPLETION; + $task->save(); + + // Возвращаем подтверждение + return [ + 'success' => true, + 'task_id' => $task->id, + 'request_id' => $task->request_id, + 'message' => 'Task queued for processing' + ]; +} +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + ApiCron { + int id PK + int status "0=awaiting 1=complete" + datetime date "Creation date" + datetime date_up "Completion date" + text json_post "JSON task data" + string request_id "GUID" + } +``` + +```mermaid +stateDiagram-v2 + [*] --> AWAITING_COMPLETION: Создание задачи + AWAITING_COMPLETION --> COMPLETE: Успешная обработка + AWAITING_COMPLETION --> AWAITING_COMPLETION: Ошибка (повтор) + COMPLETE --> [*]: Архивация +``` + +--- + +## Бизнес-логика + +### Асинхронная обработка + +Модель ApiCron реализует паттерн очереди задач (Queue): + +1. **Создание задачи**: API endpoint получает данные и создает запись со статусом AWAITING_COMPLETION +2. **Обработка**: Cron скрипт выбирает необработанные задачи и выполняет их +3. **Завершение**: После успешной обработки статус меняется на COMPLETE, заполняется date_up +4. **Архивация**: Старые выполненные задачи периодически удаляются + +### Типичные сценарии использования + +**Импорт данных из 1С:** +```json +{ + "action": "import_orders", + "store_id": "uuid-123", + "date_from": "2025-01-01", + "date_to": "2025-01-10" +} +``` + +**Синхронизация продуктов:** +```json +{ + "action": "sync_products", + "category": "flowers", + "force": true +} +``` + +**Обновление остатков:** +```json +{ + "action": "update_stock", + "store_id": "uuid-456", + "products": [ + {"id": "prod-1", "quantity": 10}, + {"id": "prod-2", "quantity": 5} + ] +} +``` + +### Обработка ошибок + +При ошибке обработки задача остается со статусом AWAITING_COMPLETION и будет повторно обработана при следующем запуске Cron. Рекомендуется: + +1. Логировать ошибки в ApiErrorLog +2. Ограничивать количество повторных попыток +3. Добавить поле retry_count для отслеживания попыток + +--- + +## Связи с другими моделями + +Модель ApiCron не имеет явных relations, но логически связана с: + +- **ApiLogs** — логирование API запросов +- **ApiErrorLog** — логирование ошибок обработки +- Внешние системы (1С) — источник задач + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (id) + +-- Поиск необработанных задач +CREATE INDEX idx_api_cron_status ON api_cron(status); + +-- Поиск по request_id +CREATE INDEX idx_api_cron_request_id ON api_cron(request_id); + +-- Поиск по дате создания +CREATE INDEX idx_api_cron_date ON api_cron(date); + +-- Композитный для выборки задач +CREATE INDEX idx_api_cron_status_date ON api_cron(status, date); +``` + +### Оптимизация обработки + +```php +// Плохо: обработка всех задач за раз (может быть очень долго) +$tasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->all(); + +// Хорошо: обработка порциями с лимитом +$batchSize = 10; +$tasks = ApiCron::find() + ->where(['status' => ApiCron::AWAITING_COMPLETION]) + ->orderBy(['date' => SORT_ASC]) + ->limit($batchSize) + ->all(); +``` + +--- + +## Замечания + +1. **Первичный ключ** — автоинкремент ID +2. **request_id** — GUID для трассировки запросов +3. **json_post** — произвольные JSON данные для задачи +4. **Статусы** — только два: 0 (ожидает) и 1 (выполнено) +5. **date** — время создания задачи +6. **date_up** — время завершения обработки (заполняется при COMPLETE) +7. **FIFO обработка** — задачи обрабатываются в порядке создания +8. **Без повторов** — нет встроенного механизма retry +9. **Архивация** — требуется регулярная очистка выполненных задач +10. **Cron** — требуется настройка планировщика для обработки + +--- + +## Связанные документы + +- [ApiLogs.md](./ApiLogs.md) — логирование API запросов +- [ApiErrorLog.md](./ApiErrorLog.md) — логирование ошибок +- [SchedulerTask.md](./SchedulerTask.md) — задачи планировщика diff --git a/erp24/docs/models/ApiCronBuh.md b/erp24/docs/models/ApiCronBuh.md new file mode 100644 index 00000000..1345eaf5 --- /dev/null +++ b/erp24/docs/models/ApiCronBuh.md @@ -0,0 +1,204 @@ +# Класс: ApiCronBuh + + +## Mindmap + +```mermaid +mindmap + root((ApiCronBuh)) + Таблица БД + api_cron_buh + Свойства + id + int + date + string + date_up + string + status + int + request_id + string + inn + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель для хранения и обработки запросов в бухгалтерскую систему через cron. Используется для асинхронной интеграции ERP24 с внешними бухгалтерскими сервисами, отслеживая статус отправки запросов по ИНН организации. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`api_cron_buh` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы статусов + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `WAIT` | 0 | Ожидает отправки — запрос поставлен в очередь | +| `SEND` | 1 | Отправлено — запрос отправлен во внешнюю систему | +| `RECEIVED` | 2 | Обработано — получен ответ от внешней системы | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `date` | datetime | Дата создания запроса | +| `date_up` | datetime | Дата последней обработки/обновления статуса | +| `status` | int | Статус запроса (0=WAIT, 1=SEND, 2=RECEIVED) | +| `json_post` | text / null | Тело запроса в формате JSON | +| `request_id` | varchar(36) | Уникальный идентификатор запроса (GUID) | +| `inn` | int | ИНН организации для идентификации получателя | + +## Методы + +### getStatus() +Возвращает текстовое описание статуса запроса. + +```php +public static function getStatus(int $status): string +``` + +**Параметры**: +- `$status` (int) — числовой код статуса + +**Возвращает**: string — человекочитаемое название статуса + +**Маппинг статусов**: +- `0` (WAIT) → "Ожидает" +- `1` (SEND) → "Отправлено" +- `2` (RECEIVED) → "Обработано" + +**Пример**: +```php +$record = ApiCronBuh::findOne(123); +$statusText = ApiCronBuh::getStatus($record->status); +// "Ожидает" или "Отправлено" или "Обработано" +``` + +## Диаграмма состояний + +```mermaid +stateDiagram-v2 + [*] --> WAIT: Создание запроса + WAIT --> SEND: Cron отправляет + SEND --> RECEIVED: Получен ответ + RECEIVED --> [*] + + WAIT: status=0 + WAIT: Ожидает + + SEND: status=1 + SEND: Отправлено + + RECEIVED: status=2 + RECEIVED: Обработано +``` + +## Диаграмма связей + +```mermaid +erDiagram + ApiCronBuh { + int id PK + datetime date + datetime date_up + int status + text json_post + varchar request_id + int inn + } + + Firms { + int id PK + int inn + } + + ApiCronBuh }o--|| Firms : "inn (логическая)" +``` + +## Примеры использования + +### Создание нового запроса в очередь +```php +$request = new ApiCronBuh(); +$request->date = date('Y-m-d H:i:s'); +$request->date_up = date('Y-m-d H:i:s'); +$request->status = ApiCronBuh::WAIT; +$request->request_id = Yii::$app->security->generateRandomString(36); +$request->inn = 7712345678; +$request->json_post = json_encode([ + 'operation' => 'sync_documents', + 'period' => '2024-01', + 'data' => [...] +]); +$request->save(); +``` + +### Получение необработанных запросов для cron +```php +$pendingRequests = ApiCronBuh::find() + ->where(['status' => ApiCronBuh::WAIT]) + ->orderBy(['date' => SORT_ASC]) + ->limit(100) + ->all(); + +foreach ($pendingRequests as $request) { + // Отправка запроса + $response = $this->sendToBuhApi($request->json_post); + + // Обновление статуса + $request->status = ApiCronBuh::SEND; + $request->date_up = date('Y-m-d H:i:s'); + $request->save(); +} +``` + +### Получение статистики по статусам +```php +$stats = ApiCronBuh::find() + ->select(['status', 'COUNT(*) as count']) + ->groupBy('status') + ->asArray() + ->all(); +``` + +### Поиск запросов по ИНН +```php +$requests = ApiCronBuh::find() + ->where(['inn' => 7712345678]) + ->andWhere(['>=', 'date', '2024-01-01']) + ->orderBy(['date' => SORT_DESC]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `date` | required, safe | +| `date_up` | safe | +| `status` | integer | +| `json_post` | string, nullable | +| `request_id` | required, string (max 36) | +| `inn` | required, integer | + +## Связанные модели + +- [Firms](./Firms.md) — организации по ИНН (логическая связь) +- [ApiCronTest](./ApiCronTest.md) — аналогичная модель для тестирования + +## Особенности реализации + +1. **Асинхронная обработка**: Запросы создаются со статусом WAIT и обрабатываются cron-задачей +2. **GUID идентификатор**: Каждый запрос имеет уникальный request_id для отслеживания в внешней системе +3. **Привязка к ИНН**: Запросы группируются по ИНН организации для маршрутизации +4. **JSON payload**: Тело запроса хранится в JSON формате для гибкости структуры данных +5. **Трекинг времени**: Поля date и date_up позволяют отслеживать время создания и обработки diff --git a/erp24/docs/models/ApiCronTest.md b/erp24/docs/models/ApiCronTest.md new file mode 100644 index 00000000..9b2afdf2 --- /dev/null +++ b/erp24/docs/models/ApiCronTest.md @@ -0,0 +1,169 @@ +# Класс: ApiCronTest + + +## Mindmap + +```mermaid +mindmap + root((ApiCronTest)) + Таблица БД + api_cron_test + Свойства + id + int + date + string + date_up + string + status + int + request_id + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель для хранения и обработки тестовых API-запросов через cron-задачи. Используется для отладки интеграций и тестирования асинхронной отправки данных во внешние системы с возможностью указания направления данных. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`api_cron_test` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `date` | datetime | Дата создания тестового запроса | +| `date_up` | datetime | Дата обработки/обновления запроса | +| `status` | int | Статус обработки запроса | +| `json_post` | text / null | Тело запроса в формате JSON | +| `request_id` | varchar(36) | Уникальный идентификатор запроса (GUID) | +| `direct_id` | int / null | Идентификатор направления данных | + +## Диаграмма связей + +```mermaid +erDiagram + ApiCronTest { + int id PK + datetime date + datetime date_up + int status + text json_post + varchar request_id + int direct_id + } + + ApiCronBuh { + int id PK + datetime date + datetime date_up + int status + text json_post + varchar request_id + int inn + } + + note "ApiCronTest - тестовая версия" + note "ApiCronBuh - production версия" +``` + +## Сравнение с ApiCronBuh + +| Характеристика | ApiCronTest | ApiCronBuh | +|----------------|-------------|------------| +| Назначение | Тестирование интеграций | Production бухгалтерия | +| Идентификатор получателя | `direct_id` (направление) | `inn` (ИНН организации) | +| Константы статусов | Нет | WAIT, SEND, RECEIVED | +| Метод getStatus() | Нет | Да | + +## Примеры использования + +### Создание тестового запроса +```php +$testRequest = new ApiCronTest(); +$testRequest->date = date('Y-m-d H:i:s'); +$testRequest->date_up = date('Y-m-d H:i:s'); +$testRequest->status = 0; // Ожидает +$testRequest->request_id = Yii::$app->security->generateRandomString(36); +$testRequest->direct_id = 1; // Направление: например, 1С +$testRequest->json_post = json_encode([ + 'test' => true, + 'operation' => 'ping', + 'timestamp' => time() +]); +$testRequest->save(); +``` + +### Получение тестовых запросов по направлению +```php +$requests = ApiCronTest::find() + ->where(['direct_id' => 1]) + ->andWhere(['status' => 0]) + ->orderBy(['date' => SORT_ASC]) + ->all(); +``` + +### Обработка тестовой очереди +```php +$pendingTests = ApiCronTest::find() + ->where(['status' => 0]) + ->limit(10) + ->all(); + +foreach ($pendingTests as $test) { + try { + // Имитация отправки + $result = $this->mockSend($test->json_post, $test->direct_id); + + $test->status = 2; // Обработано + $test->date_up = date('Y-m-d H:i:s'); + $test->save(); + + Yii::info("Test request {$test->request_id} processed", 'api-cron-test'); + } catch (\Exception $e) { + $test->status = -1; // Ошибка + $test->save(); + } +} +``` + +### Очистка старых тестовых записей +```php +ApiCronTest::deleteAll([ + 'AND', + ['<', 'date', date('Y-m-d', strtotime('-30 days'))], + ['status' => 2] // Только обработанные +]); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `date` | required, safe | +| `date_up` | required, safe | +| `status` | integer | +| `json_post` | string, nullable | +| `request_id` | required, string (max 36) | +| `direct_id` | integer, nullable | + +## Связанные модели + +- [ApiCronBuh](./ApiCronBuh.md) — production версия для бухгалтерских запросов +- [ApiIntegrationLogs](./ApiIntegrationLogs.md) — логи интеграций + +## Особенности реализации + +1. **Тестовое назначение**: Модель предназначена для отладки и тестирования интеграционных процессов +2. **Направление данных**: Поле `direct_id` позволяет маршрутизировать тестовые запросы в разные системы +3. **Отсутствие констант**: В отличие от ApiCronBuh, не имеет предопределённых констант статусов +4. **GUID запроса**: Уникальный request_id для трассировки запроса +5. **Изолированность**: Тестовые запросы хранятся отдельно от production данных diff --git a/erp24/docs/models/ApiCronTestSearch.md b/erp24/docs/models/ApiCronTestSearch.md new file mode 100644 index 00000000..6656e000 --- /dev/null +++ b/erp24/docs/models/ApiCronTestSearch.md @@ -0,0 +1,157 @@ +# Класс: ApiCronTestSearch + + +## Mindmap + +```mermaid +mindmap + root((ApiCronTestSearch)) + Таблица БД + ActiveRecord + Наследование + extends ApiCronTest +``` + +## Назначение +Search-модель для поиска и фильтрации тестовых записей CRON-задач API в ERP24. Обеспечивает поиск по статусу, датам и данным JSON для отладки интеграций. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ApiCronTest` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `status`, `direct_id` — integer +- `date`, `date_up`, `json_post`, `request_id` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ApiCronTest::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, date, date_up, status, direct_id + - LIKE: json_post, request_id + +## Диаграмма процесса + +```mermaid +flowchart TD + A[CRON запускает API] --> B[ApiCronTest запись] + B --> C[Сохранение json_post] + C --> D[Обновление status] + + E[ApiCronTestSearch] --> F[Фильтр по status] + F --> G[Фильтр по date] + G --> H[LIKE по json_post] + H --> I[Результаты для отладки] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ApiCronTestSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по статусу +```php +$searchModel = new ApiCronTestSearch(); +$dataProvider = $searchModel->search([ + 'ApiCronTestSearch' => [ + 'status' => 1, // Успешные + ] +]); +``` + +### Поиск по дате +```php +$searchModel = new ApiCronTestSearch(); +$dataProvider = $searchModel->search([ + 'ApiCronTestSearch' => [ + 'date' => '2024-06-15', + ] +]); +``` + +### Поиск по JSON-данным +```php +$searchModel = new ApiCronTestSearch(); +$dataProvider = $searchModel->search([ + 'ApiCronTestSearch' => [ + 'json_post' => 'order_id', + ] +]); +``` + +### Поиск по request_id +```php +$searchModel = new ApiCronTestSearch(); +$dataProvider = $searchModel->search([ + 'ApiCronTestSearch' => [ + 'request_id' => 'req_123456', + ] +]); +``` + +### GridView для отладки +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'date', + 'status', + 'direct_id', + [ + 'attribute' => 'json_post', + 'format' => 'ntext', + 'contentOptions' => ['style' => 'max-width: 300px; overflow: hidden;'], + ], + 'request_id', + ], +]) ?> +``` + +## Связанные модели + +- [ApiCronTest](./ApiCronTest.md) — базовая модель тестовых CRON-задач +- [ApiLogs](./ApiLogs.md) — логи API + +## Особенности реализации + +1. **Отладочный инструмент**: Для мониторинга CRON-задач API +2. **JSON-поиск**: LIKE-фильтр по json_post для поиска в данных +3. **Request tracking**: Поиск по request_id для трассировки +4. **Статусы**: Фильтрация успешных/неуспешных задач +5. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/ApiErrorLog.md b/erp24/docs/models/ApiErrorLog.md new file mode 100644 index 00000000..cca6c2de --- /dev/null +++ b/erp24/docs/models/ApiErrorLog.md @@ -0,0 +1,578 @@ +# Class: ApiErrorLog + +## 🧠 Mindmap: Модель ApiErrorLog + +```mermaid +mindmap + root((ApiErrorLog)) + Идентификация + id PK автоинкремент + Запрос + url адрес API + input входные параметры + hash_input хеш входных данных + ip IP адрес + Ошибка + payload сообщение об ошибке + Временные метки + created_at первое возникновение + actualed_at последнее возникновение + Дедупликация + hash_input поиск повторов + actualed_at обновление времени +``` + +--- + +## Назначение + +Модель логирования ошибок API в системе ERP24. Сохраняет информацию об ошибках, возникающих при обработке API запросов. Поддерживает дедупликацию: повторяющиеся ошибки не создают новые записи, а обновляют поле actualed_at существующей записи. + +Используется для мониторинга проблем API, анализа частоты ошибок, отладки интеграций и оповещения администраторов о критических проблемах. Хранит полное описание ошибки, входные параметры и метаданные запроса. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`api_error_log` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ, автоинкремент | + +### Данные запроса + +| Имя | Тип | Описание | +|-----|-----|----------| +| `url` | string(255) | **URL API endpoint** где произошла ошибка (обязательное) | +| `input` | text | **Входные параметры** запроса (JSON, POST данные) | +| `hash_input` | string(45) | **Хеш входных данных** для дедупликации ошибок | +| `ip` | string(25) | **IP адрес клиента** | + +### Данные ошибки + +| Имя | Тип | Описание | +|-----|-----|----------| +| `payload` | text | **Сообщение об ошибке**, стек трейс, детали (обязательное) | + +### Временные метки + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | datetime | **Дата первого возникновения** ошибки (обязательное) | +| `actualed_at` | datetime | **Дата последнего возникновения** этой же ошибки | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'url', // URL где произошла ошибка + 'created_at', // дата первого возникновения + 'payload' // описание ошибки +] +``` + +### Типы данных +```php +['created_at', 'actualed_at'] // safe (datetime) +['input', 'payload'] // text +``` + +### Ограничения длины +```php +['url'] // max:255 +['hash_input'] // max:45 +['ip'] // max:25 +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'api_error_log' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = ApiErrorLog::tableName(); // 'api_error_log' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательные поля: url, created_at, payload +2. Безопасные поля для временных меток (datetime) +3. Тип данных text для input и payload +4. Ограничения длины строковых полей (url, hash_input, ip) + +**Пример:** +```php +$errorLog = new ApiErrorLog(); +$errorLog->url = '/api/v1/orders'; +$errorLog->created_at = date('Y-m-d H:i:s'); +$errorLog->payload = 'Database connection failed'; +$errorLog->input = json_encode($_POST); +$errorLog->hash_input = md5($errorLog->input); +$errorLog->ip = $_SERVER['REMOTE_ADDR']; +if ($errorLog->validate()) { + $errorLog->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив с названиями атрибутов. Интересно, что для поля actualed_at используется название на русском языке в комментарии PHPDoc, что указывает на важность этого поля для отслеживания повторяющихся ошибок. + +**Пример:** +```php +$labels = (new ApiErrorLog())->attributeLabels(); +echo $labels['url']; // "Url" +echo $labels['payload']; // "Payload" +``` + +--- + +## Примеры использования + +### Логирование ошибки с дедупликацией + +```php +use yii_app\records\ApiErrorLog; + +// В try-catch блоке API контроллера +try { + // Обработка запроса + $result = $this->processApiRequest($data); +} catch (\Exception $e) { + // Создаем хеш для дедупликации + $inputData = json_encode([ + 'url' => Yii::$app->request->url, + 'post' => $_POST, + 'get' => $_GET + ]); + $hashInput = md5($inputData); + + // Проверяем существование ошибки + $existingError = ApiErrorLog::find() + ->where([ + 'url' => Yii::$app->request->url, + 'hash_input' => $hashInput + ]) + ->one(); + + if ($existingError) { + // Обновляем время последнего возникновения + $existingError->actualed_at = date('Y-m-d H:i:s'); + $existingError->save(); + } else { + // Создаем новую запись + $errorLog = new ApiErrorLog(); + $errorLog->url = Yii::$app->request->url; + $errorLog->created_at = date('Y-m-d H:i:s'); + $errorLog->input = $inputData; + $errorLog->hash_input = $hashInput; + $errorLog->payload = $e->getMessage() . "\n" . $e->getTraceAsString(); + $errorLog->ip = Yii::$app->request->userIP; + $errorLog->save(); + } + + // Возвращаем ошибку клиенту + throw $e; +} +``` + +### Простое логирование ошибки + +```php +// Быстрое логирование без дедупликации +$errorLog = new ApiErrorLog(); +$errorLog->url = '/api/v1/payments/process'; +$errorLog->created_at = date('Y-m-d H:i:s'); +$errorLog->payload = 'Payment gateway timeout'; +$errorLog->input = json_encode([ + 'amount' => 100, + 'currency' => 'RUB' +]); +$errorLog->ip = '192.168.1.1'; +$errorLog->save(); +``` + +### Получение всех ошибок для endpoint + +```php +$url = '/api/v1/orders'; +$errors = ApiErrorLog::find() + ->where(['url' => $url]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +echo "Errors for {$url}:\n"; +foreach ($errors as $error) { + echo "[{$error->created_at}] {$error->payload}\n"; + if ($error->actualed_at) { + echo " Last occurrence: {$error->actualed_at}\n"; + } +} +``` + +### Поиск повторяющихся ошибок + +```php +// Ошибки, которые возникали несколько раз (actualed_at заполнено) +$recurringErrors = ApiErrorLog::find() + ->where(['not', ['actualed_at' => null]]) + ->orderBy(['actualed_at' => SORT_DESC]) + ->all(); + +echo "Recurring errors:\n"; +foreach ($recurringErrors as $error) { + $first = new DateTime($error->created_at); + $last = new DateTime($error->actualed_at); + $diff = $last->diff($first); + + echo "URL: {$error->url}\n"; + echo "First: {$error->created_at}\n"; + echo "Last: {$error->actualed_at}\n"; + echo "Period: {$diff->days} days\n"; + echo "Error: " . substr($error->payload, 0, 100) . "...\n\n"; +} +``` + +### Статистика ошибок по endpoint'ам + +```php +$stats = ApiErrorLog::find() + ->select(['url', 'COUNT(*) as count']) + ->where(['>=', 'created_at', date('Y-m-d', strtotime('-7 days'))]) + ->groupBy('url') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); + +echo "Error statistics (last 7 days):\n"; +foreach ($stats as $stat) { + echo "{$stat['url']}: {$stat['count']} errors\n"; +} +``` + +### Поиск последних ошибок + +```php +$recentErrors = ApiErrorLog::find() + ->orderBy(['created_at' => SORT_DESC]) + ->limit(10) + ->all(); + +echo "Recent errors:\n"; +foreach ($recentErrors as $error) { + echo "[{$error->created_at}] {$error->url}\n"; + echo "IP: {$error->ip}\n"; + echo "Error: " . substr($error->payload, 0, 200) . "\n\n"; +} +``` + +### Анализ частоты повторения ошибок + +```php +$error = ApiErrorLog::findOne($errorId); + +if ($error->actualed_at) { + $first = strtotime($error->created_at); + $last = strtotime($error->actualed_at); + $hours = ($last - $first) / 3600; + + echo "Error frequency analysis:\n"; + echo "First occurrence: {$error->created_at}\n"; + echo "Last occurrence: {$error->actualed_at}\n"; + echo "Time span: " . round($hours, 2) . " hours\n"; + echo "\nThis error has been recurring!\n"; +} else { + echo "Error occurred once at {$error->created_at}\n"; +} +``` + +### Очистка старых ошибок + +```php +// Удаление ошибок старше 90 дней +$date = date('Y-m-d', strtotime('-90 days')); +$deleted = ApiErrorLog::deleteAll(['<', 'created_at', $date]); + +echo "Deleted {$deleted} old error records\n"; +``` + +### Экспорт ошибок для анализа + +```php +$errors = ApiErrorLog::find() + ->where(['>=', 'created_at', date('Y-m-d', strtotime('-1 day'))]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +$report = "API Error Report\n"; +$report .= "Date: " . date('Y-m-d H:i:s') . "\n"; +$report .= "Period: Last 24 hours\n"; +$report .= "Total errors: " . count($errors) . "\n\n"; + +foreach ($errors as $error) { + $report .= "=" . str_repeat("=", 70) . "\n"; + $report .= "Time: {$error->created_at}\n"; + $report .= "URL: {$error->url}\n"; + $report .= "IP: {$error->ip}\n"; + if ($error->actualed_at) { + $report .= "Last recurrence: {$error->actualed_at}\n"; + } + $report .= "\nInput:\n{$error->input}\n"; + $report .= "\nError:\n{$error->payload}\n\n"; +} + +file_put_contents('error_report.txt', $report); +echo "Error report exported\n"; +``` + +### Поиск ошибок по IP + +```php +$ip = '192.168.1.100'; +$errors = ApiErrorLog::find() + ->where(['ip' => $ip]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +echo "Errors from IP {$ip}:\n"; +foreach ($errors as $error) { + echo "[{$error->created_at}] {$error->url}\n"; +} +``` + +### Уведомление администраторов о критических ошибках + +```php +// После сохранения новой ошибки +$errorLog = new ApiErrorLog(); +$errorLog->url = $url; +$errorLog->created_at = date('Y-m-d H:i:s'); +$errorLog->payload = $errorMessage; +$errorLog->save(); + +// Проверка критичности +$criticalKeywords = ['database', 'connection', 'timeout', 'fatal']; +$isCritical = false; + +foreach ($criticalKeywords as $keyword) { + if (stripos($errorLog->payload, $keyword) !== false) { + $isCritical = true; + break; + } +} + +if ($isCritical) { + // Отправка уведомления + Yii::$app->mailer->compose() + ->setTo('admin@example.com') + ->setSubject('Critical API Error') + ->setTextBody("URL: {$errorLog->url}\nError: {$errorLog->payload}") + ->send(); +} +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + ApiErrorLog { + int id PK + string url "API endpoint" + datetime created_at "First occurrence" + datetime actualed_at "Last occurrence" + text input "Request parameters" + string hash_input "MD5 hash for dedup" + text payload "Error message" + string ip "Client IP" + } +``` + +--- + +## Бизнес-логика + +### Дедупликация ошибок + +Система предотвращает дублирование одинаковых ошибок: + +1. **При возникновении ошибки**: + - Вычисляется hash_input от входных параметров + - Проверяется существование записи с таким же url и hash_input + +2. **Если ошибка уже существует**: + - Обновляется поле actualed_at текущим временем + - Не создается новая запись + +3. **Если ошибка новая**: + - Создается запись с filled created_at + - actualed_at остается null + +### Преимущества дедупликации + +- **Экономия места**: не растет таблица при повторяющихся ошибках +- **Анализ частоты**: разница между created_at и actualed_at показывает период повторения +- **Приоритизация**: ошибки с actualed_at требуют внимания (повторяются) + +### Мониторинг и алертинг + +```php +// Ежечасная проверка новых критических ошибок +$newErrors = ApiErrorLog::find() + ->where(['>=', 'created_at', date('Y-m-d H:i:s', strtotime('-1 hour'))]) + ->andWhere(['actualed_at' => null]) // только новые + ->all(); + +foreach ($newErrors as $error) { + // Отправка в систему мониторинга + $this->sendToMonitoring($error); +} + +// Проверка повторяющихся ошибок +$recurringErrors = ApiErrorLog::find() + ->where(['>=', 'actualed_at', date('Y-m-d H:i:s', strtotime('-10 minutes'))]) + ->all(); + +foreach ($recurringErrors as $error) { + // Эскалация: ошибка повторяется! + $this->escalateError($error); +} +``` + +### Типы ошибок + +Payload может содержать: + +1. **Exception messages**: текст исключения PHP +2. **Stack traces**: полный трейс выполнения +3. **Validation errors**: ошибки валидации данных +4. **Integration errors**: ошибки внешних API +5. **Database errors**: ошибки БД + +--- + +## Связи с другими моделями + +Модель ApiErrorLog не имеет явных relations, но логически связана с: + +- **ApiLogs** — общее логирование API запросов +- Внешние системы мониторинга (через интеграции) + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (id) + +-- Поиск по URL +CREATE INDEX idx_api_error_log_url ON api_error_log(url); + +-- Поиск по дате создания +CREATE INDEX idx_api_error_log_created ON api_error_log(created_at); + +-- Поиск по дате последнего возникновения +CREATE INDEX idx_api_error_log_actualed ON api_error_log(actualed_at); + +-- Дедупликация +CREATE INDEX idx_api_error_log_hash ON api_error_log(hash_input); + +-- Композитный для дедупликации +CREATE INDEX idx_api_error_log_url_hash ON api_error_log(url, hash_input); + +-- Поиск по IP +CREATE INDEX idx_api_error_log_ip ON api_error_log(ip); +``` + +### Оптимизация запросов + +```php +// Плохо: поиск по payload (нет индекса, полное сканирование) +$errors = ApiErrorLog::find() + ->where(['like', 'payload', 'database']) + ->all(); + +// Хорошо: использование индексированных полей +$errors = ApiErrorLog::find() + ->where(['url' => $targetUrl]) + ->andWhere(['>=', 'created_at', $startDate]) + ->all(); + +// Затем фильтрация по payload в PHP +$filteredErrors = array_filter($errors, function($error) { + return stripos($error->payload, 'database') !== false; +}); +``` + +--- + +## Замечания + +1. **Первичный ключ** — автоинкремент ID +2. **Дедупликация** — через hash_input и actualed_at +3. **created_at** — момент первого возникновения ошибки +4. **actualed_at** — момент последнего повторения (null для однократных ошибок) +5. **hash_input** — MD5 хеш для быстрого поиска дубликатов +6. **payload** — может содержать большие тексты (stack traces) +7. **Обязательные поля** — только url, created_at, payload +8. **Производительность** — требуется регулярная архивация старых записей +9. **Мониторинг** — следить за ошибками с заполненным actualed_at +10. **Алертинг** — настроить уведомления для критических ошибок + +--- + +## Связанные документы + +- [ApiLogs.md](./ApiLogs.md) — логирование API запросов +- [ApiCron.md](./ApiCron.md) — задачи Cron для API +- [ScriptLauncherLog.md](./ScriptLauncherLog.md) — логи запуска скриптов diff --git a/erp24/docs/models/ApiErrorLogSearch.md b/erp24/docs/models/ApiErrorLogSearch.md new file mode 100644 index 00000000..59916b21 --- /dev/null +++ b/erp24/docs/models/ApiErrorLogSearch.md @@ -0,0 +1,158 @@ +# Класс: ApiErrorLogSearch + + +## Mindmap + +```mermaid +mindmap + root((ApiErrorLogSearch)) + Таблица БД + ActiveRecord + Наследование + extends ApiErrorLog +``` + +## Назначение +Search-модель для поиска и фильтрации ошибок API в ERP24. Обеспечивает поиск по URL, IP-адресу, входным данным и payload для отладки и мониторинга API-интеграций. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ApiErrorLog` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `url`, `created_at`, `input`, `hash_input`, `payload`, `ip` — safe (текстовый поиск) + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ApiErrorLog::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, created_at + - LIKE: url, input, hash_input, payload, ip + +## Диаграмма мониторинга ошибок + +```mermaid +flowchart TD + A[API Request] --> B{Ошибка?} + B -->|Да| C[ApiErrorLog] + B -->|Нет| D[Успешный ответ] + + C --> E[Запись url, input, payload, ip] + + F[ApiErrorLogSearch] --> G[Фильтр по url] + G --> H[Фильтр по ip] + H --> I[LIKE по payload] + I --> J[Анализ ошибок] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ApiErrorLogSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по URL +```php +$searchModel = new ApiErrorLogSearch(); +$dataProvider = $searchModel->search([ + 'ApiErrorLogSearch' => [ + 'url' => '/api/v2/orders', + ] +]); +``` + +### Поиск по IP-адресу +```php +$searchModel = new ApiErrorLogSearch(); +$dataProvider = $searchModel->search([ + 'ApiErrorLogSearch' => [ + 'ip' => '192.168.1.100', + ] +]); +``` + +### Поиск по содержимому payload +```php +$searchModel = new ApiErrorLogSearch(); +$dataProvider = $searchModel->search([ + 'ApiErrorLogSearch' => [ + 'payload' => 'validation error', + ] +]); +``` + +### Поиск по hash_input для дедупликации +```php +$searchModel = new ApiErrorLogSearch(); +$dataProvider = $searchModel->search([ + 'ApiErrorLogSearch' => [ + 'hash_input' => 'abc123def456', + ] +]); +``` + +### GridView для мониторинга +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'created_at', + 'url', + 'ip', + [ + 'attribute' => 'payload', + 'format' => 'ntext', + 'contentOptions' => ['style' => 'max-width: 300px;'], + ], + ], +]) ?> +``` + +## Связанные модели + +- [ApiErrorLog](./ApiErrorLog.md) — базовая модель логов ошибок +- [ApiLogs](./ApiLogs.md) — общие логи API + +## Особенности реализации + +1. **Мониторинг ошибок**: Инструмент для отладки API +2. **LIKE-поиск**: По URL, payload, input для анализа +3. **Hash дедупликация**: Поиск по hash_input +4. **IP-трекинг**: Фильтрация по IP-адресам +5. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/ApiIntegrationLogs.md b/erp24/docs/models/ApiIntegrationLogs.md new file mode 100644 index 00000000..7f632142 --- /dev/null +++ b/erp24/docs/models/ApiIntegrationLogs.md @@ -0,0 +1,165 @@ +# Модель ApiIntegrationLogs + + +## Mindmap + +```mermaid +mindmap + root((ApiIntegrationLogs)) + Таблица БД + api_integration_logs + Свойства + id + int + export_id + int + entity_id + string + entity + string + date + string + export_val + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ApiIntegrationLogs` ведёт журнал изменений данных при интеграционном экспорте через API. Фиксирует изменения экспортируемых значений сущностей с указанием старого и нового значения, времени и автора изменения. Используется для аудита синхронизации данных с внешними системами. + +**Файл модели:** `erp24/records/ApiIntegrationLogs.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `api_integration_logs` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `export_id` | INTEGER | ID настройки экспорта (FK → export_import_integrations.id) | +| `entity_id` | VARCHAR(36) | Идентификатор экспортируемой сущности | +| `entity` | VARCHAR(36) | Тип сущности (products, orders, users и др.) | +| `date` | TIMESTAMP | Дата и время изменения | +| `export_val` | VARCHAR(120) | Новое значение для экспорта | +| `export_val_old` | VARCHAR(120) | Предыдущее значение | +| `admin_id` | INTEGER | ID сотрудника, инициировавшего изменение | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + api_integration_logs }o--|| export_import_integrations : "export" + api_integration_logs }o--|| admin : "author" + + api_integration_logs { + int id PK + int export_id FK + string entity_id + string entity + timestamp date + string export_val + string export_val_old + int admin_id FK + } + + export_import_integrations { + int id PK + string name + string type + } +``` + +--- + +## Примеры использования + +### Создание записи лога + +```php +$log = new ApiIntegrationLogs(); +$log->export_id = $exportId; +$log->entity_id = $productGuid; +$log->entity = 'products'; +$log->date = date('Y-m-d H:i:s'); +$log->export_val = '1500.00'; +$log->export_val_old = '1200.00'; +$log->admin_id = Yii::$app->user->id; +$log->save(); +``` + +### Получение истории изменений сущности + +```php +$history = ApiIntegrationLogs::find() + ->where([ + 'entity' => 'products', + 'entity_id' => $productGuid + ]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($history as $log) { + echo "{$log->date}: {$log->export_val_old} → {$log->export_val}\n"; +} +``` + +### Получение логов интеграции за период + +```php +$logs = ApiIntegrationLogs::find() + ->where(['export_id' => $exportId]) + ->andWhere(['between', 'date', $dateFrom, $dateTo]) + ->orderBy(['date' => SORT_DESC]) + ->all(); +``` + +### Статистика изменений по типам сущностей + +```php +$stats = ApiIntegrationLogs::find() + ->select(['entity', 'COUNT(*) as count']) + ->where(['export_id' => $exportId]) + ->groupBy('entity') + ->asArray() + ->all(); +``` + +### Поиск изменений конкретного сотрудника + +```php +$adminLogs = ApiIntegrationLogs::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date' => SORT_DESC]) + ->limit(100) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| Все поля | Обязательные | +| `export_id`, `admin_id` | Целые числа | +| `entity_id`, `entity` | Строка, макс. 36 символов | +| `export_val`, `export_val_old` | Строка, макс. 120 символов | + +--- + +## Связанные модели + +- **[ExportImportIntegrations](./ExportImportIntegrations.md)** — настройки интеграций +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ApiLogs.md b/erp24/docs/models/ApiLogs.md new file mode 100644 index 00000000..70eed8cb --- /dev/null +++ b/erp24/docs/models/ApiLogs.md @@ -0,0 +1,610 @@ +# Class: ApiLogs + +## 🧠 Mindmap: Модель ApiLogs + +```mermaid +mindmap + root((ApiLogs)) + Идентификация + id PK автоинкремент + request_id уникальный ID запроса + Запрос + url адрес API + content тело запроса + hash_content хеш контента + ip IP адрес клиента + Ответ + result результат обработки + status HTTP статус + date дата и время + Связанные данные + store_id ID магазина + seller_id ID продавца + phone телефон +``` + +--- + +## Назначение + +Модель логирования API запросов в системе ERP24. Сохраняет информацию о всех входящих API запросах: URL, содержимое запроса, результат обработки, статус ответа и метаданные (магазин, продавец, телефон). + +Используется для отладки интеграций, анализа работы API, отслеживания ошибок и аудита операций. Каждый запрос получает уникальный request_id для трассировки через систему. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`api_logs` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ, автоинкремент | +| `request_id` | string(40) | **Уникальный ID запроса** для трассировки | + +### Данные запроса + +| Имя | Тип | Описание | +|-----|-----|----------| +| `url` | string(255) | **URL API endpoint** (обязательное) | +| `content` | text | **Тело запроса** (JSON, XML или другие данные) | +| `hash_content` | string(45) | **Хеш содержимого** для дедупликации | +| `ip` | string(25) | **IP адрес клиента** (обязательное) | + +### Данные ответа + +| Имя | Тип | Описание | +|-----|-----|----------| +| `result` | text | **Результат обработки** (ответ сервера, обязательное) | +| `status` | int | **HTTP статус ответа** (200, 400, 500 и т.д., обязательное) | +| `date` | datetime | **Дата и время запроса** (обязательное) | + +### Метаданные + +| Имя | Тип | Описание | +|-----|-----|----------| +| `store_id` | string(36) | **ID магазина** (GUID из 1С) | +| `seller_id` | string(36) | **ID продавца** (GUID из 1С) | +| `phone` | int | **Телефон клиента** (если запрос связан с клиентом) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'url', // URL endpoint + 'date', // дата запроса + 'result', // результат обработки + 'status', // HTTP статус + 'ip' // IP адрес +] +// Обратите внимание: content, hash_content, store_id, seller_id, phone +// закомментированы в коде и не обязательны +``` + +### Типы данных +```php +['date'] // safe (datetime) +['content', 'result'] // text +['status', 'phone'] // integer +``` + +### Ограничения длины +```php +['url'] // max:255 +['hash_content'] // max:45 +['request_id'] // max:40 +['store_id', 'seller_id'] // max:36 (GUID) +['ip'] // max:25 +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'api_logs' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = ApiLogs::tableName(); // 'api_logs' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательные поля: url, date, result, status, ip +2. Безопасное поле date (datetime) +3. Типы данных: text для content и result, integer для status и phone +4. Ограничения длины строковых полей +5. Закомментированные обязательные поля: content, hash_content, store_id, seller_id, phone (не требуются для сохранения) + +**Примечание:** В коде видно, что некоторые поля изначально были обязательными, но позже требование было снято (закомментировано). + +**Пример:** +```php +$log = new ApiLogs(); +$log->request_id = uniqid('req_'); +$log->url = '/api/v1/orders'; +$log->date = date('Y-m-d H:i:s'); +$log->content = json_encode($_POST); +$log->result = json_encode(['status' => 'ok']); +$log->status = 200; +$log->ip = $_SERVER['REMOTE_ADDR']; +if ($log->validate()) { + $log->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив, где ключи - имена атрибутов, а значения - их отображаемые названия на английском языке. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new ApiLogs())->attributeLabels(); +echo $labels['url']; // "Url" +echo $labels['status']; // "Status" +``` + +--- + +## Примеры использования + +### Логирование входящего API запроса + +```php +use yii_app\records\ApiLogs; + +// В API контроллере +$log = new ApiLogs(); +$log->request_id = uniqid('req_', true); // уникальный ID +$log->url = Yii::$app->request->url; +$log->date = date('Y-m-d H:i:s'); +$log->content = Yii::$app->request->rawBody; // тело запроса +$log->hash_content = md5($log->content); // хеш для дедупликации +$log->result = ''; // заполнится позже +$log->status = 0; // заполнится позже +$log->ip = Yii::$app->request->userIP; + +// Дополнительные данные +$log->store_id = Yii::$app->request->post('store_id'); +$log->seller_id = Yii::$app->request->post('seller_id'); +$log->phone = Yii::$app->request->post('phone'); + +$log->save(); + +// Сохраняем request_id для последующего обновления +$requestId = $log->request_id; +``` + +### Обновление лога после обработки запроса + +```php +// После обработки запроса +$log = ApiLogs::findOne(['request_id' => $requestId]); + +if ($log) { + $log->result = json_encode($response); + $log->status = 200; // или другой HTTP статус + $log->save(); +} +``` + +### Логирование ошибки API + +```php +try { + // Обработка запроса + $response = $this->processRequest($data); + $status = 200; +} catch (\Exception $e) { + $response = [ + 'error' => true, + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]; + $status = 500; +} + +// Обновляем лог +$log = ApiLogs::findOne(['request_id' => $requestId]); +$log->result = json_encode($response); +$log->status = $status; +$log->save(); +``` + +### Поиск логов по URL + +```php +// Все запросы к определенному endpoint +$logs = ApiLogs::find() + ->where(['like', 'url', '/api/v1/orders']) + ->orderBy(['date' => SORT_DESC]) + ->limit(100) + ->all(); + +foreach ($logs as $log) { + echo "[{$log->date}] {$log->url} - Status: {$log->status}\n"; +} +``` + +### Поиск ошибочных запросов + +```php +// Все запросы с ошибками (статус 4xx и 5xx) +$errorLogs = ApiLogs::find() + ->where(['>=', 'status', 400]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +echo "Error logs:\n"; +foreach ($errorLogs as $log) { + echo "[{$log->date}] {$log->url} - Status {$log->status}\n"; + echo "Content: {$log->content}\n"; + echo "Result: {$log->result}\n\n"; +} +``` + +### Поиск логов по магазину + +```php +$storeId = 'uuid-store-123'; +$storeLogs = ApiLogs::find() + ->where(['store_id' => $storeId]) + ->orderBy(['date' => SORT_DESC]) + ->limit(50) + ->all(); + +echo "Logs for store {$storeId}:\n"; +foreach ($storeLogs as $log) { + echo "{$log->date} - {$log->url} - {$log->status}\n"; +} +``` + +### Поиск дублирующихся запросов по хешу + +```php +// Запросы с одинаковым содержимым +$hash = md5($requestBody); + +$duplicates = ApiLogs::find() + ->where(['hash_content' => $hash]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +if (count($duplicates) > 1) { + echo "Warning: Found " . count($duplicates) . " duplicate requests\n"; + foreach ($duplicates as $log) { + echo "- [{$log->date}] Request ID: {$log->request_id}\n"; + } +} +``` + +### Статистика API запросов + +```php +// Количество запросов по статусам +$stats = ApiLogs::find() + ->select(['status', 'COUNT(*) as count']) + ->where(['>=', 'date', date('Y-m-d 00:00:00')]) // за сегодня + ->groupBy('status') + ->asArray() + ->all(); + +echo "API statistics for today:\n"; +foreach ($stats as $stat) { + echo "Status {$stat['status']}: {$stat['count']} requests\n"; +} + +// Общее количество +$total = ApiLogs::find() + ->where(['>=', 'date', date('Y-m-d 00:00:00')]) + ->count(); +echo "Total: {$total} requests\n"; +``` + +### Статистика по endpoint'ам + +```php +$endpointStats = ApiLogs::find() + ->select(['url', 'COUNT(*) as count', 'AVG(status) as avg_status']) + ->where(['>=', 'date', date('Y-m-d', strtotime('-7 days'))]) + ->groupBy('url') + ->orderBy(['count' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); + +echo "Top 10 endpoints (last 7 days):\n"; +foreach ($endpointStats as $stat) { + echo "{$stat['url']}: {$stat['count']} requests, avg status: {$stat['avg_status']}\n"; +} +``` + +### Трассировка запроса по request_id + +```php +$requestId = 'req_12345'; + +$log = ApiLogs::find() + ->where(['request_id' => $requestId]) + ->one(); + +if ($log) { + echo "Request trace:\n"; + echo "ID: {$log->request_id}\n"; + echo "URL: {$log->url}\n"; + echo "Date: {$log->date}\n"; + echo "IP: {$log->ip}\n"; + echo "Store ID: {$log->store_id}\n"; + echo "Seller ID: {$log->seller_id}\n"; + echo "Phone: {$log->phone}\n"; + echo "\nRequest content:\n{$log->content}\n"; + echo "\nResponse:\n{$log->result}\n"; + echo "\nStatus: {$log->status}\n"; +} +``` + +### Поиск медленных запросов + +```php +// Предполагая, что в result сохраняется время выполнения +$logs = ApiLogs::find() + ->where(['>=', 'date', date('Y-m-d', strtotime('-1 day'))]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +$slowLogs = []; +foreach ($logs as $log) { + $result = json_decode($log->result, true); + if (isset($result['execution_time']) && $result['execution_time'] > 5) { + $slowLogs[] = $log; + } +} + +echo "Slow requests (> 5 seconds):\n"; +foreach ($slowLogs as $log) { + $result = json_decode($log->result, true); + echo "[{$log->date}] {$log->url} - {$result['execution_time']}s\n"; +} +``` + +### Очистка старых логов + +```php +// Удаление логов старше 30 дней +$date = date('Y-m-d', strtotime('-30 days')); +$deleted = ApiLogs::deleteAll(['<', 'date', $date]); + +echo "Deleted {$deleted} old log records\n"; +``` + +### Экспорт логов в CSV + +```php +$logs = ApiLogs::find() + ->where(['>=', 'date', date('Y-m-d', strtotime('-1 day'))]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$fp = fopen('api_logs.csv', 'w'); +fputcsv($fp, ['Date', 'URL', 'Status', 'IP', 'Store ID', 'Request ID']); + +foreach ($logs as $log) { + fputcsv($fp, [ + $log->date, + $log->url, + $log->status, + $log->ip, + $log->store_id, + $log->request_id + ]); +} + +fclose($fp); +echo "Logs exported to api_logs.csv\n"; +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + ApiLogs { + int id PK + string request_id "Unique request ID" + string url "API endpoint" + datetime date + text content "Request body" + string hash_content "MD5 hash" + text result "Response" + int status "HTTP status" + string store_id "Store GUID" + string seller_id "Seller GUID" + int phone "Customer phone" + string ip "Client IP" + } +``` + +--- + +## Бизнес-логика + +### Назначение логирования + +API логи используются для: + +1. **Отладки интеграций** + - Просмотр содержимого запросов и ответов + - Трассировка запросов через систему + - Воспроизведение проблемных сценариев + +2. **Мониторинга** + - Отслеживание ошибок API + - Анализ производительности + - Статистика использования endpoint'ов + +3. **Аудита** + - Кто и когда обращался к API + - Какие данные были переданы + - Результат обработки запроса + +4. **Дедупликации** + - Обнаружение повторных запросов через hash_content + - Предотвращение дублирования операций + +### Жизненный цикл записи лога + +1. **Получение запроса**: создается запись с request_id, url, content, ip +2. **Обработка**: выполняется бизнес-логика +3. **Завершение**: обновляется result и status +4. **Анализ**: логи используются для отладки и статистики +5. **Очистка**: старые логи удаляются по расписанию + +### HTTP статусы + +```php +200 - OK (успешный запрос) +400 - Bad Request (ошибка валидации) +401 - Unauthorized (ошибка аутентификации) +403 - Forbidden (нет прав доступа) +404 - Not Found (ресурс не найден) +500 - Internal Server Error (ошибка сервера) +``` + +### Особенности хранения + +- **request_id**: генерируется через `uniqid()` для уникальности +- **hash_content**: MD5 хеш для быстрого поиска дубликатов +- **content и result**: могут содержать большие JSON/XML документы +- **store_id и seller_id**: GUID из 1С (36 символов) +- **phone**: integer, может быть null если запрос не связан с клиентом + +--- + +## Связи с другими моделями + +Модель ApiLogs не имеет явных relations с другими моделями (нет внешних ключей), но логически связана с: + +- **Store** — через store_id (GUID) +- **Users** (Admin/Seller) — через seller_id (GUID) +- **Users** (клиенты) — через phone +- **ApiErrorLog** — дублирующее логирование ошибок + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (id) + +-- Поиск по request_id +CREATE INDEX idx_api_logs_request_id ON api_logs(request_id); + +-- Поиск по URL +CREATE INDEX idx_api_logs_url ON api_logs(url); + +-- Поиск по дате +CREATE INDEX idx_api_logs_date ON api_logs(date); + +-- Поиск по статусу +CREATE INDEX idx_api_logs_status ON api_logs(status); + +-- Поиск по хешу +CREATE INDEX idx_api_logs_hash ON api_logs(hash_content); + +-- Поиск по магазину +CREATE INDEX idx_api_logs_store_id ON api_logs(store_id); + +-- Поиск по IP +CREATE INDEX idx_api_logs_ip ON api_logs(ip); + +-- Композитный индекс для статистики +CREATE INDEX idx_api_logs_date_status ON api_logs(date, status); +``` + +### Оптимизация хранения + +```php +// Использование партиционирования по датам (PostgreSQL) +// Или регулярная очистка старых записей + +// Архивирование старых логов +$archiveDate = date('Y-m-d', strtotime('-90 days')); +$oldLogs = ApiLogs::find()->where(['<', 'date', $archiveDate])->all(); + +// Экспорт в архив и удаление +foreach ($oldLogs as $log) { + // сохранить в архивное хранилище + $log->delete(); +} +``` + +--- + +## Замечания + +1. **Первичный ключ** — автоинкремент ID +2. **request_id** — уникальный идентификатор для трассировки +3. **Обязательные поля** — url, date, result, status, ip +4. **Необязательные поля** — content, hash_content, store_id, seller_id, phone +5. **Текстовые поля** — content и result могут содержать большие объемы данных +6. **GUID поля** — store_id и seller_id имеют длину 36 символов +7. **Дедупликация** — hash_content используется для поиска повторных запросов +8. **Производительность** — требуется регулярная очистка старых записей +9. **Связи** — нет foreign key constraints, только логические связи +10. **Формат данных** — content и result обычно содержат JSON + +--- + +## Связанные документы + +- [ApiErrorLog.md](./ApiErrorLog.md) — логирование ошибок API +- [ApiCron.md](./ApiCron.md) — задачи Cron для API +- [Store.md](./Store.md) — модель магазинов +- [Users.md](./Users.md) — модель пользователей diff --git a/erp24/docs/models/ApiLogsSearch.md b/erp24/docs/models/ApiLogsSearch.md new file mode 100644 index 00000000..71796e74 --- /dev/null +++ b/erp24/docs/models/ApiLogsSearch.md @@ -0,0 +1,166 @@ +# Класс: ApiLogsSearch + + +## Mindmap + +```mermaid +mindmap + root((ApiLogsSearch)) + Таблица БД + ActiveRecord + Наследование + extends ApiLogs +``` + +## Назначение +Search-модель для поиска и фильтрации логов API-запросов в ERP24. Обеспечивает поиск по URL, статусу, телефону, магазину и продавцу для мониторинга и аналитики API. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ApiLogs` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `status`, `phone` — integer +- `url`, `date`, `content`, `hash_content`, `result`, `store_id`, `seller_id`, `ip` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ApiLogs::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, date, status, phone + - LIKE: url, content, hash_content, result, store_id, seller_id, ip + +## Диаграмма процесса логирования + +```mermaid +flowchart TD + A[API Request] --> B[Логирование] + B --> C[ApiLogs запись] + C --> D[url, content, result] + + E[ApiLogsSearch] --> F[Фильтр по status] + F --> G[Фильтр по store_id] + G --> H[LIKE по content] + H --> I[Результаты поиска] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ApiLogsSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по статусу +```php +$searchModel = new ApiLogsSearch(); +$dataProvider = $searchModel->search([ + 'ApiLogsSearch' => [ + 'status' => 200, + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new ApiLogsSearch(); +$dataProvider = $searchModel->search([ + 'ApiLogsSearch' => [ + 'store_id' => '10', + ] +]); +``` + +### Поиск по телефону клиента +```php +$searchModel = new ApiLogsSearch(); +$dataProvider = $searchModel->search([ + 'ApiLogsSearch' => [ + 'phone' => 79001234567, + ] +]); +``` + +### Поиск по URL эндпоинта +```php +$searchModel = new ApiLogsSearch(); +$dataProvider = $searchModel->search([ + 'ApiLogsSearch' => [ + 'url' => '/api/v2/sales', + ] +]); +``` + +### Поиск по IP-адресу +```php +$searchModel = new ApiLogsSearch(); +$dataProvider = $searchModel->search([ + 'ApiLogsSearch' => [ + 'ip' => '10.0.0.1', + ] +]); +``` + +### GridView для мониторинга +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'date', + 'url', + 'status', + 'store_id', + 'seller_id', + 'ip', + ], +]) ?> +``` + +## Связанные модели + +- [ApiLogs](./ApiLogs.md) — базовая модель логов API +- [ApiErrorLog](./ApiErrorLog.md) — логи ошибок +- [CityStore](./CityStore.md) — магазины (store_id) +- [Admin](./Admin.md) — продавцы (seller_id) + +## Особенности реализации + +1. **Полное логирование**: Поиск по всем данным запроса +2. **Контекст продажи**: Фильтрация по store_id, seller_id, phone +3. **Hash дедупликация**: Поиск по hash_content +4. **Result-анализ**: LIKE-поиск по результату для отладки +5. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/Assemblies.md b/erp24/docs/models/Assemblies.md new file mode 100644 index 00000000..c6f5912a --- /dev/null +++ b/erp24/docs/models/Assemblies.md @@ -0,0 +1,295 @@ +# Класс: Assemblies + + +## Mindmap + +```mermaid +mindmap + root((Assemblies)) + Таблица БД + assemblies + Свойства + id + int + guid + string + store_id + string + seller_id + string + created_at + string + products_json + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель сборок (букетов) в ERP24. Отслеживает полный жизненный цикл букета от момента сборки флористом до продажи или разборки. Хранит информацию о составе букета, его стоимости, статусе и всей истории редактирования. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`assemblies` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(36) | Уникальный GUID сборки | +| `store_id` | varchar(36) | GUID магазина, где создана сборка | +| `seller_id` | varchar(36) | GUID флориста, создавшего сборку | +| `created_at` | timestamp | Время создания сборки | +| `disassembling_seller_id` | varchar(36) / null | GUID флориста, разобравшего букет | +| `edit_time` | timestamp / null | Время последнего редактирования | +| `edit_json` | text / null | История редактирования в JSON | +| `products_json` | text | Состав букета в JSON формате | +| `summ` | decimal | Сумма сборки (себестоимость) | +| `summ_matrix` | decimal / null | Сумма матричного букета в сборке | +| `status_id` | int | Статус сборки (0, -1, 1, 2) | +| `check_id` | varchar(36) / null | GUID чека при продаже | +| `date_close` | timestamp / null | Время продажи букета | +| `with_return` | int / null | Флаг наличия возврата (1 = был возврат) | + +## Статусы сборки (status_id) + +| Значение | Описание | +|----------|----------| +| `0` | Актуальная сборка (собран, ожидает продажи) или редактирование | +| `-1` | Разобранная сборка (букет разобран на компоненты) | +| `1` | Проданная сборка (букет продан) | +| `2` | Возврат (букет возвращён покупателем) | + +## Структура JSON полей + +### products_json +Хранит состав букета: +```json +[ + { + "product_id": "abc-123-def", + "color": "красный", + "quantity": 5, + "price": 150.00 + }, + { + "product_id": "ghi-456-jkl", + "color": "белый", + "quantity": 3, + "price": 200.00 + } +] +``` + +### edit_json +История редактирования: +```json +[ + { + "date": "2024-01-15 14:30:00", + "comment": "Заменена роза на хризантему", + "products_from": [...], + "products_to": [ + { + "product_id": "...", + "color": "...", + "quantity": 2 + } + ] + } +] +``` + +## Диаграмма состояний + +```mermaid +stateDiagram-v2 + [*] --> Актуальная: Создание сборки + Актуальная --> Актуальная: Редактирование + Актуальная --> Проданная: Продажа (check_id) + Актуальная --> Разобранная: Разборка + Проданная --> Возврат: Возврат покупателем + Возврат --> Разобранная: Разборка после возврата + + Актуальная: status_id=0 + Разобранная: status_id=-1 + Проданная: status_id=1 + Возврат: status_id=2 +``` + +## Диаграмма связей + +```mermaid +erDiagram + Assemblies { + int id PK + varchar guid UK + varchar store_id FK + varchar seller_id FK + timestamp created_at + varchar disassembling_seller_id FK + timestamp edit_time + text edit_json + text products_json + decimal summ + decimal summ_matrix + int status_id + varchar check_id FK + timestamp date_close + int with_return + } + + Store { + varchar id PK + varchar name + } + + Admin { + varchar guid PK + varchar name + } + + CreateChecks { + varchar guid PK + decimal summ + } + + Products1c { + varchar id PK + varchar name + } + + Assemblies }o--|| Store : "store_id" + Assemblies }o--|| Admin : "seller_id" + Assemblies }o--o| Admin : "disassembling_seller_id" + Assemblies }o--o| CreateChecks : "check_id" + Assemblies }o--|{ Products1c : "products_json (логическая)" +``` + +## Примеры использования + +### Создание новой сборки +```php +$assembly = new Assemblies(); +$assembly->guid = Yii::$app->security->generateRandomString(36); +$assembly->store_id = $currentStore->guid; +$assembly->seller_id = Yii::$app->user->identity->guid; +$assembly->created_at = date('Y-m-d H:i:s'); +$assembly->products_json = json_encode([ + ['product_id' => 'rose-001', 'color' => 'красный', 'quantity' => 7, 'price' => 150], + ['product_id' => 'gyps-002', 'color' => 'белый', 'quantity' => 3, 'price' => 50], +]); +$assembly->summ = 7 * 150 + 3 * 50; // 1200 +$assembly->status_id = 0; +$assembly->save(); +``` + +### Продажа сборки +```php +$assembly = Assemblies::findOne(['guid' => $assemblyGuid]); +$assembly->status_id = 1; // Проданная +$assembly->check_id = $check->guid; +$assembly->date_close = date('Y-m-d H:i:s'); +$assembly->save(); +``` + +### Разборка букета +```php +$assembly = Assemblies::findOne(['guid' => $assemblyGuid]); +$assembly->status_id = -1; // Разобранная +$assembly->disassembling_seller_id = Yii::$app->user->identity->guid; +$assembly->save(); + +// Возврат товаров на склад +$products = json_decode($assembly->products_json, true); +foreach ($products as $product) { + // Логика возврата на склад... +} +``` + +### Редактирование состава букета +```php +$assembly = Assemblies::findOne(['guid' => $assemblyGuid]); +$oldProducts = json_decode($assembly->products_json, true); + +// Добавление записи в историю редактирования +$editHistory = $assembly->edit_json ? json_decode($assembly->edit_json, true) : []; +$editHistory[] = [ + 'date' => date('Y-m-d H:i:s'), + 'comment' => 'Замена 2 роз на хризантемы', + 'products_from' => $oldProducts, + 'products_to' => $newProducts, +]; + +$assembly->products_json = json_encode($newProducts); +$assembly->edit_json = json_encode($editHistory); +$assembly->edit_time = date('Y-m-d H:i:s'); +$assembly->summ = $this->calculateSum($newProducts); +$assembly->save(); +``` + +### Получение актуальных сборок магазина +```php +$activeAssemblies = Assemblies::find() + ->where(['store_id' => $storeGuid]) + ->andWhere(['status_id' => 0]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Статистика сборок за период +```php +$stats = Assemblies::find() + ->select([ + 'status_id', + 'COUNT(*) as count', + 'SUM(summ) as total_summ' + ]) + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'created_at', $startDate]) + ->andWhere(['<=', 'created_at', $endDate]) + ->groupBy('status_id') + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | required, string (max 36), unique | +| `store_id` | required, string (max 36) | +| `seller_id` | required, string (max 36) | +| `created_at` | required, safe | +| `disassembling_seller_id` | string (max 36), nullable | +| `edit_time` | safe, nullable | +| `edit_json` | string, nullable | +| `products_json` | required, string | +| `summ` | required, number | +| `summ_matrix` | number, nullable | +| `status_id` | integer | +| `check_id` | string (max 36), nullable | +| `date_close` | safe, nullable | +| `with_return` | integer, nullable | + +## Связанные модели + +- [Store](./Store.md) — магазин, где создана сборка +- [Admin](./Admin.md) — флорист (создатель/разборщик) +- [CreateChecks](./CreateChecks.md) — чек продажи +- [Products1c](./Products1c.md) — товары в составе сборки + +## Особенности реализации + +1. **GUID идентификация**: Уникальный GUID для синхронизации с 1С и другими системами +2. **JSON состав**: Гибкая структура products_json позволяет хранить любой состав букета +3. **История редактирования**: Полная история изменений в edit_json для аудита +4. **Матричные букеты**: Поддержка специальной суммы для букетов по матрице (summ_matrix) +5. **Трекинг возвратов**: Флаг with_return отмечает сборки с историей возврата +6. **Связь с чеком**: При продаже сборка привязывается к чеку через check_id diff --git a/erp24/docs/models/AuthAssignment.md b/erp24/docs/models/AuthAssignment.md new file mode 100644 index 00000000..33c2e513 --- /dev/null +++ b/erp24/docs/models/AuthAssignment.md @@ -0,0 +1,539 @@ +# Class: AuthAssignment + +## 🧠 Mindmap: Модель AuthAssignment + +```mermaid +mindmap + root((AuthAssignment)) + Идентификация + item_name PK FK имя роли + user_id PK ID пользователя + Временная метка + created_at дата назначения + Связи + AuthItem связь с ролью + Назначение + Связка пользователь-роль + Композитный ключ + Уникальная пара +``` + +--- + +## Назначение + +Модель назначения ролей и разрешений пользователям в системе RBAC. Представляет связь между пользователем и элементом авторизации (ролью). Каждая запись означает, что определенной пользователю назначена определенная роль. + +Используется Yii2 RBAC Manager для хранения информации о том, какие роли назначены каким пользователям. Является связующей таблицей между пользователями и элементами авторизации. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`auth_assignment` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `item_name` | string(64) | **PK, FK** Имя роли или разрешения из auth_item | +| `user_id` | string(64) | **PK** Идентификатор пользователя | + +### Временная метка + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | int | Unix timestamp создания назначения | + +--- + +## Правила валидации + +### Обязательные поля +```php +['item_name', 'user_id'] // required +``` + +### Числовые поля +```php +['created_at'] // integer с default null +``` + +### Строковые поля +```php +['item_name', 'user_id'] // max:64 +``` + +### Уникальность +```php +['item_name', 'user_id'] // unique composite - пара должна быть уникальной +``` + +### Внешние ключи +```php +['item_name'] => AuthItem::name // связь с таблицей auth_item +``` + +--- + +## Отношения (Relations) + +### getItemName() +**Тип:** `hasOne` +**Модель:** `AuthItem` +**Ключ:** `['name' => 'item_name']` +**Описание:** Элемент авторизации (роль или разрешение), назначенный пользователю + +**Логика работы:** +Возвращает ActiveQuery для получения связанного элемента AuthItem по полю item_name. Используется для получения полной информации о назначенной роли, включая её описание, тип и правила. + +**Вызовы сторонних методов:** +- `hasOne(AuthItem::class, ['name' => 'item_name'])` - создает связь один-к-одному с таблицей auth_item + +**Пример:** +```php +$assignment = AuthAssignment::findOne(['user_id' => '123', 'item_name' => 'admin']); +$item = $assignment->itemName; // получаем объект AuthItem +echo "Role: {$item->name}\n"; +echo "Description: {$item->description}\n"; +echo "Type: {$item->type}\n"; // 1 = роль, 2 = разрешение +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'auth_assignment' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = AuthAssignment::tableName(); // 'auth_assignment' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательность полей item_name и user_id +2. Значение по умолчанию для created_at (null) +3. Тип данных integer для created_at +4. Ограничение длины строк (64 символа для item_name и user_id) +5. Уникальность композитного ключа (item_name, user_id) +6. Проверку существования item_name в таблице auth_item + +**Пример:** +```php +$assignment = new AuthAssignment(); +$assignment->item_name = 'admin'; +$assignment->user_id = '123'; +$assignment->created_at = time(); +if ($assignment->validate()) { + $assignment->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив, где ключи - имена атрибутов, а значения - их отображаемые названия. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new AuthAssignment())->attributeLabels(); +echo $labels['item_name']; // "Item Name" +echo $labels['user_id']; // "User ID" +``` + +--- + +## Примеры использования + +### Назначение роли пользователю + +```php +$assignment = new AuthAssignment(); +$assignment->item_name = 'admin'; +$assignment->user_id = '123'; +$assignment->created_at = time(); +$assignment->save(); + +echo "Role 'admin' assigned to user 123\n"; +``` + +### Проверка наличия роли у пользователя + +```php +$assignment = AuthAssignment::findOne([ + 'user_id' => '123', + 'item_name' => 'admin' +]); + +if ($assignment) { + echo "User has admin role\n"; +} else { + echo "User does not have admin role\n"; +} +``` + +### Получение всех ролей пользователя + +```php +$userId = '123'; +$assignments = AuthAssignment::find() + ->where(['user_id' => $userId]) + ->all(); + +echo "User roles:\n"; +foreach ($assignments as $assignment) { + echo "- {$assignment->item_name}\n"; +} +``` + +### Получение всех пользователей с определенной ролью + +```php +$roleName = 'manager'; +$assignments = AuthAssignment::find() + ->where(['item_name' => $roleName]) + ->all(); + +echo "Users with role 'manager':\n"; +foreach ($assignments as $assignment) { + echo "- User ID: {$assignment->user_id}\n"; +} +``` + +### Удаление роли у пользователя + +```php +$assignment = AuthAssignment::findOne([ + 'user_id' => '123', + 'item_name' => 'editor' +]); + +if ($assignment) { + $assignment->delete(); + echo "Role removed\n"; +} +``` + +### Получение полной информации о роли + +```php +$assignment = AuthAssignment::findOne([ + 'user_id' => '123', + 'item_name' => 'admin' +]); + +if ($assignment) { + $item = $assignment->itemName; + echo "Role: {$item->name}\n"; + echo "Description: {$item->description}\n"; + echo "Type: " . ($item->type == 1 ? 'Role' : 'Permission') . "\n"; + echo "Assigned at: " . date('Y-m-d H:i:s', $assignment->created_at) . "\n"; +} +``` + +### Массовое назначение ролей + +```php +$userIds = ['101', '102', '103']; +$roleName = 'user'; + +foreach ($userIds as $userId) { + $assignment = new AuthAssignment(); + $assignment->item_name = $roleName; + $assignment->user_id = $userId; + $assignment->created_at = time(); + $assignment->save(); +} + +echo "Role assigned to " . count($userIds) . " users\n"; +``` + +### Замена роли пользователя + +```php +$userId = '123'; +$oldRole = 'user'; +$newRole = 'manager'; + +// Удаляем старую роль +AuthAssignment::deleteAll([ + 'user_id' => $userId, + 'item_name' => $oldRole +]); + +// Назначаем новую роль +$assignment = new AuthAssignment(); +$assignment->item_name = $newRole; +$assignment->user_id = $userId; +$assignment->created_at = time(); +$assignment->save(); + +echo "User role changed from '{$oldRole}' to '{$newRole}'\n"; +``` + +### Получение даты назначения роли + +```php +$assignment = AuthAssignment::findOne([ + 'user_id' => '123', + 'item_name' => 'admin' +]); + +if ($assignment) { + $assignedDate = date('d.m.Y H:i', $assignment->created_at); + echo "Admin role assigned on: {$assignedDate}\n"; + + $daysAgo = floor((time() - $assignment->created_at) / 86400); + echo "Days since assignment: {$daysAgo}\n"; +} +``` + +### Проверка множественных ролей + +```php +$userId = '123'; +$requiredRoles = ['admin', 'manager']; + +$assignments = AuthAssignment::find() + ->where(['user_id' => $userId]) + ->andWhere(['in', 'item_name', $requiredRoles]) + ->all(); + +$hasAllRoles = (count($assignments) === count($requiredRoles)); + +if ($hasAllRoles) { + echo "User has all required roles\n"; +} else { + echo "User is missing some roles\n"; +} +``` + +### Статистика назначений + +```php +// Количество пользователей для каждой роли +$stats = AuthAssignment::find() + ->select(['item_name', 'COUNT(*) as count']) + ->groupBy('item_name') + ->asArray() + ->all(); + +echo "Role assignment statistics:\n"; +foreach ($stats as $stat) { + echo "- {$stat['item_name']}: {$stat['count']} users\n"; +} + +// Общее количество назначений +$totalAssignments = AuthAssignment::find()->count(); +echo "\nTotal assignments: {$totalAssignments}\n"; +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + AuthAssignment }o--|| AuthItem : "has item" + Users ||--o{ AuthAssignment : "has roles" + + AuthAssignment { + string item_name PK_FK + string user_id PK + int created_at + } + + AuthItem { + string name PK + int type + text description + string rule_name FK + resource data + int created_at + int updated_at + } + + Users { + int id PK + string phone + string name + int black_list + } +``` + +--- + +## Бизнес-логика + +### Назначение ролей + +Модель AuthAssignment реализует связь many-to-many между пользователями и ролями: + +1. **Один пользователь** может иметь **несколько ролей** +2. **Одна роль** может быть назначена **нескольким пользователям** +3. **Композитный первичный ключ** (item_name, user_id) гарантирует уникальность назначения +4. **Временная метка** created_at фиксирует момент назначения роли + +### Типичные сценарии + +**При регистрации нового пользователя:** +```php +// Назначаем базовую роль 'user' +$assignment = new AuthAssignment(); +$assignment->item_name = 'user'; +$assignment->user_id = $newUserId; +$assignment->created_at = time(); +$assignment->save(); +``` + +**При повышении прав:** +```php +// Добавляем роль 'manager' (не удаляя 'user') +$assignment = new AuthAssignment(); +$assignment->item_name = 'manager'; +$assignment->user_id = $userId; +$assignment->created_at = time(); +$assignment->save(); +``` + +**При блокировке пользователя:** +```php +// Удаляем все роли +AuthAssignment::deleteAll(['user_id' => $userId]); +``` + +### Иерархия и наследование + +Хотя AuthAssignment хранит только прямые назначения, RBAC система автоматически учитывает иерархию через AuthItemChild: + +``` +User ID: 123 + └─ Assigned: manager (прямое назначение) + └─ Includes: viewReports (через иерархию) + └─ Includes: createReport (через иерархию) + └─ Includes: updateReport (через иерархию) +``` + +### Проверка доступа + +Yii2 RBAC использует AuthAssignment для проверки: + +```php +// Проверка в контроллере +if (Yii::$app->user->can('deletePost')) { + // Пользователь имеет разрешение deletePost + // либо напрямую, либо через назначенную роль +} +``` + +--- + +## Связи с другими моделями + +### Прямые связи +- **AuthItem** — элемент авторизации (роль/разрешение) + +### Обратные связи +- От **Users** — пользователь, которому назначена роль +- От **AuthItem** — все назначения данной роли + +--- + +## Индексы и производительность + +### Первичный ключ (композитный) + +```sql +PRIMARY KEY (item_name, user_id) +``` + +### Рекомендуемые индексы + +```sql +-- Внешний ключ на auth_item +CREATE INDEX idx_auth_assignment_item_name ON auth_assignment(item_name); + +-- Поиск по пользователю +CREATE INDEX idx_auth_assignment_user_id ON auth_assignment(user_id); + +-- Временная метка +CREATE INDEX idx_auth_assignment_created ON auth_assignment(created_at); +``` + +### Оптимизация запросов + +```php +// Плохо: N+1 запросов +$assignments = AuthAssignment::find()->all(); +foreach ($assignments as $assignment) { + echo $assignment->itemName->description; // +N запросов +} + +// Хорошо: 1 запрос с JOIN +$assignments = AuthAssignment::find() + ->joinWith('itemName') + ->all(); +foreach ($assignments as $assignment) { + echo $assignment->itemName->description; // данные уже загружены +} +``` + +--- + +## Замечания + +1. **Композитный первичный ключ** — (item_name, user_id) обеспечивает уникальность +2. **Внешний ключ** — item_name должен существовать в auth_item +3. **user_id** — строковый тип (64 символа), может хранить как числовые ID, так и UUID +4. **Каскадное удаление** — при удалении роли из auth_item должны удаляться все назначения +5. **Временная метка** — Unix timestamp (integer), фиксирует момент назначения +6. **Нет прямой связи с таблицей пользователей** — user_id это просто строка +7. **Множественные роли** — один пользователь может иметь несколько записей с разными item_name +8. **RBAC проверки** — Yii2 автоматически использует эту таблицу для Yii::$app->user->can() + +--- + +## Связанные документы + +- [AuthItem.md](./AuthItem.md) — элементы авторизации RBAC +- [AuthItemChild.md](./AuthItemChild.md) — иерархия ролей и разрешений +- [AuthRule.md](./AuthRule.md) — правила авторизации +- [Users.md](./Users.md) — модель пользователей diff --git a/erp24/docs/models/AuthItem.md b/erp24/docs/models/AuthItem.md new file mode 100644 index 00000000..0c8d9456 --- /dev/null +++ b/erp24/docs/models/AuthItem.md @@ -0,0 +1,578 @@ +# Class: AuthItem + +## 🧠 Mindmap: Модель AuthItem + +```mermaid +mindmap + root((AuthItem)) + Идентификация + name PK уникальное имя + type тип элемента + description описание + Правила доступа + rule_name связь с AuthRule + data сериализованные данные + Временные метки + created_at дата создания + updated_at дата обновления + Иерархия RBAC + AuthItemChild дочерние элементы + parent родительские + child дочерние + children список дочерних + parents список родительских + Назначения + AuthAssignment назначения пользователям + Типы элементов + ROLE роль + PERMISSION разрешение +``` + +--- + +## Назначение + +Модель элемента авторизации в системе RBAC (Role-Based Access Control). Представляет роли и разрешения в системе контроля доступа ERP24. Каждый элемент может быть ролью или разрешением, иметь описание, правило проверки и иерархическую структуру через дочерние элементы. + +Используется Yii2 RBAC Manager для управления правами доступа пользователей к функциям системы. Поддерживает иерархическую структуру: роли могут включать другие роли и разрешения. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`auth_item` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `name` | string(64) | **PK** Уникальное имя роли или разрешения | +| `type` | int | **Тип элемента** (1 = роль, 2 = разрешение) | +| `description` | text | Описание роли/разрешения | + +### Правила и данные + +| Имя | Тип | Описание | +|-----|-----|----------| +| `rule_name` | string(64) | **FK** Имя правила проверки (связь с auth_rule) | +| `data` | resource | Сериализованные дополнительные данные | + +### Временные метки + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | int | Unix timestamp создания элемента | +| `updated_at` | int | Unix timestamp последнего обновления | + +--- + +## Правила валидации + +### Обязательные поля +```php +['name', 'type'] // required +``` + +### Числовые поля +```php +['type', 'created_at', 'updated_at'] // integer +``` + +### Строковые поля +```php +['description', 'data'] // text +['name', 'rule_name'] // max:64 +``` + +### Уникальность +```php +['name'] // unique +``` + +### Внешние ключи +```php +['rule_name'] => AuthRule::name // связь с таблицей auth_rule +``` + +--- + +## Отношения (Relations) + +### getAuthAssignments() +**Тип:** `hasMany` +**Модель:** `AuthAssignment` +**Ключ:** `['item_name' => 'name']` +**Описание:** Все назначения данной роли/разрешения пользователям + +**Логика работы:** +Возвращает ActiveQuery для выборки всех записей назначения данного элемента авторизации пользователям. Используется для определения, каким пользователям назначена данная роль или разрешение. + +**Пример:** +```php +$item = AuthItem::findOne('admin'); +$assignments = $item->authAssignments; // все назначения роли admin +foreach ($assignments as $assignment) { + echo "User ID: {$assignment->user_id}\n"; +} +``` + +--- + +### getAuthItemChildren() +**Тип:** `hasMany` +**Модель:** `AuthItemChild` +**Ключ:** `['parent' => 'name']` +**Описание:** Записи дочерних элементов, где данный элемент является родителем + +**Логика работы:** +Возвращает ActiveQuery для выборки всех связей auth_item_child, где данный элемент выступает в роли родителя. Используется для получения иерархии: какие элементы включены в данный. + +**Пример:** +```php +$role = AuthItem::findOne('admin'); +$childrenLinks = $role->authItemChildren; // связи с дочерними элементами +foreach ($childrenLinks as $link) { + echo "Child: {$link->child}\n"; +} +``` + +--- + +### getAuthItemChildren0() +**Тип:** `hasMany` +**Модель:** `AuthItemChild` +**Ключ:** `['child' => 'name']` +**Описание:** Записи родительских элементов, где данный элемент является дочерним + +**Логика работы:** +Возвращает ActiveQuery для выборки всех связей auth_item_child, где данный элемент выступает в роли дочернего. Используется для получения обратной иерархии: в какие элементы включен данный. + +**Пример:** +```php +$permission = AuthItem::findOne('createPost'); +$parentLinks = $permission->authItemChildren0; // связи с родительскими элементами +foreach ($parentLinks as $link) { + echo "Parent: {$link->parent}\n"; +} +``` + +--- + +### getChildren() +**Тип:** `hasMany` через `viaTable` +**Модель:** `AuthItem` +**Через таблицу:** `auth_item_child` +**Ключи:** `['name' => 'child']` via `['parent' => 'name']` +**Описание:** Список дочерних элементов (роли/разрешения, входящие в данный элемент) + +**Логика работы:** +Возвращает ActiveQuery для получения всех дочерних элементов AuthItem через промежуточную таблицу auth_item_child. Например, для роли "admin" вернет все разрешения и роли, которые в нее входят. + +**Вызовы сторонних методов:** +- `hasMany(AuthItem::class, ['name' => 'child'])` - создает связь many-to-many +- `viaTable('auth_item_child', ['parent' => 'name'])` - указывает промежуточную таблицу + +**Пример:** +```php +$role = AuthItem::findOne('admin'); +$children = $role->children; // все дочерние элементы +foreach ($children as $child) { + echo "Child item: {$child->name} (type: {$child->type})\n"; +} +``` + +--- + +### getParents() +**Тип:** `hasMany` через `viaTable` +**Модель:** `AuthItem` +**Через таблицу:** `auth_item_child` +**Ключи:** `['name' => 'parent']` via `['child' => 'name']` +**Описание:** Список родительских элементов (роли, в которые входит данный элемент) + +**Логика работы:** +Возвращает ActiveQuery для получения всех родительских элементов AuthItem через промежуточную таблицу auth_item_child. Например, для разрешения "createPost" вернет все роли, которые включают это разрешение. + +**Вызовы сторонних методов:** +- `hasMany(AuthItem::class, ['name' => 'parent'])` - создает связь many-to-many +- `viaTable('auth_item_child', ['child' => 'name'])` - указывает промежуточную таблицу + +**Пример:** +```php +$permission = AuthItem::findOne('createPost'); +$parents = $permission->parents; // все родительские роли +foreach ($parents as $parent) { + echo "Parent role: {$parent->name}\n"; +} +``` + +--- + +### getRuleName() +**Тип:** `hasOne` +**Модель:** `AuthRule` +**Ключ:** `['name' => 'rule_name']` +**Описание:** Правило проверки доступа, связанное с данным элементом + +**Логика работы:** +Возвращает ActiveQuery для получения связанного правила AuthRule. Правило содержит бизнес-логику проверки доступа, которая выполняется при проверке разрешения. + +**Пример:** +```php +$item = AuthItem::findOne('updateOwnPost'); +$rule = $item->ruleName; // правило, например AuthorRule +if ($rule) { + echo "Rule: {$rule->name}\n"; +} +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'auth_item' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = AuthItem::tableName(); // 'auth_item' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательность полей name и type +2. Значения по умолчанию для числовых полей (null) +3. Типы данных (integer для type, created_at, updated_at; string для description, data) +4. Ограничения длины строк (64 символа для name и rule_name) +5. Уникальность name +6. Проверку существования rule_name в таблице auth_rule + +**Пример:** +```php +$item = new AuthItem(); +$item->name = 'newRole'; +$item->type = 1; +if ($item->validate()) { + $item->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив, где ключи - имена атрибутов, а значения - их отображаемые названия. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new AuthItem())->attributeLabels(); +echo $labels['name']; // "Name" +``` + +--- + +## Примеры использования + +### Создание новой роли + +```php +$role = new AuthItem(); +$role->name = 'moderator'; +$role->type = 1; // роль +$role->description = 'Модератор контента'; +$role->created_at = time(); +$role->updated_at = time(); +$role->save(); +``` + +### Создание нового разрешения + +```php +$permission = new AuthItem(); +$permission->name = 'deletePost'; +$permission->type = 2; // разрешение +$permission->description = 'Удаление поста'; +$permission->created_at = time(); +$permission->updated_at = time(); +$permission->save(); +``` + +### Получение всех ролей + +```php +$roles = AuthItem::find() + ->where(['type' => 1]) + ->all(); + +foreach ($roles as $role) { + echo "Role: {$role->name} - {$role->description}\n"; +} +``` + +### Получение всех разрешений + +```php +$permissions = AuthItem::find() + ->where(['type' => 2]) + ->all(); + +foreach ($permissions as $permission) { + echo "Permission: {$permission->name}\n"; +} +``` + +### Работа с иерархией + +```php +// Получение дочерних элементов роли +$adminRole = AuthItem::findOne('admin'); +$children = $adminRole->children; + +echo "Admin role includes:\n"; +foreach ($children as $child) { + $type = ($child->type == 1) ? 'Role' : 'Permission'; + echo "- {$child->name} ({$type})\n"; +} + +// Получение родительских ролей для разрешения +$permission = AuthItem::findOne('createPost'); +$parents = $permission->parents; + +echo "\nPermission 'createPost' is included in:\n"; +foreach ($parents as $parent) { + echo "- {$parent->name}\n"; +} +``` + +### Проверка наличия правила + +```php +$item = AuthItem::findOne('updateOwnPost'); +if ($item->rule_name) { + $rule = $item->ruleName; + echo "This item has rule: {$rule->name}\n"; +} else { + echo "No rule attached\n"; +} +``` + +### Получение всех назначений роли + +```php +$role = AuthItem::findOne('manager'); +$assignments = $role->authAssignments; + +echo "Role 'manager' is assigned to:\n"; +foreach ($assignments as $assignment) { + echo "- User ID: {$assignment->user_id}\n"; +} +``` + +### Поиск элементов по описанию + +```php +$items = AuthItem::find() + ->where(['like', 'description', 'управление']) + ->all(); + +foreach ($items as $item) { + echo "{$item->name}: {$item->description}\n"; +} +``` + +### Обновление элемента + +```php +$role = AuthItem::findOne('editor'); +$role->description = 'Редактор контента с расширенными правами'; +$role->updated_at = time(); +$role->save(); +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + AuthItem ||--o{ AuthAssignment : "assigned to users" + AuthItem ||--o{ AuthItemChild : "parent" + AuthItem ||--o{ AuthItemChild : "child" + AuthItem }o--o{ AuthItem : "children/parents" + AuthItem }o--o| AuthRule : "has rule" + + AuthItem { + string name PK "Unique name" + int type "1=role 2=permission" + text description + string rule_name FK + resource data + int created_at + int updated_at + } + + AuthAssignment { + string item_name PK_FK + string user_id PK + int created_at + } + + AuthItemChild { + string parent PK_FK + string child PK_FK + } + + AuthRule { + string name PK + resource data + int created_at + int updated_at + } +``` + +--- + +## Бизнес-логика + +### Типы элементов RBAC + +**Роль (type = 1)** +- Группирует разрешения +- Может включать другие роли +- Назначается пользователям +- Примеры: admin, manager, editor, user + +**Разрешение (type = 2)** +- Атомарное право доступа +- Включается в роли +- Напрямую не назначается пользователям +- Примеры: createPost, deletePost, viewUsers + +### Иерархическая структура + +RBAC в ERP24 поддерживает иерархию через таблицу auth_item_child: + +``` +admin (роль) + ├─ manager (роль) + │ ├─ viewReports (разрешение) + │ ├─ createReport (разрешение) + │ └─ updateReport (разрешение) + ├─ editor (роль) + │ ├─ createPost (разрешение) + │ ├─ updateOwnPost (разрешение) + │ └─ deleteOwnPost (разрешение) + └─ deleteAnyPost (разрешение) +``` + +### Правила доступа + +Элементы могут иметь правила (rule_name), содержащие бизнес-логику: +- Проверка владельца ресурса +- Проверка времени доступа +- Проверка статуса пользователя +- Сложные условия доступа + +### Назначение пользователям + +Роли назначаются через AuthAssignment: +```php +// User ID 123 получает роль manager +$assignment = new AuthAssignment(); +$assignment->item_name = 'manager'; +$assignment->user_id = '123'; +$assignment->created_at = time(); +$assignment->save(); +``` + +--- + +## Связи с другими моделями + +### Прямые связи +- **AuthRule** — правило проверки доступа +- **AuthAssignment** — назначения пользователям +- **AuthItemChild** — иерархия (родители/дети) +- **AuthItem** (self) — родительские и дочерние элементы + +### Обратные связи +- От **AuthAssignment** — все назначения данной роли +- От **AuthItemChild** — все связи в иерархии + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (name) + +-- Внешний ключ +CREATE INDEX idx_auth_item_rule_name ON auth_item(rule_name); + +-- Поиск по типу +CREATE INDEX idx_auth_item_type ON auth_item(type); + +-- Временные метки +CREATE INDEX idx_auth_item_created ON auth_item(created_at); +CREATE INDEX idx_auth_item_updated ON auth_item(updated_at); +``` + +--- + +## Замечания + +1. **Первичный ключ** — строковое поле name (не автоинкремент) +2. **Тип элемента** — 1 для ролей, 2 для разрешений +3. **Иерархия** — через промежуточную таблицу auth_item_child +4. **Правила** — опциональная бизнес-логика проверки +5. **Временные метки** — Unix timestamp (integer) +6. **Уникальность** — name должен быть уникальным +7. **Назначение** — только роли назначаются пользователям напрямую +8. **Данные** — поле data хранит сериализованную дополнительную информацию + +--- + +## Связанные документы + +- [AuthAssignment.md](./AuthAssignment.md) — назначения авторизации +- [AuthItemChild.md](./AuthItemChild.md) — дочерние элементы +- [AuthRule.md](./AuthRule.md) — правила авторизации +- [Users.md](./Users.md) — модель пользователей diff --git a/erp24/docs/models/AuthItemChild.md b/erp24/docs/models/AuthItemChild.md new file mode 100644 index 00000000..71e9c6c2 --- /dev/null +++ b/erp24/docs/models/AuthItemChild.md @@ -0,0 +1,583 @@ +# Class: AuthItemChild + +## 🧠 Mindmap: Модель AuthItemChild + +```mermaid +mindmap + root((AuthItemChild)) + Идентификация + parent PK FK родительский элемент + child PK FK дочерний элемент + Иерархия RBAC + Связь родитель-ребенок + Наследование разрешений + Композитный ключ + Связи + parent0 родительский AuthItem + child0 дочерний AuthItem + Назначение + Построение иерархии ролей + Включение разрешений в роли + Many-to-many для AuthItem +``` + +--- + +## Назначение + +Модель иерархии элементов авторизации в системе RBAC. Представляет связь родитель-ребенок между элементами авторизации (ролями и разрешениями). Используется для построения иерархической структуры прав доступа, где роли могут включать другие роли и разрешения. + +Через эту модель реализуется наследование прав: если роль A включает роль B, то пользователь с ролью A автоматически получает все права роли B. Также роли могут напрямую включать разрешения. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`auth_item_child` + +--- + +## Основные свойства + +### Иерархическая связь + +| Имя | Тип | Описание | +|-----|-----|----------| +| `parent` | string(64) | **PK, FK** Имя родительского элемента (роли или разрешения) | +| `child` | string(64) | **PK, FK** Имя дочернего элемента (роли или разрешения) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['parent', 'child'] // required +``` + +### Строковые поля +```php +['parent', 'child'] // max:64 +``` + +### Уникальность +```php +['parent', 'child'] // unique composite - пара должна быть уникальной +``` + +### Внешние ключи +```php +['parent'] => AuthItem::name // связь с таблицей auth_item +['child'] => AuthItem::name // связь с таблицей auth_item +``` + +--- + +## Отношения (Relations) + +### getChild0() +**Тип:** `hasOne` +**Модель:** `AuthItem` +**Ключ:** `['name' => 'child']` +**Описание:** Дочерний элемент авторизации (роль или разрешение) + +**Логика работы:** +Возвращает ActiveQuery для получения связанного дочернего элемента AuthItem по полю child. Используется для получения полной информации о дочернем элементе в иерархии, включая его тип, описание и правила. + +**Вызовы сторонних методов:** +- `hasOne(AuthItem::class, ['name' => 'child'])` - создает связь один-к-одному с таблицей auth_item + +**Пример:** +```php +$relation = AuthItemChild::findOne(['parent' => 'admin', 'child' => 'createPost']); +$childItem = $relation->child0; // получаем объект AuthItem +echo "Child: {$childItem->name}\n"; +echo "Type: " . ($childItem->type == 1 ? 'Role' : 'Permission') . "\n"; +echo "Description: {$childItem->description}\n"; +``` + +--- + +### getParent0() +**Тип:** `hasOne` +**Модель:** `AuthItem` +**Ключ:** `['name' => 'parent']` +**Описание:** Родительский элемент авторизации (роль или разрешение) + +**Логика работы:** +Возвращает ActiveQuery для получения связанного родительского элемента AuthItem по полю parent. Используется для получения полной информации о родительском элементе в иерархии, включая его тип, описание и правила. + +**Вызовы сторонних методов:** +- `hasOne(AuthItem::class, ['name' => 'parent'])` - создает связь один-к-одному с таблицей auth_item + +**Пример:** +```php +$relation = AuthItemChild::findOne(['parent' => 'manager', 'child' => 'viewReports']); +$parentItem = $relation->parent0; // получаем объект AuthItem +echo "Parent: {$parentItem->name}\n"; +echo "Type: " . ($parentItem->type == 1 ? 'Role' : 'Permission') . "\n"; +echo "Description: {$parentItem->description}\n"; +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'auth_item_child' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = AuthItemChild::tableName(); // 'auth_item_child' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательность полей parent и child +2. Ограничение длины строк (64 символа для parent и child) +3. Уникальность композитного ключа (parent, child) - одна и та же связь не может существовать дважды +4. Проверку существования parent в таблице auth_item +5. Проверку существования child в таблице auth_item + +**Пример:** +```php +$relation = new AuthItemChild(); +$relation->parent = 'admin'; +$relation->child = 'manager'; +if ($relation->validate()) { + $relation->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив, где ключи - имена атрибутов, а значения - их отображаемые названия. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new AuthItemChild())->attributeLabels(); +echo $labels['parent']; // "Parent" +echo $labels['child']; // "Child" +``` + +--- + +## Примеры использования + +### Добавление разрешения в роль + +```php +// Роль 'editor' включает разрешение 'createPost' +$relation = new AuthItemChild(); +$relation->parent = 'editor'; +$relation->child = 'createPost'; +$relation->save(); + +echo "Permission 'createPost' added to role 'editor'\n"; +``` + +### Создание иерархии ролей + +```php +// Роль 'admin' включает роль 'manager' +$relation = new AuthItemChild(); +$relation->parent = 'admin'; +$relation->child = 'manager'; +$relation->save(); + +// Роль 'manager' включает роль 'user' +$relation2 = new AuthItemChild(); +$relation2->parent = 'manager'; +$relation2->child = 'user'; +$relation2->save(); + +// Теперь admin наследует все права manager и user +``` + +### Получение всех дочерних элементов роли + +```php +$roleName = 'admin'; +$children = AuthItemChild::find() + ->where(['parent' => $roleName]) + ->all(); + +echo "Role '{$roleName}' includes:\n"; +foreach ($children as $relation) { + echo "- {$relation->child}\n"; +} +``` + +### Получение всех родительских ролей для элемента + +```php +$itemName = 'createPost'; +$parents = AuthItemChild::find() + ->where(['child' => $itemName]) + ->all(); + +echo "Item '{$itemName}' is included in:\n"; +foreach ($parents as $relation) { + echo "- {$relation->parent}\n"; +} +``` + +### Удаление разрешения из роли + +```php +$relation = AuthItemChild::findOne([ + 'parent' => 'editor', + 'child' => 'deletePost' +]); + +if ($relation) { + $relation->delete(); + echo "Permission removed from role\n"; +} +``` + +### Построение полной иерархии роли + +```php +function getRoleHierarchy($roleName, $level = 0) { + $indent = str_repeat(' ', $level); + + // Получаем информацию о роли + $item = AuthItem::findOne($roleName); + $type = ($item->type == 1) ? 'Role' : 'Permission'; + echo "{$indent}- {$roleName} ({$type})\n"; + + // Получаем дочерние элементы + $children = AuthItemChild::find() + ->where(['parent' => $roleName]) + ->all(); + + foreach ($children as $relation) { + getRoleHierarchy($relation->child, $level + 1); + } +} + +getRoleHierarchy('admin'); +// Вывод: +// - admin (Role) +// - manager (Role) +// - viewReports (Permission) +// - createReport (Permission) +// - editor (Role) +// - createPost (Permission) +// - updatePost (Permission) +``` + +### Проверка существования связи + +```php +$exists = AuthItemChild::find() + ->where([ + 'parent' => 'admin', + 'child' => 'deletePost' + ]) + ->exists(); + +if ($exists) { + echo "Role 'admin' includes permission 'deletePost'\n"; +} else { + echo "Role 'admin' does not include permission 'deletePost'\n"; +} +``` + +### Массовое добавление разрешений в роль + +```php +$roleName = 'moderator'; +$permissions = ['viewUsers', 'banUsers', 'deleteComments']; + +foreach ($permissions as $permission) { + $relation = new AuthItemChild(); + $relation->parent = $roleName; + $relation->child = $permission; + $relation->save(); +} + +echo count($permissions) . " permissions added to role '{$roleName}'\n"; +``` + +### Копирование структуры роли + +```php +// Копируем все дочерние элементы из роли 'editor' в роль 'moderator' +$sourceRole = 'editor'; +$targetRole = 'moderator'; + +$children = AuthItemChild::find() + ->where(['parent' => $sourceRole]) + ->all(); + +foreach ($children as $relation) { + $newRelation = new AuthItemChild(); + $newRelation->parent = $targetRole; + $newRelation->child = $relation->child; + $newRelation->save(); +} + +echo "Copied " . count($children) . " items from '{$sourceRole}' to '{$targetRole}'\n"; +``` + +### Получение подробной информации об иерархии + +```php +$relations = AuthItemChild::find() + ->joinWith(['parent0', 'child0']) + ->where(['parent' => 'admin']) + ->all(); + +echo "Admin role hierarchy:\n"; +foreach ($relations as $relation) { + $parentItem = $relation->parent0; + $childItem = $relation->child0; + + echo "Parent: {$parentItem->name} ({$parentItem->description})\n"; + echo " └─ Child: {$childItem->name} ({$childItem->description})\n"; +} +``` + +### Удаление всех дочерних элементов роли + +```php +$roleName = 'oldRole'; +$deletedCount = AuthItemChild::deleteAll(['parent' => $roleName]); +echo "Removed {$deletedCount} children from role '{$roleName}'\n"; +``` + +### Поиск циклических зависимостей + +```php +// Проверка на цикл: если A включает B, то B не должен включать A +function hasCycle($itemName, $visited = []) { + if (in_array($itemName, $visited)) { + return true; // Цикл найден + } + + $visited[] = $itemName; + + $children = AuthItemChild::find() + ->where(['parent' => $itemName]) + ->all(); + + foreach ($children as $relation) { + if (hasCycle($relation->child, $visited)) { + return true; + } + } + + return false; +} + +if (hasCycle('admin')) { + echo "Warning: Circular dependency detected!\n"; +} +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + AuthItemChild }o--|| AuthItem : "parent item" + AuthItemChild }o--|| AuthItem : "child item" + AuthItem ||--o{ AuthItemChild : "as parent" + AuthItem ||--o{ AuthItemChild : "as child" + + AuthItemChild { + string parent PK_FK + string child PK_FK + } + + AuthItem { + string name PK + int type "1=role 2=permission" + text description + string rule_name FK + resource data + int created_at + int updated_at + } +``` + +--- + +## Бизнес-логика + +### Иерархия RBAC + +AuthItemChild реализует иерархическую систему прав доступа через отношение parent-child: + +**Типы связей:** + +1. **Роль включает роль** + ``` + admin (роль) → manager (роль) + ``` + Пользователь с ролью admin автоматически получает все права роли manager + +2. **Роль включает разрешение** + ``` + editor (роль) → createPost (разрешение) + ``` + Пользователь с ролью editor получает разрешение createPost + +3. **Разрешение включает разрешение** (редко используется) + ``` + updatePost → viewPost + ``` + Для обновления поста нужно право его просмотра + +### Наследование прав + +Пример иерархии: + +``` +admin (роль) + ├─ manager (роль) + │ ├─ viewReports (разрешение) + │ ├─ createReport (разрешение) + │ └─ user (роль) + │ └─ viewOwnProfile (разрешение) + ├─ editor (роль) + │ ├─ createPost (разрешение) + │ ├─ updateOwnPost (разрешение) + │ └─ deleteOwnPost (разрешение) + └─ deleteAnyPost (разрешение) +``` + +Пользователь с ролью `admin` автоматически получает: +- Все права `manager`: viewReports, createReport +- Все права `user` (через manager): viewOwnProfile +- Все права `editor`: createPost, updateOwnPost, deleteOwnPost +- Прямое разрешение: deleteAnyPost + +### Композитный ключ + +Таблица использует композитный первичный ключ (parent, child), что обеспечивает: +- **Уникальность связи**: одна пара parent-child может существовать только один раз +- **Быстрый поиск**: эффективные запросы по parent или child +- **Целостность данных**: невозможно создать дубликаты связей + +### Предотвращение циклов + +RBAC система должна предотвращать циклические зависимости: +``` +❌ НЕПРАВИЛЬНО: +admin → manager → admin (цикл) + +✅ ПРАВИЛЬНО: +admin → manager → user (иерархия) +``` + +Yii2 DbManager автоматически проверяет циклы при добавлении связей. + +--- + +## Связи с другими моделями + +### Прямые связи +- **AuthItem** (parent0) — родительский элемент +- **AuthItem** (child0) — дочерний элемент + +### Обратные связи +- От **AuthItem** — все дочерние элементы (через parent) +- От **AuthItem** — все родительские элементы (через child) + +--- + +## Индексы и производительность + +### Первичный ключ (композитный) + +```sql +PRIMARY KEY (parent, child) +``` + +### Рекомендуемые индексы + +```sql +-- Внешний ключ на parent +CREATE INDEX idx_auth_item_child_parent ON auth_item_child(parent); + +-- Внешний ключ на child +CREATE INDEX idx_auth_item_child_child ON auth_item_child(child); +``` + +### Оптимизация запросов + +```php +// Плохо: N+1 запросов +$relations = AuthItemChild::find()->where(['parent' => 'admin'])->all(); +foreach ($relations as $relation) { + echo $relation->child0->description; // +N запросов +} + +// Хорошо: 1 запрос с JOIN +$relations = AuthItemChild::find() + ->joinWith(['child0']) + ->where(['parent' => 'admin']) + ->all(); +foreach ($relations as $relation) { + echo $relation->child0->description; // данные уже загружены +} +``` + +--- + +## Замечания + +1. **Композитный первичный ключ** — (parent, child) обеспечивает уникальность связи +2. **Оба поля являются внешними ключами** — ссылаются на auth_item.name +3. **Нет временных меток** — таблица не хранит когда была создана связь +4. **Транзитивное наследование** — права наследуются через всю цепочку +5. **Каскадное удаление** — при удалении элемента из auth_item удаляются все его связи +6. **Проверка циклов** — Yii2 DbManager автоматически предотвращает циклические зависимости +7. **Many-to-many для AuthItem** — один элемент может быть parent для многих child и наоборот +8. **Используется Yii2 RBAC** — таблица управляется через DbManager, не рекомендуется прямое редактирование + +--- + +## Связанные документы + +- [AuthItem.md](./AuthItem.md) — элементы авторизации RBAC +- [AuthAssignment.md](./AuthAssignment.md) — назначения авторизации +- [AuthRule.md](./AuthRule.md) — правила авторизации +- [Users.md](./Users.md) — модель пользователей diff --git a/erp24/docs/models/AuthRule.md b/erp24/docs/models/AuthRule.md new file mode 100644 index 00000000..3e18e58f --- /dev/null +++ b/erp24/docs/models/AuthRule.md @@ -0,0 +1,585 @@ +# Class: AuthRule + +## 🧠 Mindmap: Модель AuthRule + +```mermaid +mindmap + root((AuthRule)) + Идентификация + name PK уникальное имя + Данные правила + data сериализованный код + Временные метки + created_at дата создания + updated_at дата обновления + Связи + AuthItem элементы использующие правило + Назначение + Бизнес-логика проверки + Кастомные правила доступа + Динамическая проверка прав +``` + +--- + +## Назначение + +Модель правил авторизации в системе RBAC. Хранит бизнес-логику для динамической проверки прав доступа. Правила позволяют добавлять дополнительные условия к разрешениям, например, проверку владельца ресурса, времени доступа или статуса пользователя. + +Используется Yii2 RBAC Manager для выполнения сложных проверок доступа, которые невозможно реализовать через простую иерархию ролей и разрешений. Правила содержат PHP-код, который выполняется при каждой проверке доступа. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`auth_rule` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `name` | string(64) | **PK** Уникальное имя правила (обычно класс правила) | + +### Данные правила + +| Имя | Тип | Описание | +|-----|-----|----------| +| `data` | resource | Сериализованный объект правила (PHP класс) | + +### Временные метки + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | int | Unix timestamp создания правила | +| `updated_at` | int | Unix timestamp последнего обновления | + +--- + +## Правила валидации + +### Обязательные поля +```php +['name'] // required +``` + +### Строковые поля +```php +['data'] // text +['name'] // max:64 +``` + +### Числовые поля +```php +['created_at', 'updated_at'] // integer с default null +``` + +### Уникальность +```php +['name'] // unique +``` + +--- + +## Отношения (Relations) + +### getAuthItems() +**Тип:** `hasMany` +**Модель:** `AuthItem` +**Ключ:** `['rule_name' => 'name']` +**Описание:** Все элементы авторизации (роли и разрешения), использующие данное правило + +**Логика работы:** +Возвращает ActiveQuery для выборки всех элементов AuthItem, которые используют данное правило. Правило может быть применено к нескольким элементам авторизации одновременно. + +**Вызовы сторонних методов:** +- `hasMany(AuthItem::class, ['rule_name' => 'name'])` - создает связь один-ко-многим с таблицей auth_item + +**Пример:** +```php +$rule = AuthRule::findOne('AuthorRule'); +$items = $rule->authItems; // все элементы использующие это правило + +echo "Rule '{$rule->name}' is used by:\n"; +foreach ($items as $item) { + $type = ($item->type == 1) ? 'Role' : 'Permission'; + echo "- {$item->name} ({$type})\n"; +} +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'auth_rule' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = AuthRule::tableName(); // 'auth_rule' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Обязательность поля name +2. Тип данных string для data +3. Значения по умолчанию для created_at и updated_at (null) +4. Тип данных integer для временных меток +5. Ограничение длины name (64 символа) +6. Уникальность name + +**Пример:** +```php +$rule = new AuthRule(); +$rule->name = 'AuthorRule'; +$rule->data = serialize($ruleObject); +$rule->created_at = time(); +if ($rule->validate()) { + $rule->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив, где ключи - имена атрибутов, а значения - их отображаемые названия. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new AuthRule())->attributeLabels(); +echo $labels['name']; // "Name" +echo $labels['data']; // "Data" +``` + +--- + +## Примеры использования + +### Концепция правил RBAC + +Правила в Yii2 RBAC представляют собой PHP классы, наследующие `yii\rbac\Rule`: + +```php +namespace app\rbac; + +use yii\rbac\Rule; + +/** + * Проверяет, является ли пользователь автором поста + */ +class AuthorRule extends Rule +{ + public $name = 'isAuthor'; + + public function execute($user, $item, $params) + { + // $params['post'] должен быть передан при проверке + return isset($params['post']) + ? $params['post']->created_by == $user + : false; + } +} +``` + +### Создание и сохранение правила через DbManager + +```php +$auth = Yii::$app->authManager; + +// Создаем объект правила +$rule = new \app\rbac\AuthorRule(); + +// Добавляем правило через DbManager (автоматически сохраняется в auth_rule) +$auth->add($rule); + +echo "Rule created: {$rule->name}\n"; +``` + +### Привязка правила к разрешению + +```php +$auth = Yii::$app->authManager; + +// Создаем разрешение с правилом +$updateOwnPost = $auth->createPermission('updateOwnPost'); +$updateOwnPost->description = 'Update own post'; +$updateOwnPost->ruleName = 'isAuthor'; // связываем с правилом +$auth->add($updateOwnPost); +``` + +### Проверка доступа с правилом + +```php +// В контроллере +$post = Post::findOne($id); + +// Передаем параметры для правила +if (Yii::$app->user->can('updateOwnPost', ['post' => $post])) { + // Пользователь может обновить свой пост + $post->title = 'New title'; + $post->save(); +} else { + throw new ForbiddenHttpException('You can only update your own posts.'); +} +``` + +### Получение всех правил + +```php +$rules = AuthRule::find()->all(); + +echo "Available rules:\n"; +foreach ($rules as $rule) { + echo "- {$rule->name}\n"; + echo " Created: " . date('Y-m-d', $rule->created_at) . "\n"; + echo " Updated: " . date('Y-m-d', $rule->updated_at) . "\n"; +} +``` + +### Получение элементов, использующих правило + +```php +$ruleName = 'isAuthor'; +$rule = AuthRule::findOne($ruleName); + +if ($rule) { + $items = $rule->authItems; + echo "Rule '{$ruleName}' is used by " . count($items) . " items:\n"; + foreach ($items as $item) { + echo "- {$item->name}: {$item->description}\n"; + } +} +``` + +### Обновление правила + +```php +$auth = Yii::$app->authManager; + +// Получаем правило +$rule = $auth->getRule('isAuthor'); + +if ($rule) { + // Обновляем правило + $rule = new \app\rbac\AuthorRule(); // новая версия + $auth->update('isAuthor', $rule); + + echo "Rule updated\n"; +} +``` + +### Удаление правила + +```php +$auth = Yii::$app->authManager; + +// Сначала нужно отвязать правило от всех элементов +$items = AuthItem::find()->where(['rule_name' => 'oldRule'])->all(); +foreach ($items as $item) { + $item->rule_name = null; + $item->save(); +} + +// Теперь можно удалить правило +$auth->remove($auth->getRule('oldRule')); + +echo "Rule removed\n"; +``` + +### Пример сложного правила: проверка времени доступа + +```php +namespace app\rbac; + +use yii\rbac\Rule; + +class TimeRule extends Rule +{ + public $name = 'timeAccess'; + + public function execute($user, $item, $params) + { + // Доступ только с 9:00 до 18:00 + $hour = date('H'); + return ($hour >= 9 && $hour < 18); + } +} +``` + +Использование: + +```php +$auth = Yii::$app->authManager; + +// Добавляем правило +$rule = new \app\rbac\TimeRule(); +$auth->add($rule); + +// Создаем разрешение с правилом +$accessDuringWorkHours = $auth->createPermission('accessDuringWorkHours'); +$accessDuringWorkHours->description = 'Access only during work hours'; +$accessDuringWorkHours->ruleName = 'timeAccess'; +$auth->add($accessDuringWorkHours); + +// Проверка +if (Yii::$app->user->can('accessDuringWorkHours')) { + // Доступ разрешен только в рабочее время +} +``` + +### Пример правила: проверка статуса пользователя + +```php +namespace app\rbac; + +use yii\rbac\Rule; +use app\models\User; + +class ActiveUserRule extends Rule +{ + public $name = 'activeUser'; + + public function execute($user, $item, $params) + { + $userModel = User::findOne($user); + return $userModel && $userModel->status === User::STATUS_ACTIVE; + } +} +``` + +### Пример правила: проверка группы пользователей + +```php +namespace app\rbac; + +use yii\rbac\Rule; + +class GroupRule extends Rule +{ + public $name = 'groupAccess'; + public $allowedGroups = ['premium', 'vip']; + + public function execute($user, $item, $params) + { + $userModel = \app\models\User::findOne($user); + return $userModel && in_array($userModel->group, $this->allowedGroups); + } +} +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + AuthRule ||--o{ AuthItem : "used by" + + AuthRule { + string name PK "Rule class name" + resource data "Serialized rule object" + int created_at + int updated_at + } + + AuthItem { + string name PK + int type + text description + string rule_name FK + resource data + int created_at + int updated_at + } +``` + +--- + +## Бизнес-логика + +### Назначение правил + +Правила RBAC используются для динамической проверки доступа на основе: + +1. **Контекста запроса** + - Параметры ресурса (владелец, статус) + - Время доступа + - IP адрес + +2. **Атрибутов пользователя** + - Статус аккаунта + - Группа пользователя + - Подписка + +3. **Бизнес-правил** + - Проверка квот + - Проверка лицензий + - Сложная логика доступа + +### Жизненный цикл правила + +1. **Создание**: класс правила наследует `yii\rbac\Rule` +2. **Регистрация**: правило добавляется через `$auth->add($rule)` +3. **Сериализация**: объект правила сериализуется и сохраняется в поле `data` +4. **Привязка**: правило связывается с элементами через `rule_name` +5. **Выполнение**: метод `execute()` вызывается при проверке доступа +6. **Обновление**: правило можно обновить через `$auth->update()` +7. **Удаление**: правило удаляется через `$auth->remove()` + +### Структура класса правила + +```php +namespace app\rbac; + +use yii\rbac\Rule; + +class MyRule extends Rule +{ + // Уникальное имя правила (используется в auth_rule.name) + public $name = 'myRule'; + + /** + * Выполняет проверку доступа + * + * @param string|int $user ID пользователя + * @param \yii\rbac\Item $item элемент авторизации + * @param array $params дополнительные параметры + * @return bool true если доступ разрешен + */ + public function execute($user, $item, $params) + { + // Логика проверки + return true; // или false + } +} +``` + +### Передача параметров в правило + +```php +// Проверка с параметрами +Yii::$app->user->can('updatePost', [ + 'post' => $post, + 'category' => $category, + 'customParam' => 'value' +]); + +// В правиле +public function execute($user, $item, $params) +{ + $post = $params['post'] ?? null; + $category = $params['category'] ?? null; + + // Используем параметры для проверки + return $post && $post->author_id == $user; +} +``` + +--- + +## Связи с другими моделями + +### Прямые связи +- **AuthItem** — элементы авторизации, использующие правило + +### Обратные связи +- От **AuthItem** — элементы, связанные с данным правилом через rule_name + +--- + +## Индексы и производительность + +### Первичный ключ + +```sql +PRIMARY KEY (name) +``` + +### Рекомендуемые индексы + +```sql +-- Временные метки +CREATE INDEX idx_auth_rule_created ON auth_rule(created_at); +CREATE INDEX idx_auth_rule_updated ON auth_rule(updated_at); +``` + +### Оптимизация + +```php +// Правила выполняются при каждой проверке доступа +// Оптимизируйте метод execute() + +// Плохо: запрос к БД в каждом правиле +public function execute($user, $item, $params) +{ + $userModel = User::findOne($user); // запрос при каждой проверке + return $userModel->isActive(); +} + +// Хорошо: кеширование или передача данных через параметры +public function execute($user, $item, $params) +{ + // Передаем userModel через параметры + $userModel = $params['userModel'] ?? null; + return $userModel && $userModel->isActive(); +} +``` + +--- + +## Замечания + +1. **Первичный ключ** — строковое поле name (обычно имя класса правила) +2. **Сериализация** — поле data содержит сериализованный объект правила +3. **Класс правила** — должен наследовать `yii\rbac\Rule` +4. **Метод execute()** — вызывается при каждой проверке доступа +5. **Параметры** — можно передавать через массив в `can()` +6. **Производительность** — правила выполняются синхронно, избегайте тяжелых операций +7. **Кеширование** — Yii2 не кеширует результаты правил автоматически +8. **Управление** — используйте DbManager, не редактируйте таблицу напрямую +9. **Временные метки** — Unix timestamp (integer) +10. **Уникальность** — name должен быть уникальным + +--- + +## Связанные документы + +- [AuthItem.md](./AuthItem.md) — элементы авторизации RBAC +- [AuthAssignment.md](./AuthAssignment.md) — назначения авторизации +- [AuthItemChild.md](./AuthItemChild.md) — иерархия элементов +- [Users.md](./Users.md) — модель пользователей diff --git a/erp24/docs/models/Autoplannogramma.md b/erp24/docs/models/Autoplannogramma.md new file mode 100644 index 00000000..2dfd4c2a --- /dev/null +++ b/erp24/docs/models/Autoplannogramma.md @@ -0,0 +1,267 @@ +# Класс: Autoplannogramma + + +## Mindmap + +```mermaid +mindmap + root((Autoplannogramma)) + Таблица БД + autoplannogramma + Свойства + id + int + Связи + Products + 1:N Products1cNomenclature + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель автоматической планограммы в ERP24. Хранит расчётные и скорректированные значения планограммы товаров по неделям/месяцам для каждого магазина. Поддерживает автоматическое прогнозирование и ручные корректировки закупщиком. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`autoplannogramma` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +### TimestampBehavior +Автоматическое заполнение полей `created_at` и `updated_at` текущей датой при создании и обновлении. + +### BlameableBehavior +Автоматическое заполнение полей `created_by` и `updated_by` ID текущего пользователя. + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `week` | int / null | Номер недели (1-52) | +| `month` | int / null | Месяц (1-12) | +| `year` | int / null | Год | +| `product_id` | varchar(255) / null | GUID продукта из 1С | +| `store_id` | int / null | ID магазина | +| `capacity_type` | int / null | Тип планограммы (ёмкость) | +| `calculate` | decimal / null | Суммарное расчётное значение (алгоритм) | +| `modify` | decimal / null | Значение, проставленное закупщиком | +| `total` | decimal / null | Итоговое расчётное значение продаж | +| `details` | json / null | Детализация итоговой суммы | +| `is_archive` | bool / null | Флаг архивной записи | +| `auto_forecast` | bool | Значение спрогнозировано автоматически (default: true) | +| `created_at` | timestamp / null | Дата создания | +| `updated_at` | timestamp / null | Дата обновления | +| `created_by` | int / null | Автор создания | +| `updated_by` | int / null | Автор обновления | + +## Связи (Relations) + +### getProducts() +Возвращает связанные товары номенклатуры. + +```php +public function getProducts(): \yii\db\ActiveQuery +``` + +**Возвращает**: `hasMany(Products1cNomenclature::class, ['id' => 'product_id'])` + +## Структура JSON поля details + +```json +{ + "week_1": { + "sales": 150, + "writeoffs": 10, + "returns": 5, + "forecast": 145 + }, + "week_2": { + "sales": 180, + "writeoffs": 15, + "returns": 3, + "forecast": 162 + }, + "total_forecast": 307, + "confidence": 0.85, + "method": "weighted_average" +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + Autoplannogramma { + int id PK + int week + int month + int year + varchar product_id FK + int store_id FK + int capacity_type + decimal calculate + decimal modify + decimal total + json details + bool is_archive + bool auto_forecast + timestamp created_at + timestamp updated_at + int created_by FK + int updated_by FK + } + + Products1cNomenclature { + varchar id PK + varchar name + } + + Store { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + Autoplannogramma }o--|| Products1cNomenclature : "product_id" + Autoplannogramma }o--|| Store : "store_id" + Autoplannogramma }o--o| Admin : "created_by" + Autoplannogramma }o--o| Admin : "updated_by" +``` + +## Примеры использования + +### Создание расчётной планограммы +```php +$planogram = new Autoplannogramma(); +$planogram->week = 15; +$planogram->month = 4; +$planogram->year = 2024; +$planogram->product_id = 'product-guid-123'; +$planogram->store_id = 5; +$planogram->capacity_type = 1; +$planogram->calculate = 250.5; +$planogram->total = 250.5; +$planogram->auto_forecast = true; +$planogram->details = [ + 'method' => 'weighted_average', + 'confidence' => 0.87, + 'base_data' => ['sales_4_weeks' => [245, 260, 240, 255]] +]; +$planogram->save(); +// created_at, updated_at, created_by, updated_by заполнятся автоматически +``` + +### Ручная корректировка закупщиком +```php +$planogram = Autoplannogramma::findOne([ + 'product_id' => $productGuid, + 'store_id' => $storeId, + 'week' => $currentWeek, + 'year' => $currentYear +]); + +$planogram->modify = 300; // Закупщик увеличил план +$planogram->auto_forecast = false; // Отмечаем как ручную корректировку +$planogram->save(); +// updated_at и updated_by обновятся автоматически +``` + +### Получение планограммы магазина на неделю +```php +$weekPlan = Autoplannogramma::find() + ->where([ + 'store_id' => $storeId, + 'week' => $weekNumber, + 'year' => $year, + 'is_archive' => false + ]) + ->with('products') + ->indexBy('product_id') + ->all(); +``` + +### Получение итоговых значений (с учётом корректировок) +```php +$planograms = Autoplannogramma::find() + ->where(['store_id' => $storeId, 'week' => $week, 'year' => $year]) + ->all(); + +foreach ($planograms as $plan) { + // Если есть ручная корректировка - берём её, иначе расчётное значение + $finalValue = $plan->modify ?? $plan->calculate; +} +``` + +### Архивирование старых планограмм +```php +Autoplannogramma::updateAll( + ['is_archive' => true], + [ + 'AND', + ['<', 'year', date('Y')], + ['is_archive' => false] + ] +); +``` + +### Статистика прогнозирования +```php +$stats = Autoplannogramma::find() + ->select([ + 'auto_forecast', + 'COUNT(*) as count', + 'AVG(calculate) as avg_calculate', + 'AVG(modify) as avg_modify' + ]) + ->where(['year' => 2024, 'is_archive' => false]) + ->groupBy('auto_forecast') + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `week` | integer, nullable | +| `month` | integer, nullable | +| `year` | integer, nullable | +| `product_id` | string (max 255), nullable | +| `store_id` | integer, nullable | +| `capacity_type` | integer, nullable | +| `calculate` | number, nullable | +| `modify` | number, nullable | +| `total` | number, nullable | +| `details` | safe (JSON), nullable | +| `is_archive` | boolean, nullable | +| `auto_forecast` | boolean, default: true | +| `created_at` | safe (автоматически) | +| `updated_at` | safe (автоматически) | +| `created_by` | integer (автоматически) | +| `updated_by` | integer (автоматически) | + +## Связанные модели + +- [Products1cNomenclature](./Products1cNomenclature.md) — товары номенклатуры +- [Store](./Store.md) — магазины +- [Admin](./Admin.md) — сотрудники (авторы изменений) +- [Planogramma](./Planogramma.md) — основная планограмма + +## Особенности реализации + +1. **Автоматический аудит**: TimestampBehavior и BlameableBehavior автоматически отслеживают изменения +2. **Двойные значения**: `calculate` (алгоритм) и `modify` (закупщик) позволяют сравнивать прогноз с реальностью +3. **Флаг auto_forecast**: Отмечает, было ли значение рассчитано автоматически или введено вручную +4. **Детализация в JSON**: Поле `details` хранит методологию и детали расчёта +5. **Архивирование**: Старые записи можно архивировать без удаления +6. **Гибкая периодизация**: Поддержка недельной и месячной группировки diff --git a/erp24/docs/models/BalancesSearch.md b/erp24/docs/models/BalancesSearch.md new file mode 100644 index 00000000..c10ec6b1 --- /dev/null +++ b/erp24/docs/models/BalancesSearch.md @@ -0,0 +1,215 @@ +# Класс: BalancesSearch + + +## Mindmap + +```mermaid +mindmap + root((BalancesSearch)) + Таблица БД + ActiveRecord + Наследование + extends Balances +``` + +## Назначение +Search-модель для поиска и фильтрации остатков товаров на складах в ERP24. Расширенная модель с поддержкой поиска по названию товара и eager loading связей. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Balances` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$productName` | mixed | ID товара для фильтрации по названию | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `store_id`, `product_id`, `productName` — safe +- `quantity`, `reserv` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с eager loading и увеличенной пагинацией. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Balances::find() +2. Добавляет with('product') и with('store') для eager loading +3. Устанавливает pageSize = 100 для отображения большего количества записей +4. Применяет фильтры: + - Точное совпадение: quantity, reserv + - По productName через product_id + - LIKE: store_id, product_id + +## Диаграмма связей + +```mermaid +erDiagram + Balances { + varchar store_id PK + varchar product_id PK + float quantity + float reserv + } + + Products1c { + varchar id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + Products1c ||--o{ Balances : "product_id" + CityStore ||--o{ Balances : "store_id" +``` + +## Диаграмма процесса поиска + +```mermaid +flowchart TD + A[Параметры поиска] --> B[BalancesSearch] + + B --> C[with product] + C --> D[with store] + D --> E[pageSize = 100] + + E --> F[load params] + F --> G{validate} + + G -->|OK| H[andFilterWhere quantity] + G -->|Error| I[DataProvider без фильтров] + + H --> J[andFilterWhere productName] + J --> K[LIKE store_id, product_id] + + K --> L[ActiveDataProvider] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new BalancesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по товару +```php +$searchModel = new BalancesSearch(); +$dataProvider = $searchModel->search([ + 'BalancesSearch' => [ + 'productName' => 'product-guid-123', + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new BalancesSearch(); +$dataProvider = $searchModel->search([ + 'BalancesSearch' => [ + 'store_id' => '10', + ] +]); +``` + +### Поиск по количеству +```php +$searchModel = new BalancesSearch(); +$dataProvider = $searchModel->search([ + 'BalancesSearch' => [ + 'quantity' => 100, + ] +]); +``` + +### Поиск с резервом +```php +$searchModel = new BalancesSearch(); +$dataProvider = $searchModel->search([ + 'BalancesSearch' => [ + 'reserv' => 5, + ] +]); +``` + +### GridView с товаром и магазином +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + [ + 'attribute' => 'productName', + 'value' => 'product.name', + ], + [ + 'attribute' => 'store_id', + 'value' => 'store.name', + ], + 'quantity', + 'reserv', + [ + 'label' => 'Доступно', + 'value' => function($model) { + return $model->quantity - $model->reserv; + } + ], + ], +]) ?> +``` + +### Комбинированный поиск +```php +$searchModel = new BalancesSearch(); +$dataProvider = $searchModel->search([ + 'BalancesSearch' => [ + 'store_id' => '10', + 'quantity' => 50, + ] +]); +``` + +## Связанные модели + +- [Balances](./Balances.md) — базовая модель остатков +- [Products1c](./Products1c.md) — товары (product_id) +- [CityStore](./CityStore.md) — магазины (store_id) + +## Особенности реализации + +1. **Eager loading**: with('product') и with('store') для оптимизации +2. **Увеличенная пагинация**: pageSize = 100 вместо стандартных 20 +3. **Дополнительное свойство**: productName для удобной фильтрации по товару +4. **Составной ключ**: Поиск по store_id + product_id +5. **Number-фильтры**: Для quantity и reserv diff --git a/erp24/docs/models/BonusLevels.md b/erp24/docs/models/BonusLevels.md new file mode 100644 index 00000000..1787fd9b --- /dev/null +++ b/erp24/docs/models/BonusLevels.md @@ -0,0 +1,527 @@ +# Model: BonusLevels + + +## Mindmap + +```mermaid +mindmap + root((BonusLevels)) + Таблица БД + bonus_levels + Свойства + id + int + name + string + alias + string + threshold + int + cashback_rate + int + referal_rate + int + Связи + CreatedBy + 1:1 Admin + UpdatedBy + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника уровней бонусной программы. Определяет правила начисления и списания бонусов в зависимости от суммы покупок клиента. Каждый уровень имеет пороговое значение (threshold), при достижении которого клиент переходит на новый уровень с измененными условиями кэшбека и реферальных отчислений. + +Используется для автоматического расчета бонусов при покупках, определения процента списания бонусов и начислений рефералам. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`bonus_levels` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `name` | string(255) | **Наименование уровня** (например, "Бронзовый", "Серебряный", "Золотой") | +| `alias` | string(255) | **Алиас уровня** (символьный код для использования в коде) | +| `threshold` | int | **Пороговое значение** суммы покупок для достижения уровня (в рублях) | +| `cashback_rate` | int | **Процент начисления кэшбека** (от 0 до 100) | +| `referal_rate` | int | **Процент начисления рефералу** (от 0 до 100) | +| `bonus_rate` | int | **Процент списания бонусов** (максимальный % от суммы покупки) | +| `active` | int | **Активность записи** (0 - неактивен, 1 - активен) | +| `date_start` | datetime | **Дата создания** записи | +| `date_end` | datetime | **Дата закрытия** записи (null - если активна) | +| `created_by` | int | **ID создавшего** запись (FK к таблице admin) | +| `updated_by` | int | **ID закрывшего** запись (FK к таблице admin) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['name', 'alias', 'date_start', 'created_by'] +``` + +### Целочисленные поля с значением по умолчанию null +```php +[ + 'threshold', 'cashback_rate', 'referal_rate', + 'bonus_rate', 'active', 'created_by', 'updated_by' +] // integer, default: null +``` + +### Строковые поля +```php +['name', 'alias'] // max:255 +``` + +### Даты +```php +['date_start', 'date_end'] // safe (datetime validation) +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'name' => 'Наименование уровня', + 'alias' => 'Алиас уровня', + 'threshold' => 'Пороговое значение суммы покупок', + 'cashback_rate' => 'Процент начисления кешбека', + 'referal_rate' => 'Процент начисления рефералу', + 'bonus_rate' => 'Процент списания бонусов', + 'active' => 'Активность записи', + 'date_start' => 'Дата создания записи', + 'date_end' => 'Дата закрытия записи', + 'created_by' => 'ID создавшего запись', + 'updated_by' => 'ID закрывшего запись', +] +``` + +--- + +## Связи (Relations) + +### getCreatedBy() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'created_by']` +**Описание:** Администратор, создавший уровень + +**Пример:** +```php +$level = BonusLevels::findOne($id); +$creator = $level->createdBy; +echo "Создал: {$creator->name}"; +``` + +### getUpdatedBy() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'updated_by']` +**Описание:** Администратор, закрывший уровень + +**Пример:** +```php +$level = BonusLevels::findOne($id); +if ($level->updated_by) { + $closer = $level->updatedBy; + echo "Закрыл: {$closer->name}"; +} +``` + +--- + +## Примеры использования + +### Создание нового уровня + +```php +use yii_app\records\BonusLevels; +use Yii; + +$level = new BonusLevels(); +$level->name = 'Золотой'; +$level->alias = 'gold'; +$level->threshold = 100000; // 100 тыс рублей +$level->cashback_rate = 10; // 10% кэшбека +$level->referal_rate = 5; // 5% рефералу +$level->bonus_rate = 50; // Можно списать до 50% от суммы +$level->active = 1; +$level->date_start = date('Y-m-d H:i:s'); +$level->created_by = Yii::$app->user->id; + +if ($level->save()) { + echo "Уровень создан"; +} +``` + +### Получение активных уровней + +```php +$activeLevels = BonusLevels::find() + ->where(['active' => 1]) + ->orderBy(['threshold' => SORT_ASC]) + ->all(); + +foreach ($activeLevels as $level) { + echo "{$level->name}: от {$level->threshold} руб., кэшбек {$level->cashback_rate}%\n"; +} +``` + +### Определение уровня клиента по сумме покупок + +```php +$totalPurchases = 75000; // Общая сумма покупок клиента + +$currentLevel = BonusLevels::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'threshold', $totalPurchases]) + ->orderBy(['threshold' => SORT_DESC]) + ->one(); + +if ($currentLevel) { + echo "Текущий уровень: {$currentLevel->name}\n"; + echo "Кэшбек: {$currentLevel->cashback_rate}%\n"; +} else { + echo "Базовый уровень"; +} +``` + +### Расчет кэшбека для покупки + +```php +$purchaseAmount = 5000; // Сумма покупки +$userTotalPurchases = 120000; // Общая сумма покупок клиента + +// Определение уровня +$level = BonusLevels::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'threshold', $userTotalPurchases]) + ->orderBy(['threshold' => SORT_DESC]) + ->one(); + +if ($level) { + $cashback = $purchaseAmount * ($level->cashback_rate / 100); + echo "Начислено бонусов: {$cashback} руб. (уровень {$level->name})\n"; +} +``` + +### Расчет бонусов рефералу + +```php +$purchaseAmount = 3000; // Сумма покупки друга +$referrerTotalPurchases = 50000; // Сумма покупок реферера + +$referrerLevel = BonusLevels::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'threshold', $referrerTotalPurchases]) + ->orderBy(['threshold' => SORT_DESC]) + ->one(); + +if ($referrerLevel) { + $referralBonus = $purchaseAmount * ($referrerLevel->referal_rate / 100); + echo "Начислено рефералу: {$referralBonus} руб.\n"; +} +``` + +### Проверка максимального списания бонусов + +```php +$purchaseAmount = 10000; // Сумма покупки +$userBonusBalance = 8000; // Баланс бонусов клиента +$userTotalPurchases = 200000; + +$level = BonusLevels::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'threshold', $userTotalPurchases]) + ->orderBy(['threshold' => SORT_DESC]) + ->one(); + +if ($level) { + $maxDebit = $purchaseAmount * ($level->bonus_rate / 100); + $actualDebit = min($maxDebit, $userBonusBalance); + + echo "Максимум к списанию: {$maxDebit} руб. ({$level->bonus_rate}%)\n"; + echo "Доступно бонусов: {$userBonusBalance} руб.\n"; + echo "Будет списано: {$actualDebit} руб.\n"; +} +``` + +### Закрытие уровня + +```php +$level = BonusLevels::findOne($id); +$level->active = 0; +$level->date_end = date('Y-m-d H:i:s'); +$level->updated_by = Yii::$app->user->id; +$level->save(); +``` + +### Получение истории уровней + +```php +$allLevels = BonusLevels::find() + ->orderBy(['date_start' => SORT_DESC]) + ->all(); + +foreach ($allLevels as $level) { + $status = $level->active ? 'Активен' : 'Закрыт'; + echo "{$level->name} - {$status} (с {$level->date_start})\n"; +} +``` + +### Поиск уровня по алиасу + +```php +$level = BonusLevels::find() + ->where(['alias' => 'gold', 'active' => 1]) + ->one(); + +if ($level) { + echo "Золотой уровень: кэшбек {$level->cashback_rate}%\n"; +} +``` + +--- + +## Бизнес-логика + +### Иерархия уровней + +Уровни выстраиваются по возрастанию порогового значения `threshold`: + +``` +Базовый -> 0 руб. -> 5% кэшбек +Бронзовый -> 30 000 руб. -> 7% кэшбек +Серебряный -> 70 000 руб. -> 10% кэшбек +Золотой -> 150 000 руб. -> 15% кэшбек +Платиновый -> 300 000 руб. -> 20% кэшбек +``` + +### Переход между уровнями + +Клиент автоматически переходит на новый уровень при достижении порогового значения. Уровень определяется по максимальному `threshold`, который не превышает общую сумму покупок клиента: + +```sql +SELECT * FROM bonus_levels +WHERE active = 1 + AND threshold <= :total_purchases +ORDER BY threshold DESC +LIMIT 1 +``` + +### Процентные ставки + +- **cashback_rate** - процент от суммы покупки, начисляемый клиенту в виде бонусов +- **referal_rate** - процент от покупки друга, начисляемый рефереру +- **bonus_rate** - максимальный процент от суммы покупки, который можно оплатить бонусами + +### Версионирование уровней + +При изменении условий бонусной программы: +1. Старый уровень закрывается (active = 0, date_end = now) +2. Создается новый уровень с обновленными условиями +3. Клиенты автоматически получают новые условия при следующей покупке + +### Использование алиаса + +Поле `alias` используется в коде для ссылки на уровни: + +```php +const LEVEL_BRONZE = 'bronze'; +const LEVEL_SILVER = 'silver'; +const LEVEL_GOLD = 'gold'; +const LEVEL_PLATINUM = 'platinum'; +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + BonusLevels { + int id PK + string name + string alias UK + int threshold + int cashback_rate + int referal_rate + int bonus_rate + int active + datetime date_start + datetime date_end + int created_by FK + int updated_by FK + } + + Admin ||--o{ BonusLevels : "created" + Admin ||--o{ BonusLevels : "updated" + Users ||--o| BonusLevels : "has level" + UsersBonusLevels }o--|| BonusLevels : "history" + + Admin { + int id PK + string name + } + + Users { + int id PK + string phone + string bonus_level + int sale_price + } + + UsersBonusLevels { + int id PK + string phone FK + string bonus_level FK + datetime date_from + datetime date_to + } +``` + +--- + +## Диаграмма определения уровня + +```mermaid +flowchart TD + A[Клиент совершает покупку] --> B[Получение общей суммы покупок] + B --> C{Выборка активных уровней} + C --> D[Фильтр threshold <= total_purchases] + D --> E[Сортировка по threshold DESC] + E --> F{Найден уровень?} + F -->|Да| G[Применение ставок уровня] + F -->|Нет| H[Базовый уровень] + G --> I[Расчет кэшбека] + H --> I + I --> J[Начисление бонусов] + J --> K{Есть реферер?} + K -->|Да| L[Расчет бонуса рефералу] + K -->|Нет| M[Конец] + L --> M + + style G fill:#90EE90 + style H fill:#FFD700 + style I fill:#87CEEB +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE bonus_levels ADD PRIMARY KEY (id); + +-- Уникальный индекс на алиас +CREATE UNIQUE INDEX idx_bonus_levels_alias ON bonus_levels(alias); + +-- Индекс для поиска активных уровней +CREATE INDEX idx_bonus_levels_active ON bonus_levels(active); + +-- Составной индекс для определения уровня +CREATE INDEX idx_bonus_levels_active_threshold +ON bonus_levels(active, threshold DESC); + +-- Индекс для поиска по создателю +CREATE INDEX idx_bonus_levels_created_by ON bonus_levels(created_by); +``` + +### Оптимизация запросов + +```php +// Эффективный запрос с использованием индекса +$level = BonusLevels::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'threshold', $totalPurchases]) + ->orderBy(['threshold' => SORT_DESC]) + ->limit(1) + ->one(); + +// Кэширование активных уровней +$levels = Yii::$app->cache->getOrSet('active_bonus_levels', function() { + return BonusLevels::find() + ->where(['active' => 1]) + ->orderBy(['threshold' => SORT_ASC]) + ->all(); +}, 3600); // 1 час +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [UsersBonusLevels](UsersBonusLevels.md) - История уровней клиентов +- [UsersBonus](UsersBonus.md) - Движения бонусов +- [Admin](Admin.md) - Администраторы + +--- + +## Связанные сервисы + +- **BonusService** - Расчет и начисление бонусов +- **LevelService** - Управление уровнями клиентов +- **ReferralService** - Реферальная система + +--- + +## API Endpoints + +- `GET /api2/bonus/levels` - Список активных уровней +- `GET /api2/bonus/level-info` - Информация об уровне клиента +- `POST /admin/bonus/create-level` - Создание уровня +- `PUT /admin/bonus/update-level` - Обновление уровня +- `DELETE /admin/bonus/close-level` - Закрытие уровня + +--- + +## Замечания + +1. **Алиас уникален** - используется для программной ссылки на уровни. + +2. **Версионирование** - при изменении условий создается новый уровень, старый закрывается. + +3. **Процентные значения** - хранятся как целые числа (5 = 5%, 10 = 10%). + +4. **Активность** - неактивные уровни хранятся для истории, но не используются в расчетах. + +5. **Пороговое значение** - в рублях, определяет минимальную сумму покупок для перехода на уровень. + +6. **Кэширование** - рекомендуется кэшировать список активных уровней для производительности. + +7. **Аудит** - все изменения фиксируются через created_by и updated_by. + +8. **Nullable поля** - процентные ставки могут быть null для специальных случаев. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/BouquetComposition.md b/erp24/docs/models/BouquetComposition.md new file mode 100644 index 00000000..bd2a96cd --- /dev/null +++ b/erp24/docs/models/BouquetComposition.md @@ -0,0 +1,355 @@ +# Модель BouquetComposition + + +## Mindmap + +```mermaid +mindmap + root((BouquetComposition)) + Таблица БД + erp24.bouquet_composition + Свойства + id + int + name + string + matrixType + BouquetCompositionMatrixTypeHistory + presentation + Files + buildProcess + Files + Связи + BouquetCompositionProducts + 1:N BouquetCompositionProducts + MatrixType + 1:1 BouquetCompositionMatrixTypeHistory + Presentation + 1:1 Files + BuildProcess + 1:1 Files + BouquetForecast + 1:N BouquetForecast + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `BouquetComposition` представляет композиции букетов (рецепты). Хранит информацию о составе букетов, включая название, статус, файлы (фото, видео), связь с прогнозами продаж и ценообразование. Является центральной моделью модуля управления ассортиментом букетов. + +**Файл модели:** `erp24/records/BouquetComposition.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `erp24.bouquet_composition` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(255) | GUID букета для синхронизации с 1С | +| `name` | VARCHAR(255) | Название букета | +| `status` | INTEGER | Статус букета | +| `date_confirm` | TIMESTAMP | Дата подтверждения | +| `date_1c_send` | TIMESTAMP | Дата отправки в 1С | +| `confirm_admin` | INTEGER | ID подтвердившего сотрудника | +| `error_text` | TEXT | Текст ошибки | +| `created_at` | TIMESTAMP | Дата создания | +| `updated_at` | TIMESTAMP | Дата обновления | +| `created_by` | INTEGER | ID создателя | +| `updated_by` | INTEGER | ID редактора | + +--- + +## Константы + +### Регионы (REGION_*) + +```php +const REGION_NN = 52; // Нижний Новгород +const REGION_MSK = 77; // Москва +``` + +### Типы файлов + +```php +public const PHOTO_TYPE = 'image'; +public const VIDEO_TYPE = 'video'; +public const PHOTO_BOUQUET = 'bouquet/photo_bouquet'; +public const VIDEO_PRESENTATION = 'bouquet/video_presentation'; +public const VIDEO_BUILD_PROCESS = 'bouquet/video_build_process'; +``` + +--- + +## Виртуальные атрибуты + +```php +public $photo_bouquet; // Загружаемые фото букета (до 10 файлов) +public $matrix_type_id; // ID типа матрицы +public $video_presentation; // Видео-презентация +public $video_build_process; // Видео процесса сборки +``` + +--- + +## Методы модели + +### `findModel(int $id): self` + +Поиск модели по ID с выбросом исключения. + +```php +try { + $bouquet = BouquetComposition::findModel(123); +} catch (NotFoundHttpException $e) { + echo "Букет не найден"; +} +``` + +### `applyFilters($query, $request): void` + +Применяет фильтры к запросу (год, месяц, тип матрицы). + +```php +$query = BouquetComposition::find(); +BouquetComposition::applyFilters($query, Yii::$app->request); +$bouquets = $query->all(); +``` + +### `processRelations(array $data): void` + +Обрабатывает связанные данные: матрицу, файлы, прогнозы, продукты. + +```php +$bouquet->processRelations($formData); +``` + +### `processFiles(array $data): void` + +Загружает и сохраняет файлы (фото и видео). + +### `getFiles(): array` + +Возвращает все файлы букета. + +```php +$files = $bouquet->getFiles(); +// ['photo' => [...], 'video' => [...], 'process' => [...]] +``` + +### `getSelfCost(?array $data = null): float` + +Рассчитывает себестоимость букета на основе медианной цены компонентов за 14 дней. + +```php +$selfCost = $bouquet->getSelfCost(); +echo "Себестоимость: {$selfCost} руб."; +``` + +### `setCost($region_id, $cost): void` + +Устанавливает цену букета для региона. + +```php +$bouquet->setCost(BouquetComposition::REGION_MSK, 2500); +``` + +### `getBouquetCost($region_id, $data = null, $forceDefault = false): float` + +Возвращает цену букета для региона. + +### `getCostModel($region_id, $data = null, $force = false): BouquetCompositionPrice` + +Возвращает или создаёт модель ценообразования. + +### `getYears(): array` + +Возвращает список годов для фильтра (±5 лет от текущего). + +### `disabledButtons(bool $isCreate = false): bool` + +Проверяет, должны ли кнопки быть отключены (после 10 числа). + +### `hasActuality(): bool` + +Проверяет наличие записей актуальности букета. + +--- + +## Связи (Relations) + +### `getBouquetCompositionProducts(): ActiveQuery` + +Продукты в составе букета. + +```php +$products = $bouquet->bouquetCompositionProducts; +``` + +### `getMatrixType(): ActiveQuery` + +Активный тип матрицы. + +```php +$matrixType = $bouquet->matrixType; +``` + +### `getPresentation(): ActiveQuery` + +Видео-презентация букета. + +### `getBuildProcess(): ActiveQuery` + +Видео процесса сборки. + +### `getBouquetForecast(): ActiveQuery` + +Прогнозы продаж. + +### `getPriceRel(): ActiveQuery` + +Связь с ценами. + +### `getActualities(): ActiveQuery` + +Записи актуальности букета. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + bouquet_composition ||--o{ bouquet_composition_products : "contains" + bouquet_composition ||--o{ bouquet_composition_price : "has_prices" + bouquet_composition ||--o{ bouquet_forecast : "has_forecasts" + bouquet_composition ||--o| bouquet_composition_matrix_type_history : "has_matrix_type" + bouquet_composition ||--o{ files : "has_files" + bouquet_composition ||--o{ matrix_bouquet_actuality : "has_actualities" + + bouquet_composition { + int id PK + string guid + string name + int status + timestamp date_confirm + timestamp date_1c_send + int confirm_admin + text error_text + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + bouquet_composition_products { + int id PK + int bouquet_id FK + string product_guid + float count + } + + bouquet_forecast { + int id PK + int bouquet_id FK + int year + int month + int type_sales + float type_sales_value + } +``` + +--- + +## Примеры использования + +### Создание нового букета + +```php +$bouquet = new BouquetComposition(); +$bouquet->name = 'Весенний микс'; +$bouquet->status = 1; +$bouquet->matrix_type_id = 2; + +if ($bouquet->save()) { + $bouquet->processRelations($formData); +} +``` + +### Получение букетов с фильтрацией + +```php +$query = BouquetComposition::find() + ->with(['bouquetCompositionProducts', 'matrixType']); + +BouquetComposition::applyFilters($query, Yii::$app->request); +$bouquets = $query->all(); +``` + +### Расчёт ценообразования + +```php +$bouquet = BouquetComposition::findModel($id); + +$selfCost = $bouquet->getSelfCost(); +$price = $bouquet->getBouquetCost(BouquetComposition::REGION_MSK); +$markup = $bouquet->getBouquetCostMarkup(BouquetComposition::REGION_MSK); + +echo "Себестоимость: {$selfCost} руб."; +echo "Цена: {$price} руб."; +echo "Наценка: {$markup}%"; +``` + +### Работа с файлами + +```php +$bouquet = BouquetComposition::findModel($id); +$files = $bouquet->getFiles(); + +foreach ($files['photo'] as $photo) { + echo ""; +} +``` + +### Подтверждение букета + +```php +$bouquet = BouquetComposition::findModel($id); +$bouquet->status = 2; // Подтверждён +$bouquet->date_confirm = date('Y-m-d H:i:s'); +$bouquet->confirm_admin = Yii::$app->user->id; +$bouquet->save(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | +| `matrix_type_id` | Обязательное | +| `status` | Обязательное, целое число | +| `photo_bouquet` | Файлы jpg/jpeg/png/gif, макс. 10 | +| `video_*` | Файлы mkv/mov/avi/mp4 | + +--- + +## Связанные модели + +- **[BouquetCompositionProducts](./BouquetCompositionProducts.md)** — продукты в составе +- **[BouquetCompositionPrice](./BouquetCompositionPrice.md)** — цены по регионам +- **[BouquetForecast](./BouquetForecast.md)** — прогнозы продаж +- **[Files](./Files.md)** — файлы букета +- **[Products1c](./Products1c.md)** — справочник продуктов +- **[SelfCostProduct](./SelfCostProduct.md)** — себестоимость продуктов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/BouquetCompositionMatrixTypeHistory.md b/erp24/docs/models/BouquetCompositionMatrixTypeHistory.md new file mode 100644 index 00000000..3525ccbd --- /dev/null +++ b/erp24/docs/models/BouquetCompositionMatrixTypeHistory.md @@ -0,0 +1,290 @@ +# Модель BouquetCompositionMatrixTypeHistory + + +## Mindmap + +```mermaid +mindmap + root((BouquetCompositionMatrixTypeHistory)) + Таблица БД + {{%erp24.bouquet_composition_matrix_type_history}} + Свойства + id + int + is_active + bool + Связи + Bouquet + 1:1 BouquetComposition + MatrixType + 1:1 MatrixType + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `BouquetCompositionMatrixTypeHistory` хранит историю изменений типа матрицы для букетов. Реализует паттерн темпорального версионирования с полями `date_from` и `date_to`, позволяя отслеживать, какой тип матрицы был присвоен букету в каждый момент времени. Только одна запись на букет может быть активной одновременно. + +**Файл модели:** `erp24/records/BouquetCompositionMatrixTypeHistory.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `erp24.bouquet_composition_matrix_type_history` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `bouquet_id` | INTEGER | ID букета (FK → bouquet_composition.id) | +| `matrix_type_id` | INTEGER | ID типа матрицы (FK → matrix_type.id) | +| `date_from` | TIMESTAMP | Дата начала действия (момент установки типа) | +| `date_to` | TIMESTAMP | Дата окончания действия (момент замены на другой тип) | +| `is_active` | BOOLEAN | Флаг активности записи (true — текущий тип) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `created_by` | INTEGER | ID создателя записи | +| `updated_by` | INTEGER | ID обновителя записи | + +--- + +## Константы + +### Статусы активности + +```php +public const IS_ACTIVE = true; // Активная запись (текущий тип матрицы) +public const IS_INACTIVE = false; // Неактивная запись (архивный тип) +``` + +### Специальные значения + +```php +public const MATRIX_TYPE_NONE = '__none'; // Тип матрицы не указан +``` + +--- + +## Описание полей + +### `is_active` — Флаг активности + +Определяет, является ли данная запись текущим активным типом матрицы для букета: +- `true` — это текущий тип матрицы букета +- `false` — архивная запись (был заменён на другой тип) + +Для каждого букета может быть только одна активная запись. + +### `date_from` / `date_to` — Период действия + +- `date_from` — момент, когда этот тип матрицы был установлен для букета +- `date_to` — момент, когда тип был заменён на другой (NULL для активной записи) + +--- + +## Методы модели + +### `updateMatrixType(int $bouquetId, ?int $matrixTypeId): void` + +Статический метод обновления типа матрицы для букета. Выполняет атомарную операцию в транзакции: + +1. Деактивирует текущую активную запись (устанавливает `date_to` и `is_active = false`) +2. Создаёт новую активную запись с указанным типом матрицы + +**Параметры:** +- `$bouquetId` (int) — ID букета +- `$matrixTypeId` (int|null) — ID нового типа матрицы (null — ничего не делает) + +**Исключения:** +- `Exception` — если сохранение не удалось + +**Логика работы:** +1. Проверяет, что `matrixTypeId` не пустой +2. Начинает транзакцию +3. Деактивирует текущую активную запись: `is_active = false`, `date_to = NOW()` +4. Создаёт новую запись: `is_active = true`, `date_from = NOW()` +5. Коммитит транзакцию или откатывает при ошибке + +```php +// Смена типа матрицы на "Премиум" +BouquetCompositionMatrixTypeHistory::updateMatrixType($bouquetId, $premiumMatrixTypeId); + +// Если matrixTypeId = null, ничего не происходит +BouquetCompositionMatrixTypeHistory::updateMatrixType($bouquetId, null); +``` + +--- + +## Связи (Relations) + +### `getBouquet(): ActiveQuery` + +Возвращает букет, которому принадлежит запись истории. + +```php +$history = BouquetCompositionMatrixTypeHistory::findOne($id); +$bouquet = $history->bouquet; // BouquetComposition +echo "Букет: {$bouquet->name}"; +``` + +### `getMatrixType(): ActiveQuery` + +Возвращает тип матрицы. + +```php +$matrixType = $history->matrixType; // MatrixType +echo "Тип матрицы: {$matrixType->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + bouquet_composition_matrix_type_history }o--|| bouquet_composition : "belongs_to" + bouquet_composition_matrix_type_history }o--|| matrix_type : "references" + + bouquet_composition_matrix_type_history { + int id PK + int bouquet_id FK + int matrix_type_id FK + timestamp date_from + timestamp date_to + bool is_active + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + bouquet_composition { + int id PK + string name + } + + matrix_type { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение текущего типа матрицы букета + +```php +$currentType = BouquetCompositionMatrixTypeHistory::find() + ->where([ + 'bouquet_id' => $bouquetId, + 'is_active' => BouquetCompositionMatrixTypeHistory::IS_ACTIVE + ]) + ->one(); + +if ($currentType) { + echo "Текущий тип матрицы: {$currentType->matrixType->name}"; +} +``` + +### Смена типа матрицы + +```php +try { + BouquetCompositionMatrixTypeHistory::updateMatrixType($bouquetId, $newMatrixTypeId); + echo "Тип матрицы успешно изменён"; +} catch (Exception $e) { + echo "Ошибка: " . $e->getMessage(); +} +``` + +### Получение истории изменений типа матрицы + +```php +$history = BouquetCompositionMatrixTypeHistory::find() + ->with('matrixType') + ->where(['bouquet_id' => $bouquetId]) + ->orderBy(['date_from' => SORT_DESC]) + ->all(); + +foreach ($history as $record) { + $status = $record->is_active ? 'Текущий' : 'Архив'; + echo "{$record->matrixType->name} ({$status}): "; + echo "{$record->date_from} — " . ($record->date_to ?? 'настоящее время') . "\n"; +} +``` + +### Получение типа матрицы на определённую дату + +```php +$targetDate = '2025-01-15'; + +$typeAtDate = BouquetCompositionMatrixTypeHistory::find() + ->where(['bouquet_id' => $bouquetId]) + ->andWhere(['<=', 'date_from', $targetDate]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $targetDate] + ]) + ->one(); + +if ($typeAtDate) { + echo "Тип матрицы на {$targetDate}: {$typeAtDate->matrixType->name}"; +} +``` + +### Статистика изменений типов матрицы + +```php +$changes = BouquetCompositionMatrixTypeHistory::find() + ->select(['matrix_type_id', 'COUNT(*) as count']) + ->groupBy('matrix_type_id') + ->asArray() + ->all(); + +foreach ($changes as $row) { + $type = MatrixType::findOne($row['matrix_type_id']); + echo "{$type->name}: {$row['count']} назначений\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `bouquet_id` | Обязательное, целое число | +| `matrix_type_id` | Обязательное, целое число | +| `is_active` | Логическое значение | +| `date_from`, `date_to` | Безопасные (safe) | +| `created_by`, `updated_by` | Целые числа | + +--- + +## Паттерн темпорального версионирования + +Модель реализует паттерн **Temporal Versioning** для отслеживания истории: + +1. **Только одна активная запись** — `is_active = true` только у одной записи на букет +2. **Автоматическое закрытие** — при смене типа старая запись закрывается (`date_to = NOW()`) +3. **Атомарность** — смена типа выполняется в транзакции +4. **Аудит** — все изменения фиксируются через `TimestampBehavior` и `BlameableBehavior` + +--- + +## Связанные модели + +- **[BouquetComposition](./BouquetComposition.md)** — букеты +- **[MatrixType](./MatrixType.md)** — типы матриц +- **[BouquetForecast](./BouquetForecast.md)** — прогнозы продаж + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/BouquetCompositionPrice.md b/erp24/docs/models/BouquetCompositionPrice.md new file mode 100644 index 00000000..da0393fe --- /dev/null +++ b/erp24/docs/models/BouquetCompositionPrice.md @@ -0,0 +1,212 @@ +# Модель BouquetCompositionPrice + + +## Mindmap + +```mermaid +mindmap + root((BouquetCompositionPrice)) + Таблица БД + bouquet_composition_price + Свойства + id + int + bouquet_id + int + region_id + int + selfcost + float + selfcost_markup + float + selfcost_markup_price + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `BouquetCompositionPrice` хранит информацию о ценообразовании букетов по регионам. Содержит себестоимость, наценки и итоговую цену. Используется для расчёта розничных цен букетов с учётом региональных особенностей. + +**Файл модели:** `erp24/records/BouquetCompositionPrice.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `bouquet_composition_price` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `bouquet_id` | INTEGER | ID букета (FK → bouquet_composition.id) | +| `region_id` | INTEGER | ID региона (52-НН, 77-МСК) | +| `selfcost` | FLOAT | Себестоимость букета | +| `selfcost_markup` | FLOAT | Наценка над себестоимостью (%) | +| `selfcost_markup_price` | FLOAT | Наценка в рублях | +| `price` | FLOAT | Итоговая цена | +| `price_markup` | FLOAT | Наценка над базовой ценой (%) | +| `created_at` | TIMESTAMP | Дата создания | +| `updated_at` | TIMESTAMP | Дата обновления | +| `created_by` | INTEGER | ID создателя | +| `updated_by` | INTEGER | ID редактора | + +--- + +## Константы + +### Коэффициенты ценообразования + +```php +const SELF_COST_MARKUP = 30; // Наценка над себестоимостью (30%) +const SELF_COST_MARKUP_PRICE_COEF = 0.3; // Коэффициент наценки (0.3) +const SELF_COST_PRICE_COEF = 1.3; // Множитель себестоимости (1.3) +const SURCHARGE_ASSEMBLY = 1.15; // Наценка за сборку (15%) +``` + +--- + +## Формулы ценообразования + +### Базовая цена + +``` +selfcost_markup_price = selfcost × 0.3 +базовая_цена = selfcost × 1.3 +``` + +### Итоговая цена + +``` +price = (selfcost × 1.3) × 1.15 +price ≈ selfcost × 1.495 +``` + +### Наценка над базовой ценой + +``` +price_markup = (price / (selfcost_markup_price + selfcost) - 1) × 100 +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + bouquet_composition_price }o--|| bouquet_composition : "belongs_to" + + bouquet_composition_price { + int id PK + int bouquet_id FK + int region_id + float selfcost + float selfcost_markup + float selfcost_markup_price + float price + float price_markup + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + bouquet_composition { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение цены букета для региона + +```php +$priceModel = BouquetCompositionPrice::findOne([ + 'bouquet_id' => $bouquetId, + 'region_id' => BouquetComposition::REGION_MSK +]); + +if ($priceModel) { + echo "Себестоимость: {$priceModel->selfcost} руб."; + echo "Наценка: {$priceModel->selfcost_markup}%"; + echo "Цена: {$priceModel->price} руб."; +} +``` + +### Создание записи ценообразования + +```php +$priceModel = new BouquetCompositionPrice(); +$priceModel->bouquet_id = $bouquetId; +$priceModel->region_id = 52; // Нижний Новгород +$priceModel->selfcost = 1000; +$priceModel->selfcost_markup = BouquetCompositionPrice::SELF_COST_MARKUP; +$priceModel->selfcost_markup_price = $priceModel->selfcost * BouquetCompositionPrice::SELF_COST_MARKUP_PRICE_COEF; +$priceModel->price = round( + BouquetCompositionPrice::SURCHARGE_ASSEMBLY * + ($priceModel->selfcost_markup_price + $priceModel->selfcost) +); +$priceModel->price_markup = ($priceModel->price / ($priceModel->selfcost_markup_price + $priceModel->selfcost) - 1) * 100; +$priceModel->save(); +``` + +### Расчёт цены по формуле + +```php +$selfcost = 1000; // Себестоимость + +// Базовые расчёты +$selfcostMarkupPrice = $selfcost * 0.3; // 300 руб. +$basePrice = $selfcost * 1.3; // 1300 руб. +$finalPrice = $basePrice * 1.15; // 1495 руб. + +echo "Себестоимость: {$selfcost} руб."; +echo "Наценка над себестоимостью: {$selfcostMarkupPrice} руб."; +echo "Итоговая цена: " . round($finalPrice) . " руб."; +``` + +### Сравнение цен по регионам + +```php +$prices = BouquetCompositionPrice::find() + ->where(['bouquet_id' => $bouquetId]) + ->indexBy('region_id') + ->all(); + +$regions = [52 => 'Нижний Новгород', 77 => 'Москва']; +foreach ($prices as $regionId => $price) { + echo "{$regions[$regionId]}: {$price->price} руб.\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `bouquet_id` | Обязательное, целое число | +| `region_id` | Обязательное, целое число | +| `selfcost` | Обязательное, число | +| `selfcost_markup_price` | Обязательное, число | +| `price` | Обязательное, число | +| `selfcost_markup`, `price_markup` | Числа | + +--- + +## Связанные модели + +- **[BouquetComposition](./BouquetComposition.md)** — букеты +- **[PricesDynamic](./PricesDynamic.md)** — динамические цены +- **[PricesRegion](./PricesRegion.md)** — региональные цены + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/BouquetCompositionProducts.md b/erp24/docs/models/BouquetCompositionProducts.md new file mode 100644 index 00000000..ac5ce927 --- /dev/null +++ b/erp24/docs/models/BouquetCompositionProducts.md @@ -0,0 +1,290 @@ +# Модель BouquetCompositionProducts + + +## Mindmap + +```mermaid +mindmap + root((BouquetCompositionProducts)) + Таблица БД + erp24.bouquet_composition_products + Свойства + id + int + bouquet_id + int + product_guid + string + bouquet + BouquetComposition + product + Products1c + Связи + Bouquet + 1:1 BouquetComposition + Product + 1:1 Products1c + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `BouquetCompositionProducts` представляет продукты (компоненты) в составе букетов. Хранит связь букета с товарами из номенклатуры и количество каждого товара. Используется для расчёта себестоимости и управления рецептурой букетов. + +**Файл модели:** `erp24/records/BouquetCompositionProducts.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `erp24.bouquet_composition_products` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `bouquet_id` | INTEGER | ID букета (FK → bouquet_composition.id) | +| `product_guid` | VARCHAR(255) | GUID продукта из номенклатуры | +| `count` | FLOAT | Количество продукта в букете | +| `created_at` | TIMESTAMP | Дата создания | +| `updated_at` | TIMESTAMP | Дата обновления | +| `created_by` | INTEGER | ID создателя | +| `updated_by` | INTEGER | ID редактора | + +--- + +## Методы модели + +### `updateProducts(int $bouquetId, array $products): void` + +Статический метод обновления состава букета. Удаляет старые записи и создаёт новые. + +**Параметры:** +- `$bouquetId` (int) — ID букета +- `$products` (array) — Массив [product_guid => count] + +```php +BouquetCompositionProducts::updateProducts($bouquetId, [ + 'abc-123-def' => 3, // 3 розы + 'ghi-456-jkl' => 2, // 2 хризантемы + 'mno-789-pqr' => 1, // 1 зелень +]); +``` + +### `getAvailableItems(bool $isNewRecord, ?int $bouquetId = null): array` + +Возвращает список доступных продуктов для выбора. + +**Параметры:** +- `$isNewRecord` (bool) — Новый ли букет +- `$bouquetId` (int|null) — ID букета для исключения уже выбранных + +```php +$availableProducts = BouquetCompositionProducts::getAvailableItems(false, $bouquetId); +// ['abc-123' => 'Роза красная', 'def-456' => 'Хризантема белая', ...] +``` + +### `getSelectedItems(int $bouquetId): array` + +Возвращает выбранные продукты букета. + +```php +$selected = BouquetCompositionProducts::getSelectedItems($bouquetId); +// [['id' => 'abc-123', 'count' => 3, 'text' => 'Роза красная'], ...] +``` + +### `getCompositionProducts(int $bouquetId): array` + +Возвращает все продукты букета как массив моделей. + +```php +$products = BouquetCompositionProducts::getCompositionProducts($bouquetId); +``` + +### `getProfitability(): float` + +Рассчитывает медианную рентабельность продукта (%). + +```php +$product = BouquetCompositionProducts::findOne($id); +$profitability = $product->getProfitability(); +echo "Рентабельность: {$profitability}%"; +``` + +### `getBuildPercentage(int $year = null, int $month = null): float` + +Возвращает процент использования продукта в сборках за период. + +```php +$percentage = $product->getBuildPercentage(2025, 1); +echo "Используется в {$percentage}% букетов"; +``` + +### `getAverageNumberOfPieces(int $year = null, int $month = null): float` + +Возвращает среднее количество единиц продукта в сборках. + +```php +$avg = $product->getAverageNumberOfPieces(); +echo "Среднее количество: {$avg} шт."; +``` + +### `getWriteOffPercentage(): float` + +Рассчитывает процент списаний относительно продаж за последний месяц. + +```php +$writeOff = $product->getWriteOffPercentage(); +echo "Процент списаний: {$writeOff}%"; +``` + +--- + +## Связи (Relations) + +### `getBouquet(): ActiveQuery` + +Возвращает букет. + +```php +$product = BouquetCompositionProducts::findOne($id); +$bouquet = $product->bouquet; // BouquetComposition +``` + +### `getProduct(): ActiveQuery` + +Возвращает продукт из справочника. + +```php +$nomenclature = $product->product; // Products1c +echo "Продукт: {$nomenclature->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + bouquet_composition_products }o--|| bouquet_composition : "belongs_to" + bouquet_composition_products }o--|| products_1c : "references" + + bouquet_composition_products { + int id PK + int bouquet_id FK + string product_guid FK + float count + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + bouquet_composition { + int id PK + string name + } + + products_1c { + string id PK + string name + } +``` + +--- + +## Примеры использования + +### Добавление продукта в букет + +```php +$product = new BouquetCompositionProducts(); +$product->bouquet_id = $bouquetId; +$product->product_guid = $productGuid; +$product->count = 5; +$product->save(); +``` + +### Получение состава букета с названиями + +```php +$composition = BouquetCompositionProducts::find() + ->with('product') + ->where(['bouquet_id' => $bouquetId]) + ->all(); + +foreach ($composition as $item) { + echo "{$item->product->name}: {$item->count} шт.\n"; +} +``` + +### Массовое обновление состава + +```php +// Из формы приходит ['products_quantity' => ['guid1' => 3, 'guid2' => 2]] +if (!empty($data['products_quantity'])) { + BouquetCompositionProducts::updateProducts($bouquet->id, $data['products_quantity']); +} +``` + +### Анализ использования продукта + +```php +$product = BouquetCompositionProducts::findOne($id); + +echo "Рентабельность: " . $product->getProfitability() . "%\n"; +echo "В сборках: " . $product->getBuildPercentage() . "%\n"; +echo "Среднее кол-во: " . $product->getAverageNumberOfPieces() . " шт.\n"; +echo "Списания: " . $product->getWriteOffPercentage() . "%\n"; +``` + +### Расчёт стоимости букета + +```php +$composition = BouquetCompositionProducts::find() + ->where(['bouquet_id' => $bouquetId]) + ->all(); + +$totalCost = 0; +foreach ($composition as $item) { + $price = SelfCostProduct::find() + ->where(['product_guid' => $item->product_guid]) + ->orderBy(['date' => SORT_DESC]) + ->one(); + + if ($price) { + $totalCost += $price->price * $item->count; + } +} + +echo "Себестоимость букета: {$totalCost} руб."; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `bouquet_id` | Обязательное, целое число, существует в bouquet_composition | +| `product_guid` | Обязательное, макс. 255 символов | +| `count` | Число | +| `created_by`, `updated_by` | Целые числа | + +--- + +## Связанные модели + +- **[BouquetComposition](./BouquetComposition.md)** — букеты +- **[Products1c](./Products1c.md)** — справочник продуктов +- **[Products1cNomenclature](./Products1cNomenclature.md)** — номенклатура +- **[SelfCostProduct](./SelfCostProduct.md)** — себестоимость +- **[PricesDynamic](./PricesDynamic.md)** — динамические цены +- **[BouquetForecast](./BouquetForecast.md)** — прогнозы продаж + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/BouquetCompositionSearch.md b/erp24/docs/models/BouquetCompositionSearch.md new file mode 100644 index 00000000..0584493c --- /dev/null +++ b/erp24/docs/models/BouquetCompositionSearch.md @@ -0,0 +1,142 @@ +# Класс: BouquetCompositionSearch + + +## Mindmap + +```mermaid +mindmap + root((BouquetCompositionSearch)) + Таблица БД + ActiveRecord + Наследование + extends BouquetComposition +``` + +## Назначение +Search-модель для поиска и фильтрации составов букетов в ERP24. Упрощённая модель, основная фильтрация которой происходит в контроллере, а Search-модель используется только для GridView. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`BouquetComposition` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$bouquet_name` | string | Название букета для фильтрации | + +## Методы + +### rules(): array +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `bouquet_name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт базовый провайдер данных без фильтрации. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос BouquetComposition::find() с алиасом 'bc' +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Не применяет фильтры (фильтрация в контроллере) + +**Примечание:** Модель используется только для фильтрации по имени в GridView. Основная фильтрация остаётся в контроллере. + +## Диаграмма процесса + +```mermaid +flowchart TD + A[Controller] --> B[Основная фильтрация] + B --> C[Query с условиями] + + D[BouquetCompositionSearch] --> E[GridView filterModel] + E --> F[Фильтр по bouquet_name] + + C --> G[Объединение запросов] + F --> G + G --> H[Результат] +``` + +## Примеры использования + +### Использование в контроллере +```php +public function actionIndex() +{ + $searchModel = new BouquetCompositionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + // Основная фильтрация в контроллере + $query = $dataProvider->query; + $query->andWhere(['bouquet_id' => $bouquetId]); + $query->joinWith('product'); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### GridView с фильтром +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + [ + 'attribute' => 'bouquet_name', + 'value' => 'bouquet.name', + ], + 'product_id', + 'quantity', + ], +]) ?> +``` + +### Поиск с дополнительными условиями в контроллере +```php +public function actionComposition($bouquetId) +{ + $searchModel = new BouquetCompositionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + $dataProvider->query + ->andWhere(['bouquet_id' => $bouquetId]) + ->orderBy(['position' => SORT_ASC]); + + return $this->render('composition', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +## Связанные модели + +- [BouquetComposition](./BouquetComposition.md) — базовая модель составов букетов +- [Products1c](./Products1c.md) — товары (компоненты букета) + +## Особенности реализации + +1. **Минимальная фильтрация**: Search-модель почти не фильтрует, логика в контроллере +2. **Алиас таблицы**: Использует 'bc' для BouquetComposition +3. **Дополнительное свойство**: bouquet_name для фильтра GridView +4. **Разделение ответственности**: Контроллер фильтрует, Search только для GridView +5. **Комментарий в коде**: Явно указано, что основная фильтрация в контроллере diff --git a/erp24/docs/models/BouquetForecast.md b/erp24/docs/models/BouquetForecast.md new file mode 100644 index 00000000..090acb11 --- /dev/null +++ b/erp24/docs/models/BouquetForecast.md @@ -0,0 +1,305 @@ +# Модель BouquetForecast + + +## Mindmap + +```mermaid +mindmap + root((BouquetForecast)) + Таблица БД + {{%erp24.bouquet_forecast}} + Свойства + id + int + Связи + BouquetHistory + 1:1 BouquetCompositionMatrixTypeHistory + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `BouquetForecast` представляет прогнозы продаж букетов по каналам и точкам. Хранит плановые значения продаж по месяцам для разных типов продаж: оффлайн-магазины, онлайн-магазины, маркетплейсы. Используется для планирования ассортимента и закупок. + +**Файл модели:** `erp24/records/BouquetForecast.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `erp24.bouquet_forecast` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `bouquet_id` | INTEGER | ID букета (FK → bouquet_composition.id) | +| `year` | INTEGER | Год прогноза | +| `month` | INTEGER | Месяц прогноза (1-12) | +| `type_sales` | INTEGER | Тип продаж (1-оффлайн, 2-онлайн, 3-маркетплейс) | +| `type_sales_id` | INTEGER | ID сущности типа продаж (магазин/тип) | +| `type_sales_value` | FLOAT | Плановое значение продаж | +| `created_at` | TIMESTAMP | Дата создания | +| `updated_at` | TIMESTAMP | Дата обновления | +| `created_by` | INTEGER | ID создателя | +| `updated_by` | INTEGER | ID редактора | + +--- + +## Константы + +### Типы продаж (TYPE_*) + +```php +public const OFFLINE_STORES = 1; // Оффлайн-магазины +public const ONLINE_STORES = 2; // Онлайн-магазины +public const MARKETPLACE = 3; // Маркетплейсы +``` + +--- + +## Описание полей + +### `type_sales` — Тип продаж + +Определяет канал продаж: +- `1` (OFFLINE_STORES) — розничные точки продаж +- `2` (ONLINE_STORES) — интернет-магазин +- `3` (MARKETPLACE) — маркетплейсы (Wildberries, Ozon и др.) + +### `type_sales_id` — ID сущности + +Зависит от type_sales: +- Для OFFLINE_STORES: ID магазина (city_store.id) +- Для ONLINE_STORES: ID магазина (city_store.id) +- Для MARKETPLACE: ID типа магазина (store_type.id) + +### `type_sales_value` — Плановое значение + +Количество или сумма планируемых продаж за период. + +--- + +## Методы модели + +### `getStoresList(...): array` + +Статический метод получения списка магазинов с данными о продажах. + +**Параметры:** +- `$bouquetId` (int|null) — ID букета +- `$typeSales` (int) — Тип продаж (константа) +- `$defaultModel` (string) — Класс модели (CityStore или StoreType) +- `$defaultCondition` (array) — Условия выборки +- `$month` (int|null) — Месяц (по умолчанию текущий) +- `$year` (int|null) — Год (по умолчанию текущий) + +**Возвращает:** Массив магазинов с прогнозами + +```php +$offlineStores = BouquetForecast::getStoresList( + $bouquetId, + BouquetForecast::OFFLINE_STORES, + CityStore::class, + ['active' => 1], + 1, + 2025 +); +// [['name' => 'Магазин 1', 'value' => 50, 'id' => 1], ...] +``` + +### `processSalesData(int $bouquetId, array $data): void` + +Обрабатывает данные о продажах из формы. + +**Параметры:** +- `$bouquetId` (int) — ID букета +- `$data` (array) — Данные формы с ключом BouquetForecast[type_sales_value] + +```php +// Данные из формы +$data = [ + 'year' => 2025, + 'month' => 1, + 'BouquetForecast' => [ + 'type_sales_value' => [ + 'offline' => [1 => 50, 2 => 30], + 'online' => [1 => 20], + 'marketplace' => [1 => 100] + ] + ] +]; + +BouquetForecast::processSalesData($bouquetId, $data); +``` + +--- + +## Связи (Relations) + +### `getBouquetHistory(): ActiveQuery` + +Возвращает историю типа матрицы букета. + +```php +$forecast = BouquetForecast::findOne($id); +$history = $forecast->bouquetHistory; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + bouquet_forecast }o--|| bouquet_composition : "belongs_to" + bouquet_forecast }o--o| city_store : "references_store" + bouquet_forecast }o--o| store_type : "references_type" + + bouquet_forecast { + int id PK + int bouquet_id FK + int year + int month + int type_sales + int type_sales_id + float type_sales_value + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + bouquet_composition { + int id PK + string name + } + + city_store { + int id PK + string name + } + + store_type { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание прогноза + +```php +$forecast = new BouquetForecast(); +$forecast->bouquet_id = $bouquetId; +$forecast->year = 2025; +$forecast->month = 2; +$forecast->type_sales = BouquetForecast::OFFLINE_STORES; +$forecast->type_sales_id = $storeId; +$forecast->type_sales_value = 75; +$forecast->save(); +``` + +### Получение прогнозов букета за месяц + +```php +$forecasts = BouquetForecast::find() + ->where([ + 'bouquet_id' => $bouquetId, + 'year' => 2025, + 'month' => 1 + ]) + ->all(); + +$types = [1 => 'Оффлайн', 2 => 'Онлайн', 3 => 'Маркетплейс']; +foreach ($forecasts as $f) { + echo "{$types[$f->type_sales]}: {$f->type_sales_value}\n"; +} +``` + +### Суммарный прогноз по типам + +```php +$summary = BouquetForecast::find() + ->select(['type_sales', 'SUM(type_sales_value) as total']) + ->where([ + 'bouquet_id' => $bouquetId, + 'year' => 2025, + 'month' => 1 + ]) + ->groupBy('type_sales') + ->asArray() + ->all(); + +foreach ($summary as $row) { + echo "Тип {$row['type_sales']}: {$row['total']}\n"; +} +``` + +### Получение прогнозов для всех магазинов + +```php +// Оффлайн-магазины +$offline = BouquetForecast::getStoresList( + $bouquetId, + BouquetForecast::OFFLINE_STORES, + CityStore::class, + ['active' => 1] +); + +// Маркетплейсы +$marketplace = BouquetForecast::getStoresList( + $bouquetId, + BouquetForecast::MARKETPLACE, + StoreType::class, + ['is_marketplace' => 1] +); +``` + +### Сравнение прогнозов по месяцам + +```php +$comparison = BouquetForecast::find() + ->select(['month', 'SUM(type_sales_value) as total']) + ->where(['bouquet_id' => $bouquetId, 'year' => 2025]) + ->groupBy('month') + ->orderBy('month') + ->asArray() + ->all(); + +foreach ($comparison as $row) { + echo "Месяц {$row['month']}: {$row['total']}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `bouquet_id` | Обязательное, целое число | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `type_sales` | Обязательное, целое число | +| `type_sales_id` | Обязательное, целое число | +| `type_sales_value` | Число | + +--- + +## Связанные модели + +- **[BouquetComposition](./BouquetComposition.md)** — букеты +- **[BouquetCompositionMatrixTypeHistory](./BouquetCompositionMatrixTypeHistory.md)** — история типов матриц +- **[CityStore](./CityStore.md)** — магазины +- **[StoreType](./StoreType.md)** — типы магазинов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CalendarAdminLink.md b/erp24/docs/models/CalendarAdminLink.md new file mode 100644 index 00000000..bb287460 --- /dev/null +++ b/erp24/docs/models/CalendarAdminLink.md @@ -0,0 +1,223 @@ +# Модель CalendarAdminLink + + +## Mindmap + +```mermaid +mindmap + root((CalendarAdminLink)) + Таблица БД + calendar_admin_link + Свойства + id + int + admin_id + int + calendar_admin_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CalendarAdminLink` реализует механизм совместного доступа к календарям сотрудников. Позволяет одному сотруднику открыть доступ к своему календарю другому сотруднику. Используется для организации совместной работы и планирования в команде. + +**Файл модели:** `erp24/records/CalendarAdminLink.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `calendar_admin_link` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника, которому открыт доступ (FK → admin.id) | +| `calendar_admin_id` | INTEGER | ID владельца календаря (FK → admin.id) | + +--- + +## Описание полей + +### `admin_id` — С кем поделились + +ID сотрудника, который получает доступ к чужому календарю. Это "получатель" доступа. + +### `calendar_admin_id` — Кому принадлежит календарь + +ID владельца календаря — сотрудника, который делится своим календарём. По умолчанию устанавливается текущий авторизованный пользователь. + +--- + +## Особенности + +- Таблица реализует связь **many-to-many** между сотрудниками (owner ↔ viewer) +- Нельзя поделиться календарём с самим собой (валидация) +- По умолчанию владельцем календаря устанавливается текущий пользователь +- Один владелец может открыть доступ нескольким сотрудникам +- Один сотрудник может иметь доступ к календарям нескольких владельцев + +--- + +## Валидация + +### `adminValidate()` + +Кастомный валидатор, проверяющий что пользователь не пытается поделиться календарём с самим собой. + +**Логика:** +- Если `admin_id` равен ID текущего пользователя, добавляется ошибка валидации + +```php +public function adminValidate() +{ + if ($this->admin_id == Yii::$app->user->id) { + $this->addError('admin_id', 'Нельзя поделиться с самим собой.'); + } +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + calendar_admin_link }o--|| admin : "shared_with" + calendar_admin_link }o--|| admin : "calendar_owner" + + calendar_admin_link { + int id PK + int admin_id FK "С кем поделились" + int calendar_admin_id FK "Владелец календаря" + } + + admin { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Открытие доступа к календарю + +```php +$link = new CalendarAdminLink(); +$link->admin_id = $targetEmployeeId; // Кому открываем доступ +// calendar_admin_id установится автоматически (текущий пользователь) + +if ($link->save()) { + echo "Доступ к календарю открыт"; +} else { + echo "Ошибка: " . implode(', ', $link->getFirstErrors()); +} +``` + +### Получение списка сотрудников с доступом к моему календарю + +```php +$myCalendarId = Yii::$app->user->id; + +$sharedWith = CalendarAdminLink::find() + ->where(['calendar_admin_id' => $myCalendarId]) + ->all(); + +foreach ($sharedWith as $link) { + $employee = Admin::findOne($link->admin_id); + echo "Доступ открыт: {$employee->name}\n"; +} +``` + +### Получение списка календарей, к которым у меня есть доступ + +```php +$myId = Yii::$app->user->id; + +$accessibleCalendars = CalendarAdminLink::find() + ->where(['admin_id' => $myId]) + ->all(); + +foreach ($accessibleCalendars as $link) { + $owner = Admin::findOne($link->calendar_admin_id); + echo "Доступен календарь: {$owner->name}\n"; +} +``` + +### Проверка доступа к календарю + +```php +$hasAccess = CalendarAdminLink::find() + ->where([ + 'admin_id' => Yii::$app->user->id, + 'calendar_admin_id' => $targetCalendarOwnerId + ]) + ->exists(); + +if ($hasAccess) { + echo "У вас есть доступ к этому календарю"; +} +``` + +### Закрытие доступа к календарю + +```php +CalendarAdminLink::deleteAll([ + 'calendar_admin_id' => Yii::$app->user->id, + 'admin_id' => $employeeId +]); +echo "Доступ закрыт"; +``` + +### Массовое открытие доступа к календарю + +```php +$employeeIds = [10, 15, 20, 25]; // ID сотрудников + +foreach ($employeeIds as $empId) { + $link = new CalendarAdminLink([ + 'admin_id' => $empId, + 'calendar_admin_id' => Yii::$app->user->id + ]); + + if (!$link->save()) { + Yii::warning("Не удалось открыть доступ для {$empId}"); + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число, не равен текущему пользователю | +| `calendar_admin_id` | Целое число, по умолчанию = ID текущего пользователя | + +--- + +## Бизнес-логика + +1. **Совместный доступ** — руководители могут просматривать календари подчинённых +2. **Командная работа** — члены команды делятся календарями для координации +3. **Права доступа** — модель определяет только связь, не уровни прав (read/write) +4. **Односторонний доступ** — если A поделился с B, это не означает, что B поделился с A + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[Timetable](./Timetable.md)** — расписание +- **[TimetableEvent](./TimetableEvent.md)** — события календаря + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Cashes.md b/erp24/docs/models/Cashes.md new file mode 100644 index 00000000..813cdad0 --- /dev/null +++ b/erp24/docs/models/Cashes.md @@ -0,0 +1,249 @@ +# Модель Cashes + + +## Mindmap + +```mermaid +mindmap + root((Cashes)) + Таблица БД + cashes + Свойства + id + string + name + string + store_id + string + kkm_id + string + is_central + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Cashes` представляет справочник кассовых аппаратов (касс). Хранит информацию о кассах из 1С: название, привязку к магазину и контрольно-кассовой машине (ККМ). Используется для учёта продаж по кассам и формирования кассовых отчётов. + +**Файл модели:** `erp24/records/Cashes.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `cashes` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(36) | GUID кассы из 1С (первичный ключ) | +| `name` | VARCHAR(155) | Наименование кассы | +| `store_id` | VARCHAR(36) | GUID магазина из 1С | +| `kkm_id` | VARCHAR(36) | GUID контрольно-кассовой машины | +| `is_central` | INTEGER | Признак центральной кассы (0/1) | + +--- + +## Описание полей + +### `id` — Идентификатор кассы + +GUID кассы, синхронизированный из 1С. + +**Формат:** UUID (36 символов) + +**Пример:** `"a1b2c3d4-e5f6-7890-abcd-ef1234567890"` + +### `name` — Наименование + +Название кассы из 1С. + +**Примеры:** +- `"Касса №1 (ТЦ Мега)"` +- `"Касса основная"` +- `"Касса доставки"` + +### `store_id` — Магазин + +GUID магазина, к которому привязана касса. + +**Связь:** логическая связь с `products_1c` (склад/магазин) через GUID + +### `kkm_id` — ККМ + +GUID контрольно-кассовой машины, связанной с данной кассой. + +### `is_central` — Центральная касса + +Признак того, является ли касса основной (центральной) в магазине. + +| Значение | Описание | +|----------|----------| +| 0 | Обычная касса | +| 1 | Центральная касса магазина | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + cashes }o--|| products_1c : "belongs_to_store" + cashes ||--o{ sales : "records_sales" + + cashes { + string id PK + string name + string store_id FK + string kkm_id + int is_central + } + + products_1c { + string id PK + string name + string tip + } + + sales { + int id PK + string cash_id FK + float summ + timestamp date + } +``` + +--- + +## Примеры использования + +### Получение всех касс + +```php +$cashes = Cashes::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Получение кассы по GUID + +```php +$cash = Cashes::findOne($cashGuid); +echo "Касса: {$cash->name}"; +``` + +### Получение касс магазина + +```php +$storeCashes = Cashes::find() + ->where(['store_id' => $storeGuid]) + ->all(); +``` + +### Получение центральной кассы магазина + +```php +$centralCash = Cashes::findOne([ + 'store_id' => $storeGuid, + 'is_central' => 1 +]); +``` + +### Использование в выпадающем списке + +```php +use yii\helpers\ArrayHelper; + +$cashes = Cashes::find() + ->where(['store_id' => $storeGuid]) + ->all(); + +$cashList = ArrayHelper::map($cashes, 'id', 'name'); + +echo Html::dropDownList('cash_id', null, $cashList, [ + 'prompt' => 'Выберите кассу' +]); +``` + +### Продажи по кассе + +```php +$cash = Cashes::findOne($cashId); + +$totalSales = Sales::find() + ->where(['cash_id' => $cash->id]) + ->andWhere(['>=', 'date', $startDate]) + ->andWhere(['<=', 'date', $endDate]) + ->sum('summ'); + +echo "Выручка кассы {$cash->name}: {$totalSales} руб."; +``` + +### Проверка существования кассы + +```php +$exists = Cashes::find() + ->where(['id' => $cashGuid]) + ->exists(); + +if (!$exists) { + // Создание новой кассы из данных 1С + $cash = new Cashes(); + $cash->id = $data['guid']; + $cash->name = $data['name']; + $cash->store_id = $data['store_guid']; + $cash->kkm_id = $data['kkm_guid']; + $cash->is_central = $data['is_central'] ? 1 : 0; + $cash->save(); +} +``` + +### Статистика касс по магазинам + +```php +$stats = Cashes::find() + ->select([ + 'store_id', + 'COUNT(*) as cash_count', + 'SUM(is_central) as central_count' + ]) + ->groupBy('store_id') + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `id` | Обязательное, уникальное, макс. 36 символов | +| `name` | Обязательное, макс. 155 символов | +| `store_id` | Обязательное, макс. 36 символов | +| `kkm_id` | Обязательное, макс. 36 символов | +| `is_central` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — справочник 1С (магазины/склады) +- **[Sales](./Sales.md)** — продажи по кассам +- **SalesProducts** — товары в продажах + +--- + +## Синхронизация с 1С + +Данные касс загружаются из 1С и обновляются при: +- Создании новой кассы в 1С +- Изменении названия или привязки кассы +- Изменении статуса центральной кассы + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CatProperty.md b/erp24/docs/models/CatProperty.md new file mode 100644 index 00000000..7667db3e --- /dev/null +++ b/erp24/docs/models/CatProperty.md @@ -0,0 +1,261 @@ +# Модель CatProperty + + +## Mindmap + +```mermaid +mindmap + root((CatProperty)) + Таблица БД + cat_property + Свойства + id + int + parent_id + int + name + string + tip + int + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CatProperty` представляет свойства категорий товаров в иерархической структуре. Используется для построения древовидного справочника характеристик товаров с возможностью группировки по типам и упорядочивания по позиции. + +**Файл модели:** `erp24/records/CatProperty.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `cat_property` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `parent_id` | INTEGER | ID родительского элемента (для иерархии) | +| `name` | TEXT | Название свойства | +| `tip` | INTEGER | Тип свойства (числовой код типа) | +| `posit` | INTEGER | Позиция для сортировки | + +--- + +## Описание полей + +### `parent_id` — Родительский элемент + +Используется для построения иерархической структуры свойств: +- `0` или `null` — корневой элемент (категория верхнего уровня) +- Значение > 0 — ссылка на родительское свойство + +### `tip` — Тип свойства + +Числовой идентификатор типа свойства. Определяет: +- Тип данных свойства (строка, число, выбор из списка и т.д.) +- Способ отображения и редактирования + +### `posit` — Позиция + +Порядковый номер для сортировки свойств при отображении. Меньшее значение = выше в списке. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + cat_property ||--o{ cat_property : "parent" + + cat_property { + int id PK + int parent_id FK + text name + int tip + int posit + } +``` + +--- + +## Примеры использования + +### Создание корневой категории свойств + +```php +$rootProperty = new CatProperty(); +$rootProperty->parent_id = 0; +$rootProperty->name = 'Цветы'; +$rootProperty->tip = 1; +$rootProperty->posit = 1; +$rootProperty->save(); +``` + +### Создание дочернего свойства + +```php +$childProperty = new CatProperty(); +$childProperty->parent_id = $rootProperty->id; +$childProperty->name = 'Розы'; +$childProperty->tip = 1; +$childProperty->posit = 1; +$childProperty->save(); +``` + +### Получение корневых категорий + +```php +$rootCategories = CatProperty::find() + ->where(['parent_id' => 0]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($rootCategories as $category) { + echo "{$category->name}\n"; +} +``` + +### Получение дочерних элементов + +```php +$children = CatProperty::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($children as $child) { + echo " - {$child->name}\n"; +} +``` + +### Построение дерева свойств + +```php +function buildTree($parentId = 0, $level = 0) { + $items = CatProperty::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + + foreach ($items as $item) { + echo str_repeat(' ', $level) . "- {$item->name}\n"; + buildTree($item->id, $level + 1); + } +} + +buildTree(); +// Вывод: +// - Цветы +// - Розы +// - Тюльпаны +// - Упаковка +// - Бумага +// - Пакеты +``` + +### Фильтрация по типу + +```php +$typeOneProperties = CatProperty::find() + ->where(['tip' => 1]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Перемещение элемента в списке + +```php +// Поднять элемент выше +$property = CatProperty::findOne($id); +$property->posit = $property->posit - 1; +$property->save(); + +// Обновить позиции всех элементов +$properties = CatProperty::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +$position = 1; +foreach ($properties as $prop) { + $prop->posit = $position++; + $prop->save(false, ['posit']); +} +``` + +### Получение пути к элементу (breadcrumbs) + +```php +function getPath($id) { + $path = []; + $current = CatProperty::findOne($id); + + while ($current) { + array_unshift($path, $current->name); + if ($current->parent_id > 0) { + $current = CatProperty::findOne($current->parent_id); + } else { + break; + } + } + + return implode(' > ', $path); +} + +echo getPath($propertyId); +// "Цветы > Розы > Красные розы" +``` + +### Поиск по имени + +```php +$found = CatProperty::find() + ->where(['like', 'name', 'роз']) + ->all(); + +foreach ($found as $item) { + echo "{$item->name} (ID: {$item->id})\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `parent_id` | Обязательное, целое число | +| `name` | Обязательное, строка | +| `tip` | Обязательное, целое число | +| `posit` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары +- **[Products1cNomenclature](./Products1cNomenclature.md)** — номенклатура +- **[ProductCategory](./ProductCategory.md)** — категории продуктов + +--- + +## Паттерны работы с иерархией + +### Nested Set vs Adjacency List + +Модель использует паттерн **Adjacency List** (parent_id), что обеспечивает: +- Простоту изменения структуры (перемещение элементов) +- Лёгкость добавления новых элементов +- Необходимость рекурсии для построения полного дерева + +Для сложных операций с деревьями рекомендуется использовать расширения Yii2 для работы с иерархическими структурами. + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CategoryPlan.md b/erp24/docs/models/CategoryPlan.md new file mode 100644 index 00000000..0c523fb6 --- /dev/null +++ b/erp24/docs/models/CategoryPlan.md @@ -0,0 +1,280 @@ +# Модель CategoryPlan + + +## Mindmap + +```mermaid +mindmap + root((CategoryPlan)) + Таблица БД + category_plan + Свойства + id + int + year + int + month + int + store_id + int + category + string + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CategoryPlan` хранит плановые показатели по категориям товаров для магазинов. Содержит проценты планов продаж по каналам сбыта (оффлайн, интернет-магазин, маркетплейс) и процент списаний. Используется для планирования и анализа эффективности работы магазинов по товарным категориям. + +**Файл модели:** `erp24/records/CategoryPlan.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `category_plan` +**Родительский класс:** `yii\db\ActiveRecord` +**Behaviors:** `TimestampBehavior`, `BlameableBehavior` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `year` | INTEGER | Год создания отчёта | +| `month` | INTEGER | Месяц создания отчёта (1-12) | +| `store_id` | INTEGER | ID магазина в ERP (FK → city_store.id) | +| `category` | VARCHAR(100) | Название категории товаров | +| `offline` | FLOAT | Оффлайн план (процент) | +| `internet_shop` | FLOAT | Интернет-магазин план (процент) | +| `marketplace` | FLOAT | Маркетплейс план (процент) | +| `write_offs` | FLOAT | Списания план (процент) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `created_by` | INTEGER | ID создателя записи | +| `updated_by` | INTEGER | ID обновителя записи | + +--- + +## Описание полей + +### `category` — Категория товаров + +Название товарной категории, для которой устанавливается план. Типичные категории: +- **Срезка** — срезанные цветы +- **Горшечные** — горшечные растения +- **Сопутствующие товары** — аксессуары, упаковка и т.д. + +### Плановые показатели (проценты) + +| Поле | Описание | Пример | +|------|----------|--------| +| `offline` | План продаж в оффлайн-магазине | 60% | +| `internet_shop` | План продаж через интернет-магазин | 25% | +| `marketplace` | План продаж через маркетплейсы | 10% | +| `write_offs` | Допустимый процент списаний | 5% | + +Сумма `offline + internet_shop + marketplace` обычно составляет ~100% (за вычетом списаний). + +--- + +## Диаграмма связей + +```mermaid +erDiagram + category_plan }o--|| city_store : "belongs_to" + + category_plan { + int id PK + int year + int month + int store_id FK + string category + float offline + float internet_shop + float marketplace + float write_offs + timestamp created_at + timestamp updated_at + int created_by + int updated_by + } + + city_store { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание плана для категории + +```php +$plan = new CategoryPlan(); +$plan->year = 2025; +$plan->month = 1; +$plan->store_id = $storeId; +$plan->category = 'Срезка'; +$plan->offline = 55.0; // 55% оффлайн +$plan->internet_shop = 30.0; // 30% интернет +$plan->marketplace = 10.0; // 10% маркетплейсы +$plan->write_offs = 5.0; // 5% списания +$plan->save(); +``` + +### Получение планов магазина за месяц + +```php +$plans = CategoryPlan::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2025, + 'month' => 1 + ]) + ->all(); + +foreach ($plans as $plan) { + echo "{$plan->category}:\n"; + echo " Оффлайн: {$plan->offline}%\n"; + echo " Интернет: {$plan->internet_shop}%\n"; + echo " Маркетплейс: {$plan->marketplace}%\n"; + echo " Списания: {$plan->write_offs}%\n"; +} +``` + +### Сравнение планов по магазинам + +```php +$comparison = CategoryPlan::find() + ->select([ + 'store_id', + 'AVG(offline) as avg_offline', + 'AVG(internet_shop) as avg_internet', + 'AVG(marketplace) as avg_marketplace', + 'AVG(write_offs) as avg_writeoffs' + ]) + ->where([ + 'year' => 2025, + 'month' => 1, + 'category' => 'Срезка' + ]) + ->groupBy('store_id') + ->asArray() + ->all(); + +foreach ($comparison as $row) { + $store = CityStore::findOne($row['store_id']); + echo "{$store->name}: Оффлайн {$row['avg_offline']}%\n"; +} +``` + +### Анализ динамики планов + +```php +// Планы по месяцам для категории +$dynamics = CategoryPlan::find() + ->select(['month', 'SUM(offline) as total_offline']) + ->where([ + 'store_id' => $storeId, + 'year' => 2025, + 'category' => 'Срезка' + ]) + ->groupBy('month') + ->orderBy('month') + ->asArray() + ->all(); + +foreach ($dynamics as $row) { + echo "Месяц {$row['month']}: План оффлайн {$row['total_offline']}%\n"; +} +``` + +### Массовое обновление планов + +```php +$categories = ['Срезка', 'Горшечные', 'Сопутствующие товары']; +$defaultPlans = [ + 'Срезка' => ['offline' => 60, 'internet_shop' => 25, 'marketplace' => 10, 'write_offs' => 5], + 'Горшечные' => ['offline' => 70, 'internet_shop' => 20, 'marketplace' => 5, 'write_offs' => 5], + 'Сопутствующие товары' => ['offline' => 50, 'internet_shop' => 35, 'marketplace' => 12, 'write_offs' => 3], +]; + +foreach ($categories as $category) { + $plan = new CategoryPlan(); + $plan->year = 2025; + $plan->month = 2; + $plan->store_id = $storeId; + $plan->category = $category; + $plan->offline = $defaultPlans[$category]['offline']; + $plan->internet_shop = $defaultPlans[$category]['internet_shop']; + $plan->marketplace = $defaultPlans[$category]['marketplace']; + $plan->write_offs = $defaultPlans[$category]['write_offs']; + $plan->save(); +} +``` + +### Проверка выполнения плана + +```php +$plan = CategoryPlan::findOne([ + 'store_id' => $storeId, + 'year' => 2025, + 'month' => 1, + 'category' => 'Срезка' +]); + +// Допустим, фактические данные +$actualOffline = 58.5; +$actualWriteOffs = 4.2; + +if ($plan) { + $offlineDeviation = $actualOffline - $plan->offline; + $writeOffsStatus = $actualWriteOffs <= $plan->write_offs ? 'В норме' : 'Превышение'; + + echo "План оффлайн: {$plan->offline}%, Факт: {$actualOffline}%\n"; + echo "Отклонение: {$offlineDeviation}%\n"; + echo "Списания: {$writeOffsStatus}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число (1-12) | +| `store_id` | Обязательное, целое число | +| `category` | Обязательное, макс. 100 символов | +| `offline`, `internet_shop`, `marketplace`, `write_offs` | Числа (float) | +| `created_at`, `updated_at` | Обязательные, безопасные | +| `created_by`, `updated_by` | Обязательные, целые числа | + +--- + +## Бизнес-логика + +1. **Ежемесячное планирование** — планы устанавливаются на каждый месяц +2. **Декомпозиция по категориям** — каждая категория имеет свои плановые показатели +3. **Многоканальность** — учитываются все каналы продаж (оффлайн, онлайн, маркетплейсы) +4. **Контроль списаний** — отдельный показатель для мониторинга потерь +5. **Региональная специфика** — разные магазины могут иметь разные планы + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазины +- **[BouquetForecast](./BouquetForecast.md)** — прогнозы продаж букетов +- **[SalesProducts](./SalesProducts.md)** — фактические продажи +- **[WriteOffs](./WriteOffs.md)** — списания товаров + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ChartDataSearch.md b/erp24/docs/models/ChartDataSearch.md new file mode 100644 index 00000000..2e67c193 --- /dev/null +++ b/erp24/docs/models/ChartDataSearch.md @@ -0,0 +1,216 @@ +# Класс: ChartDataSearch + +## Назначение +Сложная Search-модель для построения аналитических графиков и дашбордов в ERP24. Не является ActiveRecord-моделью, а представляет собой класс для построения SQL-запросов с агрегацией данных по различным метрикам: продажи, ФОТ, конверсия, списания, бонусы и др. + +## Пространство имён +`yii_app\records` + +## Родительский класс +Нет (обычный PHP-класс) + +## Свойства + +### Параметры фильтрации +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$attribute_name` | string | Название анализируемого атрибута | +| `$mode_level` | int | Уровень: 1 — Розница, 2 — Куст, 3 — Магазин | +| `$date_start` | string | Начало периода | +| `$date_end` | string | Конец периода | +| `$cluster_id` | int | ID кластера | +| `$store_id` | int | ID магазина | +| `$select_cluster` | mixed | Выбранные кластеры | +| `$mode_shift` | int | Тип смены: 1 — День, 2 — Ночь, 3 — День+Ночь, 4 — День/Ночь, 5 — Среднее | + +### Справочники +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$day_of_week` | array | Дни недели: 0 — Вс, 1 — Пн, ..., 6 — Сб | +| `$attributes_array` | array | Конфигурация атрибутов для анализа | + +## Конфигурация атрибутов (attributes_array) + +```php +$attributes_array = [ + 'plan_completed_this_day' => [ + 'attributes' => ['sales_sum'], + 'plan_attribute' => ['SUM(plan_store.plan_sales)', 'SUM(plan_store.plan)'] + ], + 'plan_completed_this_month' => [...], + 'sales' => [ + 'attributes' => ['sales_sum'], + 'plan_attribute' => ['SUM(plan_store.plan_sales)', 'SUM(plan_store.plan)'] + ], + 'avg_sales_value' => [ + 'attributes' => ['sales_sum', 'count_sales'], + 'plan_attribute' => ['AVG(plan_store.plan_avg_sales_value)', 'AVG(plan_store.plan)'] + ], + 'count_sales_in_hour' => [ + 'attributes' => [ + 'count_sales_in_0_hour', 'count_sales_in_1_hour', ..., 'count_sales_in_23_hour' + ], + 'plan_attribute' => NULL + ], + 'fot' => [ + 'attributes' => ['day_payroll', 'sales_sum_admin'], + 'plan_attribute' => ['AVG(plan_store.plan_fot)', 'AVG(plan_store.plan)'] + ], + 'sales_sum_on_admin' => [ + 'attributes' => ['sales_sum_admin', 'admin_count'], + 'plan_attribute' => NULL + ], + 'write_offs' => [ + 'attributes' => ['sales_sum', 'write_offs'], + 'plan_attribute' => NULL + ], + 'user_bonus' => [ + 'attributes' => ['count_sales', 'count_users', 'first_minus_user_bonus', 'second_minus_user_bonus'], + 'plan_attribute' => NULL + ], + // ... и другие метрики +]; +``` + +## Диаграмма структуры метрик + +```mermaid +mindmap + root((ChartDataSearch)) + Продажи + sales + avg_sales_value + count_sales_in_hour + plan_completed_this_day + plan_completed_this_month + Персонал + fot + sales_sum_on_admin + admin_count + Клиенты + user_bonus + count_users + Операции + write_offs +``` + +## Диаграмма уровней анализа + +```mermaid +flowchart TD + A[ChartDataSearch] --> B{mode_level} + + B -->|1| C[Розница
Все магазины] + B -->|2| D[Куст
По кластерам] + B -->|3| E[Магазин
Конкретный store_id] + + C --> F[Агрегация по всей сети] + D --> G[Агрегация по cluster_id] + E --> H[Данные одного магазина] +``` + +## Диаграмма типов смен + +```mermaid +flowchart LR + A[mode_shift] --> B{Значение} + + B -->|1| C[День] + B -->|2| D[Ночь] + B -->|3| E[День + Ночь] + B -->|4| F[День / Ночь
раздельно] + B -->|5| G[Среднее
Дня и Ночи] +``` + +## Примеры использования + +### Анализ продаж за период +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'sales'; +$chartSearch->mode_level = 1; // Розница +$chartSearch->date_start = '2024-01-01'; +$chartSearch->date_end = '2024-01-31'; +$chartSearch->mode_shift = 3; // День + Ночь + +$data = $chartSearch->getData(); +``` + +### Анализ ФОТ по кластеру +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'fot'; +$chartSearch->mode_level = 2; // Куст +$chartSearch->cluster_id = 5; +$chartSearch->date_start = '2024-06-01'; +$chartSearch->date_end = '2024-06-30'; + +$data = $chartSearch->getData(); +``` + +### Анализ конверсии по магазину +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'user_bonus'; +$chartSearch->mode_level = 3; // Магазин +$chartSearch->store_id = 10; +$chartSearch->date_start = date('Y-m-01'); +$chartSearch->date_end = date('Y-m-d'); + +$data = $chartSearch->getData(); +``` + +### Почасовой анализ продаж +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'count_sales_in_hour'; +$chartSearch->mode_level = 3; +$chartSearch->store_id = 10; +$chartSearch->date_start = '2024-06-15'; +$chartSearch->date_end = '2024-06-15'; + +$hourlyData = $chartSearch->getData(); +// Возвращает данные по 24 часам +``` + +### Сравнение дневной и ночной смены +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'sales'; +$chartSearch->mode_level = 3; +$chartSearch->store_id = 10; +$chartSearch->mode_shift = 4; // День/Ночь раздельно + +$data = $chartSearch->getData(); +// Возвращает отдельные данные для дня и ночи +``` + +### Выполнение плана +```php +$chartSearch = new ChartDataSearch(); +$chartSearch->attribute_name = 'plan_completed_this_month'; +$chartSearch->mode_level = 2; +$chartSearch->cluster_id = 3; + +$planData = $chartSearch->getData(); +// Сравнивает факт с планом +``` + +## Связанные модели и таблицы + +- [DashboardSales](./DashboardSales.md) — агрегированные данные продаж +- [StorePlan](./StorePlan.md) — планы магазинов (plan_store) +- [AdminPayrollDays](./AdminPayrollDays.md) — данные ФОТ +- [CityStore](./CityStore.md) — магазины +- [Cluster](./Cluster.md) — кластеры + +## Особенности реализации + +1. **Не ActiveRecord**: Обычный PHP-класс для построения запросов +2. **Гибкая конфигурация**: attributes_array определяет структуру метрик +3. **Многоуровневый анализ**: mode_level для розницы/куста/магазина +4. **Типы смен**: mode_shift для фильтрации по времени работы +5. **Почасовой анализ**: 24 атрибута count_sales_in_X_hour +6. **План/Факт**: plan_attribute для сравнения с планами +7. **Дни недели**: Русскоязычные сокращения в day_of_week +8. **Сложные запросы**: Использует yii\db\Query и Expression diff --git a/erp24/docs/models/ChatbotAction.md b/erp24/docs/models/ChatbotAction.md new file mode 100644 index 00000000..69a5b40f --- /dev/null +++ b/erp24/docs/models/ChatbotAction.md @@ -0,0 +1,268 @@ +# Модель ChatbotAction + + +## Mindmap + +```mermaid +mindmap + root((ChatbotAction)) + Таблица БД + chatbot_action + Свойства + id + int + phone + string + created_at + string + action + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ChatbotAction` логирует действия клиентов в чат-боте. Сохраняет информацию о взаимодействии клиента с ботом: нажатия кнопок, выбор опций, отправка команд. Используется для аналитики поведения пользователей и улучшения UX чат-бота. + +**Файл модели:** `erp24/records/ChatbotAction.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `chatbot_action` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `phone` | VARCHAR(16) | Номер телефона клиента | +| `created_at` | TIMESTAMP | Дата и время действия | +| `action` | VARCHAR(255) | Текстовое описание действия | +| `json` | TEXT | Дополнительные данные в формате JSON | + +--- + +## Описание полей + +### `phone` — Номер телефона + +Идентификатор клиента по номеру телефона. Формат: международный или внутренний (до 16 символов). + +### `action` — Действие + +Текстовый код или описание действия. Примеры: +- `pressInfoBtn` — нажатие кнопки "Информация" +- `selectCity` — выбор города +- `startOrder` — начало оформления заказа +- `askQuestion` — отправка вопроса +- `viewCatalog` — просмотр каталога + +### `json` — Дополнительные данные + +JSON-объект с контекстом действия. Может содержать: +- Параметры выбора (ID города, товара) +- Текст сообщения клиента +- Состояние сессии +- Метаданные устройства + +--- + +## Диаграмма связей + +```mermaid +erDiagram + chatbot_action }o--o| users : "references_by_phone" + + chatbot_action { + int id PK + string phone + timestamp created_at + string action + text json + } + + users { + int id PK + string phone + } +``` + +--- + +## Примеры использования + +### Логирование действия клиента + +```php +$log = new ChatbotAction(); +$log->phone = '+79001234567'; +$log->created_at = date('Y-m-d H:i:s'); +$log->action = 'pressInfoBtn'; +$log->json = json_encode([ + 'button_id' => 'info_delivery', + 'session_id' => 'abc123', + 'platform' => 'telegram' +]); +$log->save(); +``` + +### Получение истории действий клиента + +```php +$actions = ChatbotAction::find() + ->where(['phone' => '+79001234567']) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(50) + ->all(); + +foreach ($actions as $action) { + $data = json_decode($action->json, true); + echo "{$action->created_at}: {$action->action}\n"; + if ($data) { + print_r($data); + } +} +``` + +### Анализ популярных действий + +```php +$popular = ChatbotAction::find() + ->select(['action', 'COUNT(*) as count']) + ->groupBy('action') + ->orderBy(['count' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); + +foreach ($popular as $row) { + echo "{$row['action']}: {$row['count']} раз\n"; +} +``` + +### Статистика по дням + +```php +$daily = ChatbotAction::find() + ->select(['DATE(created_at) as date', 'COUNT(*) as count']) + ->where(['>=', 'created_at', '2025-01-01']) + ->groupBy(['DATE(created_at)']) + ->orderBy(['date' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($daily as $row) { + echo "{$row['date']}: {$row['count']} действий\n"; +} +``` + +### Поиск конкретного действия + +```php +// Найти всех, кто начинал заказ +$startedOrders = ChatbotAction::find() + ->where(['action' => 'startOrder']) + ->andWhere(['>=', 'created_at', '2025-01-01']) + ->all(); + +echo "Начато заказов: " . count($startedOrders); +``` + +### Воронка конверсии + +```php +// Анализ воронки: просмотр каталога → добавление в корзину → заказ +$viewCatalog = ChatbotAction::find() + ->where(['action' => 'viewCatalog']) + ->count(); + +$addToCart = ChatbotAction::find() + ->where(['action' => 'addToCart']) + ->count(); + +$placeOrder = ChatbotAction::find() + ->where(['action' => 'placeOrder']) + ->count(); + +echo "Воронка:\n"; +echo "Просмотр каталога: {$viewCatalog}\n"; +echo "Добавление в корзину: {$addToCart} (" . round($addToCart / $viewCatalog * 100, 1) . "%)\n"; +echo "Оформление заказа: {$placeOrder} (" . round($placeOrder / $viewCatalog * 100, 1) . "%)\n"; +``` + +### Работа с JSON-данными + +```php +// Извлечение данных из JSON +$action = ChatbotAction::findOne($id); +$data = json_decode($action->json, true); + +if (isset($data['city_id'])) { + echo "Выбран город: {$data['city_id']}"; +} + +// Поиск по JSON-полю (PostgreSQL) +$actions = ChatbotAction::find() + ->where("json::jsonb->>'platform' = 'telegram'") + ->all(); +``` + +### Сегментация клиентов + +```php +// Активные клиенты (более 10 действий за месяц) +$activeClients = ChatbotAction::find() + ->select(['phone', 'COUNT(*) as actions']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('phone') + ->having(['>', 'COUNT(*)', 10]) + ->asArray() + ->all(); + +echo "Активных клиентов: " . count($activeClients); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `phone` | Обязательное, макс. 16 символов | +| `created_at` | Обязательное, безопасное | +| `action` | Обязательное, макс. 255 символов | +| `json` | Строка (необязательное) | + +--- + +## Типичные действия (action) + +| Код действия | Описание | +|--------------|----------| +| `startBot` | Запуск бота | +| `pressInfoBtn` | Нажатие кнопки информации | +| `selectCity` | Выбор города | +| `viewCatalog` | Просмотр каталога | +| `viewProduct` | Просмотр товара | +| `addToCart` | Добавление в корзину | +| `removeFromCart` | Удаление из корзины | +| `startOrder` | Начало оформления заказа | +| `placeOrder` | Размещение заказа | +| `askQuestion` | Отправка вопроса | +| `callSupport` | Запрос связи с поддержкой | +| `rateService` | Оценка сервиса | + +--- + +## Связанные модели + +- **[Users](./Users.md)** — клиенты (связь по phone) +- **[StoreOrders](./StoreOrders.md)** — заказы +- **[TelegramBot](./TelegramBot.md)** — настройки Telegram-бота + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CheckConduct.md b/erp24/docs/models/CheckConduct.md new file mode 100644 index 00000000..4fcba8fe --- /dev/null +++ b/erp24/docs/models/CheckConduct.md @@ -0,0 +1,28 @@ + +## Mindmap + +```mermaid +mindmap + root((CheckConduct)) + Таблица БД + check_conduct + Свойства + id + int + check_type_id + int + status + string + created_at + string + created_by + int + updated_by + int + Связи + Type + 1:1 CheckType + Наследование + extends yiidbActiveRecord +``` + diff --git a/erp24/docs/models/CheckConductItem.md b/erp24/docs/models/CheckConductItem.md new file mode 100644 index 00000000..9a02a120 --- /dev/null +++ b/erp24/docs/models/CheckConductItem.md @@ -0,0 +1,294 @@ +# Модель CheckConductItem + + +## Mindmap + +```mermaid +mindmap + root((CheckConductItem)) + Таблица БД + check_conduct_item + Свойства + id + int + check_conduct_id + int + check_criteria_id + int + score + int + Связи + Criteria + 1:1 CheckCriteria + AttachedFiles + 1:N Files + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CheckConductItem` представляет результат оценки по конкретному критерию в рамках проведённой проверки. Хранит балл, комментарий и прикреплённые файлы (фото/видео доказательства). Используется в системе чек-листов и аудита качества работы магазинов. + +**Файл модели:** `erp24/records/CheckConductItem.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `check_conduct_item` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `check_conduct_id` | INTEGER | ID проведённой проверки (FK → check_conduct.id) | +| `check_criteria_id` | INTEGER | ID критерия оценки (FK → check_criteria.id) | +| `score` | INTEGER | Оценка (балл) за критерий | +| `comment` | VARCHAR(255) | Комментарий к оценке | + +--- + +## Виртуальные атрибуты + +```php +public $files; // Загружаемые файлы (до 20 файлов) +``` + +--- + +## Описание полей + +### `check_conduct_id` — Проведённая проверка + +Ссылка на запись проверки (CheckConduct), в рамках которой выставлена оценка. + +### `check_criteria_id` — Критерий + +Ссылка на критерий оценки (CheckCriteria), по которому выставлен балл. + +### `score` — Балл + +Числовая оценка по критерию. Диапазон зависит от настроек критерия (обычно 0 — максимальный балл). + +### `comment` — Комментарий + +Текстовый комментарий проверяющего. Обычно содержит: +- Описание выявленных нарушений +- Рекомендации по улучшению +- Положительные моменты + +--- + +## Методы модели + +### `validateSaveAndManageImages(): bool` + +Валидирует, сохраняет запись и загружает прикреплённые файлы. + +**Логика работы:** +1. Валидация модели +2. Сохранение в БД +3. Загрузка каждого файла через FileService + +```php +$item = new CheckConductItem(); +$item->check_conduct_id = $conductId; +$item->check_criteria_id = $criteriaId; +$item->score = 8; +$item->comment = 'Чистота в норме, небольшие замечания'; +$item->files = UploadedFile::getInstances($item, 'files'); + +if ($item->validateSaveAndManageImages()) { + echo "Оценка сохранена с файлами"; +} +``` + +--- + +## Связи (Relations) + +### `getCriteria(): ActiveQuery` + +Возвращает критерий оценки. + +```php +$item = CheckConductItem::findOne($id); +$criteria = $item->criteria; // CheckCriteria +echo "Критерий: {$criteria->name}"; +``` + +### `getAttachedFiles(): ActiveQuery` + +Возвращает прикреплённые файлы. + +```php +$files = $item->attachedFiles; // Files[] +foreach ($files as $file) { + echo ""; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + check_conduct_item }o--|| check_conduct : "belongs_to" + check_conduct_item }o--|| check_criteria : "evaluates" + check_conduct_item ||--o{ files : "has_attachments" + + check_conduct_item { + int id PK + int check_conduct_id FK + int check_criteria_id FK + int score + string comment + } + + check_conduct { + int id PK + string name + } + + check_criteria { + int id PK + string name + int max_score + } + + files { + int id PK + string entity + int entity_id + string path + } +``` + +--- + +## Примеры использования + +### Создание оценки по критерию + +```php +$item = new CheckConductItem(); +$item->check_conduct_id = $checkConductId; +$item->check_criteria_id = $criteriaId; +$item->score = 9; // из 10 +$item->comment = 'Отличное состояние торгового зала'; +$item->save(); +``` + +### Получение всех оценок проверки + +```php +$items = CheckConductItem::find() + ->with(['criteria']) + ->where(['check_conduct_id' => $conductId]) + ->all(); + +$totalScore = 0; +$maxScore = 0; + +foreach ($items as $item) { + echo "{$item->criteria->name}: {$item->score}/{$item->criteria->max_score}\n"; + echo " Комментарий: {$item->comment}\n"; + + $totalScore += $item->score; + $maxScore += $item->criteria->max_score; +} + +$percentage = round($totalScore / $maxScore * 100, 1); +echo "Итого: {$totalScore}/{$maxScore} ({$percentage}%)"; +``` + +### Загрузка файлов-доказательств + +```php +// В контроллере +$item = new CheckConductItem(); +$item->load(Yii::$app->request->post()); +$item->files = UploadedFile::getInstances($item, 'files'); + +if ($item->validateSaveAndManageImages()) { + return $this->redirect(['view', 'id' => $item->check_conduct_id]); +} +``` + +### Статистика по критериям + +```php +// Средние баллы по критериям +$stats = CheckConductItem::find() + ->select(['check_criteria_id', 'AVG(score) as avg_score', 'COUNT(*) as count']) + ->groupBy('check_criteria_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $criteria = CheckCriteria::findOne($row['check_criteria_id']); + echo "{$criteria->name}: " . round($row['avg_score'], 1) . " (проверок: {$row['count']})\n"; +} +``` + +### Поиск проблемных оценок + +```php +// Оценки ниже 50% от максимума +$problematic = CheckConductItem::find() + ->alias('ci') + ->joinWith('criteria c') + ->where('ci.score < c.max_score * 0.5') + ->all(); + +foreach ($problematic as $item) { + echo "Критерий: {$item->criteria->name}\n"; + echo "Оценка: {$item->score}/{$item->criteria->max_score}\n"; + echo "Комментарий: {$item->comment}\n\n"; +} +``` + +### Получение файлов оценки + +```php +$item = CheckConductItem::findOne($id); +$files = $item->attachedFiles; + +if (count($files) > 0) { + echo "Прикреплённые файлы:\n"; + foreach ($files as $file) { + echo "- {$file->original_name}: {$file->path}\n"; + } +} else { + echo "Файлы не прикреплены"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `check_conduct_id` | Обязательное, целое число | +| `check_criteria_id` | Обязательное, целое число | +| `score` | Обязательное, целое число | +| `comment` | Строка, макс. 255 символов | +| `files` | Файлы, макс. 20 шт., необязательно | + +--- + +## Связанные модели + +- **[CheckConduct](./CheckConduct.md)** — проведённые проверки +- **[CheckCriteria](./CheckCriteria.md)** — критерии оценки +- **[CheckCriteriaItem](./CheckCriteriaItem.md)** — варианты оценок критерия +- **[Files](./Files.md)** — прикреплённые файлы +- **[CityStore](./CityStore.md)** — проверяемые магазины + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CheckCriteria.md b/erp24/docs/models/CheckCriteria.md new file mode 100644 index 00000000..1cedf3a3 --- /dev/null +++ b/erp24/docs/models/CheckCriteria.md @@ -0,0 +1,26 @@ + +## Mindmap + +```mermaid +mindmap + root((CheckCriteria)) + Таблица БД + check_criteria + Свойства + id + int + name + string + score + int + weight + int + check_type_id + int + Связи + Items + 1:N CheckCriteriaItem + Наследование + extends yiidbActiveRecord +``` + diff --git a/erp24/docs/models/CheckCriteriaItem.md b/erp24/docs/models/CheckCriteriaItem.md new file mode 100644 index 00000000..f30904d9 --- /dev/null +++ b/erp24/docs/models/CheckCriteriaItem.md @@ -0,0 +1,273 @@ +# Модель CheckCriteriaItem + + +## Mindmap + +```mermaid +mindmap + root((CheckCriteriaItem)) + Таблица БД + check_criteria_item + Свойства + id + int + name + string + score + int + image + int + video + int + doc + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CheckCriteriaItem` описывает конкретные варианты оценки для критерия проверки. Содержит описание критерия для каждого балла и требования к количеству прикрепляемых медиафайлов (фото, видео, документы). Используется для стандартизации оценивания и обеспечения доказательной базы. + +**Файл модели:** `erp24/records/CheckCriteriaItem.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `check_criteria_item` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(1000) | Описание критерия для данного балла | +| `score` | INTEGER | Балл, к которому относится описание | +| `image` | INTEGER | Необходимое количество изображений | +| `video` | INTEGER | Необходимое количество видео | +| `doc` | INTEGER | Необходимое количество документов | +| `check_criteria_id` | INTEGER | ID критерия (FK → check_criteria.id) | + +--- + +## Описание полей + +### `name` — Описание критерия + +Текстовое описание того, что означает данный балл. Например: +- Для балла 0: "Критические нарушения: товар отсутствует на витрине" +- Для балла 5: "Частичное соответствие: некоторые позиции отсутствуют" +- Для балла 10: "Полное соответствие: весь ассортимент представлен" + +### `score` — Балл + +Числовое значение оценки. Диапазон от 0 до максимального балла критерия. + +### Требования к медиафайлам + +| Поле | Назначение | Пример | +|------|------------|--------| +| `image` | Минимум фотографий | 2 фото витрины | +| `video` | Минимум видеозаписей | 1 видео обхода | +| `doc` | Минимум документов | 0 документов | + +Эти поля определяют, сколько доказательных материалов должен прикрепить проверяющий при выставлении данного балла. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + check_criteria_item }o--|| check_criteria : "belongs_to" + + check_criteria_item { + int id PK + string name + int score + int image + int video + int doc + int check_criteria_id FK + } + + check_criteria { + int id PK + string name + int max_score + } +``` + +--- + +## Примеры использования + +### Создание вариантов оценки для критерия + +```php +// Критерий "Чистота торгового зала" (макс. 10 баллов) +$criteria_id = $criteriaId; + +// Балл 0 - критическое нарушение +$item0 = new CheckCriteriaItem(); +$item0->check_criteria_id = $criteria_id; +$item0->score = 0; +$item0->name = 'Критическое загрязнение: мусор, грязь на полу и полках'; +$item0->image = 3; // 3 фото нарушений +$item0->video = 1; // 1 видео +$item0->doc = 0; +$item0->save(); + +// Балл 5 - частичное соответствие +$item5 = new CheckCriteriaItem(); +$item5->check_criteria_id = $criteria_id; +$item5->score = 5; +$item5->name = 'Незначительные замечания: пыль на верхних полках'; +$item5->image = 1; +$item5->video = 0; +$item5->doc = 0; +$item5->save(); + +// Балл 10 - полное соответствие +$item10 = new CheckCriteriaItem(); +$item10->check_criteria_id = $criteria_id; +$item10->score = 10; +$item10->name = 'Идеальная чистота: все поверхности чистые'; +$item10->image = 1; // 1 фото подтверждения +$item10->video = 0; +$item10->doc = 0; +$item10->save(); +``` + +### Получение всех вариантов оценки для критерия + +```php +$items = CheckCriteriaItem::find() + ->where(['check_criteria_id' => $criteriaId]) + ->orderBy(['score' => SORT_ASC]) + ->all(); + +foreach ($items as $item) { + echo "Балл {$item->score}: {$item->name}\n"; + echo " Требуется: {$item->image} фото, {$item->video} видео, {$item->doc} док.\n"; +} +``` + +### Получение описания для конкретного балла + +```php +$description = CheckCriteriaItem::findOne([ + 'check_criteria_id' => $criteriaId, + 'score' => $selectedScore +]); + +if ($description) { + echo "Выбранный балл {$selectedScore}:\n"; + echo "{$description->name}\n"; + echo "Необходимо прикрепить:\n"; + if ($description->image > 0) echo "- {$description->image} фотографий\n"; + if ($description->video > 0) echo "- {$description->video} видео\n"; + if ($description->doc > 0) echo "- {$description->doc} документов\n"; +} +``` + +### Проверка соответствия требованиям к файлам + +```php +$item = CheckCriteriaItem::findOne([ + 'check_criteria_id' => $criteriaId, + 'score' => $score +]); + +$attachedImages = count($uploadedImages); +$attachedVideos = count($uploadedVideos); +$attachedDocs = count($uploadedDocs); + +$errors = []; +if ($attachedImages < $item->image) { + $errors[] = "Требуется минимум {$item->image} фото, загружено {$attachedImages}"; +} +if ($attachedVideos < $item->video) { + $errors[] = "Требуется минимум {$item->video} видео, загружено {$attachedVideos}"; +} +if ($attachedDocs < $item->doc) { + $errors[] = "Требуется минимум {$item->doc} документов, загружено {$attachedDocs}"; +} + +if (empty($errors)) { + echo "Все требования выполнены"; +} else { + echo "Ошибки:\n" . implode("\n", $errors); +} +``` + +### Построение шкалы оценки для UI + +```php +$items = CheckCriteriaItem::find() + ->where(['check_criteria_id' => $criteriaId]) + ->orderBy(['score' => SORT_ASC]) + ->asArray() + ->all(); + +// Для Select2 или radio-группы +$scale = []; +foreach ($items as $item) { + $scale[$item['score']] = "{$item['score']} - {$item['name']}"; +} + +// $scale = [0 => '0 - Критическое...', 5 => '5 - Незначительные...', 10 => '10 - Идеальная...'] +``` + +### Статистика использования оценок + +```php +// Как часто выставляется каждый балл +$usage = Yii::$app->db->createCommand(" + SELECT cci.score, cci.name, COUNT(ci.id) as used_count + FROM check_criteria_item cci + LEFT JOIN check_conduct_item ci ON ci.check_criteria_id = cci.check_criteria_id + AND ci.score = cci.score + WHERE cci.check_criteria_id = :criteria_id + GROUP BY cci.score, cci.name + ORDER BY cci.score +", [':criteria_id' => $criteriaId])->queryAll(); + +foreach ($usage as $row) { + echo "Балл {$row['score']}: выставлен {$row['used_count']} раз\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 1000 символов | +| `score` | Обязательное, целое число | +| `check_criteria_id` | Обязательное, целое число | +| `image`, `video`, `doc` | Целые числа (по умолчанию 0) | + +--- + +## Бизнес-логика + +1. **Стандартизация оценок** — чёткие описания для каждого балла +2. **Доказательная база** — требования к количеству медиафайлов +3. **Обучение проверяющих** — описания помогают единообразно оценивать +4. **Аудит** — возможность проверить корректность выставленных оценок + +--- + +## Связанные модели + +- **[CheckCriteria](./CheckCriteria.md)** — критерии проверки +- **[CheckConductItem](./CheckConductItem.md)** — результаты оценки +- **[CheckConduct](./CheckConduct.md)** — проведённые проверки +- **[Files](./Files.md)** — прикреплённые файлы + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CheckGroup.md b/erp24/docs/models/CheckGroup.md new file mode 100644 index 00000000..f8f92e6e --- /dev/null +++ b/erp24/docs/models/CheckGroup.md @@ -0,0 +1,26 @@ + +## Mindmap + +```mermaid +mindmap + root((CheckGroup)) + Таблица БД + check_group + Свойства + id + int + name + string + created_at + string + created_by + int + Связи + CompanyFunction + 1:1 CompanyFunctions + CreatedBy + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + diff --git a/erp24/docs/models/CheckStatus.md b/erp24/docs/models/CheckStatus.md new file mode 100644 index 00000000..d94d9345 --- /dev/null +++ b/erp24/docs/models/CheckStatus.md @@ -0,0 +1,419 @@ +# ;0AA CheckStatus + + +## Mindmap + +```mermaid +mindmap + root((CheckStatus)) + Таблица БД + ActiveRecord + Наследование + extends ActiveRecord +``` + +## 07=0G5=85 + +;0AA `CheckStatus` O2;O5BAO A?@02>G=8:>< AB0BCA>2 4;O A8AB525@>: (0C48B0). -B> AB0B8G5A:89 :;0AA-M=C<5@0B>@, :>B>@K9 >?@545;O5B 2A5 2>7<>6=K5 AB0BCAK ?@>E>645=8O ?@>25@:8 >B A>740=8O G5@=>28:0 4> 0@E828@>20=8O. + +**$09; :;0AA0:** `erp24/records/CheckStatus.php` +**Namespace:** `yii_app\records` +**"8?:** !B0B8G5A:89 :;0AA-A?@02>G=8: (=5 ActiveRecord) + +--- + +## >=AB0=BK AB0BCA>2 + +```php +const DRAFT = "draft"; // '5@=>28: +const IN_WORK = "inwork"; //  @01>B5 +const WAITING = "waiting"; // 68405B ?>4B25@645=8O +const ACCEPTED = "accepted"; // @8=OB0 +const REJECTED = "rejected"; // B:;>=5=0 +const ARCHIVED = "archived"; //  0@E825 +``` + +--- + +## !;>20@L AB0BCA>2 + +### `LABELS` + +0AA82 A G5;>25:>G8B052 =0 @CAA:>< O7K:5. + +```php +const LABELS = [ + self::DRAFT => ''5@=>28:', + self::IN_WORK => ' @01>B5', + self::WAITING => '68405B ?>4B25@645=8O', + self::ACCEPTED => '@8=OB0', + self::REJECTED => 'B:;>=5=0', + self::ARCHIVED => ' @E825' +]; +``` + +--- + +## 5B>4K + +### `getStatusLabelByStatusName(string $statusName): string` + +>72@0I05B @CAA:>5 =0720=85 AB0BCA0 ?> 53> 845=B8D8:0B>@C. + +**0@0<5B@K:** +- `$statusName` (string) - 845=B8D8:0B>@ AB0BCA0 (>4=0 87 :>=AB0=B) + +**>72@0I05B:** AB@>:C A @CAA:8< =0720=85< AB0BCA0 + +**>38:0:** +@8=8<05B 845=B8D8:0B>@ AB0BCA0 (=0?@8<5@, "draft") 8 2>72@0I05B A>>B25BAB2CNI55 =0720=85 87 <0AA820 LABELS (=0?@8<5@, "'5@=>28:"). + +**@8<5@:** +```php +$label = CheckStatus::getStatusLabelByStatusName(CheckStatus::DRAFT); +echo $label; // "'5@=>28:" + +$label = CheckStatus::getStatusLabelByStatusName(CheckStatus::IN_WORK); +echo $label; // " @01>B5" + +$label = CheckStatus::getStatusLabelByStatusName(CheckStatus::ACCEPTED); +echo $label; // "@8=OB0" +``` + +--- + +## ?8A0=85 AB0BCA>2 + +### DRAFT ('5@=>28:) + +**=0G5=85:** `"draft"` +**5B:0:** "'5@=>28:" + +**?8A0=85:** +0G0;L=K9 AB0BCA ?@>25@:8. @>25@:0 B>;L:> A>740=0, => 5IQ =5 70?CI5=0 2 @01>BC.  MB>< AB0BCA5 <>6=> @540:B8@>20BL ?0@0<5B@K ?@>25@:8, :@8B5@88 >F5=:8 8 =07=0G0BL 8A?>;=8B5;59. + +**5@5E>4K:** +- IN_WORK (>B?@02:0 2 @01>BC) + +--- + +### IN_WORK ( @01>B5) + +**=0G5=85:** `"inwork"` +**5B:0:** " @01>B5" + +**?8A0=85:** +@>25@:0 =0E>48BAO 2 ?@>F5AA5 2K?>;=5=8O. A?>;=8B5;L 70?>;=O5B :@8B5@88 >F5=:8, A>18@05B 40==K5, ?@>2>48B 0C48B. + +**5@5E>4K:** +- WAITING (>B?@02:0 =0 ?>4B25@645=85) +- DRAFT (2>72@0B 2 G5@=>28:) + +--- + +### WAITING (68405B ?>4B25@645=8O) + +**=0G5=85:** `"waiting"` +**5B:0:** "68405B ?>4B25@645=8O" + +**?8A0=85:** +@>25@:0 2K?>;=5=0 8A?>;=8B5;5< 8 >B?@02;5=0 :>=B@>;;Q@C =0 @0AA<>B@5=85. 68405B @5H5=8O > ?@8=OB88 8;8 >B:;>=5=88. + +**5@5E>4K:** +- ACCEPTED (?>4B25@645=85) +- REJECTED (>B:;>=5=85) +- IN_WORK (2>72@0B =0 4>@01>B:C) + +--- + +### ACCEPTED (@8=OB0) + +**=0G5=85:** `"accepted"` +**5B:0:** "@8=OB0" + +**?8A0=85:** +@>25@:0 ?@8=OB0 :>=B@>;;Q@><, @57C;LB0BK CB25@645=K. @>25@:0 7025@H5=0 CA?5H=>. + +**5@5E>4K:** +- ARCHIVED (>B?@02:0 2 0@E82) + +--- + +### REJECTED (B:;>=5=0) + +**=0G5=85:** `"rejected"` +**5B:0:** "B:;>=5=0" + +**?8A0=85:** +@>25@:0 >B:;>=5=0 :>=B@>;;Q@><. 57C;LB0BK =5 ?@8=OBK, B@51C5BAO ?>2B>@=>5 2K?>;=5=85 8;8 4>@01>B:0. + +**5@5E>4K:** +- IN_WORK (2>72@0B =0 4>@01>B:C) +- ARCHIVED (>B?@02:0 2 0@E82 157 4>@01>B:8) + +--- + +### ARCHIVED ( 0@E825) + +**=0G5=85:** `"archived"` +**5B:0:** " @E825" + +**?8A0=85:** +@>25@:0 7025@H5=0 8 ?5@5<5I5=0 2 0@E82. $8=0;L=K9 AB0BCA, 87<5=5=8O =5 4>?CA:0NBAO. + +**5@5E>4K:** +5B (D8=0;L=K9 AB0BCA) + +--- + +## 803@0<<0 ?5@5E>4>2 AB0BCA>2 + +```mermaid +stateDiagram-v2 + [*] --> DRAFT: !>740=85 + DRAFT --> IN_WORK: B?@02:0 2 @01>BC + IN_WORK --> WAITING: B?@02:0 =0 ?@>25@:C + IN_WORK --> DRAFT: >72@0B 2 G5@=>28: + WAITING --> ACCEPTED: @8=OB85 + WAITING --> REJECTED: B:;>=5=85 + WAITING --> IN_WORK: >72@0B =0 4>@01>B:C + ACCEPTED --> ARCHIVED: @E828@>20=85 + REJECTED --> IN_WORK: >2B>@=>5 2K?>;=5=85 + REJECTED --> ARCHIVED: @E828@>20=85 157 4>@01>B:8 + ARCHIVED --> [*] +``` + +--- + +## @8<5@K 8A?>;L7>20=8O + +### >;CG5=85 =0720=8O AB0BCA0 + +```php +use yii_app\records\CheckStatus; + +// > :>=AB0=B5 +$statusName = CheckStatus::getStatusLabelByStatusName(CheckStatus::DRAFT); +echo $statusName; // "'5@=>28:" + +// > ?5@5<5==>9 +$currentStatus = "inwork"; +$statusName = CheckStatus::getStatusLabelByStatusName($currentStatus); +echo $statusName; // " @01>B5" +``` + +--- + +### A?>;L7>20=85 2 <>45;8 CheckConduct + +```php +$conduct = new CheckConduct(); +$conduct->status = CheckStatus::DRAFT; +$conduct->save(); + +// >;CG5=85 =0720=8O AB0BCA0 +echo CheckStatus::getStatusLabelByStatusName($conduct->status); +// "'5@=>28:" +``` + +--- + +### !>740=85 2K?040NI53> A?8A:0 AB0BCA>2 + +```php +use yii\helpers\Html; + +// A5 AB0BCAK 4;O 2K1>@0 +$statusList = CheckStatus::LABELS; + +echo Html::dropDownList('status', CheckStatus::DRAFT, $statusList, [ + 'class' => 'form-control' +]); + +// B>1@078B A?8A>:: +// - '5@=>28: +// -  @01>B5 +// - 68405B ?>4B25@645=8O +// - @8=OB0 +// - B:;>=5=0 +// -  @E825 +``` + +--- + +### @>25@:0 AB0BCA0 + +```php +$conduct = CheckConduct::findOne($id); + +if ($conduct->status === CheckStatus::DRAFT) { + echo "@>25@:0 2 G5@=>28:5, <>6=> @540:B8@>20BL"; +} + +if ($conduct->status === CheckStatus::WAITING) { + echo "@>25@:0 >68405B ?>4B25@645=8O"; +} + +if ($conduct->status === CheckStatus::ACCEPTED) { + echo "@>25@:0 ?@8=OB0"; +} +``` + +--- + +### 7<5=5=85 AB0BCA0 + +```php +$conduct = CheckConduct::findOne($id); + +// B?@02:0 2 @01>BC +if ($conduct->status === CheckStatus::DRAFT) { + $conduct->status = CheckStatus::IN_WORK; + $conduct->save(); +} + +// B?@02:0 =0 ?>4B25@645=85 +if ($conduct->status === CheckStatus::IN_WORK) { + $conduct->status = CheckStatus::WAITING; + $conduct->save(); +} + +// @8=OB85 ?@>25@:8 +if ($conduct->status === CheckStatus::WAITING) { + $conduct->status = CheckStatus::ACCEPTED; + $conduct->published_at = date('Y-m-d H:i:s'); + $conduct->save(); +} +``` + +--- + +### $8;LB@0F8O ?> AB0BCA0< + +```php +// A5 G5@=>28:8 +$drafts = CheckConduct::find() + ->where(['status' => CheckStatus::DRAFT]) + ->all(); + +// @>25@:8 2 @01>B5 +$inWork = CheckConduct::find() + ->where(['status' => CheckStatus::IN_WORK]) + ->all(); + +// 6840NI85 ?>4B25@645=8O +$waiting = CheckConduct::find() + ->where(['status' => CheckStatus::WAITING]) + ->all(); + +// @8=OBK5 8 >B:;>=Q==K5 +$finished = CheckConduct::find() + ->where(['IN', 'status', [CheckStatus::ACCEPTED, CheckStatus::REJECTED]]) + ->all(); + +// @E82=K5 +$archived = CheckConduct::find() + ->where(['status' => CheckStatus::ARCHIVED]) + ->all(); +``` + +--- + +### >;CG5=85 2A5E AB0BCA>2 + +```php +// 0AA82 2A5E AB0BCA>2 A <5B:0<8 +$allStatuses = CheckStatus::LABELS; + +foreach ($allStatuses as $code => $label) { + echo ">4: {$code}, 0720=85: {$label}\n"; +} + +// K2>4: +// >4: draft, 0720=85: '5@=>28: +// >4: inwork, 0720=85:  @01>B5 +// >4: waiting, 0720=85: 68405B ?>4B25@645=8O +// >4: accepted, 0720=85: @8=OB0 +// >4: rejected, 0720=85: B:;>=5=0 +// >4: archived, 0720=85:  @E825 +``` + +--- + +### @>25@:0 4>?CAB8<>AB8 ?5@5E>40 + +```php +function canTransitionTo($currentStatus, $newStatus) { + $allowedTransitions = [ + CheckStatus::DRAFT => [CheckStatus::IN_WORK], + CheckStatus::IN_WORK => [CheckStatus::WAITING, CheckStatus::DRAFT], + CheckStatus::WAITING => [CheckStatus::ACCEPTED, CheckStatus::REJECTED, CheckStatus::IN_WORK], + CheckStatus::ACCEPTED => [CheckStatus::ARCHIVED], + CheckStatus::REJECTED => [CheckStatus::IN_WORK, CheckStatus::ARCHIVED], + CheckStatus::ARCHIVED => [], + ]; + + return in_array($newStatus, $allowedTransitions[$currentStatus] ?? []); +} + +// A?>;L7>20=85 +$conduct = CheckConduct::findOne($id); +if (canTransitionTo($conduct->status, CheckStatus::ACCEPTED)) { + $conduct->status = CheckStatus::ACCEPTED; + $conduct->save(); +} else { + echo "54>?CAB84 AB0BCA0"; +} +``` + +--- + +## !2O70==K5 <>45;8 + +- **[CheckConduct](./CheckConduct.md)**  ?@>2545=85 ?@>25@>: (8A?>;L7C5B AB0BCAK) +- **[CheckType](./CheckType.md)**  B8?K ?@>25@>: +- **[CheckCriteria](./CheckCriteria.md)**  :@8B5@88 >F5=:8 ?@>25@>: + +--- + +## A>15==>AB8 + +1. **!B0B8G5A:89 :;0AA** - =5 O2;O5BAO ActiveRecord, =5 @01>B05B A  =0?@O;L7C5BAO :0: A?@02>G=8:** - >?@545;O5B 4>?CAB82 +3. **>=AB0=BK 2<5AB> enum** - PHP =5 8<55B 2AB@>5==KE enum (4> PHP 8.1) +4. **&5=B@0;87>20==>5 C?@02;5=85** - 2A5 AB0BCAK 2 >4=>< <5AB5 +5. ** CAA:85 =0720=8O** - G5@57 <0AA82 LABELS 4;O 8=B5@D59A0 + +--- + +## 5:><5=40F88 8A?>;L7>20=8O + +1. **A5340 8A?>;L7C9B5 :>=AB0=BK** 2<5AB> AB@>:: + ```php + // @028;L=> + $conduct->status = CheckStatus::DRAFT; + + // 5?@028;L=> + $conduct->status = "draft"; + ``` + +2. **@>25@O9B5 AB0BCA G5@57 :>=AB0=BK**: + ```php + // @028;L=> + if ($conduct->status === CheckStatus::ACCEPTED) + + // 5?@028;L=> + if ($conduct->status === "accepted") + ``` + +3. **A?>;L7C9B5 getStatusLabelByStatusName()** 4;O >B>1@065=8O: + ```php + echo CheckStatus::getStatusLabelByStatusName($conduct->status); + ``` + +4. **>=B@>;8@C9B5 ?5@5E>4K** <564C AB0BCA0<8 2 187=5A-;>38:5 + +--- + +**5@A8O:** 1.0 +**0B0:** 2025-12-11 diff --git a/erp24/docs/models/CheckType.md b/erp24/docs/models/CheckType.md new file mode 100644 index 00000000..8eb130f1 --- /dev/null +++ b/erp24/docs/models/CheckType.md @@ -0,0 +1,22 @@ + +## Mindmap + +```mermaid +mindmap + root((CheckType)) + Таблица БД + check_type + Свойства + id + int + name + string + Связи + CompanyFunction + 1:1 CompanyFunctions + CheckGroup + 1:1 CheckGroup + Наследование + extends yiidbActiveRecord +``` + diff --git a/erp24/docs/models/City.md b/erp24/docs/models/City.md new file mode 100644 index 00000000..a98537fc --- /dev/null +++ b/erp24/docs/models/City.md @@ -0,0 +1,483 @@ +# Class: City + + +## Mindmap + +```mermaid +mindmap + root((City)) + Таблица БД + city + Свойства + id_city + int + id_region + int + id_country + int + oid + int + city_name_ru_sklon + string + city_name_en + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `City` представляет сущность города в системе ERP24. Она управляет справочником городов, включая географическую информацию, SEO-параметры, режим работы офисов и складов. Модель используется для организации территориальной структуры сети магазинов, настройки региональных параметров и формирования локализованного контента. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`city` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id_city` | int | да | Уникальный идентификатор города | +| `id_region` | int | да | ID региона из справочника | +| `id_country` | int | да | ID страны из справочника | +| `oid` | int | да | Внешний идентификатор (OID) | +| `city_name_ru` | string(255) | нет | Название города на русском языке | +| `city_name_ru_sklon` | text | да | Название города в родительном падеже (например, "Москвы") | +| `city_name_en` | string(255) | да | Название города на английском языке | +| `city_url` | string(255) | да | URL-часть для формирования ссылок | +| `h1` | string(255) | да | Шаблон H1 заголовка для SEO | +| `seo_title` | text | да | SEO заголовок страницы города | +| `seo_description_ru` | text | нет | SEO описание на русском языке | +| `seo_description_eng` | text | да | SEO описание на английском языке | +| `seo_content` | text | да | SEO контент страницы города | +| `region_name` | string(255) | да | Название региона | +| `dop` | text | да | Дополнительная информация | +| `visible` | text | да | Флаг видимости города (1=виден, 0=скрыт) | +| `generate` | text | нет | Флаг автогенерации контента | +| `main` | string(1) | да | Признак главного города (1=да, 0=нет) | +| `naselenie` | float | да | Население города (в тысячах человек) | +| `gps_center` | string(35) | да | GPS координаты центра города (lat,lon) | +| `org_id` | int | да | ID фирмы по умолчанию для города | +| `work_time` | text | да | Режим работы офиса в городе | +| `work_time_sklad` | text | да | Режим работы склада в городе | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'city' + +**Пример:** +```php +$tableName = City::tableName(); // 'city' +``` + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. Устанавливает требования к заполнению полей, типам данных и ограничениям. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Проверяет обязательность всех основных полей кроме `city_name_ru`, `seo_description_ru` и `generate` +- Валидирует целочисленные поля: `id_region`, `id_country`, `oid`, `org_id` +- Валидирует текстовые поля с различными ограничениями длины +- Проверяет числовое значение поля `naselenie` +- Ограничивает длину строковых полей (255 символов для названий, 35 для GPS, 1 для `main`) + +**Правила валидации:** +- `required` - обязательные поля: id_region, id_country, oid, city_name_ru_sklon, city_name_en, city_url, h1, seo_title, seo_description_eng, seo_content, region_name, naselenie, gps_center, org_id, work_time, work_time_sklad +- `integer` - целочисленные: id_region, id_country, oid, org_id +- `string` - текстовые с ограничением длины и без +- `number` - числовое: naselenie + +**Пример:** +```php +$city = new City(); +$city->city_name_ru = 'Москва'; +$city->validate(); // проверка правил +if ($city->hasErrors()) { + print_r($city->getErrors()); +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает человекочитаемые метки (labels) для атрибутов модели. Используется в формах и представлениях для отображения названий полей. + +**Возвращает:** `array` - ассоциативный массив вида [атрибут => метка] + +**Логика работы:** +- Формирует массив с английскими метками для всех атрибутов модели +- Метки используются автоматически в генераторах форм Yii2 +- Позволяет изменить отображаемые названия полей без изменения структуры БД + +**Пример:** +```php +$city = new City(); +$labels = $city->attributeLabels(); +echo $labels['city_name_ru']; // 'City Name Ru' + +// Использование в форме +echo $form->field($city, 'city_name_ru')->textInput(); +// Автоматически отобразит метку "City Name Ru" +``` + +--- + +## Связи (Relations) + +На данный момент в модели не определены явные связи через методы `hasOne` или `hasMany`, однако модель связана с другими сущностями через внешние ключи: + +### Внешние связи (неявные) +- **Регион** - через `id_region` связь со справочником регионов +- **Страна** - через `id_country` связь со справочником стран +- **Организация** - через `org_id` связь с таблицей организаций/фирм + +### Обратные связи +- **CityStore** - магазины используют `city_id` для связи с городом +- **Admin** - сотрудники могут быть привязаны к городам +- **Orders** - заказы содержат информацию о городе доставки + +--- + +## Примеры использования + +### 1. Создание нового города +```php +$city = new City(); +$city->id_region = 77; +$city->id_country = 1; +$city->oid = 12345; +$city->city_name_ru = 'Москва'; +$city->city_name_ru_sklon = 'Москвы'; +$city->city_name_en = 'Moscow'; +$city->city_url = 'moskva'; +$city->h1 = 'Доставка цветов в Москве'; +$city->seo_title = 'Доставка цветов в Москве - круглосуточно'; +$city->seo_description_ru = 'Быстрая доставка цветов в Москве'; +$city->seo_description_eng = 'Fast flower delivery in Moscow'; +$city->seo_content = 'Подробное описание услуг доставки...'; +$city->region_name = 'Московская область'; +$city->dop = 'Столица России'; +$city->visible = '1'; +$city->generate = '1'; +$city->main = '1'; +$city->naselenie = 12692.0; // 12.692 млн человек +$city->gps_center = '55.7558,37.6173'; +$city->org_id = 1; +$city->work_time = 'Пн-Вс: 09:00-21:00'; +$city->work_time_sklad = 'Пн-Пт: 08:00-18:00'; + +if ($city->save()) { + echo "Город создан с ID: " . $city->id_city; +} else { + print_r($city->getErrors()); +} +``` + +### 2. Получение списка видимых городов +```php +$cities = City::find() + ->where(['visible' => '1']) + ->orderBy(['naselenie' => SORT_DESC]) + ->all(); + +foreach ($cities as $city) { + echo "{$city->city_name_ru} - {$city->naselenie} тыс. чел.\n"; +} +``` + +### 3. Поиск города по URL +```php +$cityUrl = 'moskva'; +$city = City::find() + ->where(['city_url' => $cityUrl, 'visible' => '1']) + ->one(); + +if ($city) { + echo "SEO Title: {$city->seo_title}\n"; + echo "H1: {$city->h1}\n"; + echo "GPS: {$city->gps_center}\n"; +} +``` + +### 4. Получение главных городов +```php +$mainCities = City::find() + ->where(['main' => '1', 'visible' => '1']) + ->orderBy(['naselenie' => SORT_DESC]) + ->all(); + +foreach ($mainCities as $city) { + echo "Главный город: {$city->city_name_ru}\n"; + echo "Офис работает: {$city->work_time}\n"; + echo "Склад работает: {$city->work_time_sklad}\n"; +} +``` + +### 5. Формирование dropdown списка городов +```php +use yii\helpers\ArrayHelper; + +$citiesList = City::find() + ->where(['visible' => '1']) + ->orderBy(['city_name_ru' => SORT_ASC]) + ->all(); + +$citiesDropdown = ArrayHelper::map($citiesList, 'id_city', 'city_name_ru'); + +// Использование в форме +echo Html::dropDownList('city_id', null, $citiesDropdown, [ + 'prompt' => 'Выберите город', + 'class' => 'form-control' +]); +``` + +### 6. Работа с GPS координатами +```php +$city = City::findOne(['city_url' => 'moskva']); + +if ($city && $city->gps_center) { + [$lat, $lon] = explode(',', $city->gps_center); + + echo "Широта: {$lat}\n"; + echo "Долгота: {$lon}\n"; + + // Формирование ссылки на карту + $mapUrl = "https://maps.google.com/?q={$lat},{$lon}"; + echo "Карта: {$mapUrl}\n"; +} +``` + +### 7. Обновление режима работы +```php +$city = City::findOne(1); + +if ($city) { + $city->work_time = 'Пн-Вс: 08:00-22:00'; + $city->work_time_sklad = 'Пн-Сб: 07:00-19:00, Вс: выходной'; + + if ($city->save()) { + echo "Режим работы обновлён"; + } +} +``` + +### 8. Поиск городов по региону +```php +$regionId = 77; // Москва и МО + +$cities = City::find() + ->where(['id_region' => $regionId, 'visible' => '1']) + ->orderBy(['city_name_ru' => SORT_ASC]) + ->all(); + +foreach ($cities as $city) { + echo "{$city->city_name_ru} ({$city->city_name_ru_sklon})\n"; +} +``` + +### 9. Формирование SEO-контента для города +```php +$city = City::findOne(['city_url' => 'spb']); + +if ($city) { + // Использование склонения названия в тексте + $content = "Доставка цветов в городе {$city->city_name_ru_sklon}"; + + // Формирование meta-тегов + $metaTags = [ + 'title' => $city->seo_title, + 'description' => $city->seo_description_ru, + 'h1' => $city->h1, + ]; + + print_r($metaTags); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + CITY ||--o{ CITY_STORE : "has stores" + CITY ||--o| REGION : "belongs to" + CITY ||--o| COUNTRY : "belongs to" + CITY ||--o| ORGANIZATION : "has default org" + CITY ||--o{ ORDERS : "has orders" + + CITY { + int id_city PK + int id_region FK + int id_country FK + int oid "Внешний ID" + string city_name_ru "Название РУ" + text city_name_ru_sklon "Склонение" + string city_name_en "Название EN" + string city_url "URL города" + string h1 "SEO H1" + text seo_title "SEO Title" + text seo_description_ru "SEO Desc RU" + text seo_description_eng "SEO Desc EN" + text seo_content "SEO Content" + string region_name "Регион" + text dop "Доп. инфо" + text visible "Видимость" + text generate "Автогенерация" + string main "Главный город" + float naselenie "Население" + string gps_center "GPS центра" + int org_id FK "Фирма по умолчанию" + text work_time "Режим офиса" + text work_time_sklad "Режим склада" + } + + CITY_STORE { + int id PK + int city_id FK + string name + } + + REGION { + int id PK + string name + } + + COUNTRY { + int id PK + string name + } + + ORGANIZATION { + int id PK + string name + } + + ORDERS { + int id PK + int city_id FK + } +``` + +--- + +## Особенности реализации + +### Склонение названий +Модель содержит специальное поле `city_name_ru_sklon` для хранения названия города в родительном падеже. Это позволяет корректно формировать фразы типа "Доставка цветов в Москвы" → "Доставка цветов в Москве" через правильное склонение. + +### SEO-оптимизация +Модель содержит полный набор SEO-полей: +- `h1` - заголовок первого уровня +- `seo_title` - title страницы +- `seo_description_ru/eng` - description на двух языках +- `seo_content` - контент страницы города +- `city_url` - ЧПУ для формирования красивых ссылок + +### GPS координаты +GPS координаты центра города хранятся в формате "latitude,longitude" (например, "55.7558,37.6173"). Это позволяет: +- Отображать город на картах +- Рассчитывать расстояния до адресов доставки +- Определять зоны обслуживания + +### Режимы работы +Модель хранит два отдельных режима работы: +- `work_time` - режим работы офиса/магазина +- `work_time_sklad` - режим работы склада + +Это позволяет информировать клиентов о времени приёма заказов и времени работы логистики. + +### Мультиязычность +Поддержка названий на русском и английском языках, а также отдельных SEO-описаний для разных языковых версий сайта. + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Территориальная структура** - организация сети магазинов по городам +2. **SEO и маркетинг** - формирование посадочных страниц для городов +3. **Логистика** - определение зон доставки и режимов работы +4. **Аналитика** - анализ продаж по городам и регионам +5. **CRM** - сегментация клиентов по географии + +### Ключевые сценарии + +- Выбор города на сайте для определения ассортимента и цен +- Расчёт стоимости и времени доставки +- Формирование локализованного контента +- Управление региональными акциями +- Планирование открытия новых точек + +--- + +## Связанные компоненты + +- **Модели:** CityStore, Region, Country, Organization, Orders, Admin +- **Контроллеры:** CityController, StoreController +- **API:** api1, api2 (используют данные городов для формирования ответов) +- **Виджеты:** CitySelector, MapWidget + +--- + +## Примечания + +**Рекомендации по улучшению:** + +1. Добавить методы связей `getRegion()`, `getCountry()`, `getOrganization()` для упрощения работы +2. Создать геттеры для GPS координат (getLat(), getLon()) аналогично CityStore +3. Добавить валидацию формата GPS координат +4. Вынести текстовые поля (visible, generate, main) в целочисленные для оптимизации +5. Создать константы для значений поля `main` и `visible` +6. Добавить scope-методы для частых запросов (visible, main cities и т.д.) + +**Потенциальные улучшения:** + +```php +// Пример добавления констант +class City extends \yii\db\ActiveRecord +{ + const IS_VISIBLE = '1'; + const IS_MAIN = '1'; + + // Scope для видимых городов + public static function findVisible() + { + return static::find()->where(['visible' => self::IS_VISIBLE]); + } + + // Scope для главных городов + public static function findMain() + { + return static::find()->where(['main' => self::IS_MAIN, 'visible' => self::IS_VISIBLE]); + } + + // Геттеры для GPS + public function getLat() + { + if ($this->gps_center) { + return (float) explode(',', $this->gps_center)[0]; + } + return null; + } + + public function getLon() + { + if ($this->gps_center) { + return (float) explode(',', $this->gps_center)[1]; + } + return null; + } +} +``` diff --git a/erp24/docs/models/CityStore.md b/erp24/docs/models/CityStore.md index a40a592c..2c3b1c8b 100644 --- a/erp24/docs/models/CityStore.md +++ b/erp24/docs/models/CityStore.md @@ -1,5 +1,37 @@ # Class: CityStore + +## Mindmap + +```mermaid +mindmap + root((CityStore)) + Таблица БД + city_store + Свойства + id + int + f_id + int + firma_id + int + name + string + name_full + string + city_id + int + Связи + StoreGuid + 1:1 ExportImportTable + City + 1:1 City + Administrator + 1:1 Admin + Наследование + extends ActiveRecord +``` + ## Назначение Модель `CityStore` представляет сущность магазина/торговой точки в системе ERP24. Она управляет информацией о физических магазинах сети, включая их местоположение, контактные данные, интеграции с картами, SEO-параметры и операционные характеристики. Модель используется для координации работы магазинов, планирования, учета продаж и коммуникации. diff --git a/erp24/docs/models/CityStoreParams.md b/erp24/docs/models/CityStoreParams.md new file mode 100644 index 00000000..7bb97725 --- /dev/null +++ b/erp24/docs/models/CityStoreParams.md @@ -0,0 +1,711 @@ +# Class: CityStoreParams + + +## Mindmap + +```mermaid +mindmap + root((CityStoreParams)) + Таблица БД + {{%city_store_params}} + Свойства + id + int + store_id + int + created_by + int + created_at + string + Связи + Store + 1:1 CityStore + UpdatedBy + 1:1 Admin + CreatedBy + 1:1 Admin + TerritorialManager + 1:1 Admin + BushChefFlorist + 1:1 Admin + Наследование + extends ActiveRecord +``` + +## Назначение +Модель `CityStoreParams` представляет расширенные параметры магазина в системе ERP24. Она хранит дополнительную информацию о характеристиках торговой точки: тип магазина, географическое положение, площадные и объёмные характеристики, тип матрицы ассортимента, а также информацию об ответственных руководителях. Модель используется для управления параметрами магазинов, планирования ассортимента и аналитики. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`city_store_params` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи | +| `store_id` | int | да | ID магазина (связь с city_store) | +| `store_type` | int | нет | Тип магазина (связь со справочником StoreType) | +| `address_city` | text | нет | Город расположения магазина (связь со StoreCityList) | +| `address_region` | text | нет | Регион расположения магазина (связь со StoreCityList) | +| `address_district` | text | нет | Район расположения магазина (связь со StoreCityList) | +| `store_area` | float | нет | Площадь магазина в кв.м | +| `showcase_volume` | float | нет | Объём витрины в куб.м | +| `freeze_area` | float | нет | Площадь холодильника в кв.м | +| `freeze_volume` | float | нет | Объём холодильника в куб.м | +| `matrix_type` | text | нет | Тип матрицы ассортимента (связь с MatrixType) | +| `created_by` | int | да | ID пользователя, создавшего запись (автозаполнение) | +| `created_at` | timestamp | да | Дата и время создания записи (автозаполнение) | +| `updated_by` | int | нет | ID пользователя, обновившего запись (автозаполнение) | +| `updated_at` | timestamp | нет | Дата и время последнего обновления (автозаполнение) | + +## Виртуальные свойства + +| Имя | Тип | Источник | Описание | +|-----|-----|----------|----------| +| `bush_chef_florist` | int | публичное свойство | ID кустового шеф-флориста (используется в формах) | +| `territorial_manager` | int | публичное свойство | ID территориального управляющего (используется в формах) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных с поддержкой префиксов Yii2. + +**Возвращает:** `string` - '{{%city_store_params}}' + +**Логика работы:** +- Использует нотацию `{{%...}}` для автоматической подстановки префикса таблицы из конфигурации +- Позволяет использовать разные префиксы в разных окружениях + +**Пример:** +```php +$tableName = CityStoreParams::tableName(); // '{{%city_store_params}}' +// В реальном SQL будет преобразовано в: 'erp24.city_store_params' или 'city_store_params' +``` + +--- + +### `behaviors()` +**Описание:** Определяет поведения (behaviors) модели для автоматизации работы со временными метками и авторством записей. + +**Возвращает:** `array` - массив конфигураций поведений + +**Логика работы:** +- **TimestampBehavior** - автоматически заполняет поля `created_at` и `updated_at` текущим временем при создании и обновлении записи +- **BlameableBehavior** - автоматически заполняет поля `created_by` и `updated_by` ID текущего пользователя + +**Используемые поведения:** +1. `TimestampBehavior` - управление временными метками + - `createdAtAttribute` = 'created_at' - поле даты создания + - `updatedAtAttribute` = 'updated_at' - поле даты обновления + - `value` = `NOW()` - SQL функция для получения текущего времени + +2. `BlameableBehavior` - управление авторством + - `createdByAttribute` = 'created_by' - поле автора создания + - `updatedByAttribute` = 'updated_by' - поле автора обновления + - Автоматически берёт ID из `Yii::$app->user->id` + +**Пример:** +```php +$params = new CityStoreParams(); +$params->store_id = 1; +$params->save(); + +// Автоматически заполнятся: +// $params->created_by = Yii::$app->user->id; +// $params->created_at = '2024-01-15 10:30:00'; +``` + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Проверяет обязательность поля `store_id` +- Валидирует целочисленные поля: store_id, store_type, territorial_manager, bush_chef_florist, created_by, updated_by +- Валидирует числовые поля с плавающей точкой: store_area, showcase_volume, freeze_area, freeze_volume +- Проверяет формат даты для created_at и updated_at +- Допускает текстовые значения для полей адреса и типа матрицы + +**Правила валидации:** +- `required` - обязательное поле: store_id +- `integer` - целочисленные поля для ID +- `number` - числовые поля для площадей и объёмов +- `safe` - безопасные поля для дат (обрабатываются поведениями) +- `string` - текстовые поля + +**Пример:** +```php +$params = new CityStoreParams(); +$params->store_id = 'abc'; // ошибка - должно быть число + +if (!$params->validate()) { + print_r($params->getErrors()); + // ['store_id' => ['Store ID must be an integer.']] +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает человекочитаемые метки (labels) для атрибутов модели на русском языке. + +**Возвращает:** `array` - ассоциативный массив вида [атрибут => метка] + +**Логика работы:** +- Предоставляет русские названия для всех полей модели +- Используется автоматически в формах и сообщениях об ошибках +- Помогает локализовать интерфейс без изменения кода представлений + +**Пример:** +```php +$params = new CityStoreParams(); +echo $params->getAttributeLabel('store_area'); // 'Площадь магазина' + +// В форме автоматически: +echo $form->field($params, 'store_area'); +// Отобразит метку "Площадь магазина" +``` + +--- + +## Связи (Relations) + +### `getStore()` +**Тип связи:** hasOne + +**Связанная модель:** `CityStore` + +**Условие:** `['id' => 'store_id']` + +**Описание:** Получает информацию о магазине, к которому относятся параметры. + +**Логика работы:** +- Создаёт связь один-к-одному с моделью CityStore +- Соединяет по полю `store_id` текущей модели с полем `id` модели CityStore +- Позволяет получить полную информацию о магазине через связь + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +$storeName = $params->store->name; +$storeAddress = $params->store->adress; +``` + +--- + +### `getUpdatedBy()` +**Тип связи:** hasOne + +**Связанная модель:** `Admin` + +**Условие:** `['id' => 'updated_by']` + +**Описание:** Получает информацию о пользователе, который последним обновлял запись. + +**Логика работы:** +- Связывает поле `updated_by` с ID администратора +- Возвращает объект Admin или null, если запись не обновлялась + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->updatedBy) { + echo "Обновил: {$params->updatedBy->name_full}"; +} +``` + +--- + +### `getCreatedBy()` +**Тип связи:** hasOne + +**Связанная модель:** `Admin` + +**Условие:** `['id' => 'created_by']` + +**Описание:** Получает информацию о пользователе, создавшем запись. + +**Логика работы:** +- Связывает поле `created_by` с ID администратора +- Возвращает объект Admin с данными создателя записи + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +echo "Создал: {$params->createdBy->name_full} ({$params->created_at})"; +``` + +--- + +### `getTerritorialManager()` +**Тип связи:** hasOne + +**Связанная модель:** `Admin` + +**Условие:** `['id' => 'territorial_manager']` + +**Описание:** Получает информацию о территориальном управляющем, ответственном за магазин. + +**Логика работы:** +- Связывает виртуальное поле `territorial_manager` с таблицей Admin +- Возвращает данные руководителя территориального уровня + +**Примечание:** Поле `territorial_manager` не хранится в БД, используется только в формах. + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->territorialManager) { + echo "Территориальный управляющий: {$params->territorialManager->name_full}"; +} +``` + +--- + +### `getBushChefFlorist()` +**Тип связи:** hasOne + +**Связанная модель:** `Admin` + +**Условие:** `['id' => 'bush_chef_florist']` + +**Описание:** Получает информацию о кустовом шеф-флористе, ответственном за магазин. + +**Логика работы:** +- Связывает виртуальное поле `bush_chef_florist` с таблицей Admin +- Возвращает данные шеф-флориста, курирующего несколько магазинов (куст) + +**Примечание:** Поле `bush_chef_florist` не хранится в БД, используется только в формах. + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->bushChefFlorist) { + echo "Кустовой шеф-флорист: {$params->bushChefFlorist->name_full}"; +} +``` + +--- + +### `getMatrixType()` +**Тип связи:** hasOne + +**Связанная модель:** `MatrixType` + +**Условие:** `['id' => 'matrix_type']` + +**Описание:** Получает информацию о типе матрицы ассортимента магазина. + +**Логика работы:** +- Связывает поле `matrix_type` со справочником типов матриц +- Возвращает данные о типе ассортиментной матрицы (базовая, расширенная, премиум и т.д.) + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->matrixType) { + echo "Тип матрицы: {$params->matrixType->name}"; +} +``` + +--- + +### `getStoreType()` +**Тип связи:** hasOne + +**Связанная модель:** `StoreType` + +**Условие:** `['id' => 'store_type']` + +**Описание:** Получает информацию о типе магазина из справочника. + +**Логика работы:** +- Связывает поле `store_type` со справочником типов магазинов +- Возвращает данные о типе торговой точки (павильон, магазин, киоск и т.д.) + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->storeType) { + echo "Тип магазина: {$params->storeType->name}"; +} +``` + +--- + +### `getAddressRegion()` +**Тип связи:** hasOne + +**Связанная модель:** `StoreCityList` + +**Условие:** `['id' => 'address_region']` + +**Описание:** Получает информацию о регионе расположения магазина из справочника. + +**Логика работы:** +- Связывает поле `address_region` со справочником StoreCityList +- Возвращает данные о регионе (область, край, республика) + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->addressRegion) { + echo "Регион: {$params->addressRegion->name}"; +} +``` + +--- + +### `getAddressDistrict()` +**Тип связи:** hasOne + +**Связанная модель:** `StoreCityList` + +**Условие:** `['id' => 'address_district']` + +**Описание:** Получает информацию о районе расположения магазина из справочника. + +**Логика работы:** +- Связывает поле `address_district` со справочником StoreCityList +- Возвращает данные о районе города или области + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->addressDistrict) { + echo "Район: {$params->addressDistrict->name}"; +} +``` + +--- + +### `getAddressCity()` +**Тип связи:** hasOne + +**Связанная модель:** `StoreCityList` + +**Условие:** `['id' => 'address_city']` + +**Описание:** Получает информацию о городе расположения магазина из справочника. + +**Логика работы:** +- Связывает поле `address_city` со справочником StoreCityList +- Возвращает данные о городе/населённом пункте + +**Пример:** +```php +$params = CityStoreParams::findOne(1); +if ($params->addressCity) { + echo "Город: {$params->addressCity->name}"; +} +``` + +--- + +## Примеры использования + +### 1. Создание параметров нового магазина +```php +$params = new CityStoreParams(); +$params->store_id = 15; +$params->store_type = 2; // Магазин формата "У дома" +$params->address_city = 77; // Москва +$params->address_region = 1; // Московская область +$params->address_district = 10; // Центральный район +$params->store_area = 85.5; // 85.5 кв.м +$params->showcase_volume = 12.3; // 12.3 куб.м +$params->freeze_area = 8.0; // 8 кв.м +$params->freeze_volume = 4.5; // 4.5 куб.м +$params->matrix_type = 3; // Базовая матрица + +// created_by, created_at заполнятся автоматически через behaviors + +if ($params->save()) { + echo "Параметры магазина созданы с ID: {$params->id}"; +} else { + print_r($params->getErrors()); +} +``` + +### 2. Получение параметров магазина с загрузкой связей +```php +$params = CityStoreParams::find() + ->where(['store_id' => 15]) + ->with([ + 'store', + 'storeType', + 'matrixType', + 'addressCity', + 'addressRegion', + 'addressDistrict', + 'createdBy', + 'updatedBy' + ]) + ->one(); + +if ($params) { + echo "Магазин: {$params->store->name}\n"; + echo "Тип: {$params->storeType->name}\n"; + echo "Площадь: {$params->store_area} кв.м\n"; + echo "Объём витрины: {$params->showcase_volume} куб.м\n"; + echo "Матрица: {$params->matrixType->name}\n"; + echo "Адрес: {$params->addressRegion->name}, {$params->addressCity->name}, {$params->addressDistrict->name}\n"; + echo "Создал: {$params->createdBy->name_full} ({$params->created_at})\n"; +} +``` + +### 3. Обновление параметров магазина +```php +$params = CityStoreParams::find()->where(['store_id' => 15])->one(); + +if ($params) { + $params->store_area = 95.0; // Увеличили площадь + $params->freeze_volume = 6.0; // Увеличили объём холодильника + $params->matrix_type = 4; // Перешли на расширенную матрицу + + // updated_by, updated_at заполнятся автоматически + + if ($params->save()) { + echo "Параметры обновлены\n"; + echo "Обновил: {$params->updatedBy->name_full} ({$params->updated_at})\n"; + } +} +``` + +### 4. Поиск магазинов по типу +```php +$storeTypeId = 2; // Магазины формата "У дома" + +$paramsList = CityStoreParams::find() + ->joinWith('store') + ->where(['store_type' => $storeTypeId]) + ->orderBy(['store_area' => SORT_DESC]) + ->all(); + +foreach ($paramsList as $params) { + echo "{$params->store->name}: {$params->store_area} кв.м\n"; +} +``` + +### 5. Расчёт средней площади магазинов по регионам +```php +use yii\db\Query; + +$stats = (new Query()) + ->select([ + 'address_region', + 'COUNT(*) as stores_count', + 'AVG(store_area) as avg_area', + 'AVG(showcase_volume) as avg_showcase', + 'AVG(freeze_volume) as avg_freeze' + ]) + ->from(CityStoreParams::tableName()) + ->groupBy('address_region') + ->orderBy(['avg_area' => SORT_DESC]) + ->all(); + +foreach ($stats as $stat) { + $region = StoreCityList::findOne($stat['address_region']); + echo "{$region->name}:\n"; + echo " Магазинов: {$stat['stores_count']}\n"; + echo " Средняя площадь: " . round($stat['avg_area'], 2) . " кв.м\n"; + echo " Средний объём витрины: " . round($stat['avg_showcase'], 2) . " куб.м\n"; +} +``` + +### 6. Проверка наличия параметров у магазина +```php +$storeId = 15; + +$hasParams = CityStoreParams::find() + ->where(['store_id' => $storeId]) + ->exists(); + +if (!$hasParams) { + echo "У магазина ID={$storeId} отсутствуют параметры. Необходимо создать."; + + // Создаём параметры с минимальными данными + $params = new CityStoreParams(); + $params->store_id = $storeId; + $params->save(); +} +``` + +### 7. Получение магазинов с большими площадями +```php +$minArea = 100; // кв.м + +$largeStores = CityStoreParams::find() + ->joinWith('store') + ->where(['>=', 'store_area', $minArea]) + ->orderBy(['store_area' => SORT_DESC]) + ->all(); + +echo "Магазины с площадью от {$minArea} кв.м:\n"; +foreach ($largeStores as $params) { + echo "{$params->store->name}: {$params->store_area} кв.м"; + echo " (витрина: {$params->showcase_volume} куб.м)\n"; +} +``` + +### 8. Фильтрация по городу и типу матрицы +```php +$cityId = 77; // Москва +$matrixTypeId = 4; // Расширенная матрица + +$stores = CityStoreParams::find() + ->joinWith(['store', 'matrixType']) + ->where([ + 'address_city' => $cityId, + 'matrix_type' => $matrixTypeId + ]) + ->all(); + +foreach ($stores as $params) { + echo "{$params->store->name} - {$params->matrixType->name}\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + CITY_STORE_PARAMS ||--|| CITY_STORE : "belongs to" + CITY_STORE_PARAMS ||--o| STORE_TYPE : "has type" + CITY_STORE_PARAMS ||--o| MATRIX_TYPE : "has matrix" + CITY_STORE_PARAMS ||--o| STORE_CITY_LIST : "city" + CITY_STORE_PARAMS ||--o| STORE_CITY_LIST : "region" + CITY_STORE_PARAMS ||--o| STORE_CITY_LIST : "district" + CITY_STORE_PARAMS ||--|| ADMIN : "created by" + CITY_STORE_PARAMS ||--o| ADMIN : "updated by" + + CITY_STORE_PARAMS { + int id PK + int store_id FK "ID магазина" + int store_type FK "Тип магазина" + text address_city FK "Город" + text address_region FK "Регион" + text address_district FK "Район" + float store_area "Площадь магазина" + float showcase_volume "Объём витрины" + float freeze_area "Площадь холодильника" + float freeze_volume "Объём холодильника" + text matrix_type FK "Тип матрицы" + int created_by FK "Создал" + timestamp created_at "Дата создания" + int updated_by FK "Обновил" + timestamp updated_at "Дата обновления" + } + + CITY_STORE { + int id PK + string name "Название" + string adress "Адрес" + } + + STORE_TYPE { + int id PK + string name "Тип магазина" + } + + MATRIX_TYPE { + int id PK + string name "Тип матрицы" + } + + STORE_CITY_LIST { + int id PK + string name "Название" + string type "Тип (город/регион/район)" + } + + ADMIN { + int id PK + string name_full "ФИО" + string email "Email" + } +``` + +--- + +## Особенности реализации + +### Автоматическое управление метаданными +Модель использует два поведения для автоматизации: +- **TimestampBehavior** - автоматически проставляет дату и время создания/обновления записи +- **BlameableBehavior** - автоматически сохраняет ID пользователя, создавшего или обновившего запись + +Это освобождает разработчика от ручного заполнения этих полей и обеспечивает целостность данных аудита. + +### Виртуальные поля для руководителей +Поля `bush_chef_florist` и `territorial_manager` объявлены как публичные свойства, но не хранятся в базе данных. Это связано с тем, что данная информация может храниться в других таблицах или вычисляться динамически. Однако для них определены связи с моделью Admin для удобства работы в формах. + +### Гибкая адресация +Модель использует отдельные поля для города, региона и района, что позволяет: +- Строить многоуровневую иерархию адресов +- Фильтровать магазины по территориальному принципу +- Агрегировать данные по регионам + +### Физические характеристики +Хранение площадей и объёмов позволяет: +- Планировать ассортимент с учётом возможностей магазина +- Рассчитывать оптимальную загрузку торгового зала +- Оптимизировать поставки под вместимость холодильников +- Сравнивать эффективность магазинов разного размера + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Планирование ассортимента** - тип матрицы определяет набор товаров для магазина +2. **Логистика** - объём холодильника влияет на размер поставок +3. **Аналитика** - площадь магазина учитывается при анализе эффективности продаж +4. **Управление** - территориальные руководители контролируют группы магазинов +5. **Отчётность** - параметры используются для сегментации в отчётах + +### Ключевые метрики + +- **Эффективность на кв.м** - продажи делятся на площадь магазина +- **Оборачиваемость витрины** - объём продаж к объёму витрины +- **Использование холодильника** - соотношение запасов к вместимости + +--- + +## Связанные компоненты + +- **Модели:** CityStore, StoreType, MatrixType, StoreCityList, Admin +- **Сервисы:** StoreService, MatrixService +- **Контроллеры:** StoreParamsController, StoreController +- **Формы:** CityStoreParamsForm + +--- + +## Примечания + +**Рекомендации по использованию:** + +1. Всегда заполнять параметры при создании нового магазина +2. Обновлять площади при ремонте или расширении +3. Регулярно проверять актуальность типа матрицы +4. Использовать eager loading (`with()`) при массовых выборках + +**Потенциальные улучшения:** + +1. Добавить валидацию минимальных значений для площадей и объёмов +2. Создать метод для расчёта общей вместимости магазина +3. Добавить константы для типовых значений матриц +4. Реализовать историю изменений параметров +5. Добавить расчётные поля (эффективность на кв.м) diff --git a/erp24/docs/models/CityStoreSearch.md b/erp24/docs/models/CityStoreSearch.md new file mode 100644 index 00000000..9775cee2 --- /dev/null +++ b/erp24/docs/models/CityStoreSearch.md @@ -0,0 +1,214 @@ +# Класс: CityStoreSearch + +## Назначение +Search-модель для поиска и фильтрации магазинов в ERP24. Полнофункциональная модель с поддержкой всех атрибутов магазина: идентификаторы, адреса, SEO, изображения, настройки. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`CityStore` + +## Методы + +### rules() +**Описание:** Правила валидации для всех атрибутов поиска. + +**Возвращает:** `array` — массив правил + +**Категории полей:** + +**Integer (точное совпадение):** +- Идентификаторы: id, f_id, firma_id, firm_group_id, firma_group_id, city_id, setka_id +- Настройки: posit, order_1c, visible, type_id, administrator_id +- Метрики: sale_plan_avg, visitor_day_avg, visitor_avg + +**Safe (LIKE-поиск):** +- Названия: name, name_full, url, h1 +- Адреса: adress, adress_amo, adress_sm, gps +- Карты: 2gis, yamap, googlemap, mapiframe +- SEO: seo_title, seo_description, content +- Контакты: email, tg_chat_id, sprav_id +- Изображения: images, image_sm, image_big, image2_sm, image2_big +- Даты: open_date + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с полным набором фильтров. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос CityStore::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет andFilterWhere для числовых полей (~15 полей) +5. Применяет andFilterWhere с LIKE для текстовых полей (~20 полей) + +**Примечание:** Поле '2gis' закомментировано из-за невалидного имени переменной PHP. + +## Диаграмма категорий полей + +```mermaid +mindmap + root((CityStoreSearch)) + Идентификация + id + f_id + firma_id + city_id + Адреса + adress + adress_amo + gps + SEO + seo_title + seo_description + url + h1 + Карты + yamap + googlemap + mapiframe + Изображения + images + image_sm + image_big + Настройки + visible + type_id + posit +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new CityStoreSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'name' => 'Центральный', + ] +]); +``` + +### Поиск по городу +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'city_id' => 1, + ] +]); +``` + +### Поиск видимых магазинов +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'visible' => 1, + ] +]); +``` + +### Поиск по типу +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'type_id' => 2, // Например, франшиза + ] +]); +``` + +### Поиск по адресу +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'adress' => 'Ленина', + ] +]); +``` + +### Поиск по email +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'email' => 'store@example.com', + ] +]); +``` + +### GridView с основными полями +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'adress', + 'city_id', + [ + 'attribute' => 'visible', + 'filter' => [0 => 'Скрыт', 1 => 'Виден'], + ], + 'type_id', + 'email', + ], +]) ?> +``` + +### Комплексный поиск +```php +$searchModel = new CityStoreSearch(); +$dataProvider = $searchModel->search([ + 'CityStoreSearch' => [ + 'city_id' => 1, + 'visible' => 1, + 'type_id' => 1, + ] +]); +``` + +## Связанные модели + +- [CityStore](./CityStore.md) — базовая модель магазина +- [City](./City.md) — города (city_id) +- [StoreType](./StoreType.md) — типы магазинов (type_id) +- [Admin](./Admin.md) — администратор (administrator_id) +- [Company](./Company.md) — фирмы (firma_id) + +## Особенности реализации + +1. **Полный набор полей**: ~35 атрибутов для поиска +2. **Закомментированное поле**: '2gis' не работает как имя PHP-переменной +3. **SEO-поля**: Поиск по seo_title, seo_description, url, h1 +4. **Геоданные**: Поиск по gps, yamap, googlemap +5. **Изображения**: LIKE-поиск по путям к изображениям +6. **Метрики**: Фильтрация по sale_plan_avg, visitor_avg +7. **Стандартный Gii-шаблон**: Автогенерация со всеми полями diff --git a/erp24/docs/models/Cluster.md b/erp24/docs/models/Cluster.md new file mode 100644 index 00000000..b541b116 --- /dev/null +++ b/erp24/docs/models/Cluster.md @@ -0,0 +1,203 @@ +# Модель Cluster + + +## Mindmap + +```mermaid +mindmap + root((Cluster)) + Таблица БД + cluster + Свойства + id + int + name + string + short_name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Cluster` представляет кластеры (группы) магазинов. Используется для объединения магазинов по территориальному или организационному признаку. Кластеры применяются для построения отчётов, планирования и управления группами магазинов. + +**Файл модели:** `erp24/records/Cluster.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `cluster` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Полное название кластера | +| `short_name` | VARCHAR(255) | Сокращённое название кластера | + +--- + +## Описание полей + +### `name` — Название кластера + +Полное название кластера для отображения в отчётах и интерфейсе управления. + +**Примеры значений:** +- "Северный кластер" +- "Центральный кластер" +- "Московская область" + +### `short_name` — Короткое название + +Сокращённое название для компактного отображения в таблицах и списках. + +**Примеры значений:** +- "СЕВ" +- "ЦЕНТР" +- "МО" + +--- + +## Методы модели + +### `getNames(): array` (static) + +Возвращает ассоциативный массив кластеров в формате `[id => short_name]`. + +**Возвращает:** `array` — массив `['id' => 'short_name']` + +**Логика работы:** +1. Выполняет запрос к таблице `cluster` +2. Выбирает поля `id` и `short_name` +3. Индексирует результат по `id` +4. Возвращает результат как одномерный массив + +```php +$clusterNames = Cluster::getNames(); +// [1 => 'СЕВ', 2 => 'ЦЕНТР', 3 => 'МО', ...] +``` + +**Использование:** +- Заполнение выпадающих списков в формах +- Фильтрация данных по кластерам +- Построение отчётов с группировкой по кластерам + +--- + +## Диаграмма связей + +```mermaid +erDiagram + cluster ||--o{ city_store : "contains" + cluster ||--o{ store_plan : "planning" + + cluster { + int id PK + string name + string short_name + } + + city_store { + int id PK + int cluster_id FK + string name + int city_id + } + + store_plan { + int id PK + int cluster_id FK + date period + float plan_sum + } +``` + +--- + +## Примеры использования + +### Получение всех кластеров + +```php +$clusters = Cluster::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Использование в выпадающем списке + +```php +use yii\helpers\Html; + +echo Html::dropDownList( + 'cluster_id', + $selectedClusterId, + Cluster::getNames(), + ['prompt' => 'Выберите кластер'] +); +``` + +### Получение магазинов кластера + +```php +$cluster = Cluster::findOne($clusterId); +$stores = CityStore::find() + ->where(['cluster_id' => $cluster->id]) + ->all(); +``` + +### Статистика по кластерам + +```php +$stats = CityStore::find() + ->select([ + 'cluster_id', + 'COUNT(*) as store_count' + ]) + ->groupBy('cluster_id') + ->asArray() + ->all(); + +$clusterNames = Cluster::getNames(); +foreach ($stats as $stat) { + $name = $clusterNames[$stat['cluster_id']] ?? 'Неизвестно'; + echo "{$name}: {$stat['store_count']} магазинов\n"; +} +``` + +### Фильтрация данных по кластеру + +```php +// Продажи по кластеру +$sales = Sales::find() + ->alias('s') + ->innerJoin('city_store cs', 'cs.id = s.store_id') + ->where(['cs.cluster_id' => $clusterId]) + ->sum('s.summ'); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | +| `short_name` | Обязательное, макс. 255 символов | + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазины, входящие в кластер +- **StorePlan** — планы продаж по кластерам +- **ClusterDirector** — директора кластеров + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ClusterAdmin.md b/erp24/docs/models/ClusterAdmin.md new file mode 100644 index 00000000..5a7a773c --- /dev/null +++ b/erp24/docs/models/ClusterAdmin.md @@ -0,0 +1,385 @@ +# Class: ClusterAdmin + + +## Mindmap + +```mermaid +mindmap + root((ClusterAdmin)) + Таблица БД + cluster_admin + Свойства + id + int + cluster_id + int + admin_id + int + date_start + string + date_end + string + active + int + Связи + Cluster + 1:1 Cluster + Admin + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `ClusterAdmin` представляет связь администраторов (кустовых руководителей) с кластерами магазинов. Она хранит информацию о том, какой руководитель отвечает за какой кластер и в какой период времени. Модель используется для управления территориальной ответственностью руководителей и формирования отчётности по кластерам. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`cluster_admin` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи | +| `cluster_id` | int | да | ID кластера (куста) магазинов | +| `admin_id` | int | да | ID администратора (кустового руководителя) | +| `date_start` | date | да | Дата начала ответственности | +| `date_end` | date | нет | Дата окончания ответственности | +| `active` | int | да | Флаг активности записи (1=активна, 0=неактивна) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'cluster_admin' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Правила валидации:** +- `cluster_id`, `admin_id`, `date_start` - обязательные поля +- `cluster_id`, `admin_id`, `active` - целые числа +- `date_start`, `date_end` - даты в формате 'Y-m-d' +- Валидация: `date_end` должна быть >= `date_start` + +**Пример:** +```php +$clusterAdmin = new ClusterAdmin(); +$clusterAdmin->cluster_id = 5; +$clusterAdmin->admin_id = 10; +$clusterAdmin->date_start = '2024-01-01'; +$clusterAdmin->date_end = '2023-12-31'; // Ошибка! + +if (!$clusterAdmin->validate()) { + print_r($clusterAdmin->getErrors()); + // ['date_end' => ['Дата окончания должна быть больше или равна дате начала']] +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает русские метки для атрибутов модели. + +**Возвращает:** `array` - массив меток + +**Пример:** +```php +$labels = (new ClusterAdmin())->attributeLabels(); +echo $labels['admin_id']; // 'Кустовой' +echo $labels['cluster_id']; // 'Куст' +``` + +--- + +### `getCluster()` +**Описание:** Получает связь с кластером магазинов. + +**Тип связи:** hasOne + +**Связанная модель:** `Cluster` + +**Условие:** `['id' => 'cluster_id']` + +**Пример:** +```php +$clusterAdmin = ClusterAdmin::findOne(1); +echo $clusterAdmin->cluster->name; // Название кластера +``` + +--- + +### `getAdmin()` +**Описание:** Получает связь с администратором (кустовым руководителем). + +**Тип связи:** hasOne + +**Связанная модель:** `Admin` + +**Условие:** `['id' => 'admin_id']` + +**Возвращает:** `ActiveQuery` + +**Пример:** +```php +$clusterAdmin = ClusterAdmin::findOne(1); +echo $clusterAdmin->admin->name_full; // ФИО кустового +``` + +--- + +## Примеры использования + +### 1. Назначение кустового руководителя на кластер +```php +$clusterAdmin = new ClusterAdmin(); +$clusterAdmin->cluster_id = 5; +$clusterAdmin->admin_id = 10; +$clusterAdmin->date_start = date('Y-m-d'); +$clusterAdmin->active = 1; + +if ($clusterAdmin->save()) { + echo "Кустовой назначен на кластер"; +} +``` + +### 2. Получение текущих кустовых руководителей +```php +$currentDate = date('Y-m-d'); + +$activeClusterAdmins = ClusterAdmin::find() + ->joinWith(['cluster', 'admin']) + ->where(['cluster_admin.active' => 1]) + ->andWhere(['<=', 'date_start', $currentDate]) + ->andWhere(['or', + ['date_end' => null], + ['>=', 'date_end', $currentDate] + ]) + ->all(); + +foreach ($activeClusterAdmins as $ca) { + echo "{$ca->admin->name_full} - {$ca->cluster->name}\n"; +} +``` + +### 3. Завершение назначения кустового +```php +$clusterAdmin = ClusterAdmin::findOne([ + 'cluster_id' => 5, + 'admin_id' => 10, + 'active' => 1 +]); + +if ($clusterAdmin) { + $clusterAdmin->date_end = date('Y-m-d'); + $clusterAdmin->active = 0; + $clusterAdmin->save(); + echo "Назначение завершено"; +} +``` + +### 4. Получение истории назначений кластера +```php +$clusterId = 5; + +$history = ClusterAdmin::find() + ->joinWith('admin') + ->where(['cluster_id' => $clusterId]) + ->orderBy(['date_start' => SORT_DESC]) + ->all(); + +echo "История кустовых руководителей кластера {$clusterId}:\n"; +foreach ($history as $record) { + $dateEnd = $record->date_end ?? 'по настоящее время'; + echo "{$record->admin->name_full}: {$record->date_start} - {$dateEnd}\n"; +} +``` + +### 5. Получение всех кластеров администратора +```php +$adminId = 10; + +$clusters = ClusterAdmin::find() + ->joinWith('cluster') + ->where(['admin_id' => $adminId, 'active' => 1]) + ->all(); + +echo "Кластеры администратора {$adminId}:\n"; +foreach ($clusters as $ca) { + echo "- {$ca->cluster->name} (с {$ca->date_start})\n"; +} +``` + +### 6. Смена кустового руководителя +```php +$clusterId = 5; +$oldAdminId = 10; +$newAdminId = 15; +$today = date('Y-m-d'); + +// Закрываем старое назначение +$oldAssignment = ClusterAdmin::findOne([ + 'cluster_id' => $clusterId, + 'admin_id' => $oldAdminId, + 'active' => 1 +]); + +if ($oldAssignment) { + $oldAssignment->date_end = $today; + $oldAssignment->active = 0; + $oldAssignment->save(); +} + +// Создаём новое назначение +$newAssignment = new ClusterAdmin(); +$newAssignment->cluster_id = $clusterId; +$newAssignment->admin_id = $newAdminId; +$newAssignment->date_start = $today; +$newAssignment->active = 1; +$newAssignment->save(); + +echo "Кустовой руководитель сменён"; +``` + +### 7. Проверка доступа администратора к кластеру +```php +function hasAccessToCluster($adminId, $clusterId) +{ + $currentDate = date('Y-m-d'); + + return ClusterAdmin::find() + ->where([ + 'cluster_id' => $clusterId, + 'admin_id' => $adminId, + 'active' => 1 + ]) + ->andWhere(['<=', 'date_start', $currentDate]) + ->andWhere(['or', + ['date_end' => null], + ['>=', 'date_end', $currentDate] + ]) + ->exists(); +} + +if (hasAccessToCluster(10, 5)) { + echo "Администратор имеет доступ к кластеру"; +} +``` + +### 8. Отчёт по загрузке кустовых руководителей +```php +$stats = ClusterAdmin::find() + ->select(['admin_id', 'COUNT(*) as clusters_count']) + ->where(['active' => 1]) + ->groupBy('admin_id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $admin = Admin::findOne($stat['admin_id']); + echo "{$admin->name_full}: {$stat['clusters_count']} кластеров\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + CLUSTER_ADMIN ||--|| CLUSTER : "belongs to" + CLUSTER_ADMIN ||--|| ADMIN : "managed by" + + CLUSTER_ADMIN { + int id PK + int cluster_id FK "ID кластера" + int admin_id FK "ID администратора" + date date_start "Дата начала" + date date_end "Дата окончания" + int active "Активность" + } + + CLUSTER { + int id PK + string name "Название кластера" + } + + ADMIN { + int id PK + string name_full "ФИО" + string email "Email" + } +``` + +--- + +## Особенности реализации + +### Версионирование ответственности +Модель поддерживает историческое версионирование через поля `date_start`, `date_end` и `active`. Это позволяет: +- Хранить историю смены руководителей +- Определять актуального руководителя на любую дату +- Планировать будущие назначения + +### Валидация дат +Встроенная валидация гарантирует, что `date_end >= date_start`, что предотвращает логические ошибки в данных. + +### Флаг активности +Поле `active` позволяет помечать запись как неактивную без её удаления, сохраняя историю. + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Управление** - распределение территориальной ответственности +2. **Отчётность** - формирование отчётов по кластерам для руководителей +3. **Контроль доступа** - ограничение доступа к данным кластера +4. **Аналитика** - оценка эффективности работы кустовых руководителей +5. **Планирование** - ротация и оптимизация назначений + +### Бизнес-сценарии + +- Назначение нового кустового руководителя +- Ротация руководителей между кластерами +- Временная замена руководителя (отпуск, болезнь) +- Передача кластера при увольнении +- Расширение/сокращение зоны ответственности + +--- + +## Связанные компоненты + +- **Модели:** Cluster, Admin, CityStore +- **Сервисы:** ClusterService, AccessService +- **Контроллеры:** ClusterAdminController, ClusterController +- **RBAC:** Контроль доступа на основе привязок + +--- + +## Примечания + +**Рекомендации:** + +1. Всегда устанавливать `date_start` при создании назначения +2. Закрывать старые назначения (`date_end` и `active=0`) при смене руководителя +3. Проверять пересечения дат при назначении нескольких руководителей на один кластер +4. Использовать транзакции при массовых изменениях назначений + +**Потенциальные улучшения:** + +1. Добавить проверку на пересечение периодов для одного кластера +2. Создать методы для автоматической смены руководителя +3. Реализовать уведомления при изменении назначений +4. Добавить поддержку множественной ответственности (несколько руководителей на кластер) +5. Создать индексы на комбинации полей для оптимизации запросов diff --git a/erp24/docs/models/ClusterAdminSearch.md b/erp24/docs/models/ClusterAdminSearch.md new file mode 100644 index 00000000..e78f2d1c --- /dev/null +++ b/erp24/docs/models/ClusterAdminSearch.md @@ -0,0 +1,138 @@ +# Класс: ClusterAdminSearch + + +## Mindmap + +```mermaid +mindmap + root((ClusterAdminSearch)) + Таблица БД + ActiveRecord + Наследование + extends ClusterAdmin +``` + +## Назначение +Search-модель для поиска и фильтрации привязок сотрудников к кластерам в ERP24. Обеспечивает поиск по кластеру, сотруднику и периоду действия назначения. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ClusterAdmin` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `cluster_id`, `admin_id` — integer +- `date_start`, `date_end` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ClusterAdmin::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет точное совпадение для всех полей: id, cluster_id, admin_id, date_start, date_end + +## Диаграмма связей + +```mermaid +erDiagram + ClusterAdmin { + int id PK + int cluster_id FK + int admin_id FK + date date_start + date date_end + } + + Cluster { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + Cluster ||--o{ ClusterAdmin : "cluster_id" + Admin ||--o{ ClusterAdmin : "admin_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ClusterAdminSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по кластеру +```php +$searchModel = new ClusterAdminSearch(); +$dataProvider = $searchModel->search([ + 'ClusterAdminSearch' => [ + 'cluster_id' => 5, + ] +]); +``` + +### Поиск по сотруднику +```php +$searchModel = new ClusterAdminSearch(); +$dataProvider = $searchModel->search([ + 'ClusterAdminSearch' => [ + 'admin_id' => 123, + ] +]); +``` + +### Поиск по периоду +```php +$searchModel = new ClusterAdminSearch(); +$dataProvider = $searchModel->search([ + 'ClusterAdminSearch' => [ + 'date_start' => '2024-01-01', + 'date_end' => '2024-12-31', + ] +]); +``` + +## Связанные модели + +- [ClusterAdmin](./ClusterAdmin.md) — базовая модель привязок +- [Cluster](./Cluster.md) — кластеры +- [Admin](./Admin.md) — сотрудники + +## Особенности реализации + +1. **Простая модель**: Минимальный набор полей +2. **Точное совпадение**: Даже для дат используется точное совпадение +3. **История назначений**: date_start/date_end для периодов +4. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/ClusterCalendar.md b/erp24/docs/models/ClusterCalendar.md new file mode 100644 index 00000000..afff2d9e --- /dev/null +++ b/erp24/docs/models/ClusterCalendar.md @@ -0,0 +1,429 @@ +# Class: ClusterCalendar + + +## Mindmap + +```mermaid +mindmap + root((ClusterCalendar)) + Таблица БД + cluster_calendar + Свойства + id + int + cluster_id + int + value_type + string + date_from + string + date_to + string + category_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `ClusterCalendar` представляет календарь событий и динамических параметров кластера магазинов. Она хранит версионированные данные о различных характеристиках кластера (магазины, администраторы, метрики) с указанием периода действия. Модель используется для отслеживания изменений параметров кластера во времени и синхронизации с историей изменений администраторов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`cluster_calendar` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи (первичный ключ) | +| `cluster_id` | int | да | ID кластера | +| `value_type` | string(100) | да | Тип значения (int/string) | +| `value_int` | int | нет | Числовое значение параметра | +| `value_string` | string(255) | нет | Строковое значение параметра | +| `year` | int | нет | Год события/параметра | +| `date_from` | date | да | Дата начала действия значения | +| `date_to` | date | нет | Дата окончания действия значения | +| `category_id` | int | да | ID категории значения (тип параметра) | +| `created_admin_id` | int | да | ID администратора, создавшего запись | +| `updated_admin_id` | int | нет | ID администратора, обновившего запись | +| `created_at` | timestamp | да | Дата и время создания записи | +| `updated_at` | timestamp | нет | Дата и время обновления записи | + +## Статические свойства + +### `$clusterFieldsList` +**Описание:** Массив полей кластера для автоматической синхронизации с AdminDynamic. + +**Структура:** +```php +[ + ['field' => 'store_id', 'categoryAlias' => 'store'], + ['field' => 'admin_id', 'categoryAlias' => 'admin'] +] +``` + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'cluster_calendar' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Правила:** +- Обязательные: cluster_id, value_type, date_from, category_id, created_admin_id, created_at +- Целые числа: cluster_id, value_int, year, category_id, created_admin_id, updated_admin_id +- Даты: date_from, date_to, created_at, updated_at +- Строки: value_type (до 100), value_string (до 255) + +--- + +### `attributeLabels()` +**Описание:** Возвращает русские метки для атрибутов. + +**Примеры меток:** +- `cluster_id` → "cluster ID" +- `value_type` → "Тип значения" +- `date_from` → "Дата начала" +- `category_id` → "ID категории значения" + +--- + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех свойств с поддержкой fluent interface: + +**Основные методы:** + +- `getId()` → `int` +- `getClusterId()` / `setClusterId(int $cluster_id)` → `object` +- `getValueType()` / `setValueType(string $value_type)` → `void` +- `getValueInt()` / `setValueInt(?int $value_int)` → `void` +- `getValueString()` / `setValueString(?string $value_string)` → `void` +- `getYear()` / `setYear(?int $year)` → `object` +- `getDateFrom()` / `setDateFrom(string $date_from)` → `object` +- `getDateTo()` / `setDateTo(string $date_to)` → `object` +- `getCategoryId()` / `setCategoryId(int $category_id)` → `void` +- `getCreatedAdminId()` / `setCreatedAdminId(string $created_admin_id)` → `object` +- `getUpdatedAdminId()` / `setUpdatedAdminId(?string $updated_admin_id)` → `object` +- `getCreatedAt()` / `setCreatedAt(string $created_at)` → `object` +- `getUpdatedAt()` / `setUpdatedAt(?string $updated_at)` → `object` + +**Пример использования (fluent interface):** +```php +$calendar = (new ClusterCalendar()) + ->setClusterId(5) + ->setDateFrom('2024-01-01') + ->setDateTo('2024-12-31') + ->setYear(2024) + ->setCreatedAdminId(10) + ->setCreatedAt(date('Y-m-d H:i:s')); +``` + +--- + +### `setClusterCalendar(ClusterCalendar $model)` +**Описание:** Статический метод для автоматической синхронизации изменений календаря кластера с историей администраторов. + +**Параметры:** +- `$model` (ClusterCalendar) - модель календаря кластера + +**Возвращает:** `void` + +**Логика работы:** +1. Проходит по списку полей из `$clusterFieldsList` (store_id, admin_id) +2. Для каждого непустого поля: + - Получает категорию из справочника `ClusterCalendarCategoryDict` + - Извлекает `category_id` и `value_type` + - Вызывает `setHistoryAdmin()` для создания/обновления истории + +**Вызываемые методы:** +- `ClusterCalendarCategoryDict::getCategory()` - получение категории +- `setHistoryAdmin()` - обновление истории администратора + +**Пример:** +```php +$calendar = new ClusterCalendar(); +$calendar->cluster_id = 5; +$calendar->admin_id = 10; // виртуальное свойство +$calendar->store_id = 15; // виртуальное свойство +// ... заполнение других полей + +ClusterCalendar::setClusterCalendar($calendar); +// Автоматически создадутся записи в AdminDynamic +``` + +--- + +### `setHistoryAdmin(int $adminId, $adminValue, int $categoryId, string $valueType)` +**Описание:** Создаёт или обновляет историю изменения параметра администратора. + +**Параметры:** +- `$adminId` (int) - ID администратора +- `$adminValue` (mixed) - новое значение параметра +- `$categoryId` (int) - ID категории параметра +- `$valueType` (string) - тип значения (int/string) + +**Возвращает:** `void` + +**Логика работы:** + +1. **Поиск текущей активной записи:** + - Ищет запись в `AdminDynamic` с условиями: admin_id, active=1, category_id, value_type + +2. **Сравнение значений:** + - Получает текущее значение через `getValue()` + - Сравнивает с новым значением + +3. **Обновление при изменении:** + - Если значение изменилось: + - Деактивирует старую запись (`disableRecord()`) + - Создаёт новую запись с новым значением + - Если значение не изменилось: + - Пропускает создание новой записи + +4. **Валидация и сохранение:** + - Валидирует новую запись + - Сохраняет в БД + +**Вызываемые методы:** +- `AdminDynamic::find()` - поиск текущей записи +- `AdminDynamic::getValue()` - получение значения +- `AdminDynamic::disableRecord()` - деактивация записи +- `AdminDynamic::setValue()` - установка значения +- `AdminDynamic::validate()` - валидация +- `AdminDynamic::save()` - сохранение + +**Пример:** +```php +ClusterCalendar::setHistoryAdmin( + adminId: 10, + adminValue: 5, // новый cluster_id + categoryId: 1, // категория "кластер" + valueType: 'int' +); +// Создаст новую запись в AdminDynamic о смене кластера +``` + +--- + +## Примеры использования + +### 1. Создание записи календаря кластера +```php +$calendar = new ClusterCalendar(); +$calendar->setClusterId(5); +$calendar->setValueType('int'); +$calendar->setValueInt(150); // Например, целевая метрика +$calendar->setDateFrom('2024-01-01'); +$calendar->setDateTo('2024-12-31'); +$calendar->setYear(2024); +$calendar->setCategoryId(3); // Категория "план продаж" +$calendar->setCreatedAdminId(Yii::$app->user->id); +$calendar->setCreatedAt(date('Y-m-d H:i:s')); + +if ($calendar->save()) { + echo "Запись календаря создана с ID: {$calendar->getId()}"; +} +``` + +### 2. Получение активных параметров кластера +```php +$clusterId = 5; +$currentDate = date('Y-m-d'); + +$activeParams = ClusterCalendar::find() + ->where(['cluster_id' => $clusterId]) + ->andWhere(['<=', 'date_from', $currentDate]) + ->andWhere(['or', + ['date_to' => null], + ['>=', 'date_to', $currentDate] + ]) + ->all(); + +foreach ($activeParams as $param) { + $value = $param->getValueType() === 'int' + ? $param->getValueInt() + : $param->getValueString(); + + echo "Категория {$param->getCategoryId()}: {$value}\n"; +} +``` + +### 3. История изменений параметра +```php +$clusterId = 5; +$categoryId = 1; + +$history = ClusterCalendar::find() + ->where(['cluster_id' => $clusterId, 'category_id' => $categoryId]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +echo "История изменений:\n"; +foreach ($history as $record) { + $value = $record->getValueType() === 'int' + ? $record->getValueInt() + : $record->getValueString(); + + $dateTo = $record->getDateTo() ?? 'по настоящее время'; + echo "{$record->getDateFrom()} - {$dateTo}: {$value}\n"; +} +``` + +### 4. Использование fluent interface для создания +```php +$calendar = (new ClusterCalendar()) + ->setClusterId(5) + ->setYear(2024) + ->setDateFrom('2024-06-01') + ->setDateTo('2024-12-31') + ->setCreatedAdminId(10) + ->setCreatedAt(date('Y-m-d H:i:s')); + +$calendar->setValueType('string'); +$calendar->setValueString('Премиум сегмент'); +$calendar->setCategoryId(5); + +$calendar->save(); +``` + +### 5. Обновление параметра с созданием истории +```php +$calendar = ClusterCalendar::findOne([ + 'cluster_id' => 5, + 'category_id' => 3, + 'date_to' => null // Текущий активный +]); + +if ($calendar) { + // Закрываем старую запись + $calendar->setDateTo(date('Y-m-d')); + $calendar->setUpdatedAdminId(Yii::$app->user->id); + $calendar->setUpdatedAt(date('Y-m-d H:i:s')); + $calendar->save(); + + // Создаём новую запись + $newCalendar = (new ClusterCalendar()) + ->setClusterId($calendar->getClusterId()) + ->setValueType($calendar->getValueType()) + ->setValueInt(200) // Новое значение + ->setDateFrom(date('Y-m-d')) + ->setCategoryId($calendar->getCategoryId()) + ->setYear(date('Y')) + ->setCreatedAdminId(Yii::$app->user->id) + ->setCreatedAt(date('Y-m-d H:i:s')); + + $newCalendar->save(); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + CLUSTER_CALENDAR ||--|| CLUSTER : "belongs to" + CLUSTER_CALENDAR ||--|| CLUSTER_CALENDAR_CATEGORY_DICT : "has category" + CLUSTER_CALENDAR ||--|| ADMIN : "created by" + CLUSTER_CALENDAR ||--o| ADMIN : "updated by" + CLUSTER_CALENDAR ||--o{ ADMIN_DYNAMIC : "syncs with" + + CLUSTER_CALENDAR { + int id PK + int cluster_id FK "ID кластера" + string value_type "Тип значения" + int value_int "Числовое значение" + string value_string "Строковое значение" + int year "Год" + date date_from "Дата начала" + date date_to "Дата окончания" + int category_id FK "ID категории" + int created_admin_id FK "Создал" + int updated_admin_id FK "Обновил" + timestamp created_at "Дата создания" + timestamp updated_at "Дата обновления" + } + + CLUSTER { + int id PK + string name "Название" + } + + ADMIN { + int id PK + string name_full "ФИО" + } + + CLUSTER_CALENDAR_CATEGORY_DICT { + int id PK + string alias "Алиас" + string name "Название" + string value_type "Тип значения" + } + + ADMIN_DYNAMIC { + int id PK + int admin_id FK + int category_id FK + string value_type + int value_int + string value_string + } +``` + +--- + +## Особенности реализации + +### Версионирование параметров +Модель поддерживает полное версионирование через `date_from` и `date_to`, позволяя хранить историю всех изменений параметров кластера. + +### Гибкая типизация +Использование двух полей (`value_int` и `value_string`) с указателем `value_type` позволяет хранить разнородные данные. + +### Автоматическая синхронизация +Метод `setClusterCalendar()` автоматически синхронизирует изменения с таблицей `AdminDynamic`, обеспечивая целостность данных. + +### Fluent Interface +Большинство сеттеров возвращают `$this`, что позволяет использовать цепочки вызовов методов. + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Планирование** - установка целевых показателей для кластеров +2. **История изменений** - отслеживание эволюции параметров +3. **Аналитика** - анализ исторических данных +4. **Синхронизация** - связь параметров кластера с администраторами + +--- + +## Связанные компоненты + +- **Модели:** Cluster, Admin, AdminDynamic, ClusterCalendarCategoryDict +- **Сервисы:** ClusterService, CalendarService +- **Контроллеры:** ClusterCalendarController + +--- + +## Примечания + +**Рекомендации:** +1. Использовать версионирование для всех изменений параметров +2. Регулярно синхронизировать с AdminDynamic +3. Валидировать корректность дат (date_to >= date_from) +4. Использовать справочник категорий для типизации параметров diff --git a/erp24/docs/models/ClusterCalendarCategoryDict.md b/erp24/docs/models/ClusterCalendarCategoryDict.md new file mode 100644 index 00000000..eb7173cf --- /dev/null +++ b/erp24/docs/models/ClusterCalendarCategoryDict.md @@ -0,0 +1,284 @@ +# Модель ClusterCalendarCategoryDict + + +## Mindmap + +```mermaid +mindmap + root((ClusterCalendarCategoryDict)) + Таблица БД + cluster_calendar_category_dict + Свойства + id + int + name + string + value_type + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ClusterCalendarCategoryDict` представляет справочник категорий для кластерного календаря. Хранит названия, типы значений и алиасы категорий, используемых для группировки и классификации событий в кластерном планировании. Применяется для структурирования данных календаря по регионам и кластерам магазинов. + +**Файл модели:** `erp24/records/ClusterCalendarCategoryDict.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `cluster_calendar_category_dict` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Название категории | +| `value_type` | VARCHAR(255) | Тип значения (формат данных) | +| `alias` | VARCHAR(255) | Алиас для программного использования | + +--- + +## Описание полей + +### `name` — Название категории + +Человекочитаемое название категории. Примеры: +- "Праздничные дни" +- "Плановые мероприятия" +- "Сезонные акции" + +### `value_type` — Тип значения + +Определяет формат данных для значений этой категории: +- `string` — текстовое значение +- `number` — числовое значение +- `date` — дата +- `boolean` — логическое значение +- `json` — сложная структура + +### `alias` — Алиас + +Программный идентификатор категории для использования в коде: +- `holidays` — праздники +- `events` — мероприятия +- `promotions` — акции +- `deadlines` — дедлайны + +--- + +## Методы модели + +### `getCategory(string $categoryAlias): ?object` + +Статический метод получения категории по алиасу. + +**Параметры:** +- `$categoryAlias` (string) — алиас категории + +**Возвращает:** Объект категории или null + +```php +$category = ClusterCalendarCategoryDict::getCategory('holidays'); +if ($category) { + echo "ID: {$category->id}, Тип: {$category->value_type}"; +} +``` + +### Геттеры + +| Метод | Возвращает | +|-------|------------| +| `getId()` | int — ID категории | +| `getName()` | string — Название | +| `getValueType()` | string — Тип значения | +| `getAlias()` | string — Алиас | + +```php +$category = ClusterCalendarCategoryDict::findOne($id); +echo "Категория: " . $category->getName(); +echo "Тип данных: " . $category->getValueType(); +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + cluster_calendar_category_dict ||--o{ cluster_calendar : "categorizes" + + cluster_calendar_category_dict { + int id PK + string name + string value_type + string alias + } + + cluster_calendar { + int id PK + int category_id FK + string value + date date + } +``` + +--- + +## Примеры использования + +### Создание категории + +```php +$category = new ClusterCalendarCategoryDict(); +$category->name = 'Праздничные дни'; +$category->value_type = 'string'; +$category->alias = 'holidays'; +$category->save(); +``` + +### Получение всех категорий + +```php +$categories = ClusterCalendarCategoryDict::find()->all(); + +foreach ($categories as $cat) { + echo "{$cat->name} ({$cat->alias}): тип {$cat->value_type}\n"; +} +``` + +### Поиск категории по алиасу + +```php +$category = ClusterCalendarCategoryDict::getCategory('promotions'); + +if ($category) { + echo "Найдена категория: {$category->name}"; + echo "ID для использования: {$category->id}"; +} +``` + +### Группировка событий по категориям + +```php +$categories = ClusterCalendarCategoryDict::find()->all(); + +foreach ($categories as $category) { + echo "\n{$category->getName()}:\n"; + + // Предполагаем связь с ClusterCalendar + $events = ClusterCalendar::find() + ->where(['category_id' => $category->getId()]) + ->all(); + + foreach ($events as $event) { + echo " - {$event->date}: {$event->value}\n"; + } +} +``` + +### Построение выпадающего списка + +```php +$list = ClusterCalendarCategoryDict::find() + ->select(['name', 'id']) + ->indexBy('id') + ->column(); + +// $list = [1 => 'Праздничные дни', 2 => 'Плановые мероприятия', ...] + +// Использование в форме +echo Html::dropDownList('category_id', $model->category_id, $list); +``` + +### Валидация типа значения + +```php +function validateEventValue($categoryAlias, $value) { + $category = ClusterCalendarCategoryDict::getCategory($categoryAlias); + + if (!$category) { + return ['valid' => false, 'error' => 'Категория не найдена']; + } + + switch ($category->getValueType()) { + case 'number': + if (!is_numeric($value)) { + return ['valid' => false, 'error' => 'Ожидается число']; + } + break; + case 'date': + if (!strtotime($value)) { + return ['valid' => false, 'error' => 'Некорректная дата']; + } + break; + case 'boolean': + if (!in_array($value, [true, false, 0, 1, '0', '1'], true)) { + return ['valid' => false, 'error' => 'Ожидается boolean']; + } + break; + case 'json': + if (json_decode($value) === null && $value !== 'null') { + return ['valid' => false, 'error' => 'Некорректный JSON']; + } + break; + } + + return ['valid' => true]; +} + +$result = validateEventValue('holidays', 'Новый год'); +``` + +### Получение категорий определённого типа + +```php +// Все категории с числовыми значениями +$numericCategories = ClusterCalendarCategoryDict::find() + ->where(['value_type' => 'number']) + ->all(); + +foreach ($numericCategories as $cat) { + echo "{$cat->alias}: {$cat->name}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | +| `value_type` | Обязательное, макс. 255 символов | +| `alias` | Обязательное, макс. 255 символов | + +--- + +## Типичные категории календаря + +| Алиас | Название | Тип значения | +|-------|----------|--------------| +| `holidays` | Праздничные дни | string | +| `events` | Мероприятия | string | +| `promotions` | Акции | json | +| `deadlines` | Дедлайны | date | +| `metrics` | Метрики | number | +| `notes` | Заметки | string | + +--- + +## Связанные модели + +- **[ClusterCalendar](./ClusterCalendar.md)** — события календаря +- **[CityStore](./CityStore.md)** — магазины кластера +- **[StoreCluster](./StoreCluster.md)** — кластеры магазинов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ClusterCalendarCategoryDictSearch.md b/erp24/docs/models/ClusterCalendarCategoryDictSearch.md new file mode 100644 index 00000000..50d209eb --- /dev/null +++ b/erp24/docs/models/ClusterCalendarCategoryDictSearch.md @@ -0,0 +1,134 @@ +# Класс: ClusterCalendarCategoryDictSearch + + +## Mindmap + +```mermaid +mindmap + root((ClusterCalendarCategoryDictSearch)) + Таблица БД + ActiveRecord + Наследование + extends ClusterDynamicCategoryDict +``` + +## Назначение +Search-модель для поиска и фильтрации справочника категорий динамических параметров кластеров в ERP24. Обеспечивает поиск по названию, типу значения и алиасу. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ClusterDynamicCategoryDict` + +**Примечание:** Название класса не совпадает с родительской моделью — ClusterCalendarCategoryDictSearch расширяет ClusterDynamicCategoryDict. + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name`, `value_type`, `alias` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ClusterDynamicCategoryDict::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id + - LIKE: name, value_type, alias + +## Диаграмма структуры справочника + +```mermaid +erDiagram + ClusterDynamicCategoryDict { + int id PK + varchar name + varchar value_type + varchar alias + } + + ClusterDynamic { + int id PK + int category_id FK + int cluster_id FK + } + + ClusterDynamicCategoryDict ||--o{ ClusterDynamic : "category_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ClusterCalendarCategoryDictSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new ClusterCalendarCategoryDictSearch(); +$dataProvider = $searchModel->search([ + 'ClusterCalendarCategoryDictSearch' => [ + 'name' => 'Праздник', + ] +]); +``` + +### Поиск по типу значения +```php +$searchModel = new ClusterCalendarCategoryDictSearch(); +$dataProvider = $searchModel->search([ + 'ClusterCalendarCategoryDictSearch' => [ + 'value_type' => 'date', + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new ClusterCalendarCategoryDictSearch(); +$dataProvider = $searchModel->search([ + 'ClusterCalendarCategoryDictSearch' => [ + 'alias' => 'holiday', + ] +]); +``` + +## Связанные модели + +- [ClusterDynamicCategoryDict](./ClusterDynamicCategoryDict.md) — базовая модель справочника +- [ClusterDynamic](./ClusterDynamic.md) — динамические параметры кластеров + +## Особенности реализации + +1. **Несовпадение имён**: Класс Search ≠ родительская модель +2. **Справочник категорий**: Для типизации динамических параметров +3. **LIKE-поиск**: По name, value_type, alias +4. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/ClusterSearch.md b/erp24/docs/models/ClusterSearch.md new file mode 100644 index 00000000..5cc44c09 --- /dev/null +++ b/erp24/docs/models/ClusterSearch.md @@ -0,0 +1,143 @@ +# Класс: ClusterSearch + + +## Mindmap + +```mermaid +mindmap + root((ClusterSearch)) + Таблица БД + ActiveRecord + Наследование + extends Cluster +``` + +## Назначение +Search-модель для поиска и фильтрации кластеров (кустов магазинов) в ERP24. Простая модель для поиска по ID и названию кластера. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Cluster` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Cluster::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id + - LIKE: name + +## Диаграмма связей + +```mermaid +erDiagram + Cluster { + int id PK + varchar name + } + + CityStore { + int id PK + int cluster_id FK + } + + ClusterAdmin { + int id PK + int cluster_id FK + } + + Cluster ||--o{ CityStore : "cluster_id" + Cluster ||--o{ ClusterAdmin : "cluster_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ClusterSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new ClusterSearch(); +$dataProvider = $searchModel->search([ + 'ClusterSearch' => [ + 'name' => 'Центр', + ] +]); +``` + +### Поиск по ID +```php +$searchModel = new ClusterSearch(); +$dataProvider = $searchModel->search([ + 'ClusterSearch' => [ + 'id' => 5, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'label' => 'Магазинов', + 'value' => function($model) { + return count($model->stores); + } + ], + ], +]) ?> +``` + +## Связанные модели + +- [Cluster](./Cluster.md) — базовая модель кластера +- [CityStore](./CityStore.md) — магазины в кластере +- [ClusterAdmin](./ClusterAdmin.md) — сотрудники кластера + +## Особенности реализации + +1. **Минимальная модель**: Только id и name +2. **LIKE-поиск**: Для названия кластера +3. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/Comment.md b/erp24/docs/models/Comment.md new file mode 100644 index 00000000..3ff01da6 --- /dev/null +++ b/erp24/docs/models/Comment.md @@ -0,0 +1,350 @@ +# Модель Comment + + +## Mindmap + +```mermaid +mindmap + root((Comment)) + Таблица БД + comment + Свойства + id + int + msg + string + created_at + string + created_by + int + entity + string + entity_id + int + Связи + CreatedBy + 1:1 Admin + AttachedFiles + 1:N Files + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Comment` представляет комментарии к различным сущностям системы. Реализует полиморфную связь через поля `entity` и `entity_id`, что позволяет прикреплять комментарии к любым объектам: задачам, проверкам, заказам и т.д. Поддерживает прикрепление файлов. + +**Файл модели:** `erp24/records/Comment.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `comment` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `msg` | VARCHAR(1000) | Текст комментария | +| `created_at` | TIMESTAMP | Дата создания комментария | +| `created_by` | INTEGER | ID автора комментария (FK → admin.id) | +| `entity` | VARCHAR(50) | Название сущности, к которой прикреплён комментарий | +| `entity_id` | INTEGER | ID сущности | + +--- + +## Виртуальные атрибуты + +```php +public $files; // Загружаемые файлы (до 20 файлов) +``` + +--- + +## Описание полей + +### `entity` — Сущность + +Строковый идентификатор типа объекта, к которому относится комментарий: +- `task` — задачи +- `check_conduct` — проверки +- `order` — заказы +- `incident` — инциденты + +### `entity_id` — ID сущности + +Первичный ключ записи в таблице соответствующей сущности. + +--- + +## Методы модели + +### `validateSaveAndManageImages(): bool` + +Валидирует, сохраняет комментарий и загружает прикреплённые файлы. + +**Логика работы:** +1. Валидация модели +2. Сохранение в БД +3. Загрузка файлов через FileService с entity = 'task_comment' + +```php +$comment = new Comment(); +$comment->msg = 'Комментарий к задаче'; +$comment->entity = 'task'; +$comment->entity_id = $taskId; +$comment->created_at = date('Y-m-d H:i:s'); +$comment->created_by = Yii::$app->user->id; +$comment->files = UploadedFile::getInstances($comment, 'files'); + +if ($comment->validateSaveAndManageImages()) { + echo "Комментарий сохранён"; +} +``` + +--- + +## Связи (Relations) + +### `getCreatedBy(): ActiveQuery` + +Возвращает автора комментария. + +```php +$comment = Comment::findOne($id); +$author = $comment->createdBy; // Admin +echo "Автор: {$author->name}"; +``` + +### `getAttachedFiles(): ActiveQuery` + +Возвращает прикреплённые файлы. + +```php +$files = $comment->attachedFiles; // Files[] +foreach ($files as $file) { + echo "{$file->original_name}"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + comment }o--|| admin : "created_by" + comment ||--o{ files : "has_attachments" + comment }o--o| task : "polymorphic" + comment }o--o| check_conduct : "polymorphic" + comment }o--o| store_orders : "polymorphic" + + comment { + int id PK + string msg + timestamp created_at + int created_by FK + string entity + int entity_id + } + + admin { + int id PK + string name + } + + files { + int id PK + string entity + int entity_id + string path + } +``` + +--- + +## Примеры использования + +### Добавление комментария к задаче + +```php +$comment = new Comment(); +$comment->msg = 'Работа выполнена, прошу проверить'; +$comment->created_at = date('Y-m-d H:i:s'); +$comment->created_by = Yii::$app->user->id; +$comment->entity = 'task'; +$comment->entity_id = $taskId; +$comment->save(); +``` + +### Получение комментариев к сущности + +```php +$comments = Comment::find() + ->with('createdBy') + ->where([ + 'entity' => 'task', + 'entity_id' => $taskId + ]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +foreach ($comments as $comment) { + echo "{$comment->created_at} - {$comment->createdBy->name}:\n"; + echo "{$comment->msg}\n\n"; +} +``` + +### Комментарий с файлами + +```php +// В контроллере +$comment = new Comment(); +$comment->load(Yii::$app->request->post()); +$comment->files = UploadedFile::getInstances($comment, 'files'); +$comment->created_at = date('Y-m-d H:i:s'); +$comment->created_by = Yii::$app->user->id; + +if ($comment->validateSaveAndManageImages()) { + return $this->redirect(['view', 'id' => $comment->entity_id]); +} +``` + +### Подсчёт комментариев + +```php +$count = Comment::find() + ->where([ + 'entity' => 'task', + 'entity_id' => $taskId + ]) + ->count(); + +echo "Комментариев: {$count}"; +``` + +### Последний комментарий к задаче + +```php +$lastComment = Comment::find() + ->with('createdBy') + ->where([ + 'entity' => 'task', + 'entity_id' => $taskId + ]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastComment) { + echo "Последний комментарий от {$lastComment->createdBy->name}: "; + echo substr($lastComment->msg, 0, 100) . "..."; +} +``` + +### Комментарии пользователя + +```php +$myComments = Comment::find() + ->where(['created_by' => Yii::$app->user->id]) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(20) + ->all(); + +foreach ($myComments as $comment) { + echo "{$comment->entity} #{$comment->entity_id}: {$comment->msg}\n"; +} +``` + +### Поиск по тексту комментариев + +```php +$found = Comment::find() + ->where(['like', 'msg', 'срочно']) + ->andWhere(['entity' => 'task']) + ->all(); + +echo "Найдено " . count($found) . " комментариев со словом 'срочно'"; +``` + +### Удаление комментариев сущности + +```php +// При удалении задачи удаляем все её комментарии +Comment::deleteAll([ + 'entity' => 'task', + 'entity_id' => $taskId +]); + +// Также удаляем файлы комментариев +Files::deleteAll([ + 'entity' => 'task_comment', + 'entity_id' => $commentIds +]); +``` + +### Статистика комментариев + +```php +// Количество комментариев по сущностям +$stats = Comment::find() + ->select(['entity', 'COUNT(*) as count']) + ->groupBy('entity') + ->asArray() + ->all(); + +foreach ($stats as $row) { + echo "{$row['entity']}: {$row['count']} комментариев\n"; +} + +// Самые активные комментаторы +$activeUsers = Comment::find() + ->select(['created_by', 'COUNT(*) as count']) + ->groupBy('created_by') + ->orderBy(['count' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `msg` | Обязательное, макс. 1000 символов | +| `created_at` | Обязательное, безопасное | +| `created_by` | Обязательное, целое число | +| `entity` | Обязательное, макс. 50 символов | +| `entity_id` | Обязательное, целое число | +| `files` | Файлы, макс. 20 шт., необязательно | + +--- + +## Полиморфная связь + +Модель использует паттерн **Polymorphic Association**: + +``` +entity = 'task' → Task::findOne($entity_id) +entity = 'check_conduct' → CheckConduct::findOne($entity_id) +entity = 'order' → StoreOrders::findOne($entity_id) +``` + +Это позволяет использовать одну таблицу комментариев для всех сущностей системы. + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — авторы комментариев +- **[Task](./Task.md)** — задачи +- **[CheckConduct](./CheckConduct.md)** — проведённые проверки +- **[StoreOrders](./StoreOrders.md)** — заказы +- **[Files](./Files.md)** — прикреплённые файлы + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CommunicationType.md b/erp24/docs/models/CommunicationType.md new file mode 100644 index 00000000..453de399 --- /dev/null +++ b/erp24/docs/models/CommunicationType.md @@ -0,0 +1,270 @@ +# Модель CommunicationType + + +## Mindmap + +```mermaid +mindmap + root((CommunicationType)) + Таблица БД + communication_type + Свойства + id + int + name + string + alias + string + bgcolor + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CommunicationType` представляет справочник типов коммуникаций в системе. Хранит названия, алиасы и цвета для визуального отображения различных видов взаимодействий: звонки, сообщения, встречи и т.д. Используется для классификации и визуализации коммуникаций с клиентами и сотрудниками. + +**Файл модели:** `erp24/records/CommunicationType.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `communication_type` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Название типа коммуникации | +| `alias` | VARCHAR(100) | Алиас для программного использования | +| `bgcolor` | VARCHAR(20) | Цвет фона для UI (HEX или CSS-класс) | + +--- + +## Описание полей + +### `name` — Название + +Человекочитаемое название типа коммуникации: +- "Входящий звонок" +- "Исходящий звонок" +- "SMS-сообщение" +- "Email" +- "Личная встреча" +- "Telegram" + +### `alias` — Алиас + +Программный идентификатор для использования в коде: +- `incoming_call` +- `outgoing_call` +- `sms` +- `email` +- `meeting` +- `telegram` + +### `bgcolor` — Цвет фона + +Цвет для визуального отображения в интерфейсе: +- HEX-формат: `#4CAF50`, `#2196F3` +- CSS-класс: `bg-success`, `bg-info` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + communication_type ||--o{ communication_log : "categorizes" + + communication_type { + int id PK + string name + string alias + string bgcolor + } + + communication_log { + int id PK + int type_id FK + string content + timestamp created_at + } +``` + +--- + +## Примеры использования + +### Создание типа коммуникации + +```php +$type = new CommunicationType(); +$type->name = 'WhatsApp'; +$type->alias = 'whatsapp'; +$type->bgcolor = '#25D366'; // Зелёный WhatsApp +$type->save(); +``` + +### Получение всех типов + +```php +$types = CommunicationType::find()->all(); + +foreach ($types as $type) { + echo "{$type->name}"; +} +``` + +### Построение выпадающего списка + +```php +$list = CommunicationType::find() + ->select(['name', 'id']) + ->indexBy('id') + ->column(); + +echo Html::dropDownList('type_id', $selectedId, $list, [ + 'class' => 'form-control', + 'prompt' => 'Выберите тип' +]); +``` + +### Получение типа по алиасу + +```php +$callType = CommunicationType::findOne(['alias' => 'incoming_call']); + +if ($callType) { + echo "ID типа 'Входящий звонок': {$callType->id}"; +} +``` + +### Статистика коммуникаций по типам + +```php +// Предполагая наличие модели CommunicationLog +$stats = CommunicationLog::find() + ->select(['type_id', 'COUNT(*) as count']) + ->groupBy('type_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $type = CommunicationType::findOne($row['type_id']); + echo "{$type->name}: {$row['count']} коммуникаций\n"; +} +``` + +### Цветовая маркировка в таблице + +```php +$types = CommunicationType::find()->indexBy('id')->all(); + +foreach ($communications as $comm) { + $type = $types[$comm->type_id] ?? null; + $color = $type ? $type->bgcolor : '#ccc'; + $name = $type ? $type->name : 'Неизвестно'; + + echo ""; + echo "{$name}"; + echo "{$comm->content}"; + echo "{$comm->created_at}"; + echo ""; +} +``` + +### Группировка по типам для отчёта + +```php +$types = CommunicationType::find()->indexBy('alias')->all(); + +$report = [ + 'calls' => [ + 'incoming' => 0, + 'outgoing' => 0 + ], + 'messages' => [ + 'sms' => 0, + 'email' => 0, + 'telegram' => 0 + ] +]; + +$communications = CommunicationLog::find() + ->where(['>=', 'created_at', $startDate]) + ->all(); + +foreach ($communications as $comm) { + $type = CommunicationType::findOne($comm->type_id); + + switch ($type->alias) { + case 'incoming_call': + $report['calls']['incoming']++; + break; + case 'outgoing_call': + $report['calls']['outgoing']++; + break; + case 'sms': + $report['messages']['sms']++; + break; + // и т.д. + } +} +``` + +### Фильтрация коммуникаций по типу + +```php +// Только звонки +$callTypes = CommunicationType::find() + ->where(['like', 'alias', 'call']) + ->select('id') + ->column(); + +$calls = CommunicationLog::find() + ->where(['type_id' => $callTypes]) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | +| `alias` | Обязательное, макс. 100 символов | +| `bgcolor` | Обязательное, макс. 20 символов | + +--- + +## Типичные типы коммуникаций + +| Алиас | Название | Цвет | +|-------|----------|------| +| `incoming_call` | Входящий звонок | #4CAF50 (зелёный) | +| `outgoing_call` | Исходящий звонок | #2196F3 (синий) | +| `missed_call` | Пропущенный звонок | #f44336 (красный) | +| `sms` | SMS-сообщение | #9C27B0 (фиолетовый) | +| `email` | Email | #FF9800 (оранжевый) | +| `telegram` | Telegram | #0088cc (голубой) | +| `whatsapp` | WhatsApp | #25D366 (зелёный) | +| `meeting` | Личная встреча | #795548 (коричневый) | +| `video_call` | Видеозвонок | #00BCD4 (циан) | + +--- + +## Связанные модели + +- **[CommunicationLog](./CommunicationLog.md)** — журнал коммуникаций +- **[Users](./Users.md)** — клиенты +- **[Admin](./Admin.md)** — сотрудники +- **[Task](./Task.md)** — задачи по коммуникациям + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Companies.md b/erp24/docs/models/Companies.md new file mode 100644 index 00000000..0e79e045 --- /dev/null +++ b/erp24/docs/models/Companies.md @@ -0,0 +1,261 @@ +# Модель Companies + + +## Mindmap + +```mermaid +mindmap + root((Companies)) + Таблица БД + companies + Свойства + id + int + name + string + name_type + int + yk + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Companies` представляет юридические лица (компании) в системе ERP24. Хранит информацию о наименовании, типе юрлица и принадлежности к управляющей компании. Используется для структурирования бизнеса по юридическим единицам и распределения магазинов между компаниями. + +**Файл модели:** `erp24/records/Companies.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `companies` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(40) | Наименование юридического лица | +| `name_type` | INTEGER | Тип юридического лица | +| `yk` | INTEGER | Статус принадлежности к управляющей компании | +| `created_at` | TIMESTAMP | Дата создания записи | + +--- + +## Описание полей + +### `name` — Наименование + +Краткое наименование юридического лица. Примеры: +- "ООО Цветы" +- "ИП Иванов" +- "АО Флора" + +### `name_type` — Тип юрлица + +Числовой код организационно-правовой формы: +- `1` — ООО (Общество с ограниченной ответственностью) +- `2` — ИП (Индивидуальный предприниматель) +- `3` — АО (Акционерное общество) +- `4` — ЗАО (Закрытое акционерное общество) + +### `yk` — Управляющая компания + +Флаг принадлежности к управляющей компании (холдингу): +- `1` — входит в УК +- `0` — независимая компания + +--- + +## Диаграмма связей + +```mermaid +erDiagram + companies ||--o{ company_stores : "has" + companies ||--o{ admin : "employs" + + companies { + int id PK + string name + int name_type + int yk + timestamp created_at + } + + company_stores { + int id PK + int company_id FK + int city_id FK + string address + } + + admin { + int id PK + int company_id FK + string name + } +``` + +--- + +## Примеры использования + +### Создание юрлица + +```php +$company = new Companies(); +$company->name = 'Цветочный рай'; +$company->name_type = 1; // ООО +$company->yk = 1; // Входит в УК +$company->created_at = date('Y-m-d H:i:s'); +$company->save(); +``` + +### Получение всех компаний УК + +```php +$ukCompanies = Companies::find() + ->where(['yk' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($ukCompanies as $company) { + echo "{$company->name}\n"; +} +``` + +### Построение выпадающего списка + +```php +$list = Companies::find() + ->select(['name', 'id']) + ->indexBy('id') + ->column(); + +echo Html::dropDownList('company_id', $selectedId, $list, [ + 'class' => 'form-control', + 'prompt' => 'Выберите компанию' +]); +``` + +### Получение с типом юрлица + +```php +$types = [ + 1 => 'ООО', + 2 => 'ИП', + 3 => 'АО', + 4 => 'ЗАО' +]; + +$companies = Companies::find()->all(); + +foreach ($companies as $company) { + $typeName = $types[$company->name_type] ?? 'Неизвестно'; + echo "{$typeName} \"{$company->name}\"\n"; +} +``` + +### Статистика по типам + +```php +$stats = Companies::find() + ->select(['name_type', 'COUNT(*) as count']) + ->groupBy('name_type') + ->asArray() + ->all(); + +$types = [1 => 'ООО', 2 => 'ИП', 3 => 'АО', 4 => 'ЗАО']; + +foreach ($stats as $row) { + $typeName = $types[$row['name_type']] ?? 'Другое'; + echo "{$typeName}: {$row['count']} компаний\n"; +} +``` + +### Поиск компании + +```php +$found = Companies::find() + ->where(['like', 'name', 'цвет']) + ->all(); + +echo "Найдено " . count($found) . " компаний"; +``` + +### Компании с магазинами + +```php +// Получение компаний с количеством магазинов +$companies = Companies::find() + ->select([ + 'companies.*', + 'COUNT(company_stores.id) as store_count' + ]) + ->leftJoin('company_stores', 'company_stores.company_id = companies.id') + ->groupBy('companies.id') + ->asArray() + ->all(); + +foreach ($companies as $company) { + echo "{$company['name']}: {$company['store_count']} магазинов\n"; +} +``` + +### Фильтрация по дате создания + +```php +// Компании, созданные в 2024 году +$newCompanies = Companies::find() + ->where(['between', 'created_at', '2024-01-01', '2024-12-31']) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 40 символов | +| `name_type` | Обязательное, целое число | +| `yk` | Обязательное, целое число | +| `created_at` | Обязательное, безопасное | + +--- + +## Организационно-правовые формы + +| Код | Форма | Описание | +|-----|-------|----------| +| 1 | ООО | Общество с ограниченной ответственностью | +| 2 | ИП | Индивидуальный предприниматель | +| 3 | АО | Акционерное общество | +| 4 | ЗАО | Закрытое акционерное общество | + +--- + +## Бизнес-логика + +1. **Холдинговая структура** — компании группируются по принадлежности к УК +2. **Юридическое разделение** — разные юрлица для разных регионов/направлений +3. **Учёт по компаниям** — финансовый учёт ведётся отдельно по каждой компании +4. **Трудоустройство** — сотрудники оформлены в конкретных юрлицах + +--- + +## Связанные модели + +- **[CompanyStores](./CompanyStores.md)** — магазины компании +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — торговые точки +- **[CompanyFunctions](./CompanyFunctions.md)** — функции компании + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Company.md b/erp24/docs/models/Company.md new file mode 100644 index 00000000..be0d7b98 --- /dev/null +++ b/erp24/docs/models/Company.md @@ -0,0 +1,151 @@ +# Модель Company + + +## Mindmap + +```mermaid +mindmap + root((Company)) + Таблица БД + company + Свойства + id + int + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Company` представляет справочник компаний (юридических лиц). Хранит названия компаний, входящих в холдинг или используемых для ведения учёта. Применяется для разграничения данных по юридическим лицам и формирования отчётности. + +**Файл модели:** `erp24/records/Company.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `company` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Название компании | + +--- + +## Описание полей + +### `id` — Идентификатор + +Уникальный идентификатор компании в системе. + +### `name` — Название компании + +Полное или сокращённое название юридического лица. + +**Примеры:** +- `"ООО Цветочная база"` +- `"ИП Иванов И.И."` +- `"АО Флора"` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + company ||--o{ city_store : "owns" + company ||--o{ admin : "employs" + + company { + int id PK + string name + } + + city_store { + int id PK + int company_id FK + string name + } + + admin { + int id PK + int company_id FK + string name + } +``` + +--- + +## Примеры использования + +### Получение всех компаний + +```php +$companies = Company::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Получение компании по ID + +```php +$company = Company::findOne($companyId); +echo $company->name; +``` + +### Использование в выпадающем списке + +```php +use yii\helpers\ArrayHelper; + +$companies = Company::find()->all(); +$companyList = ArrayHelper::map($companies, 'id', 'name'); + +echo Html::dropDownList('company_id', $selected, $companyList, [ + 'prompt' => 'Выберите компанию' +]); +``` + +### Фильтрация магазинов по компании + +```php +$stores = CityStore::find() + ->where(['company_id' => $companyId]) + ->all(); +``` + +### Создание новой компании + +```php +$company = new Company(); +$company->name = 'ООО Новая компания'; +if ($company->save()) { + echo "Компания создана с ID: " . $company->id; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазины компании +- **[Admin](./Admin.md)** — сотрудники компании +- **AdminGroupCompanyFunctionVisibility** — настройки видимости функций по компаниям + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CompanyFunctionAdmins.md b/erp24/docs/models/CompanyFunctionAdmins.md new file mode 100644 index 00000000..1f9e095b --- /dev/null +++ b/erp24/docs/models/CompanyFunctionAdmins.md @@ -0,0 +1,171 @@ +# Модель CompanyFunctionAdmins + + +## Mindmap + +```mermaid +mindmap + root((CompanyFunctionAdmins)) + Таблица БД + company_function_admins + Свойства + id + int + company_function_id + int + type_id + int + Связи + Admin + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CompanyFunctionAdmins` связывает сотрудников с функциями компании. Определяет, кто является администратором (ответственным) и кто исполнителем для каждой функции. Используется для распределения ответственности и назначения задач. + +**Файл модели:** `erp24/records/CompanyFunctionAdmins.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `company_function_admins` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admin.id) | +| `company_function_id` | INTEGER | ID функции (FK → company_functions.id) | +| `type_id` | INTEGER | Тип роли: 0 — администратор, 1 — исполнитель | +| `group_id` | INTEGER | ID должности (FK → admin_group.id) | + +--- + +## Описание полей + +### `type_id` — Тип роли + +- `0` — **Администратор** (ответственный за функцию) +- `1` — **Исполнитель** (выполняет задачи по функции) + +### `admin_id` vs `group_id` + +Связь может быть либо с конкретным сотрудником (`admin_id`), либо с должностью (`group_id`): +- Если указан `admin_id` — конкретный человек +- Если указан `group_id` — все сотрудники этой должности + +--- + +## Связи (Relations) + +### `getAdmin(): ActiveQuery` + +Возвращает сотрудника. + +```php +$link = CompanyFunctionAdmins::findOne($id); +$employee = $link->admin; // Admin +echo "Сотрудник: {$employee->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + company_function_admins }o--|| company_functions : "belongs_to" + company_function_admins }o--o| admin : "assigned_admin" + company_function_admins }o--o| admin_group : "assigned_group" + + company_function_admins { + int id PK + int admin_id FK + int company_function_id FK + int type_id + int group_id FK + } +``` + +--- + +## Примеры использования + +### Назначение администратора функции + +```php +$link = new CompanyFunctionAdmins(); +$link->company_function_id = $functionId; +$link->admin_id = $adminId; +$link->type_id = 0; // Администратор +$link->save(); +``` + +### Назначение исполнителя + +```php +$link = new CompanyFunctionAdmins(); +$link->company_function_id = $functionId; +$link->admin_id = $executorId; +$link->type_id = 1; // Исполнитель +$link->save(); +``` + +### Получение ответственного за функцию + +```php +$responsible = CompanyFunctionAdmins::find() + ->with('admin') + ->where([ + 'company_function_id' => $functionId, + 'type_id' => 0 + ]) + ->one(); + +if ($responsible) { + echo "Ответственный: {$responsible->admin->name}"; +} +``` + +### Получение всех исполнителей функции + +```php +$executors = CompanyFunctionAdmins::find() + ->with('admin') + ->where([ + 'company_function_id' => $functionId, + 'type_id' => 1 + ]) + ->all(); + +foreach ($executors as $exec) { + echo "Исполнитель: {$exec->admin->name}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `company_function_id` | Обязательное, целое число | +| `type_id` | Обязательное, целое число | +| `admin_id`, `group_id` | Целые числа (необязательные) | + +--- + +## Связанные модели + +- **[CompanyFunctions](./CompanyFunctions.md)** — функции компании +- **[Admin](./Admin.md)** — сотрудники +- **[AdminGroup](./AdminGroup.md)** — должности + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CompanyFunctions.md b/erp24/docs/models/CompanyFunctions.md new file mode 100644 index 00000000..a56d9d44 --- /dev/null +++ b/erp24/docs/models/CompanyFunctions.md @@ -0,0 +1,347 @@ +# Модель CompanyFunctions + + +## Mindmap + +```mermaid +mindmap + root((CompanyFunctions)) + Таблица БД + company_functions + Свойства + id + int + parent_id + int + group_id + int + type_id + int + name + string + description + string + Связи + TaskTemplates + 1:N TaskTemplates + Admin + 1:1 CompanyFunctionAdmins + Executors + 1:N CompanyFunctionAdmins + TechnicalRequests + 1:N Task + FunctionRegulations + 1:N FunctionRegulations + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CompanyFunctions` представляет функции (обязанности/процессы) компании в иерархической структуре. Описывает бизнес-процессы, их ответственных исполнителей, связанные шаблоны задач и регламенты. Используется для построения организационной структуры и управления ответственностью за процессы. + +**Файл модели:** `erp24/records/CompanyFunctions.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `company_functions` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `parent_id` | INTEGER | ID родительской функции (для иерархии) | +| `group_id` | INTEGER | ID группы функций | +| `type_id` | INTEGER | ID типа функции | +| `entity` | VARCHAR(30) | Прикреплённый список сущностей (например, магазины) | +| `name` | VARCHAR(250) | Название функции | +| `description` | TEXT | Описание функции | +| `result` | TEXT | Ожидаемый результат выполнения | +| `posit` | INTEGER | Позиция для сортировки | +| `bgcolor` | TEXT | Цвет фона для UI | + +--- + +## Описание полей + +### `parent_id` — Родительская функция + +Ссылка на родительскую функцию для построения иерархии: +- `0` — корневая функция (направление деятельности) +- Значение > 0 — подфункция + +### `entity` — Привязка к сущностям + +Определяет, к каким сущностям привязана функция: +- `stores` — магазины +- `warehouses` — склады +- `all` — все сущности + +### `result` — Ожидаемый результат + +Описание измеримого результата выполнения функции. Используется для оценки эффективности. + +--- + +## Связи (Relations) + +### `getTaskTemplates(): ActiveQuery` + +Возвращает шаблоны задач, связанные с функцией (только корневые шаблоны). + +```php +$function = CompanyFunctions::findOne($id); +$templates = $function->taskTemplates; // TaskTemplates[] +``` + +### `getAdmin(): ActiveQuery` + +Возвращает ответственного администратора функции (type_id = 0). + +```php +$responsible = $function->admin; // CompanyFunctionAdmins +echo "Ответственный: {$responsible->admin->name}"; +``` + +### `getExecutors(): ActiveQuery` + +Возвращает исполнителей функции (type_id = 1). + +```php +$executors = $function->executors; // CompanyFunctionAdmins[] +foreach ($executors as $exec) { + echo "Исполнитель: {$exec->admin->name}\n"; +} +``` + +### `getTechnicalRequests(): ActiveQuery` + +Возвращает технические заявки по функции (task_type_id = 10). + +```php +$requests = $function->technicalRequests; // Task[] +``` + +### `getFunctionRegulations(): ActiveQuery` + +Возвращает связи с регламентами. + +```php +$links = $function->functionRegulations; // FunctionRegulations[] +``` + +### `getRegulations(): ActiveQuery` + +Возвращает регламенты функции через промежуточную таблицу. + +```php +$regulations = $function->regulations; // Regulations[] +``` + +### `getVisibilityConfig(): ActiveQuery` + +Возвращает настройки видимости для текущего пользователя. + +```php +$visibility = $function->visibilityConfig; // AdminGroupCompanyFunctionVisibility +if ($visibility && $visibility->hide_function) { + echo "Функция скрыта для вашей должности"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + company_functions ||--o{ company_functions : "parent" + company_functions ||--o{ task_templates : "has_templates" + company_functions ||--o{ company_function_admins : "has_admins" + company_functions ||--o{ function_regulations : "has_regulations" + company_functions ||--o{ task : "has_requests" + company_functions ||--o| admin_group_company_function_visibility : "has_visibility" + + company_functions { + int id PK + int parent_id FK + int group_id + int type_id + string entity + string name + text description + text result + int posit + string bgcolor + } + + company_function_admins { + int id PK + int company_function_id FK + int admin_id FK + int type_id + } + + task_templates { + int id PK + int company_function_id FK + string name + } +``` + +--- + +## Примеры использования + +### Создание функции + +```php +$function = new CompanyFunctions(); +$function->parent_id = 0; // Корневая функция +$function->group_id = 1; +$function->name = 'Управление продажами'; +$function->description = 'Организация и контроль продаж во всех каналах'; +$function->result = 'Выполнение плана продаж на 100%'; +$function->posit = 1; +$function->bgcolor = '#4CAF50'; +$function->save(); +``` + +### Получение дерева функций + +```php +function buildFunctionTree($parentId = 0, $level = 0) { + $functions = CompanyFunctions::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + + foreach ($functions as $func) { + echo str_repeat(' ', $level) . "- {$func->name}\n"; + buildFunctionTree($func->id, $level + 1); + } +} + +buildFunctionTree(); +``` + +### Функции с ответственными + +```php +$functions = CompanyFunctions::find() + ->with(['admin.admin']) + ->where(['parent_id' => 0]) + ->all(); + +foreach ($functions as $func) { + $responsible = $func->admin ? $func->admin->admin->name : 'Не назначен'; + echo "{$func->name} — {$responsible}\n"; +} +``` + +### Получение регламентов функции + +```php +$function = CompanyFunctions::findOne($id); +$regulations = $function->regulations; + +echo "Регламенты для '{$function->name}':\n"; +foreach ($regulations as $reg) { + echo "- {$reg->name}\n"; +} +``` + +### Фильтрация по видимости + +```php +$visibleFunctions = CompanyFunctions::find() + ->alias('cf') + ->leftJoin( + 'admin_group_company_function_visibility v', + 'v.company_function_id = cf.id AND v.admin_group_id = :group', + [':group' => $currentUserGroupId] + ) + ->andWhere([ + 'or', + ['v.id' => null], + ['v.hide_function' => 0] + ]) + ->all(); +``` + +### Технические заявки по функции + +```php +$function = CompanyFunctions::findOne($id); +$requests = $function->technicalRequests; + +echo "Технические заявки ({$function->name}):\n"; +foreach ($requests as $task) { + echo "- #{$task->id}: {$task->name} [{$task->status}]\n"; +} +``` + +### Статистика по функциям + +```php +// Количество задач по каждой функции +$stats = Task::find() + ->select(['company_function_id', 'COUNT(*) as count']) + ->groupBy('company_function_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $func = CompanyFunctions::findOne($row['company_function_id']); + if ($func) { + echo "{$func->name}: {$row['count']} задач\n"; + } +} +``` + +### Шаблоны задач функции + +```php +$function = CompanyFunctions::findOne($id); +$templates = $function->taskTemplates; + +echo "Шаблоны задач для '{$function->name}':\n"; +foreach ($templates as $template) { + echo "- {$template->name}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `parent_id` | Обязательное, целое число | +| `group_id` | Обязательное, целое число | +| `name` | Обязательное, макс. 250 символов | +| `description` | Обязательное, строка | +| `result` | Обязательное, строка | +| `posit` | Обязательное, целое число | +| `type_id` | Целое число | +| `entity` | Строка, макс. 30 символов | +| `bgcolor` | Строка | + +--- + +## Связанные модели + +- **[CompanyFunctionAdmins](./CompanyFunctionAdmins.md)** — ответственные и исполнители +- **[TaskTemplates](./TaskTemplates.md)** — шаблоны задач +- **[FunctionRegulations](./FunctionRegulations.md)** — связь с регламентами +- **[Regulations](./Regulations.md)** — регламенты +- **[Task](./Task.md)** — задачи +- **[AdminGroupCompanyFunctionVisibility](./AdminGroupCompanyFunctionVisibility.md)** — настройки видимости +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/CompanySearch.md b/erp24/docs/models/CompanySearch.md new file mode 100644 index 00000000..b1b7f075 --- /dev/null +++ b/erp24/docs/models/CompanySearch.md @@ -0,0 +1,137 @@ +# Класс: CompanySearch + + +## Mindmap + +```mermaid +mindmap + root((CompanySearch)) + Таблица БД + ActiveRecord + Наследование + extends Company +``` + +## Назначение +Search-модель для поиска и фильтрации юридических лиц (организаций) в ERP24. Простая модель для поиска по ID и названию компании. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Company` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Company::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id + - LIKE: name + +## Диаграмма связей + +```mermaid +erDiagram + Company { + int id PK + varchar name + } + + Admin { + int id PK + int org_id FK + } + + CityStore { + int id PK + int firma_id FK + } + + Company ||--o{ Admin : "org_id" + Company ||--o{ CityStore : "firma_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new CompanySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new CompanySearch(); +$dataProvider = $searchModel->search([ + 'CompanySearch' => [ + 'name' => 'ООО', + ] +]); +``` + +### Поиск по ID +```php +$searchModel = new CompanySearch(); +$dataProvider = $searchModel->search([ + 'CompanySearch' => [ + 'id' => 1, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + ], +]) ?> +``` + +## Связанные модели + +- [Company](./Company.md) — базовая модель организации +- [Admin](./Admin.md) — сотрудники (org_id) +- [CityStore](./CityStore.md) — магазины (firma_id) + +## Особенности реализации + +1. **Минимальная модель**: Только id и name +2. **LIKE-поиск**: Для названия компании +3. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/CompanyStores.md b/erp24/docs/models/CompanyStores.md new file mode 100644 index 00000000..f29f0bbd --- /dev/null +++ b/erp24/docs/models/CompanyStores.md @@ -0,0 +1,272 @@ +# Модель CompanyStores + + +## Mindmap + +```mermaid +mindmap + root((CompanyStores)) + Таблица БД + company_stores + Свойства + id + int + company_id + int + city_id + int + address + string + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `CompanyStores` связывает юридические лица (компании) с торговыми точками. Хранит информацию об адресе магазина и датах открытия (плановой и фактической). Используется для юридического распределения магазинов между компаниями холдинга. + +**Файл модели:** `erp24/records/CompanyStores.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `company_stores` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `company_id` | INTEGER | ID компании (FK → companies.id) | +| `city_id` | INTEGER | ID города (FK → our_cities.id) | +| `address` | VARCHAR(80) | Улица, дом, корпус, литера | +| `created_at` | TIMESTAMP | Дата создания записи | +| `date_opening_plan` | DATE | Дата планового открытия | +| `date_opening_fact` | DATE | Дата фактического открытия | + +--- + +## Описание полей + +### `company_id` — Компания + +Ссылка на юридическое лицо, которому принадлежит магазин. + +### `city_id` — Город + +Ссылка на город из справочника `our_cities`. + +### `address` — Адрес + +Полный адрес магазина без указания города: +- "ул. Ленина, д. 15" +- "пр. Мира, 42, корп. 2" +- "ТЦ Гранд, 3 этаж" + +### `date_opening_plan` / `date_opening_fact` — Даты открытия + +- `date_opening_plan` — планируемая дата начала коммерческой деятельности +- `date_opening_fact` — фактическая дата открытия + +Сравнение этих дат позволяет анализировать соблюдение сроков. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + company_stores }o--|| companies : "belongs_to" + company_stores }o--|| our_cities : "located_in" + + company_stores { + int id PK + int company_id FK + int city_id FK + string address + timestamp created_at + date date_opening_plan + date date_opening_fact + } + + companies { + int id PK + string name + } + + our_cities { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание записи о магазине + +```php +$store = new CompanyStores(); +$store->company_id = $companyId; +$store->city_id = $cityId; +$store->address = 'ул. Цветочная, д. 5'; +$store->created_at = date('Y-m-d H:i:s'); +$store->date_opening_plan = '2025-03-01'; +$store->date_opening_fact = null; // Ещё не открыт +$store->save(); +``` + +### Получение магазинов компании + +```php +$stores = CompanyStores::find() + ->where(['company_id' => $companyId]) + ->all(); + +foreach ($stores as $store) { + echo "{$store->address}, город ID: {$store->city_id}\n"; +} +``` + +### Магазины с информацией о компании и городе + +```php +$stores = CompanyStores::find() + ->alias('cs') + ->select(['cs.*', 'c.name as company_name', 'oc.name as city_name']) + ->leftJoin('companies c', 'c.id = cs.company_id') + ->leftJoin('our_cities oc', 'oc.id = cs.city_id') + ->asArray() + ->all(); + +foreach ($stores as $store) { + echo "{$store['company_name']}: {$store['city_name']}, {$store['address']}\n"; +} +``` + +### Магазины, ожидающие открытия + +```php +$pendingOpening = CompanyStores::find() + ->where(['date_opening_fact' => null]) + ->andWhere(['IS NOT', 'date_opening_plan', null]) + ->orderBy(['date_opening_plan' => SORT_ASC]) + ->all(); + +foreach ($pendingOpening as $store) { + echo "Планируемое открытие: {$store->date_opening_plan}, {$store->address}\n"; +} +``` + +### Анализ сроков открытия + +```php +$stores = CompanyStores::find() + ->where(['IS NOT', 'date_opening_plan', null]) + ->andWhere(['IS NOT', 'date_opening_fact', null]) + ->all(); + +$delayed = 0; +$onTime = 0; + +foreach ($stores as $store) { + if ($store->date_opening_fact > $store->date_opening_plan) { + $delayed++; + $days = (strtotime($store->date_opening_fact) - strtotime($store->date_opening_plan)) / 86400; + echo "Задержка {$days} дней: {$store->address}\n"; + } else { + $onTime++; + } +} + +echo "\nВсего: в срок - {$onTime}, с задержкой - {$delayed}"; +``` + +### Статистика по компаниям + +```php +$stats = CompanyStores::find() + ->select(['company_id', 'COUNT(*) as store_count']) + ->groupBy('company_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $company = Companies::findOne($row['company_id']); + echo "{$company->name}: {$row['store_count']} магазинов\n"; +} +``` + +### Статистика по городам + +```php +$stats = CompanyStores::find() + ->select(['city_id', 'COUNT(*) as store_count']) + ->groupBy('city_id') + ->asArray() + ->all(); + +foreach ($stats as $row) { + $city = OurCities::findOne($row['city_id']); + echo "{$city->name}: {$row['store_count']} магазинов\n"; +} +``` + +### Фиксация фактического открытия + +```php +$store = CompanyStores::findOne($id); +$store->date_opening_fact = date('Y-m-d'); +$store->save(); + +echo "Магазин {$store->address} открыт!"; +``` + +### Поиск магазина по адресу + +```php +$found = CompanyStores::find() + ->where(['like', 'address', 'Ленина']) + ->all(); + +echo "Найдено " . count($found) . " магазинов на ул. Ленина"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `company_id` | Обязательное, целое число | +| `city_id` | Обязательное, целое число | +| `address` | Обязательное, макс. 80 символов | +| `created_at` | Обязательное, безопасное | +| `date_opening_plan` | Дата, безопасное | +| `date_opening_fact` | Дата, безопасное | + +--- + +## Бизнес-логика + +1. **Юридическая привязка** — каждый магазин принадлежит конкретному юрлицу +2. **Географическое распределение** — привязка к городам для отчётности +3. **Контроль открытий** — отслеживание плановых и фактических дат +4. **Многофилиальность** — одна компания может иметь несколько магазинов + +--- + +## Связанные модели + +- **[Companies](./Companies.md)** — юридические лица +- **[OurCities](./OurCities.md)** — города +- **[CityStore](./CityStore.md)** — торговые точки (детальная информация) +- **[Admin](./Admin.md)** — сотрудники магазинов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Contest001.md b/erp24/docs/models/Contest001.md new file mode 100644 index 00000000..05e28ba3 --- /dev/null +++ b/erp24/docs/models/Contest001.md @@ -0,0 +1,237 @@ +# Класс: Contest001 + + +## Mindmap + +```mermaid +mindmap + root((Contest001)) + Таблица БД + contest001 + Свойства + id + int + number + int + client_id + int + phone + string + checks_json + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель участника маркетингового конкурса (Contest 001). Хранит информацию об участниках конкурса с мая 2023 года, их покупках и порядковом номере участия. Интегрирована с Salebot для работы с клиентской базой. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`contest001` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `number` | int | Порядковый номер участника (1, 2, 3...) по дате первого чека с 22.05.2023 | +| `client_id` | int | ID клиента в системе Salebot | +| `phone` | varchar(50) | Телефон участника в формате 79876543210 | +| `name` | varchar(50) / null | Имя участника | +| `checks_json` | text | JSON со списком покупок участника | + +## Структура JSON поля checks_json + +```json +[ + { + "check_id": "abc-123-def", + "store_id_1c": "store-guid-001", + "date": "2023-05-22 10:30:00", + "summ": 1500.00, + "type_of_store": "розница" + }, + { + "check_id": "ghi-456-jkl", + "store_id_1c": "store-guid-002", + "date": "2023-05-25 14:15:00", + "summ": 2300.00, + "type_of_store": "доставка" + } +] +``` + +### Поля объекта покупки + +| Поле | Тип | Описание | +|------|-----|----------| +| `check_id` | string | GUID чека | +| `store_id_1c` | string | GUID магазина из 1С | +| `date` | datetime | Дата и время покупки | +| `summ` | number | Сумма покупки | +| `type_of_store` | string | Тип точки: "розница" или "доставка" | + +## Диаграмма связей + +```mermaid +erDiagram + Contest001 { + int id PK + int number + int client_id + varchar phone + varchar name + text checks_json + } + + SalebotClient { + int id PK + varchar phone + varchar name + } + + CreateChecks { + varchar guid PK + decimal summ + datetime date + } + + Store { + varchar id_1c PK + varchar name + } + + Contest001 ||--|| SalebotClient : "client_id" + Contest001 }o--|{ CreateChecks : "checks_json (логическая)" + Contest001 }o--|{ Store : "checks_json.store_id_1c" +``` + +## Примеры использования + +### Регистрация нового участника конкурса +```php +// Определяем следующий номер участника +$maxNumber = Contest001::find()->max('number') ?? 0; + +$participant = new Contest001(); +$participant->number = $maxNumber + 1; +$participant->client_id = $salebotClientId; +$participant->phone = '79876543210'; +$participant->name = 'Иван Петров'; +$participant->checks_json = json_encode([ + [ + 'check_id' => $check->guid, + 'store_id_1c' => $check->store_id, + 'date' => $check->date, + 'summ' => $check->summ, + 'type_of_store' => 'розница' + ] +]); +$participant->save(); +``` + +### Добавление покупки участнику +```php +$participant = Contest001::findOne(['phone' => '79876543210']); +$checks = json_decode($participant->checks_json, true); + +$checks[] = [ + 'check_id' => $newCheck->guid, + 'store_id_1c' => $newCheck->store_id, + 'date' => $newCheck->date, + 'summ' => $newCheck->summ, + 'type_of_store' => 'доставка' +]; + +$participant->checks_json = json_encode($checks); +$participant->save(); +``` + +### Подсчёт общей суммы покупок участника +```php +$participant = Contest001::findOne($id); +$checks = json_decode($participant->checks_json, true); +$totalSum = array_sum(array_column($checks, 'summ')); +``` + +### Получение участников с сортировкой по номеру +```php +$participants = Contest001::find() + ->orderBy(['number' => SORT_ASC]) + ->all(); +``` + +### Поиск участника по телефону +```php +$participant = Contest001::findOne(['phone' => '79876543210']); +``` + +### Фильтрация по типу покупок +```php +$participants = Contest001::find()->all(); + +$retailParticipants = array_filter($participants, function($p) { + $checks = json_decode($p->checks_json, true); + foreach ($checks as $check) { + if ($check['type_of_store'] === 'розница') { + return true; + } + } + return false; +}); +``` + +### Статистика конкурса +```php +$stats = [ + 'total_participants' => Contest001::find()->count(), + 'total_checks' => 0, + 'total_sum' => 0, + 'retail_sum' => 0, + 'delivery_sum' => 0, +]; + +$participants = Contest001::find()->all(); +foreach ($participants as $p) { + $checks = json_decode($p->checks_json, true); + $stats['total_checks'] += count($checks); + foreach ($checks as $check) { + $stats['total_sum'] += $check['summ']; + if ($check['type_of_store'] === 'розница') { + $stats['retail_sum'] += $check['summ']; + } else { + $stats['delivery_sum'] += $check['summ']; + } + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `number` | required, integer | +| `client_id` | required, integer | +| `phone` | required, string (max 50) | +| `name` | string (max 50), nullable | +| `checks_json` | required, string | + +## Связанные модели + +- [CreateChecks](./CreateChecks.md) — чеки покупок (логическая связь через JSON) +- [Store](./Store.md) — магазины (логическая связь через JSON) + +## Особенности реализации + +1. **Порядковый номер**: Участники нумеруются последовательно по дате первой покупки с 22.05.2023 +2. **Интеграция с Salebot**: Поле client_id связывает участника с клиентской базой в Salebot +3. **Формат телефона**: Хранится без форматирования (79876543210) для унификации поиска +4. **JSON покупки**: Все покупки участника хранятся в одном JSON поле для быстрого доступа +5. **Типы точек**: Различаются покупки в рознице и через доставку +6. **Денормализация**: Данные чеков дублируются в JSON для ускорения аналитики конкурса diff --git a/erp24/docs/models/CreateChecks.md b/erp24/docs/models/CreateChecks.md new file mode 100644 index 00000000..ea740f74 --- /dev/null +++ b/erp24/docs/models/CreateChecks.md @@ -0,0 +1,575 @@ +# >45;L CreateChecks + + +## Mindmap + +```mermaid +mindmap + root((CreateChecks)) + Таблица БД + create_checks + Свойства + id + int + kkm_id + string + seller_id + string + store_id + string + order_id + int + check_id + string + Связи + Sale + 1:1 Sales + MarketplaceOrder + 1:1 MarketplaceOrders + Наследование + extends yiidbActiveRecord +``` + +## 07=0G5=85 + +>45;L `CreateChecks` ?@54AB02;O5B G5:8, A>740==K5 2 ERP24 8 >B?@02;5==K5 2 1! 4;O ?@>2545=8O. -B> ?@><56CB>G=0O B01;8F0, :>B>@0O E@0=8B 8=D>@<0F8N > G5:0E 4> 8E CA?5H=>9 @538AB@0F88 2 1! 8 A2O7K205B ERP-70:07K A G5:0<8 1!. + +>45;L 8A?>;L7C5BAO 4;O: +- !>740=8O G5:>2 ?@>4068 8 2>72@0B0 2 ERP ?5@54 >B?@02:>9 2 1! +- BA;56820=8O AB0BCA0 A>740=8O G5:0 2 1! +- !2O78 8=B5@=5B-70:07>2 A G5:0<8 1! +- %@0=5=8O 8=D>@<0F88 > <0@:5B?;59A-70:070E +- B;04:8 ?@>F5AA0 A>740=8O G5:>2 + +**$09; <>45;8:** `erp24/records/CreateChecks.php` +**Namespace:** `yii_app\records` +**"01;8F0 :** `create_checks` +** >48B5;LA:89 :;0AA:** `yii\db\ActiveRecord` + +--- + +## >;O B01;8FK + +| >;5 | "8? | ?8A0=85 | +|------|-----|----------| +| `id` | INTEGER | 5@28G=K9 :;NG (02B>8=:@5<5=B) | +| `kkm_id` | STRING(36) | GUID  87 A?8A:0 (?@8=B5@ G5:>2) | +| `seller_id` | STRING(36) | GUID A>B@C4=8:0, A>740NI53> G5: (products_1c) | +| `store_id` | STRING(36) | GUID <03078=0 (products_1c) | +| `order_id` | INTEGER | ><5@ 8=B5@=5B-70:070 (FK `bazacvetov24.site_orders.idd`) | +| `check_id` | STRING(36) | GUID G5:0, A35=5@8@>20==K9 2 ERP | +| `guid` | STRING(36) | GUID, 2>72@0IQ==K9 1! ?>A;5 A>740=8O | +| `type` | TEXT | "8? G5:0: "@>4060" 8;8 ">72@0B" | +| `name` | VARCHAR(255) | 0720=85 G5:0 (70B8@05BAO 40==K<8 87 1!) | +| `sales_check` | STRING(36) | GUID G5:0 ?@>4068 (4;O 2>72@0B0) | +| `items` | TEXT | JSON <0AA82 B>20@>2 A GUID 87 products_1c | +| `payments` | TEXT | JSON <0AA82 A B8?0<8 >?;0BK | +| `held` | INTEGER | $;03 ?@>2545=8O (0 - >H81:0, 1 - CA?5H=>) | +| `status` | INTEGER | !B0BCA A>740=8O (0 - =5 A>740=, 1 - A>740= 2 1!) | +| `date` | DATETIME | 0B0 A>740=8O G5:0 2 ERP | +| `delivery_date` | DATETIME | 0B0 4>AB02:8 70:070 | +| `date_up` | DATETIME | 0B0 >1=>2;5=8O 87 1! | +| `comments` | VARCHAR(255) | ><<5=B0@89 : G5:C (=><5@ B5;5D>=0 :;85=B0) | +| `phone` | VARCHAR(20) | "5;5D>= :;85=B0 | +| `order_guid` | STRING(36) | GUID 70:070 2 <0@:5B?;59A5 | +| `marketplace_order_id` | STRING(36) | ID 70:070 <0@:5B?;59A0 | +| `is_marketplace` | INTEGER | @87=0: ?@>4068 G5@57 <0@:5B?;59A (0/1) | +| `marketplace_name` | STRING(36) | 0720=85 <0@:5B?;59A0 | + +--- + +## 5B>4K <>45;8 + +### @028;0 20;840F88 + +#### `rules(): array` + +?@545;O5B ?@028;0 20;840F88 4;O ?>;59 <>45;8. + +**1O70B5;L=K5 ?>;O:** +- `store_id` - GUID <03078=0 +- `check_id` - A35=5@8@>20==K9 GUID G5:0 +- `guid` - GUID 87 1! (?>A;5 A>740=8O) +- `items` - JSON B>20@>2 +- `held` - D;03 ?@>2545=8O +- `date` - 40B0 A>740=8O + +**?F8>=0;L=K5 ?>;O** (70:><<5=B8@>20=K 2 :>45): +- `kkm_id`, `order_id`, `name`, `sales_check`, `payments`, `comments` + +**'8A;>2K5 ?>;O:** +- Integer: `order_id`, `held`, `status`, `is_marketplace` + +**"5:AB>2K5 ?>;O:** +- TEXT: `type`, `items`, `payments`, `phone` +- STRING(36): GUID ?>;O +- STRING(255): `name`, `comments` +- STRING(20): `phone` + +**!?5F80;L=0O 20;840F8O:** +- `phone` - 20;848@C5BAO G5@57 `PhoneValidator::class` + +**#=8:0;L=>ABL:** +- ><18=0F8O `['order_id', 'check_id', 'type', 'date']` 4>;6=0 1KBL C=8:0;L=>9 + +**57>?0A=K5 ?>;O:** +- `date`, `delivery_date`, `date_up`, `phone` + +**>72@0I05B:** <0AA82 ?@028; 20;840F88 + +**@8<5@:** +```php +$check = new CreateChecks(); +$check->store_id = $storeGuid; +$check->check_id = Yii::$app->security->generateRandomString(36); +$check->items = json_encode($products); +$check->held = 1; +$check->date = date('Y-m-d H:i:s'); + +if ($check->validate()) { + $check->save(); +} +``` + +--- + +## !2O78 (Relations) + +### `getSale(): ActiveQueryInterface` + +!2O7L A G5:>< 87 1! ?>A;5 53> A>740=8O. + +**"8?:** hasOne +**!2O70==0O <>45;L:** `Sales` +**;NG8:** `guid` `id` + +**>72@0I05B:** >1J5:B ActiveQuery 4;O G5:0 2 1! + +**>38:0:** +>A;5 CA?5H=>3> A>740=8O G5:0 2 1!, ?>;5 `guid` 70?>;=O5BAO GUID->< A>740==>3> G5:0. '5@57 MBC A2O7L <>6=> ?>;CG8BL ?>;=K5 40==K5 G5:0 87 B01;8FK sales. + +**@8<5@:** +```php +$createCheck = CreateChecks::findOne($id); +if ($createCheck->status == 1) { + $sale = $createCheck->sale; + echo "'5: A>740=: " . $sale->number; + echo "!C<<0: " . $sale->summ; +} +``` + +--- + +### `getMarketplaceOrder(): ActiveQueryInterface` + +!2O7L A 70:07>< <0@:5B?;59A0. + +**"8?:** hasOne +**!2O70==0O <>45;L:** `MarketplaceOrders` +**;NG8:** `marketplace_order_id` `marketplace_order_id` + +**>72@0I05B:** >1J5:B ActiveQuery 4;O 70:070  + +**>38:0:** +;O G5:>2, A>740==KE ?> 70:070< 87 <0@:5B?;59A>2 (Wildberries, Ozon 8 4@.), A2O7K205B G5: A 40==K<8 70:070 . + +**@8<5@:** +```php +$createCheck = CreateChecks::findOne($id); +if ($createCheck->is_marketplace) { + $mpOrder = $createCheck->marketplaceOrder; + echo "0:07 : " . $mpOrder->marketplace_name; + echo "><5@: " . $mpOrder->order_number; +} +``` + +--- + +## 87=5==K9 F8:; G5:0 + +```mermaid +stateDiagram-v2 + [*] --> !>740=85: $>@<8@>20=85 2 ERP + !>740=85 --> B?@02:0: 5=5@0F8O GUID + B?@02:0 --> 6840=85: 0?@>A 2 1! + 6840=85 --> #A?5E: held=1, status=1 + 6840=85 --> H81:0: held=0 + #A?5E --> [*]: '5: 2 Sales + H81:0 --> >2B>@: A?@02;5=85 >H81>: + >2B>@ --> B?@02:0 +``` + +--- + +## 803@0<<0 A2O759 + +```mermaid +erDiagram + create_checks ||--o| sales : "creates" + create_checks }o--|| marketplace_orders : "based_on" + create_checks }o--|| products_1c : "store" + create_checks }o--|| products_1c : "seller" + + create_checks { + int id PK + string check_id UK + string guid UK + string store_id FK + string seller_id FK + int order_id + text items + text payments + int held + int status + datetime date + string phone + string marketplace_order_id FK + } + + sales { + string id PK + string number + float summ + datetime date + } + + marketplace_orders { + string marketplace_order_id PK + string marketplace_name + string order_number + } + + products_1c { + string id PK + string name + string type + } +``` + +--- + +## @8<5@K 8A?>;L7>20=8O + +### !>740=85 =>2>3> G5:0 ?@>4068 + +```php +$check = new CreateChecks(); +$check->store_id = $storeGuid; // 87 products_1c +$check->seller_id = $sellerGuid; // 87 products_1c +$check->kkm_id = $kkmGuid; +$check->check_id = Yii::$app->security->generateRandomString(36); +$check->guid = $check->check_id; // 87=0G0;L=> A>2?040NB +$check->type = "@>4060"; +$check->held = 1; +$check->status = 0; // 5I5 =5 A>740= 2 1! + +// $>@<8@>20=85 B>20@>2 +$items = [ + [ + 'id' => $productGuid1, + 'quantity' => 2, + 'price' => 500 + ], + [ + 'id' => $productGuid2, + 'quantity' => 1, + 'price' => 1000 + ] +]; +$check->items = json_encode($items); + +// !?>A>1K >?;0BK +$payments = [ + ['type' => 'card', 'amount' => 2000] +]; +$check->payments = json_encode($payments); + +$check->date = date('Y-m-d H:i:s'); +$check->phone = '+79001234567'; + +if ($check->save()) { + // B?@028BL 2 1! + // >A;5 CA?5H=>9 >1@01>B:8 1! >1=>28B guid 8 status +} +``` + +--- + +### !>740=85 G5:0 2>72@0B0 + +```php +$originalCheck = Sales::findOne($saleGuid); + +$returnCheck = new CreateChecks(); +$returnCheck->store_id = $originalCheck->store_id_1c; +$returnCheck->seller_id = $originalCheck->seller_id; +$returnCheck->check_id = Yii::$app->security->generateRandomString(36); +$returnCheck->guid = $returnCheck->check_id; +$returnCheck->type = ">72@0B"; +$returnCheck->sales_check = $originalCheck->id; // AAK;:0 =0 >@838=0;L=K9 G5: +$returnCheck->items = json_encode($returnItems); +$returnCheck->payments = json_encode($returnPayments); +$returnCheck->held = 1; +$returnCheck->status = 0; +$returnCheck->date = date('Y-m-d H:i:s'); + +$returnCheck->save(); +``` + +--- + +### !>740=85 G5:0 4;O 8=B5@=5B-70:070 + +```php +$order = SiteOrder::findOne($orderId); + +$check = new CreateChecks(); +$check->store_id = $order->store_guid; +$check->seller_id = $order->seller_guid; +$check->order_id = $order->idd; +$check->check_id = Yii::$app->security->generateRandomString(36); +$check->guid = $check->check_id; +$check->type = "@>4060"; +$check->items = $order->getItemsJson(); +$check->payments = $order->getPaymentsJson(); +$check->held = 1; +$check->status = 0; +$check->date = date('Y-m-d H:i:s'); +$check->delivery_date = $order->delivery_date; +$check->phone = $order->customer_phone; +$check->comments = $order->customer_phone; + +$check->save(); +``` + +--- + +### !>740=85 G5:0 4;O <0@:5B?;59A0 + +```php +$mpOrder = MarketplaceOrders::findOne(['marketplace_order_id' => $orderId]); + +$check = new CreateChecks(); +$check->store_id = $mpOrder->store_guid; +$check->seller_id = $defaultSellerGuid; +$check->check_id = Yii::$app->security->generateRandomString(36); +$check->guid = $check->check_id; +$check->type = "@>4060"; +$check->items = $mpOrder->getItemsJson(); +$check->payments = json_encode([['type' => 'card', 'amount' => $mpOrder->total]]); +$check->held = 1; +$check->status = 0; +$check->date = date('Y-m-d H:i:s'); +$check->is_marketplace = 1; +$check->marketplace_name = $mpOrder->marketplace_name; +$check->marketplace_order_id = $mpOrder->marketplace_order_id; +$check->order_guid = $mpOrder->order_guid; + +$check->save(); +``` + +--- + +### @>25@:0 AB0BCA0 A>740==KE G5:>2 + +```php +// '5:8, >6840NI85 A>740=8O 2 1! +$pending = CreateChecks::find() + ->where(['status' => 0]) + ->andWhere(['held' => 1]) + ->all(); + +echo "6840NB A>740=8O: " . count($pending) . " G5:>2\n"; + +// '5:8 A >H81:>9 +$failed = CreateChecks::find() + ->where(['held' => 0]) + ->all(); + +echo "! >H81:0<8: " . count($failed) . " G5:>2\n"; + +// #A?5H=> A>740==K5 +$success = CreateChecks::find() + ->where(['status' => 1, 'held' => 1]) + ->with('sale') + ->all(); + +foreach ($success as $check) { + echo "'5: A>740=: " . $check->sale->number . "\n"; +} +``` + +--- + +### >8A: G5:>2 ?> 8=B5@=5B-70:07C + +```php +$checks = CreateChecks::find() + ->where(['order_id' => $orderId]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($checks as $check) { + echo "'5: " . $check->type . ": "; + echo $check->status ? "A>740=" : "=5 A>740="; + echo " (" . $check->date . ")\n"; +} +``` + +--- + +### >;CG5=85 G5:>2 <0@:5B?;59A0 + +```php +$mpChecks = CreateChecks::find() + ->where(['is_marketplace' => 1]) + ->andWhere(['>=', 'date', date('Y-m-d')]) + ->with('marketplaceOrder') + ->all(); + +foreach ($mpChecks as $check) { + echo $check->marketplace_name . ": "; + echo $check->marketplaceOrder->order_number . "\n"; +} +``` + +--- + +### =0;87 B>20@>2 2 G5:0E + +```php +$check = CreateChecks::findOne($id); +$items = json_decode($check->items, true); + +echo "">20@K 2 G5:5:\n"; +foreach ($items as $item) { + $product = Products1c::findOne($item['id']); + echo "- " . $product->name; + echo " x " . $item['quantity']; + echo " = " . ($item['quantity'] * $item['price']) . " @C1.\n"; +} +``` + +--- + +### =0;87 A?>A>1>2 >?;0BK + +```php +$check = CreateChecks::findOne($id); +$payments = json_decode($check->payments, true); + +echo "?;0B0:\n"; +foreach ($payments as $payment) { + echo "- " . $payment['type'] . ": " . $payment['amount'] . " @C1.\n"; +} +``` + +--- + +## $>@<0B JSON ?>;59 + +### >;5 `items` (B>20@K) + +```json +[ + { + "id": "guid-B>20@0-87-products_1c", + "quantity": 2, + "price": 500.00 + }, + { + "id": "guid-B>20@0-2", + "quantity": 1, + "price": 1500.00 + } +] +``` + +### >;5 `payments` (>?;0BK) + +```json +[ + { + "type": "card", + "amount": 2000.00 + }, + { + "type": "cash", + "amount": 500.00 + }, + { + "type": "bonus", + "amount": 100.00 + } +] +``` + +--- + +## 0;840F8O + +1O70B5;L=K5 ?>;O: +- `store_id` - <03078= >1O70B5;5= +- `check_id` - GUID G5:0 35=5@8@C5BAO 2 ERP +- `guid` - 87=0G0;L=> @025= check_id, >1=>2;O5BAO 1! +- `items` - A?8A>: B>20@>2 2 JSON +- `held` - D;03 CA?5H=>AB8 (1 8;8 0) +- `date` - 40B0 A>740=8O + +?F8>=0;L=K5 ?>;O: +- `kkm_id`, `seller_id`, `order_id`, `payments` 8 4@C385 <>3CB 1KBL ?CABK<8 + +#=8:0;L=>ABL: +- ><18=0F8O (order_id, check_id, type, date) 4>;6=0 1KBL C=8:0;L=>9 +- 0I8B0 >B 4C1;8@>20=8O G5:>2 ?> >4=>=0: +- A?>;L7C5B A?5F80;L=K9 PhoneValidator 4;O ?@>25@:8 D>@<0B0 + +--- + +## !2O70==K5 <>45;8 + +- **[Sales](./Sales.md)**  G5:8 2 1! (@57C;LB0B A>740=8O) +- **[MarketplaceOrders](./MarketplaceOrders.md)**  70:07K <0@:5B?;59A>2 +- **[Products1c](./Products1c.md)**  A?@02>G=8: 1! (B>20@K, <03078=K, A>B@C4=8:8) +- **SiteOrders**  70:07K 8=B5@=5B-<03078=0 +- **CreateChecks2**  0;LB5@=0B82=0O 25@A8O <>45;8 + +--- + +## A>15==>AB8 @01>BK + +### !B0BCAK G5:0 + +1. **status = 0, held = 1** - G5: A>740= 2 ERP, >68405B >1@01>B:8 1! +2. **status = 1, held = 1** - G5: CA?5H=> A>740= 2 1! +3. **status = 0, held = 0** - >H81:0 ?@8 A>740=88 + +### @>F5AA A>740=8O + +1. ERP A>740QB 70?8AL 2 create_checks +2. 5=5@8@C5B check_id (C=8:0;L=K9 GUID) +3. $>@<8@C5B JSON B>20@>2 8 >?;0B +4. B?@02;O5B 70?@>A 2 1! +5. 1! A>740QB G5: 8 2>72@0I05B GUID +6. ERP >1=>2;O5B guid 8 status = 1 +7. '5: ?>O2;O5BAO 2 B01;8F5 sales + +### !2O7L A 70:070<8 + +- **=B5@=5B-70:07K** - G5@57 order_id (site_orders.idd) +- **0@:5B?;59AK** - G5@57 marketplace_order_id +- **>72@0BK** - G5@57 sales_check (GUID >@838=0;L=>3> G5:0) + +### 1=>2;5=85 87 1! + +- >;5 `guid` >1=>2;O5BAO ?>A;5 A>740=8O 2 1! +- >;5 `date_up` - 2@5A;54=53> >1=>2;5=8O +- >;5 `name` 70B8@05BAO =0720=85< 87 1! +- >;5 `status` <5=O5BAO =0 1 + +--- + +**5@A8O:** 1.0 +**0B0:** 2025-12-11 diff --git a/erp24/docs/models/CreateChecks2.md b/erp24/docs/models/CreateChecks2.md new file mode 100644 index 00000000..bf748b87 --- /dev/null +++ b/erp24/docs/models/CreateChecks2.md @@ -0,0 +1,214 @@ +# >45;L CreateChecks2 + + +## Mindmap + +```mermaid +mindmap + root((CreateChecks2)) + Таблица БД + create_checks2 + Свойства + id + int + kkm_id + string + seller_id + string + store_id + string + order_id + int + check_id + string + Наследование + extends yiidbActiveRecord +``` + +## 07=0G5=85 + +>45;L `CreateChecks2` ?@54AB02;O5B 2B>@CN 25@A8N B01;8FK A>740=8O G5:>2. -B> 0@E82=0O 8;8 0;LB5@=0B82=0O 25@A8O <>45;8 CreateChecks A C?@>IQ==>9 AB@C:BC@>9 ?>;59. + +**@8<5G0=85:**  ?@>5:B5 >A=>2=>9 <>45;LN 4;O A>740=8O G5:>2 O2;O5BAO **CreateChecks**. >45;L CreateChecks2 <>65B 8A?>;L7>20BLAO 4;O <83@0F88 40==KE, B5AB8@>20=8O 8;8 O2;OBLAO CAB0@52H59 25@A859. + +**$09; <>45;8:** `erp24/records/CreateChecks2.php` +**Namespace:** `yii_app\records` +**"01;8F0 :** `create_checks2` +** >48B5;LA:89 :;0AA:** `yii\db\ActiveRecord` + +--- + +## >;O B01;8FK + +| >;5 | "8? | ?8A0=85 | +|------|-----|----------| +| `id` | INTEGER | 5@28G=K9 :;NG (02B>8=:@5<5=B) | +| `kkm_id` | STRING(36) | GUID  | +| `seller_id` | STRING(36) | GUID ?@>402F0 | +| `store_id` | STRING(36) | GUID <03078=0 | +| `order_id` | INTEGER | ><5@ 70:070 | +| `check_id` | STRING(36) | GUID G5:0 | +| `guid` | STRING(36) | GUID 87 1! | +| `type` | TEXT | "8? G5:0 | +| `name` | VARCHAR(255) | 0720=85 G5:0 | +| `sales_check` | STRING(36) | GUID G5:0 2>72@0B0 | +| `items` | TEXT | JSON ?@>4C:F88 | +| `payments` | TEXT | JSON >?;0B | +| `held` | INTEGER | $;03 ?@>2545=8O | +| `status` | INTEGER | !B0BCA | +| `date` | DATETIME | 0B0 A>740=8O | +| `date_up` | DATETIME | 0B0 >1=>2;5=8O | + +--- + +## 5B>4K <>45;8 + +### `rules(): array` + +?@545;O5B ?@028;0 20;840F88 4;O ?>;59 <>45;8. + +**1O70B5;L=K5 ?>;O:** +A5 ?>;O O2;ONBAO >1O70B5;L=K<8 (required): +- `kkm_id`, `seller_id`, `store_id`, `order_id` +- `check_id`, `guid`, `name`, `sales_check` +- `items`, `payments`, `held`, `date`, `date_up` + +**'8A;>2K5 ?>;O:** +- Integer: `order_id`, `held`, `status` + +**"5:AB>2K5 ?>;O:** +- TEXT: `type`, `items`, `payments` +- STRING(36): GUID ?>;O +- STRING(255): `name` + +**57>?0A=K5 ?>;O:** +- `date`, `date_up` + +**>72@0I05B:** <0AA82 ?@028; 20;840F88 + +--- + +### `attributeLabels(): array` + +>72@0I05B <5B:8 0B@81CB>2 4;O D>@< 8 ?@54AB02;5=89. + +**>72@0I05B:** <0AA82 <5B>: + +--- + +## B;8G8O >B CreateChecks + +| %0@0:B5@8AB8:0 | CreateChecks2 | CreateChecks | +|----------------|---------------|--------------| +| 1O70B5;L=K5 ?>;O | A5 ?>;O required | '0ABL ?>;59 >?F8>=0;L=0 | +| >;O <0@:5B?;59A0 | BACBAB2CNB | ABL (marketplace_*) | +| >;5 B5;5D>=0 | BACBAB2C5B | ABL (phone, PhoneValidator) | +| 0BK | date, date_up | date, date_up, delivery_date | +| A?>;L7>20=85 | #AB0@52H0O 25@A8O | :BC0;L=0O 25@A8O | +| !2O78 (Relations) | 5B | ABL (sale, marketplaceOrder) | + +--- + +## @8<5@K 8A?>;L7>20=8O + +### !>740=85 70?8A8 + +```php +$check = new CreateChecks2(); +$check->kkm_id = $kkmGuid; +$check->seller_id = $sellerGuid; +$check->store_id = $storeGuid; +$check->order_id = $orderId; +$check->check_id = Yii::$app->security->generateRandomString(36); +$check->guid = $check->check_id; +$check->type = "@>4060"; +$check->name = "'5: 1"; +$check->sales_check = ""; +$check->items = json_encode($itemsArray); +$check->payments = json_encode($paymentsArray); +$check->held = 1; +$check->status = 0; +$check->date = date('Y-m-d H:i:s'); +$check->date_up = date('Y-m-d H:i:s'); + +if ($check->save()) { + echo "'5: A>740="; +} +``` + +--- + +### >8A: G5:>2 ?> 70:07C + +```php +$checks = CreateChecks2::find() + ->where(['order_id' => $orderId]) + ->all(); +``` + +--- + +### >;CG5=85 CA?5H=> A>740==KE G5:>2 + +```php +$success = CreateChecks2::find() + ->where(['status' => 1, 'held' => 1]) + ->all(); +``` + +--- + +## 803@0<<0 A2O759 + +```mermaid +erDiagram + create_checks2 { + int id PK + string kkm_id + string seller_id + string store_id + int order_id + string check_id + string guid + string type + string name + string sales_check + text items + text payments + int held + int status + datetime date + datetime date_up + } +``` + +--- + +## 0;840F8O + +- **A5 ?>;O >1O70B5;L=K** - required 4;O 2A5E ?>;59 +- **GUID ?>;O** - >3@0=8G5=K 36 A8<2>;0<8 +- **0BK** - 20;848@CNBAO :0: safe +- **'8A;>2K5** - ?@>25@ONBAO =0 B8? integer + +--- + +## !2O70==K5 <>45;8 + +- **[CreateChecks](./CreateChecks.md)**  0:BC0;L=0O 25@A8O <>45;8 (@5:><5=4C5BAO) +- **[Sales](./Sales.md)**  G5:8 2 1! + +--- + +## @8<5G0=8O + +1. **A?>;L7C9B5 CreateChecks 2<5AB> CreateChecks2** - MB> CAB0@52H0O 25@A8O +2. **A5 ?>;O required** - <5=55 381:0O 20;840F8O ?> A@02=5=8N A CreateChecks +3. **5B A2O759** - <>45;L =5 8<55B <5B>4>2 relations +4. **BACBAB2CNB ?>;O <0@:5B?;59A0** - =5;L7O @01>B0BL A -70:070<8 +5. **5B 20;840F88 B5;5D>=0** - >BACBAB2C5B ?>;5 phone + +--- + +**5@A8O:** 1.0 +**0B0:** 2025-12-11 diff --git a/erp24/docs/models/CreateChecksBags.md b/erp24/docs/models/CreateChecksBags.md new file mode 100644 index 00000000..367d82ea --- /dev/null +++ b/erp24/docs/models/CreateChecksBags.md @@ -0,0 +1,248 @@ +# Класс: CreateChecksBags + + +## Mindmap + +```mermaid +mindmap + root((CreateChecksBags)) + Таблица БД + create_checks_bags + Свойства + id + int + kkm_id + string + seller_id + string + store_id + string + order_id + int + check_id + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель пакетов (bags) для чеков в ERP24. Используется для группировки позиций чека по пакетам при продаже, отслеживания статуса проведения и связи с ККМ (контрольно-кассовой машиной). Поддерживает операции продажи и возврата товара. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`create_checks_bags` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `kkm_id` | varchar(36) | GUID контрольно-кассовой машины | +| `seller_id` | varchar(36) | GUID продавца | +| `store_id` | varchar(36) | GUID магазина | +| `order_id` | int | ID заказа | +| `check_id` | varchar(36) | GUID связанного чека | +| `guid` | varchar(36) | Уникальный GUID записи пакета | +| `type` | text | Тип операции (продажа/возврат) | +| `name` | varchar(255) | Название/описание пакета | +| `sales_check` | varchar(36) | GUID чека возврата (при возврате товара) | +| `items` | text | JSON с продукцией в пакете | +| `payments` | text | JSON с информацией об оплате | +| `held` | int | Флаг проведения операции | +| `status` | int | Статус пакета | +| `date` | datetime | Дата создания | +| `date_up` | datetime | Дата обновления | + +## Индексы и ограничения + +- **PRIMARY KEY**: `id` +- **UNIQUE**: (`order_id`, `check_id`, `type`, `date`) — уникальная комбинация для предотвращения дублей + +## Структура JSON полей + +### items (JSON с продукцией) +```json +[ + { + "product_id": "abc-123", + "name": "Роза красная", + "quantity": 5, + "price": 150.00, + "sum": 750.00, + "discount": 0 + }, + { + "product_id": "def-456", + "name": "Упаковка", + "quantity": 1, + "price": 100.00, + "sum": 100.00, + "discount": 10 + } +] +``` + +### payments (JSON с оплатой) +```json +{ + "cash": 500.00, + "card": 350.00, + "bonus": 0, + "total": 850.00 +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + CreateChecksBags { + int id PK + varchar kkm_id FK + varchar seller_id FK + varchar store_id FK + int order_id FK + varchar check_id FK + varchar guid UK + text type + varchar name + varchar sales_check FK + text items + text payments + int held + int status + datetime date + datetime date_up + } + + CreateChecks { + varchar guid PK + decimal summ + } + + Admin { + varchar guid PK + varchar name + } + + Store { + varchar id PK + varchar name + } + + Orders { + int id PK + } + + CreateChecksBags }o--|| CreateChecks : "check_id" + CreateChecksBags }o--o| CreateChecks : "sales_check (возврат)" + CreateChecksBags }o--|| Admin : "seller_id" + CreateChecksBags }o--|| Store : "store_id" + CreateChecksBags }o--o| Orders : "order_id" +``` + +## Примеры использования + +### Создание нового пакета для чека +```php +$bag = new CreateChecksBags(); +$bag->kkm_id = $kkm->guid; +$bag->seller_id = Yii::$app->user->identity->guid; +$bag->store_id = $currentStore->guid; +$bag->order_id = $order->id; +$bag->check_id = $check->guid; +$bag->guid = Yii::$app->security->generateRandomString(36); +$bag->type = 'sale'; +$bag->name = 'Пакет №1'; +$bag->items = json_encode($itemsArray); +$bag->payments = json_encode(['cash' => 1000, 'card' => 0, 'total' => 1000]); +$bag->held = 1; +$bag->status = 1; +$bag->date = date('Y-m-d H:i:s'); +$bag->date_up = date('Y-m-d H:i:s'); +$bag->save(); +``` + +### Создание пакета возврата +```php +$returnBag = new CreateChecksBags(); +$returnBag->kkm_id = $kkm->guid; +$returnBag->seller_id = Yii::$app->user->identity->guid; +$returnBag->store_id = $currentStore->guid; +$returnBag->order_id = $originalOrder->id; +$returnBag->check_id = $returnCheck->guid; +$returnBag->guid = Yii::$app->security->generateRandomString(36); +$returnBag->type = 'return'; +$returnBag->name = 'Возврат'; +$returnBag->sales_check = $originalCheck->guid; // Ссылка на оригинальный чек +$returnBag->items = json_encode($returnItems); +$returnBag->payments = json_encode(['cash' => -500, 'total' => -500]); +$returnBag->held = 1; +$returnBag->date = date('Y-m-d H:i:s'); +$returnBag->date_up = date('Y-m-d H:i:s'); +$returnBag->save(); +``` + +### Получение пакетов чека +```php +$bags = CreateChecksBags::find() + ->where(['check_id' => $checkGuid]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +foreach ($bags as $bag) { + $items = json_decode($bag->items, true); + foreach ($items as $item) { + echo "{$item['name']}: {$item['quantity']} x {$item['price']}\n"; + } +} +``` + +### Получение непроведённых пакетов +```php +$unheldBags = CreateChecksBags::find() + ->where(['held' => 0]) + ->andWhere(['>=', 'date', date('Y-m-d')]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `kkm_id` | required, string (max 36) | +| `seller_id` | required, string (max 36) | +| `store_id` | required, string (max 36) | +| `order_id` | required, integer | +| `check_id` | required, string (max 36) | +| `guid` | required, string (max 36) | +| `type` | string | +| `name` | required, string (max 255) | +| `sales_check` | required, string (max 36) | +| `items` | required, string | +| `payments` | required, string | +| `held` | required, integer | +| `status` | integer | +| `date` | required, safe | +| `date_up` | required, safe | + +## Связанные модели + +- [CreateChecks](./CreateChecks.md) — чеки продаж +- [Admin](./Admin.md) — продавцы +- [Store](./Store.md) — магазины +- [Orders](./Orders.md) — заказы +- [Products1c](./Products1c.md) — товары (через JSON items) + +## Особенности реализации + +1. **Композитный уникальный ключ**: Комбинация order_id + check_id + type + date предотвращает создание дубликатов +2. **JSON структуры**: items и payments хранятся в JSON для гибкости состава +3. **Связь возвратов**: Поле sales_check связывает возврат с оригинальным чеком продажи +4. **Интеграция с ККМ**: Привязка к конкретной кассе через kkm_id +5. **Флаг проведения**: held отмечает, была ли операция фискализирована diff --git a/erp24/docs/models/CrmMenu.md b/erp24/docs/models/CrmMenu.md new file mode 100644 index 00000000..e02afee4 --- /dev/null +++ b/erp24/docs/models/CrmMenu.md @@ -0,0 +1,593 @@ +# Class: CrmMenu + + +## Mindmap + +```mermaid +mindmap + root((CrmMenu)) + Таблица БД + crm_menu + Свойства + id + int + name + string + parent_id + int + menu_close + int + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель для управления иерархическим меню CRM-системы ERP24. Хранит структуру пунктов меню с поддержкой вложенности, контролем видимости и интеграцией с системой прав доступа (RBAC). Используется для построения навигационного меню с учетом прав пользователя и состояния свернутости/развернутости разделов. + +Модель поддерживает иерархическую структуру через поле `parent_id` и предоставляет методы для построения древовидного меню и проверки состояния коллапса. + +--- + +## Файл модели + +`/erp24/records/CrmMenu.php` + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`crm_menu` + +--- + +## Поля таблицы + +| Имя | Тип | Ключ | Описание | +|-----|-----|------|----------| +| `id` | int | PK | Первичный ключ | +| `name` | string | | Название пункта меню | +| `parent_id` | int | FK | ID родительского пункта меню | +| `menu_close` | int | | Свернуто ли меню для данной страницы (0 - нет, 1 - да) | +| `url` | string | | URL страницы (маршрут) | +| `icon_file` | string | | Путь к файлу иконки | +| `visible` | int | | Видимость пункта меню (0 - скрыт, 1 - виден) | +| `posit` | int | | Позиция для сортировки | + +--- + +## Описание полей + +### id +Автоинкрементный первичный ключ таблицы. Уникальный идентификатор пункта меню. + +### name +Название пункта меню, отображаемое пользователю. Может содержать любой текст. + +**Примеры:** +- `Главная` +- `Продажи` +- `Отчеты` +- `Настройки` + +### parent_id +Внешний ключ на саму таблицу `crm_menu`. Определяет родительский пункт меню для создания иерархической структуры. Значение `0` или `null` означает элемент верхнего уровня. + +### menu_close +Флаг состояния коллапса меню для конкретной страницы: +- `0` — меню развернуто +- `1` — меню свернуто + +Используется для запоминания состояния левого меню при переходе по URL. + +### url +URL-адрес или маршрут страницы, на которую ведет пункт меню. Используется для навигации и определения активного пункта. + +**Примеры:** +- `/dashboard/index` +- `/sales/list` +- `/reports/monthly` + +### icon_file +Путь к файлу иконки пункта меню. Может быть относительным путем или классом иконочного шрифта. + +**Примеры:** +- `icons/dashboard.svg` +- `fa fa-home` +- `glyphicon glyphicon-stats` + +### visible +Флаг видимости пункта меню: +- `0` — пункт скрыт +- `1` — пункт видим + +Позволяет временно скрывать пункты меню без удаления из базы данных. + +### posit +Позиция пункта меню для сортировки. Меньшее значение = выше в списке. Используется в методе `getTree()` для упорядочивания элементов. + +--- + +## Методы модели + +### tableName() +**Тип:** `public static` +**Параметры:** нет +**Возвращает:** `string` +**Описание:** Возвращает имя таблицы базы данных + +**Логика работы:** +Статический метод, возвращающий строку `'crm_menu'`. + +**Пример:** +```php +$tableName = CrmMenu::tableName(); +// Результат: 'crm_menu' +``` + +--- + +### isLeftMenuCollapsed($url) +**Тип:** `public static` +**Параметры:** +- `$url` (string) — URL страницы + +**Возвращает:** `bool` — `true` если меню свернуто, `false` если развернуто +**Описание:** Проверяет, свернуто ли левое меню для указанного URL + +**Логика работы:** +1. Ищет запись в таблице `crm_menu` по полю `url` +2. Если запись не найдена, возвращает `false` (меню не свернуто) +3. Если запись найдена, возвращает значение поля `menu_close` как boolean + +**Вызовы сторонних методов:** +- `self::find()` — создание запроса ActiveQuery +- `->andWhere(['url' => $url])` — фильтрация по URL +- `->one()` — выполнение запроса и получение одной записи +- `(bool) $record->menu_close` — приведение к boolean типу + +**Пример:** +```php +$url = '/dashboard/index'; +$isCollapsed = CrmMenu::isLeftMenuCollapsed($url); + +if ($isCollapsed) { + echo "Меню свернуто"; +} else { + echo "Меню развернуто"; +} +``` + +--- + +### getTitle($url) +**Тип:** `public static` +**Параметры:** +- `$url` (string) — URL страницы + +**Возвращает:** `string` — название страницы +**Описание:** Получает заголовок страницы по ее URL + +**Логика работы:** +1. Ищет запись в таблице `crm_menu` по полю `url` с кэшированием на 1 час +2. Если запись не найдена, записывает предупреждение в лог и возвращает сам URL +3. Если запись найдена, возвращает значение поля `name` + +**Вызовы сторонних методов:** +- `self::find()` — создание запроса ActiveQuery +- `->andWhere(['url' => $url])` — фильтрация по URL +- `->cache(3600)` — кэширование результата на 3600 секунд (1 час) +- `->one()` — выполнение запроса и получение одной записи +- `\Yii::warning($message)` — запись предупреждения в лог приложения + +**Пример:** +```php +$title = CrmMenu::getTitle('/sales/list'); +echo "Заголовок страницы: {$title}"; +// Вывод: Заголовок страницы: Продажи +``` + +--- + +### getTree() +**Тип:** `public static` +**Параметры:** нет +**Возвращает:** `array` — древовидный массив пунктов меню +**Описание:** Получает полное дерево меню без учета прав доступа + +**Логика работы:** +1. Извлекает все записи из таблицы `crm_menu`, отсортированные по полю `posit` +2. Группирует записи по `parent_id`, формируя древовидную структуру +3. Возвращает массив, где ключ — ID родителя, значение — массив дочерних элементов + +**Структура возвращаемого массива:** +```php +[ + 0 => [CrmMenu, CrmMenu, ...], // Элементы верхнего уровня + 1 => [CrmMenu, CrmMenu, ...], // Дочерние элементы родителя с ID=1 + 2 => [CrmMenu, CrmMenu, ...], // Дочерние элементы родителя с ID=2 + ... +] +``` + +**Вызовы сторонних методов:** +- `self::find()` — создание запроса ActiveQuery +- `->orderBy('posit')` — сортировка по полю позиции +- `->all()` — выполнение запроса и получение всех записей + +**Примечание:** В коде присутствует закомментированная версия с кэшированием через `\Yii::$app->getCache()`. + +**Пример:** +```php +$tree = CrmMenu::getTree(); + +// Вывод элементов верхнего уровня +foreach ($tree[0] as $item) { + echo "Раздел: {$item->name}\n"; + + // Вывод дочерних элементов + if (isset($tree[$item->id])) { + foreach ($tree[$item->id] as $child) { + echo " - {$child->name}\n"; + } + } +} +``` + +--- + +### getTreeByUserId($admin_id) +**Тип:** `public static` +**Параметры:** +- `$admin_id` (int) — ID администратора/пользователя + +**Возвращает:** `array` — древовидный массив пунктов меню с учетом прав доступа +**Описание:** Получает дерево меню, отфильтрованное по правам доступа пользователя + +**Логика работы:** +1. Получает список разрешений (permissions) пользователя из RBAC через `\Yii::$app->authManager` +2. Извлекает все записи меню с выбранными полями, отсортированные по `posit` +3. Фильтрует записи: показывает только те, для которых у пользователя есть разрешение `menu{url}` +4. Группирует отфильтрованные записи по `parent_id`, формируя дерево + +**Формат разрешения:** +Для URL `/sales/list` проверяется разрешение `menu/sales/list` + +**Вызовы сторонних методов:** +- `\Yii::$app->authManager->getPermissionsByUser($admin_id)` — получение всех разрешений пользователя из RBAC +- `array_keys($permissions)` — извлечение названий разрешений +- `self::find()->select([...])` — создание запроса с выбором определенных полей +- `->orderBy('posit')` — сортировка по позиции +- `->all()` — выполнение запроса +- `in_array('menu' . $row->url, $permissions)` — проверка наличия разрешения + +**Пример:** +```php +$userId = Yii::$app->user->id; +$userMenu = CrmMenu::getTreeByUserId($userId); + +// Построение HTML меню +foreach ($userMenu[0] as $item) { + echo "
  • "; + echo " {$item->name}"; + echo ""; + + if (isset($userMenu[$item->id])) { + echo "
      "; + foreach ($userMenu[$item->id] as $child) { + if ($child->visible) { + echo "
    • {$child->name}
    • "; + } + } + echo "
    "; + } + echo "
  • "; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + CrmMenu ||--o{ CrmMenu : "has children" + CrmMenu }o--|| Admin : "filtered by permissions" + + CrmMenu { + int id PK + string name "Название" + int parent_id FK "Родитель" + int menu_close "Свернуто" + string url "URL" + string icon_file "Иконка" + int visible "Видимость" + int posit "Позиция" + } + + Admin { + int id PK + string name + int group_id FK + } + + AuthItem { + string name PK + int type "Permission/Role" + } + + AuthAssignment { + string item_name FK + int user_id FK + } +``` + +--- + +## Примеры использования + +### Получение дерева меню для текущего пользователя + +```php +use yii_app\records\CrmMenu; +use Yii; + +$userId = Yii::$app->user->id; +$menuTree = CrmMenu::getTreeByUserId($userId); + +// Рендеринг меню +function renderMenu($tree, $parentId = 0) { + if (!isset($tree[$parentId])) { + return ''; + } + + echo '
      '; + foreach ($tree[$parentId] as $item) { + if ($item->visible) { + echo "
    • "; + echo ""; + if ($item->icon_file) { + echo " "; + } + echo $item->name; + echo ""; + + // Рекурсивный вывод дочерних элементов + renderMenu($tree, $item->id); + + echo "
    • "; + } + } + echo '
    '; +} + +renderMenu($menuTree); +``` + +--- + +### Проверка состояния коллапса меню + +```php +use yii_app\records\CrmMenu; + +$currentUrl = Yii::$app->request->url; +$isCollapsed = CrmMenu::isLeftMenuCollapsed($currentUrl); + +// В layout файле + +``` + +--- + +### Получение заголовка страницы + +```php +use yii_app\records\CrmMenu; + +$currentUrl = Yii::$app->controller->route; +$pageTitle = CrmMenu::getTitle($currentUrl); + +$this->title = $pageTitle; +``` + +--- + +### Добавление нового пункта меню + +```php +use yii_app\records\CrmMenu; + +$menuItem = new CrmMenu(); +$menuItem->name = 'Новый раздел'; +$menuItem->parent_id = 0; // Верхний уровень +$menuItem->url = '/new-section/index'; +$menuItem->icon_file = 'fa fa-star'; +$menuItem->visible = 1; +$menuItem->menu_close = 0; +$menuItem->posit = 100; // В конец списка + +if ($menuItem->save()) { + echo "Пункт меню создан"; +} +``` + +--- + +### Построение полного дерева меню (без фильтрации) + +```php +use yii_app\records\CrmMenu; + +$fullTree = CrmMenu::getTree(); + +// Функция для отображения всех уровней +function displayTree($tree, $level = 0, $parentId = 0) { + if (!isset($tree[$parentId])) { + return; + } + + foreach ($tree[$parentId] as $item) { + echo str_repeat(' ', $level) . "- {$item->name} ({$item->url})\n"; + displayTree($tree, $level + 1, $item->id); + } +} + +displayTree($fullTree); +``` + +--- + +### Обновление позиции пункта меню + +```php +use yii_app\records\CrmMenu; + +$menuItem = CrmMenu::findOne($menuId); +if ($menuItem) { + $menuItem->posit = 50; // Новая позиция + $menuItem->save(); +} +``` + +--- + +### Скрытие пункта меню + +```php +use yii_app\records\CrmMenu; + +$menuItem = CrmMenu::findOne($menuId); +if ($menuItem) { + $menuItem->visible = 0; + $menuItem->save(); +} +``` + +--- + +### Изменение состояния коллапса + +```php +use yii_app\records\CrmMenu; + +$url = '/dashboard/index'; +$menuItem = CrmMenu::find()->where(['url' => $url])->one(); + +if ($menuItem) { + // Переключение состояния + $menuItem->menu_close = $menuItem->menu_close ? 0 : 1; + $menuItem->save(); +} +``` + +--- + +## Связанные модели + +- **Admin** — пользователи системы (для фильтрации по правам) +- **AuthItem** — разрешения RBAC +- **AuthAssignment** — назначения разрешений пользователям + +--- + +## Бизнес-логика + +### Иерархическая структура + +Меню строится как дерево с помощью поля `parent_id`: +- Элементы с `parent_id = 0` или `null` — корневые +- Дочерние элементы ссылаются на ID родителя +- Неограниченная вложенность + +### Права доступа (RBAC) + +Модель интегрирована с системой RBAC Yii2: +- Для каждого URL создается разрешение формата `menu{url}` +- Пример: для URL `/sales/list` — разрешение `menu/sales/list` +- Метод `getTreeByUserId()` показывает только разрешенные пункты + +### Кэширование + +Метод `getTitle()` использует кэширование на 1 час для оптимизации производительности. Метод `getTree()` имеет закомментированную реализацию с кэшированием. + +### Сортировка + +Все методы используют поле `posit` для упорядочивания пунктов меню. Меньшее значение = выше в списке. + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Индекс по URL для быстрого поиска +CREATE INDEX idx_crm_menu_url ON crm_menu(url); + +-- Индекс по parent_id для построения дерева +CREATE INDEX idx_crm_menu_parent ON crm_menu(parent_id); + +-- Составной индекс для сортировки +CREATE INDEX idx_crm_menu_parent_posit ON crm_menu(parent_id, posit); + +-- Индекс для фильтрации видимых элементов +CREATE INDEX idx_crm_menu_visible ON crm_menu(visible); +``` + +--- + +## Расширения модели + +### Рекомендуемые дополнительные поля + +```php +// Дополнительные настройки отображения +'css_class' => 'string(100)' // CSS класс для пункта +'target' => 'string(20)' // _blank, _self и т.д. + +// Дополнительная информация +'description' => 'text' // Описание пункта меню +'is_active' => 'boolean' // Активность пункта + +// Системные поля +'created_at' => 'timestamp' // Дата создания +'updated_at' => 'timestamp' // Дата обновления +``` + +--- + +## Замечания + +1. **RBAC интеграция** — тесная связь с системой прав доступа Yii2 +2. **Кэширование** — используется для оптимизации, но закомментировано в getTree() +3. **Type hints** — в объявлении класса используется `declare(strict_types = 1)` +4. **Ограничения** — отсутствует валидация, метод rules() не определен +5. **Сортировка** — важно поддерживать уникальность и логичность значений `posit` + +--- + +## Связанные документы + +- [Admin.md](./Admin.md) — модель администраторов +- [RBAC Guide](/docs/guides/RBAC.md) — система прав доступа + +--- + +## Версия + +Документация актуальна для версии модели на 2025-12-11 diff --git a/erp24/docs/models/CrmMenuPermission.md b/erp24/docs/models/CrmMenuPermission.md new file mode 100644 index 00000000..15132f9c --- /dev/null +++ b/erp24/docs/models/CrmMenuPermission.md @@ -0,0 +1,182 @@ +# Класс: CrmMenuPermission + + +## Mindmap + +```mermaid +mindmap + root((CrmMenuPermission)) + Таблица БД + crm_menu_permission + Свойства + id + int + name + string + alias + string + menu_id + int + Связи + Menu + 1:1 CrmMenu + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель прав доступа к пунктам CRM-меню в ERP24. Определяет разрешения (permissions) для конкретных пунктов меню, позволяя настраивать гранулированный контроль доступа к функционалу системы. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`crm_menu_permission` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(250) | Название права доступа (человекочитаемое) | +| `alias` | varchar(120) | Алиас права для использования в коде | +| `posit` | int / null | Позиция для сортировки | +| `menu_id` | int | ID пункта меню из таблицы crm_menu | + +## Связи (Relations) + +### getMenu() +Возвращает связанный пункт меню CRM. + +```php +public function getMenu(): \yii\db\ActiveQuery +``` + +**Возвращает**: `hasOne(CrmMenu::class, ['id' => 'menu_id'])` + +## Диаграмма связей + +```mermaid +erDiagram + CrmMenuPermission { + int id PK + varchar name + varchar alias + int posit + int menu_id FK + } + + CrmMenu { + int id PK + varchar name + varchar alias + } + + AdminRolePermission { + int id PK + int role_id FK + int permission_id FK + } + + AdminRole { + int id PK + varchar name + } + + CrmMenuPermission }o--|| CrmMenu : "menu_id" + AdminRolePermission }o--|| CrmMenuPermission : "permission_id" + AdminRolePermission }o--|| AdminRole : "role_id" +``` + +## Примеры использования + +### Получение всех прав для пункта меню +```php +$menuId = 5; +$permissions = CrmMenuPermission::find() + ->where(['menu_id' => $menuId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($permissions as $perm) { + echo "{$perm->name} ({$perm->alias})\n"; +} +``` + +### Проверка права доступа по алиасу +```php +$permission = CrmMenuPermission::findOne(['alias' => 'orders_edit']); +if ($permission && Yii::$app->user->can($permission->alias)) { + // Пользователь имеет право редактировать заказы +} +``` + +### Создание нового права +```php +$permission = new CrmMenuPermission(); +$permission->name = 'Редактирование заказов'; +$permission->alias = 'orders_edit'; +$permission->menu_id = $ordersMenu->id; +$permission->posit = 10; +$permission->save(); +``` + +### Получение прав с информацией о меню +```php +$permissions = CrmMenuPermission::find() + ->with('menu') + ->orderBy(['menu_id' => SORT_ASC, 'posit' => SORT_ASC]) + ->all(); + +foreach ($permissions as $perm) { + echo "Меню: {$perm->menu->name}, Право: {$perm->name}\n"; +} +``` + +### Формирование списка для выбора +```php +$permissionsList = ArrayHelper::map( + CrmMenuPermission::find() + ->with('menu') + ->all(), + 'id', + function($model) { + return "{$model->menu->name} - {$model->name}"; + } +); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 250) | +| `alias` | required, string (max 120) | +| `posit` | integer, nullable | +| `menu_id` | required, integer | + +## Связанные модели + +- [CrmMenu](./CrmMenu.md) — пункты CRM-меню +- [AdminRole](./AdminRole.md) — роли пользователей (через AdminRolePermission) +- [AdminRolePermission](./AdminRolePermission.md) — связь ролей с правами + +## Особенности реализации + +1. **Гранулярность прав**: Каждый пункт меню может иметь несколько прав (просмотр, редактирование, удаление и т.д.) +2. **Алиасы для кода**: Поле alias используется в проверках can() для RBAC +3. **Сортировка**: Поле posit определяет порядок отображения прав в UI +4. **Иерархическая структура**: Права привязаны к конкретным пунктам меню через menu_id + +## Типичные права доступа + +| Алиас | Название | Описание | +|-------|----------|----------| +| `*_view` | Просмотр | Право на просмотр данных раздела | +| `*_create` | Создание | Право на создание новых записей | +| `*_edit` | Редактирование | Право на редактирование существующих записей | +| `*_delete` | Удаление | Право на удаление записей | +| `*_export` | Экспорт | Право на экспорт данных | diff --git a/erp24/docs/models/CrmMenuPermissionSearch.md b/erp24/docs/models/CrmMenuPermissionSearch.md new file mode 100644 index 00000000..7d56cbbe --- /dev/null +++ b/erp24/docs/models/CrmMenuPermissionSearch.md @@ -0,0 +1,155 @@ +# Класс: CrmMenuPermissionSearch + + +## Mindmap + +```mermaid +mindmap + root((CrmMenuPermissionSearch)) + Таблица БД + ActiveRecord + Наследование + extends CrmMenuPermission +``` + +## Назначение +Search-модель для поиска и фильтрации прав доступа к пунктам меню CRM в ERP24. Обеспечивает поиск по названию, алиасу, позиции и родительскому меню. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`CrmMenuPermission` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `posit`, `menu_id` — integer +- `name`, `alias` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос CrmMenuPermission::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, posit, menu_id + - LIKE: name, alias + +## Диаграмма структуры прав + +```mermaid +erDiagram + CrmMenuPermission { + int id PK + varchar name + varchar alias + int posit + int menu_id FK + } + + CrmMenu { + int id PK + varchar name + } + + AdminGroup { + int id PK + varchar permissions + } + + CrmMenu ||--o{ CrmMenuPermission : "menu_id" + CrmMenuPermission }o--o{ AdminGroup : "через RBAC" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new CrmMenuPermissionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new CrmMenuPermissionSearch(); +$dataProvider = $searchModel->search([ + 'CrmMenuPermissionSearch' => [ + 'name' => 'Редактирование', + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new CrmMenuPermissionSearch(); +$dataProvider = $searchModel->search([ + 'CrmMenuPermissionSearch' => [ + 'alias' => 'edit_users', + ] +]); +``` + +### Поиск по меню +```php +$searchModel = new CrmMenuPermissionSearch(); +$dataProvider = $searchModel->search([ + 'CrmMenuPermissionSearch' => [ + 'menu_id' => 5, + ] +]); +``` + +### GridView с сортировкой по позиции +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'alias', + 'posit', + 'menu_id', + ], +]) ?> +``` + +## Связанные модели + +- [CrmMenuPermission](./CrmMenuPermission.md) — базовая модель прав +- [CrmMenu](./CrmMenu.md) — пункты меню (menu_id) +- [AdminGroup](./AdminGroup.md) — должности с правами + +## Особенности реализации + +1. **RBAC-система**: Права для пунктов меню CRM +2. **Позиционирование**: posit для сортировки +3. **Алиасы**: Уникальные идентификаторы прав +4. **LIKE-поиск**: По name и alias +5. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/Dashboard.md b/erp24/docs/models/Dashboard.md index 5b7d813c..c6d9ed59 100644 --- a/erp24/docs/models/Dashboard.md +++ b/erp24/docs/models/Dashboard.md @@ -1,5 +1,29 @@ # Class: Dashboard + +## Mindmap + +```mermaid +mindmap + root((Dashboard)) + Таблица БД + dashboard + Свойства + id + int + name + string + group_id + int + Связи + DashboardFieldsLinks + 1:N DashboardFieldsLinks + FieldProperty + 1:N DashboardFieldsLinks + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель Dashboard представляет собой конфигурацию дашборда (панели мониторинга) для отображения бизнес-метрик и KPI в системе ERP24. Дашборды привязаны к группам сотрудников и содержат набор полей для визуализации данных. diff --git a/erp24/docs/models/DashboardFields.md b/erp24/docs/models/DashboardFields.md new file mode 100644 index 00000000..1bc6f4d3 --- /dev/null +++ b/erp24/docs/models/DashboardFields.md @@ -0,0 +1,540 @@ +# Class: DashboardFields + + +## Mindmap + +```mermaid +mindmap + root((DashboardFields)) + Таблица БД + dashboard_fields + Свойства + id + int + name + string + title + string + description + string + active + int + Связи + Field + 1:1 DashboardFields + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель DashboardFields представляет метаданные полей дашбордов в системе ERP24. Содержит информацию о колонках дашбордов: название, описание, формулу расчёта и статус активности. Используется для динамической конфигурации отображаемых метрик на дашбордах. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +dashboard_fields +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка для доступа к приложению +- `yii\db\ActiveQuery` - для построения запросов связей +- `yii\helpers\ArrayHelper` - для работы с массивами +- `yii\web\NotFoundHttpException` - исключение для 404 ошибки +- `DashboardFieldsProperty` - свойства полей дашборда +- `DashboardFieldsLinks` - связи дашборд-поля + +## Свойства (Properties) + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `id` | `int` | Уникальный идентификатор поля (PRIMARY KEY) | Да | +| `name` | `string` | Системное имя поля (до 100 символов), используется в коде | Да | +| `title` | `string` | Название столбца кратко (до 200 символов), отображается в интерфейсе | Да | +| `description` | `string` | Описание столбца - формула как считаем (TEXT) | Да | +| `active` | `int` | Статус активности поля (0 - неактивно, 1 - активно) | Нет (по умолчанию NULL) | +| `dashboardId` | `int` | Публичное свойство для хранения ID дашборда при фильтрации | Нет | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['id', 'name', 'title', 'description'], 'required'], // Обязательные поля + [['id', 'active'], 'integer'], // Целые числа + [['description'], 'string'], // Текстовое поле + [['name'], 'string', 'max' => 100], // Имя до 100 символов + [['title'], 'string', 'max' => 200], // Заголовок до 200 символов + [['id'], 'unique'], // ID должен быть уникальным + ]; +} +``` + +### Описание правил: +1. **required**: Поля `id`, `name`, `title`, `description` обязательны +2. **integer**: `id` и `active` должны быть целыми числами +3. **string**: `description` - текстовое поле без ограничений длины +4. **string max=100**: Системное имя ограничено 100 символами +5. **string max=200**: Заголовок ограничен 200 символами +6. **unique**: ID должен быть уникальным в таблице + +## Методы + +### __construct() + +**Описание:** Конструктор класса. Инициализирует свойство `dashboardId` из GET-параметров запроса. + +**Параметры:** Нет + +**Возвращает:** Void + +**Логика работы:** +1. Вызывает родительский конструктор `parent::__construct()` +2. Получает GET-параметры запроса через `Yii::$app->request->get()` +3. Устанавливает значение по умолчанию `dashboardId = 1` +4. Если в запросе присутствует параметр `dashboard_id`, перезаписывает значение `dashboardId` + +**Вызовы сторонних методов:** +- `parent::__construct()` - вызов конструктора родительского класса ActiveRecord +- `Yii::$app->request->get()` - получение GET-параметров запроса +- `ArrayHelper::getValue($request, 'dashboard_id')` - безопасное извлечение значения из массива + +**Пример:** +```php +// При запросе: /dashboard?dashboard_id=5 +$field = new DashboardFields(); +echo $field->dashboardId; // Выведет: 5 + +// При запросе без параметра dashboard_id +$field = new DashboardFields(); +echo $field->dashboardId; // Выведет: 1 (значение по умолчанию) +``` + +--- + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'dashboard_fields'` + +**Пример:** +```php +$tableName = DashboardFields::tableName(); +// Результат: 'dashboard_fields' +``` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели для использования в формах и сообщениях об ошибках. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +**Пример:** +```php +$labels = $field->attributeLabels(); +// Результат: +// [ +// 'id' => 'ID', +// 'name' => 'Name', +// 'title' => 'Название столбца кратко', +// 'description' => 'Описание столбца - формула как считаем', +// 'active' => 'Active', +// ] +``` + +--- + +### getField() + +**Описание:** Получает связь "один к одному" с самой собой (рекурсивная связь). Возвращает поле по связи через `field_id` только если оно активно. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного поля + +**Логика работы:** +1. Создаёт связь hasOne с DashboardFields +2. Связывает по полю `field_id` → `id` +3. Добавляет условие `active = 1` для фильтрации только активных полей + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "один к одному" +- `->onCondition(['active' => 1])` - добавление условия WHERE к связи + +**Пример:** +```php +$field = DashboardFields::findOne(5); +$relatedField = $field->field; +// Вернёт связанное активное поле или NULL +``` + +--- + +### getAllActiveIdName() + +**Описание:** Получает все активные поля в виде ассоциативного массива [id => name]. Используется для dropdown-списков в формах. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [id => name] активных полей + +**Логика работы:** +1. Инициализирует пустой массив `$result = []` +2. Вызывает метод `getAllActive()` для получения всех активных полей +3. Если найдены записи, преобразует их в ассоциативный массив с помощью `ArrayHelper::map()` +4. Возвращает результат + +**Вызовы сторонних методов:** +- `self::getAllActive()` - получение всех активных полей +- `ArrayHelper::map($values, 'id', 'name')` - преобразование массива в формат [id => name] + +**Пример:** +```php +$fields = DashboardFields::getAllActiveIdName(); +// Результат: +// [ +// 1 => 'sales_sum', +// 2 => 'sales_count', +// 5 => 'margin_percent' +// ] + +// Использование в форме +echo Html::dropDownList('field_id', null, DashboardFields::getAllActiveIdName()); +``` + +--- + +### getAllActive() + +**Описание:** Получает все активные поля из базы данных. Опционально можно отфильтровать по массиву ID. + +**Параметры:** +- `$ids` (array|null) - массив ID полей для фильтрации (опционально) + +**Возвращает:** `DashboardFields[]` - массив активных моделей DashboardFields + +**Логика работы:** +1. Создаёт базовый запрос с условием `active = 1` +2. Если передан параметр `$ids`, добавляет условие `id IN ($ids)` +3. Сортирует результаты по `id` в порядке возрастания +4. Возвращает массив объектов DashboardFields + +**Вызовы сторонних методов:** +- `self::find()` - создание запроса ActiveQuery +- `->where([self::tableName().'.active' => 1])` - фильтр активных записей +- `->andWhere(['id' => $ids])` - дополнительный фильтр по ID +- `->orderBy([self::tableName().'.id' => SORT_ASC])` - сортировка +- `->all()` - выполнение запроса и получение массива объектов + +**Пример:** +```php +// Получить все активные поля +$fields = DashboardFields::getAllActive(); +foreach ($fields as $field) { + echo $field->name . "\n"; +} + +// Получить конкретные активные поля +$selectedFields = DashboardFields::getAllActive([1, 3, 5]); +// Вернёт только поля с ID 1, 3, 5 и только если они активны +``` + +--- + +### getFieldProperty() + +**Описание:** Получает свойства поля дашборда через промежуточную таблицу `dashboard_fields_links`. Учитывает ID дашборда для фильтрации свойств. + +**Параметры:** +- `$dashboardId` (int) - ID дашборда (по умолчанию 1) + +**Возвращает:** `ActiveQuery` - запрос для получения DashboardFieldsProperty + +**Логика работы:** +1. Проверяет наличие свойства `$this->dashboardId` и использует его вместо параметра, если оно не пустое +2. Создаёт связь hasOne с DashboardFieldsProperty +3. Использует viaTable для связи через промежуточную таблицу `dashboard_fields_links` +4. В анонимной функции viaTable добавляет условие фильтрации по `dashboard_id` +5. Связывает: `field_id` → `id` в DashboardFields, затем `property_field_id` → `id` в DashboardFieldsProperty + +**Вызовы сторонних методов:** +- `$this->hasOne()` - создание связи "один к одному" +- `->viaTable('dashboard_fields_links', ['field_id'=>'id'], function($query) {...})` - связь через промежуточную таблицу +- `$query->andWhere([...])` - добавление условия фильтрации в подзапросе + +**Пример:** +```php +// Получение свойств поля для дашборда 3 +$field = DashboardFields::findOne(1); +$field->dashboardId = 3; +$property = $field->fieldProperty; + +// Или с параметром +$property = $field->getFieldProperty(5)->one(); +// Вернёт DashboardFieldsProperty для дашборда 5 +``` + +--- + +### getStyle() + +**Описание:** Получает CSS-стиль для ячейки дашборда на основе значения и диапазонов, определённых в свойствах поля. + +**Параметры:** +- `$attribute` (string) - имя атрибута (системное название поля) +- `$value` (mixed) - значение ячейки для определения стиля + +**Возвращает:** `string` - CSS-стиль для ячейки (например, "background-color: red") + +**Логика работы:** +1. Инициализирует пустую строку стиля +2. Выполняет запрос для поиска поля по системному имени (`name = $attribute`) +3. Использует joinWith для загрузки связи `fieldProperty` +4. Если поле найдено, получает объект `fieldProperty` +5. Вызывает цепочку методов для определения стиля: + - `getRanges()` - получает диапазоны значений + - `getRangeNum($value)` - определяет номер диапазона для данного значения + - `getStyleByRangeNum()` - получает CSS-стиль для этого диапазона +6. Возвращает CSS-стиль или пустую строку + +**Вызовы сторонних методов:** +- `DashboardFields::find()` - создание запроса +- `->andWhere([DashboardFields::tableName().'.name' => $attribute])` - фильтр по имени +- `->joinWith(['fieldProperty'])` - JOIN с таблицей свойств +- `->one()` - получение одной записи +- `$fieldProperty->getRanges()` - получение диапазонов из DashboardFieldsProperty +- `->getRangeNum($value)` - определение номера диапазона +- `->getStyleByRangeNum()` - получение стиля + +**Пример:** +```php +// Определение стиля для ячейки с процентом выполнения плана +$field = new DashboardFields(); +$style = $field->getStyle('plan_percent', 85); +// Результат: "background-color: yellow" (если 85% попадает в "желтую" зону) + +$style = $field->getStyle('plan_percent', 110); +// Результат: "background-color: green" (если 110% попадает в "зеленую" зону) + +// Использование в представлении +echo Html::tag('td', $value, ['style' => DashboardFields::getStyle('margin', $value)]); +``` + +--- + +### findModel() + +**Описание:** Находит модель DashboardFields по ID или генерирует исключение 404, если модель не найдена. Используется в контроллерах для безопасной загрузки моделей. + +**Параметры:** +- `$id` (int) - ID поля для поиска + +**Возвращает:** `DashboardFields` - найденная модель + +**Исключения:** `NotFoundHttpException` - если модель не найдена + +**Логика работы:** +1. Выполняет поиск модели по ID с помощью `DashboardFields::findOne(['id' => $id])` +2. Если модель найдена (`$model !== null`), возвращает её +3. Если модель не найдена, выбрасывает исключение `NotFoundHttpException` с сообщением "The requested page does not exist." + +**Вызовы сторонних методов:** +- `DashboardFields::findOne(['id' => $id])` - поиск записи по первичному ключу +- `throw new NotFoundHttpException()` - генерация 404 ошибки + +**Пример:** +```php +// В контроллере +public function actionUpdate($id) +{ + $model = DashboardFields::findModel($id); + // Если ID не существует, пользователь получит 404 ошибку + + if ($model->load(Yii::$app->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } + + return $this->render('update', ['model' => $model]); +} +``` + +## Связи (Relations) + +```mermaid +erDiagram + DASHBOARD_FIELDS ||--o{ DASHBOARD_FIELDS_LINKS : "has many" + DASHBOARD_FIELDS ||--o| DASHBOARD_FIELDS : "self reference" + DASHBOARD_FIELDS ||--o{ DASHBOARD_FIELDS_PROPERTY : "has many properties" + DASHBOARD_FIELDS_LINKS }o--|| DASHBOARD : "belongs to" + + DASHBOARD_FIELDS { + int id PK + string name + string title + text description + int active + } + + DASHBOARD_FIELDS_LINKS { + int dashboard_id FK + int field_id FK + int order_num + int property_field_id FK + } + + DASHBOARD_FIELDS_PROPERTY { + int id PK + string name + json ranges + } + + DASHBOARD { + int id PK + string name + int group_id FK + } +``` + +## Примеры использования + +### Создание нового поля дашборда + +```php +$field = new DashboardFields(); +$field->id = 10; +$field->name = 'revenue_per_employee'; +$field->title = 'Выручка на сотрудника'; +$field->description = 'Рассчитывается как: sales_sum / admin_count'; +$field->active = 1; + +if ($field->save()) { + echo "Поле создано с ID: " . $field->id; +} else { + print_r($field->errors); +} +``` + +### Получение всех активных полей для dropdown + +```php +use yii\helpers\Html; + +// В представлении формы +echo Html::activeDropDownList( + $model, + 'field_id', + DashboardFields::getAllActiveIdName(), + ['prompt' => 'Выберите поле...'] +); +``` + +### Получение поля с свойствами для конкретного дашборда + +```php +$field = DashboardFields::find() + ->where(['name' => 'sales_sum']) + ->one(); + +$field->dashboardId = 2; // Установка ID дашборда + +// Получение свойств (диапазоны, стили и т.д.) +$property = $field->fieldProperty; + +if ($property) { + echo "Диапазоны: " . json_encode($property->ranges); +} +``` + +### Применение стилей к ячейкам дашборда + +```php +// В представлении дашборда +$fields = DashboardFields::getAllActive([1, 2, 3]); + +foreach ($dataProvider->getModels() as $row) { + foreach ($fields as $field) { + $value = $row[$field->name]; + $style = DashboardFields::getStyle($field->name, $value); + + echo Html::tag('td', number_format($value, 2), [ + 'style' => $style + ]); + } +} +``` + +### Поиск поля по ID с обработкой ошибки + +```php +try { + $field = DashboardFields::findModel(15); + echo "Найдено поле: " . $field->title; +} catch (NotFoundHttpException $e) { + echo "Поле не найдено"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[Запрос дашборда] --> B[DashboardController] + B --> C[Dashboard::findOne] + C --> D[DashboardFieldsLinks::find] + D --> E[DashboardFields::getAllActive] + E --> F{Поле активно?} + F -->|Да| G[Загрузка свойств через getFieldProperty] + F -->|Нет| H[Пропустить поле] + G --> I[Получение данных из DashboardSales] + I --> J[Применение стилей через getStyle] + J --> K[Рендеринг ячейки дашборда] + H --> K +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Dashboard](./Dashboard.md) | Model | Главная модель дашборда | +| [DashboardFieldsLinks](./DashboardFieldsLinks.md) | Model | Связи дашборд-поля | +| [DashboardSales](./DashboardSales.md) | Model | Данные продаж для дашбордов | +| `DashboardFieldsProperty` | Model | Свойства полей (диапазоны, стили) | +| `DashboardService` | Service | Бизнес-логика работы с дашбордами | +| `DashboardController` | Controller | Контроллер управления дашбордами | + +## Примечания + +1. **Системное имя vs Заголовок**: `name` используется в коде для обращения к полю, `title` отображается пользователю +2. **Формула расчёта**: Поле `description` содержит текстовое описание формулы, но не исполняемый код +3. **Флаг активности**: Неактивные поля не отображаются в дашбордах и фильтруются методом `getAllActive()` +4. **Условные стили**: Метод `getStyle()` позволяет применять цветовое кодирование ячеек на основе значений +5. **Производительность**: При работе с множеством полей рекомендуется использовать `getAllActive()` с кешированием + +--- + +**Связанная документация:** +- [Dashboard](./Dashboard.md) +- [DashboardFieldsLinks](./DashboardFieldsLinks.md) +- [DashboardSales](./DashboardSales.md) +- [Архитектура системы аналитики](../architecture/analytics-system.md) diff --git a/erp24/docs/models/DashboardFieldsLinks.md b/erp24/docs/models/DashboardFieldsLinks.md new file mode 100644 index 00000000..9bc0a575 --- /dev/null +++ b/erp24/docs/models/DashboardFieldsLinks.md @@ -0,0 +1,376 @@ +# Class: DashboardFieldsLinks + + +## Mindmap + +```mermaid +mindmap + root((DashboardFieldsLinks)) + Таблица БД + dashboard_fields_links + Свойства + dashboard_id + int + field_id + int + order_num + int + Связи + Field + 1:1 DashboardFields + Property + 1:1 DashboardFieldsProperty + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель DashboardFieldsLinks представляет связь между дашбордами и полями в системе ERP24. Это промежуточная таблица (pivot table), которая определяет, какие поля отображаются на каком дашборде, их порядок отображения и свойства визуализации. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +dashboard_fields_links +``` + +## Использования (Dependencies) + +- `yii\db\ActiveQuery` - для построения запросов связей +- `Dashboard` - модель дашборда +- `DashboardFields` - модель полей дашборда +- `DashboardFieldsProperty` - модель свойств полей + +## Свойства (Properties) + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `dashboard_id` | `int` | ID дашборда (FK → dashboard.id), часть составного PRIMARY KEY | Да | +| `field_id` | `int` | ID поля (FK → dashboard_fields.id), часть составного PRIMARY KEY | Да | +| `order_num` | `int` | Порядковый номер поля при отображении на дашборде (сортировка колонок) | Да | +| `property_field_id` | `int` | ID свойств поля (FK → dashboard_fields_property.id), опционально | Нет | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['dashboard_id', 'field_id', 'order_num'], 'required'], // Обязательные поля + [['dashboard_id', 'field_id', 'order_num', 'property_field_id'], 'integer'], // Целые числа + [['dashboard_id', 'field_id'], 'unique', 'targetAttribute' => ['dashboard_id', 'field_id']], // Уникальная комбинация + ]; +} +``` + +### Описание правил: +1. **required**: Поля `dashboard_id`, `field_id`, `order_num` обязательны для заполнения +2. **integer**: Все поля должны быть целыми числами +3. **unique**: Комбинация `dashboard_id` + `field_id` должна быть уникальной (одно поле не может быть дважды привязано к одному дашборду) + +## Методы + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'dashboard_fields_links'` + +**Пример:** +```php +$tableName = DashboardFieldsLinks::tableName(); +// Результат: 'dashboard_fields_links' +``` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели для использования в формах и сообщениях об ошибках. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +**Пример:** +```php +$labels = (new DashboardFieldsLinks())->attributeLabels(); +// Результат: +// [ +// 'dashboard_id' => 'Dashbord ID', +// 'field_id' => 'Field ID', +// 'order_num' => 'Order Num', +// 'property_field_id' => 'Property Field ID', +// ] +``` + +--- + +### getField() + +**Описание:** Получает связь "многие к одному" с моделью DashboardFields. Возвращает метаданные поля, привязанного к данной связи, только если поле активно. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного DashboardFields + +**Логика работы:** +1. Создаёт связь hasOne с моделью DashboardFields +2. Связывает по полю `field_id` в DashboardFieldsLinks с `id` в DashboardFields +3. Добавляет условие `active = 1` для фильтрации только активных полей + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 ActiveRecord для связи "многие к одному" +- `DashboardFields::className()` - получение имени класса связанной модели +- `->onCondition(['active' => 1])` - добавление условия WHERE к связи + +**Пример:** +```php +$link = DashboardFieldsLinks::findOne(['dashboard_id' => 1, 'field_id' => 5]); +$field = $link->field; + +if ($field) { + echo "Название поля: " . $field->title; + echo "Системное имя: " . $field->name; +} else { + echo "Поле неактивно или не найдено"; +} +``` + +--- + +### getProperty() + +**Описание:** Получает связь "многие к одному" с моделью DashboardFieldsProperty. Возвращает свойства визуализации поля (диапазоны, цветовое кодирование, форматирование). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного DashboardFieldsProperty + +**Логика работы:** +1. Создаёт связь hasOne с моделью DashboardFieldsProperty +2. Связывает по полю `property_field_id` в DashboardFieldsLinks с `id` в DashboardFieldsProperty +3. Возвращает запрос без дополнительных условий + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "многие к одному" +- `DashboardFieldsProperty::className()` - получение имени класса + +**Пример:** +```php +$link = DashboardFieldsLinks::findOne(['dashboard_id' => 2, 'field_id' => 3]); +$property = $link->property; + +if ($property) { + echo "Диапазоны значений: " . json_encode($property->ranges); + echo "Формат отображения: " . $property->format; +} +``` + +## Связи (Relations) + +```mermaid +erDiagram + DASHBOARD ||--o{ DASHBOARD_FIELDS_LINKS : "has many" + DASHBOARD_FIELDS ||--o{ DASHBOARD_FIELDS_LINKS : "has many" + DASHBOARD_FIELDS_PROPERTY ||--o{ DASHBOARD_FIELDS_LINKS : "has many" + DASHBOARD_FIELDS_LINKS }o--|| DASHBOARD : "belongs to" + DASHBOARD_FIELDS_LINKS }o--|| DASHBOARD_FIELDS : "belongs to" + DASHBOARD_FIELDS_LINKS }o--o| DASHBOARD_FIELDS_PROPERTY : "belongs to" + + DASHBOARD { + int id PK + string name + int group_id FK + } + + DASHBOARD_FIELDS { + int id PK + string name + string title + text description + int active + } + + DASHBOARD_FIELDS_LINKS { + int dashboard_id FK,PK + int field_id FK,PK + int order_num + int property_field_id FK + } + + DASHBOARD_FIELDS_PROPERTY { + int id PK + string name + json ranges + string format + } +``` + +## Примеры использования + +### Привязка поля к дашборду + +```php +$link = new DashboardFieldsLinks(); +$link->dashboard_id = 1; // ID дашборда "Продажи" +$link->field_id = 5; // ID поля "sales_sum" +$link->order_num = 1; // Первая колонка +$link->property_field_id = 10; // ID свойств (цветовое кодирование) + +if ($link->save()) { + echo "Поле привязано к дашборду"; +} else { + print_r($link->errors); +} +``` + +### Получение всех полей дашборда в порядке отображения + +```php +$links = DashboardFieldsLinks::find() + ->where(['dashboard_id' => 1]) + ->orderBy(['order_num' => SORT_ASC]) + ->with('field') // Жадная загрузка полей + ->all(); + +foreach ($links as $link) { + echo "Колонка {$link->order_num}: {$link->field->title}\n"; +} +``` + +### Изменение порядка полей на дашборде + +```php +// Перемещаем поле с позиции 3 на позицию 1 +$links = DashboardFieldsLinks::find() + ->where(['dashboard_id' => 2]) + ->orderBy(['order_num' => SORT_ASC]) + ->all(); + +// Пересчитываем порядковые номера +$orderNum = 1; +foreach ($links as $link) { + $link->order_num = $orderNum++; + $link->save(); +} +``` + +### Получение поля со свойствами + +```php +$link = DashboardFieldsLinks::find() + ->where(['dashboard_id' => 1, 'field_id' => 7]) + ->with(['field', 'property']) // Загружаем связи + ->one(); + +if ($link) { + $field = $link->field; + $property = $link->property; + + echo "Поле: {$field->title}\n"; + echo "Описание: {$field->description}\n"; + + if ($property) { + echo "Свойства: {$property->name}\n"; + } +} +``` + +### Удаление поля из дашборда + +```php +$link = DashboardFieldsLinks::findOne([ + 'dashboard_id' => 3, + 'field_id' => 12 +]); + +if ($link && $link->delete()) { + echo "Поле удалено из дашборда"; +} +``` + +### Проверка наличия поля на дашборде + +```php +$exists = DashboardFieldsLinks::find() + ->where([ + 'dashboard_id' => 1, + 'field_id' => 5 + ]) + ->exists(); + +if ($exists) { + echo "Поле уже присутствует на дашборде"; +} else { + echo "Поле можно добавить"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[Запрос дашборда] --> B[Dashboard::findOne] + B --> C[getDashboardFieldsLinks] + C --> D[DashboardFieldsLinks::find] + D --> E[Сортировка по order_num] + E --> F{Для каждой связи} + F --> G[getField - загрузка метаданных поля] + F --> H[getProperty - загрузка свойств визуализации] + G --> I[DashboardFields::findOne + active=1] + H --> J[DashboardFieldsProperty::findOne] + I --> K[Рендеринг колонки] + J --> K + K --> L{Есть ещё поля?} + L -->|Да| F + L -->|Нет| M[Отображение дашборда] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Dashboard](./Dashboard.md) | Model | Модель дашборда | +| [DashboardFields](./DashboardFields.md) | Model | Метаданные полей дашборда | +| [DashboardSales](./DashboardSales.md) | Model | Данные продаж для отображения | +| `DashboardFieldsProperty` | Model | Свойства визуализации полей | +| `DashboardService` | Service | Бизнес-логика работы с дашбордами | +| `DashboardController` | Controller | Управление дашбордами | + +## Примечания + +1. **Составной первичный ключ**: Таблица использует композитный ключ (`dashboard_id`, `field_id`), что предотвращает дублирование полей на одном дашборде +2. **Порядок отображения**: Поле `order_num` определяет последовательность колонок слева направо на дашборде +3. **Опциональные свойства**: `property_field_id` может быть NULL, если поле не требует специальной визуализации +4. **Фильтрация активных**: Связь `getField()` автоматически фильтрует неактивные поля +5. **Производительность**: При загрузке дашборда рекомендуется использовать `with(['field', 'property'])` для минимизации запросов к БД + +## Миграции + +Связанные миграции БД: +- `m*_create_dashboard_fields_links_table` - создание таблицы связей +- `m*_add_property_field_id_to_dashboard_fields_links` - добавление колонки property_field_id +- `m*_add_foreign_keys_dashboard_fields_links` - создание внешних ключей + +--- + +**Связанная документация:** +- [Dashboard](./Dashboard.md) +- [DashboardFields](./DashboardFields.md) +- [DashboardSales](./DashboardSales.md) +- [Архитектура системы аналитики](../architecture/analytics-system.md) diff --git a/erp24/docs/models/DashboardFieldsProperty.md b/erp24/docs/models/DashboardFieldsProperty.md new file mode 100644 index 00000000..89051e3f --- /dev/null +++ b/erp24/docs/models/DashboardFieldsProperty.md @@ -0,0 +1,256 @@ +# Модель DashboardFieldsProperty + + +## Mindmap + +```mermaid +mindmap + root((DashboardFieldsProperty)) + Таблица БД + dashboard_fields_property + Свойства + id + int + name + string + level_value_3 + int + level_name_3 + string + level_value_2 + int + level_name_2 + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `DashboardFieldsProperty` хранит настройки визуального отображения метрик на дашборде с трёхуровневой системой раскраски. Определяет пороговые значения для каждого уровня и соответствующие CSS-стили (цвета). Используется для автоматической цветовой индикации показателей на дашборде магазина. + +**Файл модели:** `erp24/records/DashboardFieldsProperty.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `dashboard_fields_property` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(100) | Название метрики/показателя | +| `level_value_1` | INTEGER | Пороговое значение 1-го уровня | +| `level_name_1` | VARCHAR(100) | Название 1-го уровня | +| `level_style_1` | VARCHAR(100) | CSS-класс стиля 1-го уровня | +| `level_value_2` | INTEGER | Пороговое значение 2-го уровня | +| `level_name_2` | VARCHAR(100) | Название 2-го уровня | +| `level_style_2` | VARCHAR(100) | CSS-класс стиля 2-го уровня | +| `level_value_3` | INTEGER | Пороговое значение 3-го уровня | +| `level_name_3` | VARCHAR(100) | Название 3-го уровня | +| `level_style_3` | VARCHAR(100) | CSS-класс стиля 3-го уровня | + +--- + +## Статические свойства + +### `$styleValueMap` + +Карта соответствия CSS-классов и их текстовых описаний: + +```php +public static array $styleValueMap = [ + 'bg-success' => 'зелёный', + 'bg-gray-200' => 'серый', + 'bg-danger text-white' => 'красный', +]; +``` + +### `$configLevelFieldsNames` + +Конфигурация имён полей для каждого уровня: + +```php +public static array $configLevelFieldsNames = [ + 1 => ['value' => 'level_value_1', 'style' => 'level_style_1'], + 2 => ['value' => 'level_value_2', 'style' => 'level_style_2'], + 3 => ['value' => 'level_value_3', 'style' => 'level_style_3'], +]; +``` + +--- + +## Методы модели + +### `getRanges(): self` + +Вычисляет диапазоны значений для каждого уровня. + +```php +public function getRanges() +{ + // Возвращает массив с диапазонами [low, high] для каждого уровня + // Уровень 1: [0, level_value_1] + // Уровень 2: [level_value_1, level_value_2] + // Уровень 3: [level_value_2, level_value_3] +} +``` + +### `getAllRanges(): array` + +Статический метод, возвращающий диапазоны и стили для всех настроек. + +```php +public static function getAllRanges() +{ + return [ + 'ranges' => $ranges, // Диапазоны по ID настройки + 'styles' => $styles, // Стили по ID настройки + ]; +} +``` + +### `getRangeNum(int $val): self` + +Определяет номер уровня для заданного значения. + +### `getRangeNumByValue(int $val, array $ranges): ?int` + +Статический метод определения уровня по значению и диапазонам. + +### `getRangeNumByValueForDays(int $val, array $ranges, int $days, ?string $fieldRowTypeSum): ?int` + +Определение уровня с учётом количества дней (для накопительных показателей). + +### `getStyleByRangeNum(): string` + +Возвращает CSS-класс стиля для текущего номера уровня. + +### `getIdName(): array` + +Возвращает карту ID → название для выпадающих списков. + +### `getAll(?array $ids = null): array` + +Получает все настройки или по указанным ID. + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + dashboard_fields_property ||--o{ dashboard_fields : "used_by" + + dashboard_fields_property { + int id PK + string name + int level_value_1 + string level_name_1 + string level_style_1 + int level_value_2 + string level_name_2 + string level_style_2 + int level_value_3 + string level_name_3 + string level_style_3 + } +``` + +--- + +## Примеры использования + +### Создание настройки для метрики + +```php +$property = new DashboardFieldsProperty(); +$property->name = 'Конверсия продаж'; +$property->level_value_1 = 30; +$property->level_name_1 = 'Низкая'; +$property->level_style_1 = 'bg-danger text-white'; +$property->level_value_2 = 60; +$property->level_name_2 = 'Средняя'; +$property->level_style_2 = 'bg-gray-200'; +$property->level_value_3 = 100; +$property->level_name_3 = 'Высокая'; +$property->level_style_3 = 'bg-success'; +$property->save(); +``` + +### Определение стиля для значения + +```php +$property = DashboardFieldsProperty::findOne($propertyId); +$property->getRanges()->getRangeNum($currentValue); +$cssClass = $property->getStyleByRangeNum(); + +echo "
    {$currentValue}%
    "; +``` + +### Получение всех диапазонов для рендеринга + +```php +$allData = DashboardFieldsProperty::getAllRanges(); + +foreach ($metrics as $metric) { + $propertyId = $metric['property_id']; + $value = $metric['value']; + + $level = DashboardFieldsProperty::getRangeNumByValue( + $value, + $allData['ranges'][$propertyId] + ); + + $style = $allData['styles'][$propertyId][$level]; + echo "{$value}"; +} +``` + +### Формирование выпадающего списка + +```php +$dropdown = DashboardFieldsProperty::getIdName(); +// ['1' => 'Конверсия продаж', '2' => 'Выполнение плана', ...] + +echo Html::dropDownList('property_id', $selected, $dropdown); +``` + +### Расчёт уровня для накопительного показателя + +```php +$daysInPeriod = 7; +$totalValue = 150; +$ranges = $allData['ranges'][$propertyId]; + +$level = DashboardFieldsProperty::getRangeNumByValueForDays( + $totalValue, + $ranges, + $daysInPeriod, + 'sum' // Тип накопления +); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name`, `level_value_3`, `level_value_2`, `level_value_1` | Обязательные | +| `level_value_*` | Целые числа | +| `name`, `level_name_*`, `level_style_*` | Строка, макс. 100 символов | + +--- + +## Связанные модели + +- **[Dashboard](./Dashboard.md)** — дашборды +- **[DashboardFields](./DashboardFields.md)** — поля дашборда + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/DashboardFieldsPropertySearch.md b/erp24/docs/models/DashboardFieldsPropertySearch.md new file mode 100644 index 00000000..77a02c33 --- /dev/null +++ b/erp24/docs/models/DashboardFieldsPropertySearch.md @@ -0,0 +1,150 @@ +# Класс: DashboardFieldsPropertySearch + + +## Mindmap + +```mermaid +mindmap + root((DashboardFieldsPropertySearch)) + Таблица БД + ActiveRecord + Наследование + extends DashboardFieldsProperty +``` + +## Назначение +Search-модель для поиска и фильтрации свойств полей дашборда с уровневой системой стилизации в ERP24. Обеспечивает поиск по названию и трём уровням условного форматирования. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`DashboardFieldsProperty` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `level_value_3`, `level_value_2`, `level_value_1` — integer +- `name`, `level_name_3`, `level_style_3`, `level_name_2`, `level_style_2`, `level_name_1`, `level_style_1` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос DashboardFieldsProperty::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, level_value_3, level_value_2, level_value_1 + - LIKE: name, level_name_*, level_style_* + +## Диаграмма трёхуровневой системы + +```mermaid +flowchart TD + A[Значение поля] --> B{Сравнение} + + B -->|>= level_value_3| C[Уровень 3
    level_name_3
    level_style_3] + B -->|>= level_value_2| D[Уровень 2
    level_name_2
    level_style_2] + B -->|>= level_value_1| E[Уровень 1
    level_name_1
    level_style_1] + B -->|< level_value_1| F[Без стиля] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new DashboardFieldsPropertySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new DashboardFieldsPropertySearch(); +$dataProvider = $searchModel->search([ + 'DashboardFieldsPropertySearch' => [ + 'name' => 'Выполнение плана', + ] +]); +``` + +### Поиск по значению уровня +```php +$searchModel = new DashboardFieldsPropertySearch(); +$dataProvider = $searchModel->search([ + 'DashboardFieldsPropertySearch' => [ + 'level_value_3' => 100, // >= 100% + ] +]); +``` + +### Поиск по стилю +```php +$searchModel = new DashboardFieldsPropertySearch(); +$dataProvider = $searchModel->search([ + 'DashboardFieldsPropertySearch' => [ + 'level_style_3' => 'color: green', + ] +]); +``` + +### GridView с уровнями +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'attribute' => 'level_value_3', + 'label' => 'Порог 3', + ], + [ + 'attribute' => 'level_name_3', + 'label' => 'Название 3', + ], + [ + 'attribute' => 'level_style_3', + 'label' => 'Стиль 3', + ], + ], +]) ?> +``` + +## Связанные модели + +- [DashboardFieldsProperty](./DashboardFieldsProperty.md) — базовая модель свойств +- [DashboardFields](./DashboardFields.md) — поля дашборда +- [Dashboard](./Dashboard.md) — дашборды + +## Особенности реализации + +1. **Трёхуровневая система**: level_1, level_2, level_3 для градации +2. **Условное форматирование**: value + name + style для каждого уровня +3. **CSS-стили**: level_style_* содержит CSS для визуализации +4. **LIKE-поиск**: По названиям и стилям +5. **Integer для значений**: Точное совпадение пороговых значений diff --git a/erp24/docs/models/DashboardListSearch.md b/erp24/docs/models/DashboardListSearch.md new file mode 100644 index 00000000..a3078c96 --- /dev/null +++ b/erp24/docs/models/DashboardListSearch.md @@ -0,0 +1,102 @@ +# Класс: DashboardListSearch + + +## Mindmap + +```mermaid +mindmap + root((DashboardListSearch)) + Таблица БД + ActiveRecord + Наследование + extends Dashboard +``` + +## Назначение +Search-модель для поиска и фильтрации списка дашбордов в ERP24. Простая модель для поиска по ID, названию и группе дашборда. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Dashboard` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `group_id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Dashboard::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, group_id + - LIKE: name + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new DashboardListSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new DashboardListSearch(); +$dataProvider = $searchModel->search([ + 'DashboardListSearch' => [ + 'name' => 'Продажи', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new DashboardListSearch(); +$dataProvider = $searchModel->search([ + 'DashboardListSearch' => [ + 'group_id' => 1, + ] +]); +``` + +## Связанные модели + +- [Dashboard](./Dashboard.md) — базовая модель дашборда +- [DashboardSearch](./DashboardSearch.md) — аналогичная Search-модель + +## Особенности реализации + +1. **Дублирование**: Идентична DashboardSearch +2. **Простая модель**: Минимальный набор полей +3. **LIKE-поиск**: Для названия дашборда +4. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/DashboardSales.md b/erp24/docs/models/DashboardSales.md new file mode 100644 index 00000000..187b5a57 --- /dev/null +++ b/erp24/docs/models/DashboardSales.md @@ -0,0 +1,575 @@ +# Class: DashboardSales + + +## Mindmap + +```mermaid +mindmap + root((DashboardSales)) + Таблица БД + dashboard_sales + Свойства + date + string + store_id + int + field_name + string + field_id + int + summ + float + last_modified + string + Связи + Field + 1:1 DashboardFields + Store + 1:1 CityStore + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель DashboardSales хранит агрегированные данные продаж для отображения на дашбордах в системе ERP24. Содержит предрасчитанные метрики продаж по магазинам и датам для быстрого отображения на панелях мониторинга. Данные обновляются периодически и хранятся в денормализованном виде для повышения производительности. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +dashboard_sales +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка для доступа к приложению +- `yii\db\ActiveQuery` - для построения запросов связей +- `yii\helpers\ArrayHelper` - для работы с массивами +- `DashboardFields` - метаданные полей дашборда +- `DashboardFieldsProperty` - свойства визуализации полей +- `CityStore` - модель магазинов + +## Свойства (Properties) + +### Свойства базы данных + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `date` | `string` | Дата, к которой относятся данные (формат: Y-m-d) | Да | +| `store_id` | `int` | ID магазина (FK → city_store.id) | Да | +| `field_name` | `string` | Системное имя поля/метрики (до 25 символов) | Да | +| `field_id` | `int` | ID поля из справочника (FK → dashboard_fields.id) | Да | +| `summ` | `float` | Значение метрики (числовое) | Да | +| `last_modified` | `string` | Дата и время последнего изменения записи (формат: Y-m-d H:i:s) | Да | + +### Публичные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `fieldName` | `mixed` | Виртуальное свойство для хранения имени поля | +| `storeName` | `mixed` | Виртуальное свойство для хранения названия магазина | +| `fieldNameProperty` | `mixed` | Виртуальное свойство для хранения свойств поля | +| `dashboardId` | `mixed` | ID дашборда для фильтрации связей | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['date', 'store_id', 'field_name', 'field_id', 'summ', 'last_modified'], 'required'], + [['date', 'last_modified'], 'safe'], // Даты + [['store_id', 'field_id'], 'integer'], // Целые числа + [['summ'], 'number'], // Числовое значение (float) + [['field_name'], 'string', 'max' => 25], // Имя поля до 25 символов + [['date', 'store_id', 'field_name'], 'unique', 'targetAttribute' => ['date', 'store_id', 'field_name']], // Уникальная комбинация + ]; +} +``` + +### Описание правил: +1. **required**: Все основные поля обязательны для заполнения +2. **safe**: Поля дат проходят безопасную валидацию +3. **integer**: `store_id` и `field_id` должны быть целыми числами +4. **number**: `summ` может быть числом с плавающей точкой +5. **string max=25**: Имя поля ограничено 25 символами +6. **unique**: Комбинация дата + магазин + имя поля должна быть уникальной (предотвращает дублирование данных) + +## Методы + +### __construct() + +**Описание:** Конструктор класса. Инициализирует свойство `dashboardId` со значением по умолчанию. + +**Параметры:** Нет + +**Возвращает:** Void + +**Логика работы:** +1. Вызывает родительский конструктор `parent::__construct()` +2. Инициализирует пустой массив `$request = []` (закомментирован получение GET-параметров) +3. Устанавливает значение по умолчанию `dashboardId = 1` +4. Если в запросе есть параметр `dashboard_id`, перезаписывает значение + +**Вызовы сторонних методов:** +- `parent::__construct()` - вызов конструктора родительского класса +- `ArrayHelper::getValue($request, 'dashboard_id')` - безопасное извлечение значения + +**Пример:** +```php +$sales = new DashboardSales(); +echo $sales->dashboardId; // Выведет: 1 +``` + +--- + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'dashboard_sales'` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +**Пример:** +```php +$labels = (new DashboardSales())->attributeLabels(); +// Результат: +// [ +// 'date' => 'Дата', +// 'store_id' => 'Магазин', +// 'field_name' => 'Field Name', +// 'field_id' => 'Field ID', +// 'summ' => 'Значение', +// 'last_modified' => 'Изменено', +// ] +``` + +--- + +### getField() + +**Описание:** Получает связь "многие к одному" с моделью DashboardFields. Возвращает метаданные поля, к которому относятся данные продаж. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного DashboardFields + +**Логика работы:** +1. Создаёт связь hasOne с DashboardFields +2. Связывает по полю `field_id` в DashboardSales с `id` в DashboardFields + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "многие к одному" +- `DashboardFields::className()` - получение имени класса + +**Пример:** +```php +$sales = DashboardSales::findOne(['date' => '2025-01-15', 'store_id' => 5, 'field_name' => 'sales_sum']); +$field = $sales->field; +echo "Поле: {$field->title}"; // "Поле: Сумма продаж" +``` + +--- + +### getStore() + +**Описание:** Получает связь "многие к одному" с моделью CityStore. Возвращает информацию о магазине, к которому относятся данные. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного CityStore + +**Логика работы:** +1. Создаёт связь hasOne с CityStore +2. Связывает по полю `store_id` в DashboardSales с `id` в CityStore + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "многие к одному" +- `CityStore::className()` - получение имени класса + +**Пример:** +```php +$sales = DashboardSales::findOne(['date' => '2025-01-15', 'store_id' => 3, 'field_name' => 'sales_count']); +$store = $sales->store; +echo "Магазин: {$store->store_name}"; // "Магазин: ТЦ Мега" +``` + +--- + +### getFieldProperty() + +**Описание:** Получает свойства поля через промежуточную таблицу `dashboard_fields_links` с учётом конкретного дашборда. Позволяет получить настройки визуализации (диапазоны, цвета) для данного поля на конкретном дашборде. + +**Параметры:** +- `$dashboardId` (int) - ID дашборда (по умолчанию 1) + +**Возвращает:** `ActiveQuery` - запрос для получения DashboardFieldsProperty + +**Логика работы:** +1. Проверяет наличие свойства `$this->dashboardId` и использует его вместо параметра +2. Создаёт связь hasOne с DashboardFieldsProperty +3. Использует viaTable для связи через `dashboard_fields_links` +4. В анонимной функции добавляет фильтр по `dashboard_id` +5. Связывает `field_id` → `field_id` → `property_field_id` → `id` + +**Вызовы сторонних методов:** +- `$this->hasOne()` - создание связи "один к одному" +- `->viaTable()` - связь через промежуточную таблицу +- `$query->andWhere(['dashboard_id' => $dashboardId])` - фильтрация по дашборду + +**Пример:** +```php +$sales = DashboardSales::findOne(['date' => '2025-01-15', 'store_id' => 2, 'field_name' => 'margin']); +$sales->dashboardId = 3; +$property = $sales->fieldProperty; + +if ($property) { + echo "Диапазоны: " . json_encode($property->ranges); +} +``` + +--- + +### getTotal() + +**Описание:** Статический метод для подсчёта суммы значений по указанному полю в наборе данных (провайдере). Используется для отображения итогов в дашбордах. + +**Параметры:** +- `$provider` (array|DataProvider) - набор данных (массив или DataProvider) +- `$fieldName` (string) - имя поля для суммирования + +**Возвращает:** `string` - отформатированное числовое значение с 2 знаками после запятой + +**Логика работы:** +1. Инициализирует переменную `$total = 0` +2. Перебирает все элементы провайдера в цикле foreach +3. Суммирует значения поля `$fieldName` из каждого элемента +4. Форматирует результат с помощью `number_format($total, 2)` +5. Возвращает отформатированную строку + +**Вызовы сторонних методов:** +- `number_format($total, 2)` - форматирование числа с 2 десятичными знаками + +**Пример:** +```php +$data = [ + ['sales_sum' => 1500.50, 'store_id' => 1], + ['sales_sum' => 2300.75, 'store_id' => 2], + ['sales_sum' => 1800.00, 'store_id' => 3], +]; + +$total = DashboardSales::getTotal($data, 'sales_sum'); +echo $total; // Выведет: "5,601.25" +``` + +--- + +### getActiveCityStore() + +**Описание:** Статический метод для получения списка ID магазинов, по которым есть данные в таблице dashboard_sales. Возвращает уникальные ID магазинов. + +**Параметры:** Нет + +**Возвращает:** `array` - массив ID магазинов [1, 3, 5, 7, ...] + +**Логика работы:** +1. Выполняет запрос с DISTINCT для получения уникальных `store_id` +2. Использует Expression для создания SQL выражения `DISTINCT(store_id)` +3. Возвращает результат в виде массива +4. Применяет `ArrayHelper::getColumn()` для извлечения колонки store_id + +**Вызовы сторонних методов:** +- `self::find()` - создание запроса ActiveQuery +- `->select(['store_id' => new \yii\db\Expression("DISTINCT(store_id)")])` - выбор уникальных значений +- `->asArray()` - возврат результата в виде массива +- `->all()` - выполнение запроса +- `ArrayHelper::getColumn($result, 'store_id')` - извлечение колонки + +**Пример:** +```php +$activeStores = DashboardSales::getActiveCityStore(); +print_r($activeStores); +// Результат: [1, 2, 5, 7, 10, 15] + +// Использование для фильтра +$stores = CityStore::find() + ->where(['id' => DashboardSales::getActiveCityStore()]) + ->all(); +``` + +--- + +### getActiveDateStore() + +**Описание:** Статический метод для получения списка дат, по которым есть данные в таблице dashboard_sales. Возвращает массив [дата => дата] для использования в dropdown. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив ['2025-01-15' => '2025-01-15', ...] + +**Логика работы:** +1. Выполняет запрос с `distinct(['date'])` для получения уникальных дат +2. Применяет кеширование результата на 600 секунд (10 минут) +3. Возвращает результат в виде массива +4. Использует `ArrayHelper::map()` для создания ассоциативного массива + +**Вызовы сторонних методов:** +- `self::find()` - создание запроса +- `->distinct(['date'])` - выбор уникальных дат +- `->cache(600)` - кеширование на 10 минут +- `->asArray()` - возврат массива +- `->all()` - выполнение запроса +- `ArrayHelper::map($result, 'date', 'date')` - создание ассоциативного массива + +**Пример:** +```php +$dates = DashboardSales::getActiveDateStore(); +print_r($dates); +// Результат: [ +// '2025-01-10' => '2025-01-10', +// '2025-01-11' => '2025-01-11', +// '2025-01-12' => '2025-01-12' +// ] + +// Использование в форме +echo Html::dropDownList('filter_date', null, DashboardSales::getActiveDateStore()); +``` + +--- + +### Getter и Setter методы + +Модель содержит стандартные getter и setter методы для всех основных свойств: + +#### getDate() / setDate($date) +Получение и установка даты записи. + +#### getStoreId() / setStoreId($store_id) +Получение и установка ID магазина. + +#### getFieldName() / setFieldName($field_name) +Получение и установка системного имени поля. + +#### getFieldId() / setFieldId($field_id) +Получение и установка ID поля. + +#### getSumm() / setSumm($summ) +Получение и установка значения метрики. + +#### getLastModified() / setLastModified() +Получение даты последнего изменения. Setter автоматически устанавливает текущую дату и время. + +**Пример использования:** +```php +$sales = new DashboardSales(); +$sales->setDate('2025-01-15') + ->setStoreId(5) + ->setFieldName('sales_sum') + ->setFieldId(1) + ->setSumm(15000.50) + ->setLastModified(); + +$sales->save(); + +echo $sales->getDate(); // "2025-01-15" +echo $sales->getSumm(); // 15000.5 +``` + +## Связи (Relations) + +```mermaid +erDiagram + DASHBOARD_SALES }o--|| DASHBOARD_FIELDS : "belongs to field" + DASHBOARD_SALES }o--|| CITY_STORE : "belongs to store" + DASHBOARD_SALES }o--o| DASHBOARD_FIELDS_PROPERTY : "has property via link" + DASHBOARD_FIELDS_LINKS ||--o{ DASHBOARD_SALES : "links" + + DASHBOARD_SALES { + string date PK + int store_id PK,FK + string field_name PK + int field_id FK + float summ + datetime last_modified + } + + DASHBOARD_FIELDS { + int id PK + string name + string title + text description + int active + } + + CITY_STORE { + int id PK + string store_name + string address + int active + } + + DASHBOARD_FIELDS_PROPERTY { + int id PK + string name + json ranges + } + + DASHBOARD_FIELDS_LINKS { + int dashboard_id FK + int field_id FK + int property_field_id FK + } +``` + +## Примеры использования + +### Сохранение данных продаж + +```php +$sales = new DashboardSales(); +$sales->date = '2025-01-15'; +$sales->store_id = 5; +$sales->field_name = 'sales_sum'; +$sales->field_id = 1; +$sales->summ = 25000.75; +$sales->last_modified = date('Y-m-d H:i:s'); + +if ($sales->save()) { + echo "Данные сохранены"; +} else { + print_r($sales->errors); +} +``` + +### Получение данных по магазину и дате + +```php +$salesData = DashboardSales::find() + ->where([ + 'date' => '2025-01-15', + 'store_id' => 3 + ]) + ->with(['field', 'store']) // Жадная загрузка связей + ->all(); + +foreach ($salesData as $item) { + echo "{$item->field->title}: {$item->summ}\n"; +} +``` + +### Подсчёт итогов по дашборду + +```php +$dataProvider = new ActiveDataProvider([ + 'query' => DashboardSales::find() + ->where(['date' => '2025-01-15']) + ->orderBy(['store_id' => SORT_ASC]) +]); + +$totalSales = DashboardSales::getTotal($dataProvider->models, 'summ'); +echo "Итого: {$totalSales} руб."; +``` + +### Получение данных с группировкой + +```php +$salesByStore = DashboardSales::find() + ->select([ + 'store_id', + 'total' => 'SUM(summ)' + ]) + ->where([ + 'date' => '2025-01-15', + 'field_name' => 'sales_sum' + ]) + ->groupBy('store_id') + ->asArray() + ->all(); +``` + +### Обновление данных за период + +```php +// Обновление данных при пересчёте метрик +DashboardSales::updateAll( + [ + 'summ' => new Expression('summ * 1.1'), // Увеличить на 10% + 'last_modified' => date('Y-m-d H:i:s') + ], + [ + 'AND', + ['>=', 'date', '2025-01-01'], + ['<=', 'date', '2025-01-31'], + ['field_name' => 'sales_sum'] + ] +); +``` + +## Поток данных + +```mermaid +flowchart TD + A[Система сбора данных] --> B[Агрегация продаж из Sales] + B --> C[Расчёт метрик по полям] + C --> D[DashboardSales::save] + D --> E[(Таблица dashboard_sales)] + E --> F[Запрос дашборда] + F --> G[DashboardSales::find] + G --> H[Загрузка связей: field, store, property] + H --> I[Применение стилей через getStyle] + I --> J[Рендеринг ячейки дашборда] + J --> K[Подсчёт итогов через getTotal] + K --> L[Отображение пользователю] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Dashboard](./Dashboard.md) | Model | Модель дашборда | +| [DashboardFields](./DashboardFields.md) | Model | Метаданные полей | +| [DashboardFieldsLinks](./DashboardFieldsLinks.md) | Model | Связи полей с дашбордами | +| [CityStore](./CityStore.md) | Model | Модель магазинов | +| [Sales](./Sales.md) | Model | Исходные данные продаж | +| `DashboardService` | Service | Бизнес-логика дашбордов | +| `MetricsCalculatorJob` | Job | Фоновая задача расчёта метрик | + +## Примечания + +1. **Денормализация данных**: Таблица содержит предрасчитанные данные для быстрого отображения дашбордов +2. **Составной уникальный ключ**: Комбинация date + store_id + field_name гарантирует отсутствие дублей +3. **Кеширование**: Методы `getActiveCityStore()` и `getActiveDateStore()` используют кеширование для оптимизации +4. **Обновление данных**: Поле `last_modified` отслеживает время последнего обновления для инкрементальных пересчётов +5. **Производительность**: Рекомендуется использовать индексы на (date, store_id) и (field_name) для быстрых выборок + +--- + +**Связанная документация:** +- [Dashboard](./Dashboard.md) +- [DashboardFields](./DashboardFields.md) +- [DashboardFieldsLinks](./DashboardFieldsLinks.md) +- [Архитектура системы аналитики](../architecture/analytics-system.md) +- [Расчёт метрик продаж](../guides/metrics-calculation.md) diff --git a/erp24/docs/models/DashboardSalesSearch.md b/erp24/docs/models/DashboardSalesSearch.md new file mode 100644 index 00000000..8d20fa1d --- /dev/null +++ b/erp24/docs/models/DashboardSalesSearch.md @@ -0,0 +1,199 @@ +# Класс: DashboardSalesSearch + + +## Mindmap + +```mermaid +mindmap + root((DashboardSalesSearch)) + Таблица БД + ActiveRecord + Наследование + extends DashboardSales +``` + +## Назначение +Search-модель для поиска и фильтрации данных продаж дашборда в ERP24. Расширенная модель с поддержкой поиска по связанным таблицам (поле, магазин) и кастомной сортировкой. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`DashboardSales` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$fieldName` | mixed | ID поля для фильтрации | +| `$storeName` | mixed | ID магазина для фильтрации | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `date`, `field_name`, `last_modified`, `fieldName`, `storeName` — safe +- `store_id`, `field_id` — integer +- `summ` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к связанным таблицам. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер с кастомной сортировкой + +**Логика:** +1. Создаёт запрос DashboardSales::find() +2. Присоединяет связи: field, fieldProperty, store, DashboardFieldsLinks +3. Настраивает кастомную сортировку по fieldName и storeName +4. Применяет фильтры по всем полям + +**joinWith:** +- `field` — DashboardFields +- `fieldProperty` — свойства поля +- `store` — CityStore +- `DashboardFieldsLinks` — связи полей + +**Кастомные атрибуты сортировки:** +- `fieldName` — сортировка по dashboard_fields.id +- `storeName` — сортировка по city_store.id + +## Диаграмма связей + +```mermaid +erDiagram + DashboardSales { + date date + int store_id FK + int field_id FK + decimal summ + datetime last_modified + } + + DashboardFields { + int id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + DashboardFieldsProperty { + int id PK + int field_id FK + } + + DashboardSales }o--|| DashboardFields : "field_id" + DashboardSales }o--|| CityStore : "store_id" + DashboardFields ||--o{ DashboardFieldsProperty : "field_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new DashboardSalesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по дате +```php +$searchModel = new DashboardSalesSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSalesSearch' => [ + 'date' => '2024-06-15', + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new DashboardSalesSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSalesSearch' => [ + 'store_id' => 10, + // или через связь + 'storeName' => 10, + ] +]); +``` + +### Поиск по полю дашборда +```php +$searchModel = new DashboardSalesSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSalesSearch' => [ + 'field_id' => 5, + // или через связь + 'fieldName' => 5, + ] +]); +``` + +### Поиск по сумме +```php +$searchModel = new DashboardSalesSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSalesSearch' => [ + 'summ' => 100000, + ] +]); +``` + +### GridView с кастомной сортировкой +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'date', + [ + 'attribute' => 'fieldName', + 'value' => 'field.name', + ], + [ + 'attribute' => 'storeName', + 'value' => 'store.name', + ], + 'summ', + 'last_modified', + ], +]) ?> +``` + +## Связанные модели + +- [DashboardSales](./DashboardSales.md) — базовая модель данных +- [DashboardFields](./DashboardFields.md) — поля дашборда +- [CityStore](./CityStore.md) — магазины +- [DashboardFieldsProperty](./DashboardFieldsProperty.md) — свойства полей +- [DashboardFieldsLinks](./DashboardFieldsLinks.md) — связи полей + +## Особенности реализации + +1. **Расширенный поиск**: joinWith к 4 связанным таблицам +2. **Дополнительные свойства**: fieldName, storeName для удобной фильтрации +3. **Кастомная сортировка**: По ID связанных таблиц +4. **Eager loading**: Оптимизация через joinWith +5. **Number-фильтр**: Для суммы (summ) diff --git a/erp24/docs/models/DashboardSearch.md b/erp24/docs/models/DashboardSearch.md new file mode 100644 index 00000000..9bf663ac --- /dev/null +++ b/erp24/docs/models/DashboardSearch.md @@ -0,0 +1,116 @@ +# Класс: DashboardSearch + + +## Mindmap + +```mermaid +mindmap + root((DashboardSearch)) + Таблица БД + ActiveRecord + Наследование + extends Dashboard +``` + +## Назначение +Search-модель для поиска и фильтрации дашбордов в ERP24. Простая модель для поиска по ID, названию и группе дашборда. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Dashboard` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `group_id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Dashboard::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, group_id + - LIKE: name + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new DashboardSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new DashboardSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSearch' => [ + 'name' => 'Аналитика', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new DashboardSearch(); +$dataProvider = $searchModel->search([ + 'DashboardSearch' => [ + 'group_id' => 2, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'group_id', + ], +]) ?> +``` + +## Связанные модели + +- [Dashboard](./Dashboard.md) — базовая модель дашборда +- [DashboardFields](./DashboardFields.md) — поля дашборда +- [DashboardListSearch](./DashboardListSearch.md) — аналогичная Search-модель + +## Особенности реализации + +1. **Простая модель**: Минимальный набор полей +2. **Дублирование**: Идентична DashboardListSearch +3. **LIKE-поиск**: Для названия дашборда +4. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/ERD_DIAGRAMS.md b/erp24/docs/models/ERD_DIAGRAMS.md index 4b291c1c..e359ba91 100644 --- a/erp24/docs/models/ERD_DIAGRAMS.md +++ b/erp24/docs/models/ERD_DIAGRAMS.md @@ -1,5 +1,35 @@ # Entity Relationship Diagrams (ERD) — ERP24 +## Mindmap + +```mermaid +mindmap + root((ERD ERP24)) + Домены + HR и сотрудники + Admin + AdminGroup + Продажи + Sales + SalesProducts + Товары + Products1c + Balances + Клиенты + Users + UsersBonus + Задачи + Task + TaskUsers + Диаграммы + 10 категорий + Mermaid формат + Связи + hasOne + hasMany + belongsTo +``` + ## Обзор Документ содержит ERD диаграммы основных доменов системы ERP24. Диаграммы созданы в формате Mermaid для визуализации связей между моделями. diff --git a/erp24/docs/models/EmployeeBalance.md b/erp24/docs/models/EmployeeBalance.md new file mode 100644 index 00000000..c4bf181e --- /dev/null +++ b/erp24/docs/models/EmployeeBalance.md @@ -0,0 +1,327 @@ +# Модель EmployeeBalance + + +## Mindmap + +```mermaid +mindmap + root((EmployeeBalance)) + Таблица БД + employee_balance + Свойства + id + int + admin_id + int + name + string + created_at + string + points + float + entity_type + string + Связи + Task + 1:1 Task + Lesson + 1:1 Lessons + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeBalance` представляет историю изменений баланса «цветорублей» сотрудников. Хранит операции начисления и списания внутренней валюты, связанные с выполнением задач, прохождением обучения и другими активностями. Используется для системы мотивации и геймификации работы сотрудников. + +**Файл модели:** `erp24/records/EmployeeBalance.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_balance` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `admin_id` | INTEGER | ID сотрудника (FK → admin) | +| `name` | TEXT | Наименование/описание операции | +| `created_at` | TIMESTAMP | Дата и время операции | +| `points` | FLOAT | Количество цветорублей (+/-) | +| `entity_type` | VARCHAR(100) | Тип связанной сущности | +| `entity_id` | VARCHAR(36) | ID связанной сущности | + +--- + +## Описание полей + +### `admin_id` — Сотрудник + +Идентификатор сотрудника, которому начислены или списаны цветорубли. + +**Связь:** `admin.id` + +### `name` — Описание операции + +Текстовое описание причины изменения баланса. + +**Примеры:** +- `"Выполнение задачи #1234"` +- `"Прохождение урока: Основы флористики"` +- `"Штраф за опоздание"` +- `"Бонус за перевыполнение плана"` + +### `points` — Цветорубли + +Количество начисленных или списанных цветорублей. + +**Положительное значение:** начисление (+100.5) +**Отрицательное значение:** списание (-50.0) + +### `entity_type` — Тип сущности + +Определяет тип объекта, с которым связана операция. + +**Возможные значения:** +- `"task"` — задача +- `"lesson"` — урок/обучение +- `"bonus"` — ручное начисление бонуса +- `"penalty"` — штраф + +### `entity_id` — ID сущности + +Идентификатор связанной сущности (задачи, урока и т.д.). + +**Формат:** строка до 36 символов (поддерживает UUID) + +--- + +## Методы модели + +### `getTask(): ActiveQuery` + +Возвращает связанную задачу (если entity_type = 'task'). + +**Тип связи:** `hasOne` +**Связанная модель:** Task +**Ключ:** `['id' => 'entity_id']` + +**Пример:** +```php +$balance = EmployeeBalance::findOne($id); +if ($balance->entity_type === 'task' && $balance->task) { + echo "Задача: {$balance->task->name}"; +} +``` + +--- + +### `getLesson(): ActiveQuery` + +Возвращает связанный урок (если entity_type = 'lesson'). + +**Тип связи:** `hasOne` +**Связанная модель:** Lessons +**Ключ:** `['id' => 'entity_id']` + +**Пример:** +```php +$balance = EmployeeBalance::findOne($id); +if ($balance->entity_type === 'lesson' && $balance->lesson) { + echo "Урок: {$balance->lesson->name}"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_balance }o--|| admin : "belongs_to" + employee_balance }o--o| task : "related_task" + employee_balance }o--o| lessons : "related_lesson" + + employee_balance { + int id PK + int admin_id FK + text name + timestamp created_at + float points + string entity_type + string entity_id + } + + admin { + int id PK + string name + float balance + } + + task { + int id PK + string name + int status + } + + lessons { + int id PK + string name + int points + } +``` + +--- + +## Примеры использования + +### Начисление цветорублей за задачу + +```php +$balance = new EmployeeBalance(); +$balance->admin_id = $adminId; +$balance->name = "Выполнение задачи: {$task->name}"; +$balance->points = 100; // Начисление +$balance->entity_type = 'task'; +$balance->entity_id = $task->id; +$balance->created_at = date('Y-m-d H:i:s'); +$balance->save(); +``` + +### Списание цветорублей (штраф) + +```php +$balance = new EmployeeBalance(); +$balance->admin_id = $adminId; +$balance->name = "Штраф: опоздание на смену"; +$balance->points = -50; // Списание +$balance->entity_type = 'penalty'; +$balance->entity_id = $timetableId; +$balance->created_at = date('Y-m-d H:i:s'); +$balance->save(); +``` + +### Получение баланса сотрудника + +```php +$totalBalance = EmployeeBalance::find() + ->where(['admin_id' => $adminId]) + ->sum('points'); + +echo "Текущий баланс: {$totalBalance} цветорублей"; +``` + +### История операций сотрудника + +```php +$history = EmployeeBalance::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(50) + ->all(); + +foreach ($history as $record) { + $sign = $record->points >= 0 ? '+' : ''; + echo "{$record->created_at}: {$sign}{$record->points} - {$record->name}\n"; +} +``` + +### Начисление за прохождение урока + +```php +$lesson = Lessons::findOne($lessonId); + +$balance = new EmployeeBalance(); +$balance->admin_id = $adminId; +$balance->name = "Пройден урок: {$lesson->name}"; +$balance->points = $lesson->points; // Баллы урока +$balance->entity_type = 'lesson'; +$balance->entity_id = $lesson->id; +$balance->created_at = date('Y-m-d H:i:s'); +$balance->save(); +``` + +### Статистика по типам операций + +```php +$stats = EmployeeBalance::find() + ->select([ + 'entity_type', + 'SUM(CASE WHEN points > 0 THEN points ELSE 0 END) as earned', + 'SUM(CASE WHEN points < 0 THEN ABS(points) ELSE 0 END) as spent', + 'COUNT(*) as count' + ]) + ->where(['admin_id' => $adminId]) + ->groupBy('entity_type') + ->asArray() + ->all(); +``` + +### Топ сотрудников по балансу + +```php +$topEmployees = EmployeeBalance::find() + ->select(['admin_id', 'SUM(points) as total']) + ->groupBy('admin_id') + ->orderBy(['total' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); +``` + +### Получение операций с деталями связанных сущностей + +```php +$records = EmployeeBalance::find() + ->with(['task', 'lesson']) + ->where(['admin_id' => $adminId]) + ->andWhere(['entity_type' => ['task', 'lesson']]) + ->all(); + +foreach ($records as $record) { + if ($record->entity_type === 'task' && $record->task) { + echo "Задача: {$record->task->name}\n"; + } elseif ($record->entity_type === 'lesson' && $record->lesson) { + echo "Урок: {$record->lesson->name}\n"; + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `name` | Обязательное, строка | +| `created_at` | Обязательное | +| `points` | Обязательное, число (float) | +| `entity_type` | Обязательное, макс. 100 символов | +| `entity_id` | Обязательное, макс. 36 символов | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[Task](./Task.md)** — задачи +- **[Lessons](./Lessons.md)** — уроки обучения + +--- + +## Бизнес-логика + +Система «цветорублей» используется для: + +1. **Мотивации сотрудников** — начисление баллов за выполнение задач +2. **Геймификации обучения** — награда за прохождение уроков +3. **Системы штрафов** — списание за нарушения +4. **Рейтингования** — определение лучших сотрудников по балансу + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeeOnShift.md b/erp24/docs/models/EmployeeOnShift.md new file mode 100644 index 00000000..de863893 --- /dev/null +++ b/erp24/docs/models/EmployeeOnShift.md @@ -0,0 +1,400 @@ +# Модель EmployeeOnShift + + +## Mindmap + +```mermaid +mindmap + root((EmployeeOnShift)) + Таблица БД + employee_on_shift + Свойства + guid + string + phone + string + created_at + string + shift_date + string + shift_type + int + datetime_start + string + Связи + Admin + 1:1 Admin + Created + 1:1 Admin + Store + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeOnShift` представляет заявки на работу сотрудников в конкретных сменах. Используется для управления временными сотрудниками, подработками и гибким графиком. Хранит информацию о запланированных сменах с указанием времени, магазина, ставки оплаты и статуса подтверждения. + +**Файл модели:** `erp24/records/EmployeeOnShift.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_on_shift` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Константы + +### Статусы активности + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `ACTIVE_ON` | 1 | Активная заявка, может использоваться в операциях | +| `ACTIVE_OFF` | 0 | Неактивная заявка | + +### Статусы подтверждения + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_INITIAL` | 0 | В ожидании подтверждения | +| `STATUS_ACCEPT` | 1 | Подтверждено | +| `STATUS_REJECT` | 2 | Отказано | + +### Статусы синхронизации с 1С + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_SOURCE_ERROR` | -1 | Получена ошибка в системе 1С | +| `STATUS_SOURCE_NOT_CREATED_IN_1C` | 0 | Не создано в 1С | +| `STATUS_SOURCE_CREATED_IN_1C` | 1 | Создано в 1С | + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `guid` | VARCHAR(36) | GUID сотрудника (PK, уникальный) | +| `first_name` | VARCHAR(40) | Имя сотрудника | +| `last_name` | VARCHAR(40) | Фамилия сотрудника | +| `phone` | VARCHAR(16) | Номер телефона | +| `created_at` | TIMESTAMP | Дата создания заявки | +| `shift_date` | DATE | Дата старта смены | +| `shift_type` | INTEGER | Тип смены (1 - дневная, 2 - ночная) | +| `datetime_start` | TIMESTAMP | Время старта смены | +| `datetime_end` | TIMESTAMP | Время окончания смены | +| `created_by` | INTEGER | ID создателя (FK → `admin.id`) | +| `store_id` | VARCHAR(36) | GUID магазина | +| `price` | INTEGER | Ставка в рублях за час | +| `salary_shift` | INTEGER | Ставка в рублях за смену | +| `status` | INTEGER | Статус подтверждения (0/1/2) | +| `status_source` | INTEGER | Статус синхронизации с 1С (-1/0/1) | +| `active` | INTEGER | Статус активности (0/1) | + +--- + +## Методы модели + +### `tableName(): string` +Возвращает имя таблицы `'employee_on_shift'`. + +### `rules(): array` +Определяет правила валидации: +- Обязательные поля: `guid`, `phone`, `created_at`, `shift_date`, `shift_type`, `datetime_start`, `datetime_end`, `created_by`, `store_id`, `price` +- `shift_type`, `created_by`, `price`, `status`, `status_source`, `active` — целые числа +- `salary_shift` — должно быть в диапазоне значений из `Timetable::getSalariesDay()` +- `guid`, `store_id` — строки, максимум 36 символов (UUID) +- `phone` — строка, максимум 16 символов +- `first_name`, `last_name` — строки, максимум 40 символов +- `guid` — уникальное значение + +--- + +## Связи (Relations) + +### `getAdmin()` +Связь с основной записью сотрудника по GUID. + +**Тип:** hasOne +**Модель:** `Admin` +**Связь:** `admin.guid` → `employee_on_shift.guid` + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); +$admin = $shift->admin; +echo $admin->name; +``` + +--- + +### `getCreated()` +Связь с сотрудником, создавшим заявку. + +**Тип:** hasOne +**Модель:** `Admin` +**Связь:** `admin.id` → `employee_on_shift.created_by` + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); +$creator = $shift->created; +echo "Заявку создал: {$creator->name}"; +``` + +--- + +### `getStore()` +Связь с магазином. + +**Тип:** hasOne +**Модель:** `Products1c` +**Условие:** `tip = 'city_store'` +**Связь:** `products_1c.id` → `employee_on_shift.store_id` + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); +$store = $shift->store; +echo "Магазин: {$store->name}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_on_shift }o--|| admin : "admin_guid" + employee_on_shift }o--|| admin : "created_by" + employee_on_shift }o--|| products_1c : "store_id" + + employee_on_shift { + string guid PK + string first_name + string last_name + string phone + timestamp created_at + date shift_date + int shift_type + timestamp datetime_start + timestamp datetime_end + int created_by FK + string store_id FK + int price + int salary_shift + int status + int status_source + int active + } + + admin { + int id PK + string guid UK + string name + } + + products_1c { + string id PK + string name + string tip + } +``` + +--- + +## Примеры использования + +### Создание заявки на смену + +```php +$shift = new EmployeeOnShift(); +$shift->guid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; +$shift->first_name = 'Иван'; +$shift->last_name = 'Петров'; +$shift->phone = '+79001234567'; +$shift->created_at = date('Y-m-d H:i:s'); +$shift->shift_date = '2025-12-15'; +$shift->shift_type = 1; // Дневная смена +$shift->datetime_start = '2025-12-15 09:00:00'; +$shift->datetime_end = '2025-12-15 21:00:00'; +$shift->created_by = Yii::$app->user->id; +$shift->store_id = 'store-guid-12345'; +$shift->price = 300; // 300 руб/час +$shift->salary_shift = 3600; // 3600 руб за смену +$shift->status = EmployeeOnShift::STATUS_INITIAL; +$shift->status_source = EmployeeOnShift::STATUS_SOURCE_NOT_CREATED_IN_1C; +$shift->active = EmployeeOnShift::ACTIVE_ON; + +if ($shift->save()) { + echo "Заявка на смену создана"; +} +``` + +--- + +### Получение всех заявок в ожидании + +```php +$pendingShifts = EmployeeOnShift::find() + ->with(['admin', 'store']) + ->where(['status' => EmployeeOnShift::STATUS_INITIAL]) + ->andWhere(['active' => EmployeeOnShift::ACTIVE_ON]) + ->orderBy(['shift_date' => SORT_ASC]) + ->all(); + +foreach ($pendingShifts as $shift) { + echo "{$shift->admin->name}: смена {$shift->shift_date} в {$shift->store->name}\n"; +} +``` + +--- + +### Подтверждение заявки + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); +$shift->status = EmployeeOnShift::STATUS_ACCEPT; +$shift->save(); + +echo "Заявка подтверждена"; +``` + +--- + +### Отклонение заявки + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); +$shift->status = EmployeeOnShift::STATUS_REJECT; +$shift->active = EmployeeOnShift::ACTIVE_OFF; +$shift->save(); + +echo "Заявка отклонена"; +``` + +--- + +### Получение смен сотрудника за период + +```php +$adminGuid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; +$dateFrom = '2025-12-01'; +$dateTo = '2025-12-31'; + +$shifts = EmployeeOnShift::find() + ->with(['store']) + ->where(['guid' => $adminGuid]) + ->andWhere(['>=', 'shift_date', $dateFrom]) + ->andWhere(['<=', 'shift_date', $dateTo]) + ->andWhere(['active' => EmployeeOnShift::ACTIVE_ON]) + ->orderBy(['shift_date' => SORT_ASC]) + ->all(); + +foreach ($shifts as $shift) { + $statusText = [ + EmployeeOnShift::STATUS_INITIAL => 'Ожидание', + EmployeeOnShift::STATUS_ACCEPT => 'Подтверждено', + EmployeeOnShift::STATUS_REJECT => 'Отказано', + ][$shift->status]; + + echo "{$shift->shift_date}: {$shift->store->name} - {$statusText}\n"; +} +``` + +--- + +### Расчет заработка за смену + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); + +// Расчет по часам +$start = new DateTime($shift->datetime_start); +$end = new DateTime($shift->datetime_end); +$hours = ($end->getTimestamp() - $start->getTimestamp()) / 3600; +$earningsByHour = $hours * $shift->price; + +// Расчет по смене +$earningsByShift = $shift->salary_shift; + +echo "Заработок по часам: {$earningsByHour} руб.\n"; +echo "Заработок по смене: {$earningsByShift} руб.\n"; +echo "К выплате: " . max($earningsByHour, $earningsByShift) . " руб.\n"; +``` + +--- + +### Получение смен по магазину на дату + +```php +$storeId = 'store-guid-12345'; +$date = '2025-12-15'; + +$shifts = EmployeeOnShift::find() + ->with(['admin']) + ->where(['store_id' => $storeId]) + ->andWhere(['shift_date' => $date]) + ->andWhere(['status' => EmployeeOnShift::STATUS_ACCEPT]) + ->andWhere(['active' => EmployeeOnShift::ACTIVE_ON]) + ->all(); + +echo "Смены в магазине на {$date}:\n"; +foreach ($shifts as $shift) { + $type = $shift->shift_type == 1 ? 'Дневная' : 'Ночная'; + echo "- {$shift->admin->name} ({$type}, {$shift->datetime_start} - {$shift->datetime_end})\n"; +} +``` + +--- + +### Синхронизация с 1С + +```php +$shift = EmployeeOnShift::findOne(['guid' => '...']); + +try { + // Логика отправки в 1С... + $result = $api1C->createShift($shift); + + if ($result['success']) { + $shift->status_source = EmployeeOnShift::STATUS_SOURCE_CREATED_IN_1C; + } else { + $shift->status_source = EmployeeOnShift::STATUS_SOURCE_ERROR; + } + + $shift->save(); +} catch (\Exception $e) { + $shift->status_source = EmployeeOnShift::STATUS_SOURCE_ERROR; + $shift->save(); +} +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `guid` | Обязательное, строка (36 символов), уникальное | +| `first_name`, `last_name` | Строка, максимум 40 символов | +| `phone` | Обязательное, строка, максимум 16 символов | +| `created_at` | Обязательное, timestamp | +| `shift_date` | Обязательное, date | +| `shift_type` | Обязательное, целое число | +| `datetime_start`, `datetime_end` | Обязательные, timestamp | +| `created_by` | Обязательное, целое число | +| `store_id` | Обязательное, строка (36 символов) | +| `price` | Обязательное, целое число | +| `salary_shift` | Целое число, должно быть в диапазоне из `Timetable::getSalariesDay()` | +| `status`, `status_source`, `active` | Целые числа | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[Products1c](./Products1c.md)** — магазины +- **[Timetable](./Timetable.md)** — расписание смен + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeePayment.md b/erp24/docs/models/EmployeePayment.md new file mode 100644 index 00000000..1e0753a2 --- /dev/null +++ b/erp24/docs/models/EmployeePayment.md @@ -0,0 +1,287 @@ +# Класс: EmployeePayment + + +## Mindmap + +```mermaid +mindmap + root((EmployeePayment)) + Таблица БД + employee_payment + Свойства + id + int + admin_id + int + monthly_salary + float + daily_payment + float + admin + Admin + adminGroup + AdminGroup + Связи + Admin + 1:1 Admin + AdminGroup + 1:1 AdminGroup + EmployeePosition + 1:1 EmployeePosition + Creator + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель оплаты труда сотрудников в ERP24. Хранит историю изменения окладов и ставок сотрудников с привязкой к должностям. Поддерживает временную версионность (history) — каждая запись действует с определённой даты. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`employee_payment` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `admin_id` | int | ID сотрудника | +| `admin_group_id` | int / null | ID должностной группы сотрудника | +| `employee_position_id` | int / null | ID должности из employee_position | +| `date` | date / null | Дата начала действия правила оплаты | +| `monthly_salary` | decimal | Месячный оклад (рублей) | +| `daily_payment` | decimal | Подневная оплата (ставка за день) | +| `creator_id` | int / null | ID сотрудника, добавившего правило | + +## Связи (Relations) + +### getAdmin() +Возвращает сотрудника, к которому относится оплата. +```php +public function getAdmin(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Admin::class, ['id' => 'admin_id'])` + +### getAdminGroup() +Возвращает должностную группу. +```php +public function getAdminGroup(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(AdminGroup::class, ['id' => 'admin_group_id'])` + +### getEmployeePosition() +Возвращает должность сотрудника. +```php +public function getEmployeePosition(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(EmployeePosition::class, ['id' => 'employee_position_id'])` + +### getCreator() +Возвращает автора записи. +```php +public function getCreator(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Admin::class, ['id' => 'creator_id'])` + +## Методы + +### getMonthlySalary() +Получает актуальную запись оплаты для сотрудника на указанную дату. + +```php +public static function getMonthlySalary(int $adminId, string $date): ?array +``` + +**Параметры**: +- `$adminId` (int) — ID сотрудника +- `$date` (string) — дата в формате Y-m-d + +**Логика**: +1. Ищет записи для сотрудника с датой <= указанной +2. Сортирует по дате DESC +3. Возвращает первую (самую актуальную) запись + +**Возвращает**: array|null — данные записи или null + +### getSalary() +Получает значение зарплаты из подготовленного массива данных. + +```php +public static function getSalary(int $adminId, string $date, array $adminData): mixed +``` + +**Параметры**: +- `$adminId` (int) — ID сотрудника +- `$date` (string) — дата +- `$adminData` (array) — массив данных вида [admin_id => [date => salary, ...], ...] + +**Логика**: +1. Находит сотрудника в массиве +2. Сортирует даты в обратном порядке +3. Находит первую дату <= указанной +4. Возвращает соответствующее значение зарплаты + +**Возвращает**: mixed — значение зарплаты или 0 + +### validateDailyPayment() +Кастомная валидация: подневная оплата не может превышать месячный оклад. + +```php +public function validateDailyPayment($attribute, $params, $validator): void +``` + +### validateUniqueDate() +Проверяет уникальность записи по комбинации admin_id + date. + +```php +public function validateUniqueDate($attribute, $params): void +``` + +### beforeValidate() +Автоматически заполняет creator_id и admin_group_id перед валидацией. + +```php +public function beforeValidate(): bool +``` + +**Логика**: +1. Устанавливает creator_id из текущего пользователя +2. Копирует group_id и employee_position_id из связанного Admin + +## Диаграмма связей + +```mermaid +erDiagram + EmployeePayment { + int id PK + int admin_id FK + int admin_group_id FK + int employee_position_id FK + date date + decimal monthly_salary + decimal daily_payment + int creator_id FK + } + + Admin { + int id PK + int group_id FK + int employee_position_id FK + varchar name + } + + AdminGroup { + int id PK + varchar name + } + + EmployeePosition { + int id PK + varchar name + } + + EmployeePayment }o--|| Admin : "admin_id" + EmployeePayment }o--o| AdminGroup : "admin_group_id" + EmployeePayment }o--o| EmployeePosition : "employee_position_id" + EmployeePayment }o--o| Admin : "creator_id" +``` + +## Диаграмма временной версионности + +```mermaid +gantt + title История оплаты сотрудника + dateFormat YYYY-MM-DD + section Оклад + 50000 руб/мес :active, 2023-01-01, 2023-06-30 + 55000 руб/мес :active, 2023-07-01, 2023-12-31 + 60000 руб/мес :active, 2024-01-01, 2024-12-31 +``` + +## Примеры использования + +### Создание записи оплаты +```php +$payment = new EmployeePayment(); +$payment->admin_id = $employee->id; +$payment->date = '2024-01-01'; +$payment->monthly_salary = 60000.00; +$payment->daily_payment = 2500.00; +$payment->save(); +// creator_id и admin_group_id заполнятся автоматически +``` + +### Получение актуального оклада на дату +```php +$salaryRecord = EmployeePayment::getMonthlySalary($employeeId, '2024-03-15'); +if ($salaryRecord) { + echo "Оклад: {$salaryRecord['monthly_salary']} руб.\n"; + echo "Ставка за день: {$salaryRecord['daily_payment']} руб.\n"; +} +``` + +### Получение истории оплаты сотрудника +```php +$history = EmployeePayment::find() + ->where(['admin_id' => $employeeId]) + ->orderBy(['date' => SORT_DESC]) + ->with(['adminGroup', 'employeePosition', 'creator']) + ->all(); + +foreach ($history as $record) { + echo "С {$record->date}: {$record->monthly_salary} руб./мес.\n"; +} +``` + +### Массовый расчёт зарплат +```php +// Подготовка данных для быстрого доступа +$adminData = []; +$payments = EmployeePayment::find() + ->select(['admin_id', 'date', 'monthly_salary']) + ->asArray() + ->all(); + +foreach ($payments as $p) { + $adminData[$p['admin_id']][$p['date']] = $p['monthly_salary']; +} + +// Использование в расчётах +foreach ($employees as $emp) { + $salary = EmployeePayment::getSalary($emp->id, $calcDate, $adminData); + // Расчёт зарплаты... +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `admin_id` | required, integer, exist (Admin) | +| `admin_group_id` | integer, exist (AdminGroup), nullable | +| `employee_position_id` | integer, exist (EmployeePosition), nullable | +| `date` | date (Y-m-d), unique с admin_id, nullable | +| `monthly_salary` | required, number > 0 | +| `daily_payment` | required, number > 0, <= monthly_salary | +| `creator_id` | integer, exist (Admin), nullable | + +## Связанные модели + +- [Admin](./Admin.md) — сотрудники +- [AdminGroup](./AdminGroup.md) — должностные группы +- [EmployeePosition](./EmployeePosition.md) — должности +- [AdminPayroll](./AdminPayroll.md) — расчёт зарплаты + +## Особенности реализации + +1. **Временная версионность**: Каждая запись действует с date до следующей записи или бессрочно +2. **Автозаполнение**: creator_id, admin_group_id, employee_position_id заполняются автоматически +3. **Валидация логики**: daily_payment не может превышать monthly_salary +4. **Уникальность по дате**: Для одного сотрудника не может быть двух записей с одной датой +5. **Оптимизация расчётов**: Метод getSalary() позволяет эффективно работать с предзагруженными данными diff --git a/erp24/docs/models/EmployeePosition.md b/erp24/docs/models/EmployeePosition.md new file mode 100644 index 00000000..b4a0feaa --- /dev/null +++ b/erp24/docs/models/EmployeePosition.md @@ -0,0 +1,358 @@ +# Модель EmployeePosition + + +## Mindmap + +```mermaid +mindmap + root((EmployeePosition)) + Таблица БД + employee_position + Свойства + id + int + name + string + next_position_id + int + posit + int + alias + string + group_id + int + Связи + PositionSkill + 1:N EmployeePositionSkill + Skills + 1:N EmployeeSkill + EmployeePayments + 1:N EmployeePayment + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeePosition` представляет должности сотрудников в системе HR. Хранит информацию о названии должности, окладах (месячный и дневной), алиасах и связях с группами сотрудников. Поддерживает автоматическую синхронизацию окладов при изменении через сервис `SalarySyncService`. + +**Файл модели:** `erp24/records/EmployeePosition.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_position` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Название должности | +| `next_position_id` | INTEGER | ID следующей должности (карьерный рост) | +| `posit` | INTEGER | Позиция для сортировки | +| `monthly_salary` | FLOAT | Месячный оклад | +| `daily_payment` | FLOAT | Подневная оплата | +| `alias` | VARCHAR(255) | Алиас должности (системное имя) | +| `group_id` | INTEGER | ID группы (FK → `admin_group.id`) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `created_by` | INTEGER | ID создателя (FK → `admin.id`) | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `updated_by` | INTEGER | ID обновившего (FK → `admin.id`) | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior +Автоматически устанавливает `created_at` и `updated_at` в формате 'Y-m-d H:i:s'. + +### BlameableBehavior +Автоматически устанавливает `created_by` и `updated_by` на основе текущего пользователя. + +--- + +## Методы модели + +### `tableName(): string` +Возвращает имя таблицы `'employee_position'`. + +### `rules(): array` +Определяет правила валидации: +- `name` — обязательное, строка, максимум 255 символов +- `next_position_id`, `posit`, `group_id` — целые числа +- `monthly_salary`, `daily_payment` — числа (float) +- `alias` — строка, максимум 255 символов +- `daily_payment` — валидация через `validateDailyPayment` + +### `validateDailyPayment($attribute, $params, $validator): void` + +Валидирует, что подневная оплата не превышает месячный оклад. + +**Логика:** +1. Проверяет, что оба поля `daily_payment` и `monthly_salary` заполнены +2. Сравнивает значения +3. Если `daily_payment` > `monthly_salary`, добавляет ошибку + +**Пример:** +```php +$position = new EmployeePosition(); +$position->monthly_salary = 50000; +$position->daily_payment = 60000; // Ошибка! +$position->validate(); // false +``` + +--- + +### Статические методы + +#### `getAllIdName(): array` + +Возвращает все должности в формате `[id => name]`. + +**Возвращает:** `array` — ассоциативный массив + +**Пример:** +```php +$positions = EmployeePosition::getAllIdName(); +// [1 => 'Флорист', 2 => 'Администратор', ...] +``` + +--- + +### Callback-методы + +#### `afterSave($insert, $changedAttributes): void` + +Вызывается после сохранения модели. Автоматически синхронизирует оклады для всех сотрудников с этой должностью при изменении `monthly_salary` или `daily_payment`. + +**Параметры:** +- `$insert` (bool) — true, если запись новая +- `$changedAttributes` (array) — массив изменённых атрибутов + +**Логика:** +1. Вызывает родительский метод +2. Проверяет, изменились ли `monthly_salary` или `daily_payment` +3. Проверяет, что оба поля заполнены +4. Если условия выполнены: + - Создаёт экземпляр `SalarySyncService` + - Получает ID создателя (`updated_by` или `Yii::$app->user->id`) + - Вызывает `syncEmployeesByPosition()` для синхронизации окладов + - Логирует результат через `Yii::info()` или `Yii::error()` +5. Перехватывает исключения, чтобы не прервать сохранение + +**Используемые сервисы:** +- `SalarySyncService::syncEmployeesByPosition()` — синхронизация окладов + +**Пример:** +```php +$position = EmployeePosition::findOne(1); +$position->monthly_salary = 55000; // Изменение оклада +$position->save(); // Автоматически обновит оклады всех сотрудников на этой должности +``` + +--- + +## Связи (Relations) + +### `getPositionSkill()` +Связь с промежуточной таблицей `EmployeePositionSkill`. + +**Тип:** hasMany +**Модель:** `EmployeePositionSkill` + +### `getSkills()` +Связь с навыками через промежуточную таблицу. + +**Тип:** hasMany через via +**Модель:** `EmployeeSkill` + +```php +$position = EmployeePosition::findOne(1); +$skills = $position->skills; // EmployeeSkill[] +foreach ($skills as $skill) { + echo $skill->name . "\n"; +} +``` + +--- + +### `getAdminGroup()` +Связь с группой сотрудников. + +**Тип:** hasOne +**Модель:** `AdminGroup` + +```php +$position = EmployeePosition::findOne(1); +echo $position->adminGroup->name; // "Флористы" +``` + +--- + +### `getEmployeePayments()` +Связь с платежными записями сотрудников. + +**Тип:** hasMany +**Модель:** `EmployeePayment` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_position }o--o| admin_group : "belongs_to" + employee_position ||--o{ employee_payment : "has_many" + employee_position ||--o{ employee_position_skill : "has_many" + employee_position_skill }o--|| employee_skill : "belongs_to" + + employee_position { + int id PK + string name + int next_position_id FK + int posit + float monthly_salary + float daily_payment + string alias + int group_id FK + timestamp created_at + int created_by FK + timestamp updated_at + int updated_by FK + } + + admin_group { + int id PK + string name + } + + employee_payment { + int id PK + int employee_position_id FK + } + + employee_position_skill { + int id PK + int position_id FK + int skill_id FK + } + + employee_skill { + int id PK + string name + int lifespan + } +``` + +--- + +## Примеры использования + +### Создание новой должности + +```php +$position = new EmployeePosition(); +$position->name = 'Флорист 3 уровня'; +$position->alias = 'florist_level_3'; +$position->monthly_salary = 50000.00; +$position->daily_payment = 2272.73; +$position->group_id = 30; // GROUP_FLORIST_DAY +$position->posit = 10; +$position->next_position_id = 5; // Следующая должность + +if ($position->save()) { + echo "Должность создана с ID: {$position->id}"; + // Автоматически установятся created_at, created_by +} +``` + +--- + +### Изменение оклада должности (с автосинхронизацией) + +```php +$position = EmployeePosition::findOne(['alias' => 'florist_level_3']); +$position->monthly_salary = 55000.00; +$position->daily_payment = 2500.00; +$position->save(); + +// После сохранения автоматически обновятся оклады +// всех сотрудников с этой должностью +``` + +--- + +### Получение списка всех должностей + +```php +$positions = EmployeePosition::getAllIdName(); +foreach ($positions as $id => $name) { + echo "{$id}: {$name}\n"; +} +``` + +--- + +### Получение должности с навыками + +```php +$position = EmployeePosition::find() + ->with(['skills']) + ->where(['id' => 1]) + ->one(); + +echo "Должность: {$position->name}\n"; +echo "Необходимые навыки:\n"; +foreach ($position->skills as $skill) { + echo "- {$skill->name} (действует {$skill->lifespan} дней)\n"; +} +``` + +--- + +### Получение карьерной лестницы + +```php +$position = EmployeePosition::findOne(['alias' => 'florist_level_1']); + +while ($position) { + echo $position->name . " → "; + $position = EmployeePosition::findOne($position->next_position_id); +} +// Вывод: Флорист 1 уровня → Флорист 2 уровня → Флорист 3 уровня → +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `name` | Обязательное, строка, максимум 255 символов | +| `alias` | Строка, максимум 255 символов | +| `next_position_id` | Целое число | +| `posit` | Целое число | +| `monthly_salary` | Число (float) | +| `daily_payment` | Число (float), не должно превышать monthly_salary | +| `group_id` | Целое число | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — группы сотрудников +- **[EmployeeSkill](./EmployeeSkill.md)** — навыки сотрудников +- **[EmployeePayment](./EmployeePayment.md)** — платежные записи +- **[Admin](./Admin.md)** — сотрудники + +--- + +## Связанные сервисы + +- **SalarySyncService** — синхронизация окладов сотрудников + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeePositionSearch.md b/erp24/docs/models/EmployeePositionSearch.md new file mode 100644 index 00000000..9ffe03b8 --- /dev/null +++ b/erp24/docs/models/EmployeePositionSearch.md @@ -0,0 +1,171 @@ +# Класс: EmployeePositionSearch + + +## Mindmap + +```mermaid +mindmap + root((EmployeePositionSearch)) + Таблица БД + ActiveRecord + Наследование + extends Model +``` + +## Назначение +Search-модель для поиска и фильтрации должностей сотрудников в ERP24. Особенность — наследует от Model вместо ActiveRecord и объявляет все свойства явно. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\base\Model` (не EmployeePosition!) + +## Публичные свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$id` | int | ID должности | +| `$name` | string | Название должности | +| `$alias` | string | Алиас должности | +| `$salary` | int | Оклад | +| `$group_id` | int | ID группы должностей | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `salary`, `group_id` — integer +- `name`, `alias` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос EmployeePosition::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, salary, group_id + - LIKE: name, alias + +## Диаграмма структуры + +```mermaid +erDiagram + EmployeePosition { + int id PK + varchar name + varchar alias + int salary + int group_id FK + } + + AdminGroup { + int id PK + varchar name + } + + AdminGroup ||--o{ EmployeePosition : "group_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new EmployeePositionSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new EmployeePositionSearch(); +$dataProvider = $searchModel->search([ + 'EmployeePositionSearch' => [ + 'name' => 'Флорист', + ] +]); +``` + +### Поиск по окладу +```php +$searchModel = new EmployeePositionSearch(); +$dataProvider = $searchModel->search([ + 'EmployeePositionSearch' => [ + 'salary' => 50000, + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new EmployeePositionSearch(); +$dataProvider = $searchModel->search([ + 'EmployeePositionSearch' => [ + 'alias' => 'florist', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new EmployeePositionSearch(); +$dataProvider = $searchModel->search([ + 'EmployeePositionSearch' => [ + 'group_id' => 3, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'alias', + [ + 'attribute' => 'salary', + 'format' => 'currency', + ], + 'group_id', + ], +]) ?> +``` + +## Связанные модели + +- [EmployeePosition](./EmployeePosition.md) — базовая модель должности +- [AdminGroup](./AdminGroup.md) — группы должностей + +## Особенности реализации + +1. **Наследование от Model**: Не от EmployeePosition/ActiveRecord +2. **Явное объявление свойств**: id, name, alias, salary, group_id +3. **LIKE-поиск**: Для name и alias +4. **Salary-фильтр**: Точное совпадение по окладу +5. **Независимость**: Не использует атрибуты родительской AR-модели diff --git a/erp24/docs/models/EmployeePositionSkill.md b/erp24/docs/models/EmployeePositionSkill.md new file mode 100644 index 00000000..826e9fc7 --- /dev/null +++ b/erp24/docs/models/EmployeePositionSkill.md @@ -0,0 +1,160 @@ +# Модель EmployeePositionSkill + + +## Mindmap + +```mermaid +mindmap + root((EmployeePositionSkill)) + Таблица БД + employee_position_skill + Свойства + position_id + int + skill_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeePositionSkill` связывает должности с обязательными навыками. Определяет, какие навыки требуются для работы на конкретной должности. Используется для автоматического контроля квалификации сотрудников. + +**Файл модели:** `erp24/records/EmployeePositionSkill.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_position_skill` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `position_id` | INTEGER | ID должности (FK, часть PK) | +| `skill_id` | INTEGER | ID навыка (FK, часть PK) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `position_id` и `skill_id` +- Реализует связь **many-to-many** между должностями и навыками +- Одной должности может требоваться несколько навыков +- Один навык может требоваться для нескольких должностей + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_position_skill }o--|| admin_group : "position" + employee_position_skill }o--|| employee_skill : "skill" + + employee_position_skill { + int position_id PK,FK + int skill_id PK,FK + } +``` + +--- + +## Примеры использования + +### Назначение навыка для должности + +```php +$link = new EmployeePositionSkill(); +$link->position_id = $positionId; +$link->skill_id = $skillId; +$link->save(); +``` + +### Получение требуемых навыков для должности + +```php +$requiredSkillIds = EmployeePositionSkill::find() + ->select('skill_id') + ->where(['position_id' => $positionId]) + ->column(); + +$skills = EmployeeSkill::find() + ->where(['id' => $requiredSkillIds]) + ->all(); + +foreach ($skills as $skill) { + echo "Требуется: {$skill->name}\n"; +} +``` + +### Проверка квалификации сотрудника + +```php +$employee = Admin::findOne($adminId); +$positionId = $employee->admin_group_id; + +// Требуемые навыки +$requiredSkillIds = EmployeePositionSkill::find() + ->select('skill_id') + ->where(['position_id' => $positionId]) + ->column(); + +// Имеющиеся навыки +$activeSkillIds = EmployeeSkillStatus::find() + ->select('skill_id') + ->where([ + 'admin_id' => $adminId, + 'status' => 1 + ]) + ->andWhere(['>', 'closed_at', date('Y-m-d')]) + ->column(); + +// Недостающие навыки +$missingSkillIds = array_diff($requiredSkillIds, $activeSkillIds); + +if (empty($missingSkillIds)) { + echo "Сотрудник полностью квалифицирован"; +} else { + echo "Недостающие навыки:\n"; + $missing = EmployeeSkill::find() + ->where(['id' => $missingSkillIds]) + ->all(); + foreach ($missing as $skill) { + echo "- {$skill->name}\n"; + } +} +``` + +### Удаление связи + +```php +EmployeePositionSkill::deleteAll([ + 'position_id' => $positionId, + 'skill_id' => $skillId +]); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `position_id` | Обязательное, целое число | +| `skill_id` | Обязательное, целое число | +| `position_id + skill_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности +- **[EmployeeSkill](./EmployeeSkill.md)** — навыки +- **[EmployeeSkillStatus](./EmployeeSkillStatus.md)** — статус навыка у сотрудника + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeePositionStatus.md b/erp24/docs/models/EmployeePositionStatus.md new file mode 100644 index 00000000..5e39d0ee --- /dev/null +++ b/erp24/docs/models/EmployeePositionStatus.md @@ -0,0 +1,147 @@ +# Модель EmployeePositionStatus + + +## Mindmap + +```mermaid +mindmap + root((EmployeePositionStatus)) + Таблица БД + employee_position_status + Свойства + admin_id + int + position_id + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeePositionStatus` хранит историю занимаемых должностей сотрудника. Фиксирует дату назначения и дату окончания работы на позиции. Используется для отслеживания карьерного пути и формирования кадровой отчётности. + +**Файл модели:** `erp24/records/EmployeePositionStatus.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_position_status` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `admin_id` | INTEGER | ID сотрудника (FK → admin.id) | +| `position_id` | INTEGER | ID должности (FK → admin_group.id) | +| `created_at` | TIMESTAMP | Дата назначения на должность | +| `closed_at` | TIMESTAMP | Дата окончания работы на должности | + +--- + +## Описание полей + +### `closed_at` — Дата окончания + +- `NULL` — текущая активная должность +- Значение — должность закрыта (сотрудник переведён или уволен) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_position_status }o--|| admin : "belongs_to" + employee_position_status }o--|| admin_group : "position" + + employee_position_status { + int admin_id FK + int position_id FK + timestamp created_at + timestamp closed_at + } +``` + +--- + +## Примеры использования + +### Назначение на должность + +```php +$status = new EmployeePositionStatus(); +$status->admin_id = $adminId; +$status->position_id = $positionId; +$status->created_at = date('Y-m-d H:i:s'); +$status->save(); +``` + +### Закрытие текущей должности + +```php +EmployeePositionStatus::updateAll( + ['closed_at' => date('Y-m-d H:i:s')], + ['admin_id' => $adminId, 'closed_at' => null] +); +``` + +### Получение текущей должности + +```php +$current = EmployeePositionStatus::find() + ->where([ + 'admin_id' => $adminId, + 'closed_at' => null + ]) + ->one(); + +if ($current) { + $position = AdminGroup::findOne($current->position_id); + echo "Текущая должность: {$position->name}"; +} +``` + +### История должностей сотрудника + +```php +$history = EmployeePositionStatus::find() + ->alias('eps') + ->innerJoin('admin_group ag', 'ag.id = eps.position_id') + ->select(['ag.name', 'eps.created_at', 'eps.closed_at']) + ->where(['eps.admin_id' => $adminId]) + ->orderBy(['eps.created_at' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($history as $row) { + $end = $row['closed_at'] ?? 'настоящее время'; + echo "{$row['name']}: {$row['created_at']} — {$end}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `position_id` | Обязательное, целое число | +| `created_at` | Обязательное, безопасное | +| `closed_at` | Безопасное | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[AdminGroup](./AdminGroup.md)** — должности +- **[AdminGroupDynamic](./AdminGroupDynamic.md)** — динамика должностей + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeeSkill.md b/erp24/docs/models/EmployeeSkill.md new file mode 100644 index 00000000..175ad8f1 --- /dev/null +++ b/erp24/docs/models/EmployeeSkill.md @@ -0,0 +1,313 @@ +# Модель EmployeeSkill + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkill)) + Таблица БД + employee_skill + Свойства + id + int + name + string + lifespan + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeSkill` представляет навыки сотрудников в системе HR. Хранит информацию о различных профессиональных навыках (обучение, сертификация, квалификация) с указанием срока действия каждого навыка. Используется для контроля актуальности квалификаций сотрудников и планирования переобучения. + +**Файл модели:** `erp24/records/EmployeeSkill.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_skill` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(200) | Название навыка | +| `lifespan` | INTEGER | Продолжительность действия навыка в днях | + +**Примеры навыков:** +- Флористика базовая (lifespan: 365 дней) +- Работа с кассой (lifespan: 180 дней) +- Техника безопасности (lifespan: 365 дней) +- Обслуживание клиентов (lifespan: 90 дней) +- Упаковка букетов (lifespan: 180 дней) + +--- + +## Методы модели + +### `tableName(): string` +Возвращает имя таблицы `'employee_skill'`. + +### `rules(): array` +Определяет правила валидации: +- `name`, `lifespan` — обязательные поля +- `name` — строка, максимум 200 символов +- `lifespan` — целое число + +### `attributeLabels(): array` +Возвращает метки атрибутов: +- `id` → 'ID' +- `name` → 'Name' +- `lifespan` → 'Продолжительность действия скилла в днях' + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `name` | Обязательное, строка, максимум 200 символов | +| `lifespan` | Обязательное, целое число (количество дней) | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_skill ||--o{ employee_position_skill : "has_many" + employee_position_skill }o--|| employee_position : "belongs_to" + employee_skill ||--o{ employee_skill_assignment : "has_many" + employee_skill_assignment }o--|| admin : "belongs_to" + + employee_skill { + int id PK + string name + int lifespan + } + + employee_position_skill { + int id PK + int position_id FK + int skill_id FK + } + + employee_position { + int id PK + string name + } + + employee_skill_assignment { + int id PK + int admin_id FK + int skill_id FK + date acquired_at + date expires_at + } + + admin { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание нового навыка + +```php +$skill = new EmployeeSkill(); +$skill->name = 'Флористика базовая'; +$skill->lifespan = 365; // Действует 1 год + +if ($skill->save()) { + echo "Навык создан с ID: {$skill->id}"; +} +``` + +--- + +### Получение всех навыков + +```php +$skills = EmployeeSkill::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($skills as $skill) { + $years = floor($skill->lifespan / 365); + $days = $skill->lifespan % 365; + echo "{$skill->name}: действует {$years} лет {$days} дней\n"; +} +``` + +--- + +### Поиск навыка по названию + +```php +$skill = EmployeeSkill::find() + ->where(['like', 'name', 'Флористика']) + ->one(); + +if ($skill) { + echo "Навык найден: {$skill->name}, действует {$skill->lifespan} дней\n"; +} +``` + +--- + +### Получение навыков с коротким сроком действия + +```php +$shortTermSkills = EmployeeSkill::find() + ->where(['<', 'lifespan', 180]) // Меньше 6 месяцев + ->orderBy(['lifespan' => SORT_ASC]) + ->all(); + +foreach ($shortTermSkills as $skill) { + echo "{$skill->name}: требует переподтверждения каждые {$skill->lifespan} дней\n"; +} +``` + +--- + +### Расчет даты истечения навыка + +```php +$skill = EmployeeSkill::findOne(['name' => 'Флористика базовая']); +$acquiredDate = new DateTime('2025-01-01'); +$expiresDate = $acquiredDate->modify("+{$skill->lifespan} days"); + +echo "Навык получен: {$acquiredDate->format('Y-m-d')}\n"; +echo "Истекает: {$expiresDate->format('Y-m-d')}\n"; +``` + +--- + +### Получение навыков для конкретной должности + +```php +use yii_app\records\EmployeePosition; + +$position = EmployeePosition::find() + ->with(['skills']) + ->where(['alias' => 'florist_level_3']) + ->one(); + +echo "Требуемые навыки для должности '{$position->name}':\n"; +foreach ($position->skills as $skill) { + echo "- {$skill->name} (обновлять каждые {$skill->lifespan} дней)\n"; +} +``` + +--- + +### Создание навыков для разных категорий + +```php +// Навыки флористов +$floristSkills = [ + ['Флористика базовая', 365], + ['Флористика продвинутая', 365], + ['Упаковка букетов', 180], + ['Уход за цветами', 180], +]; + +// Навыки кассиров +$cashierSkills = [ + ['Работа с кассой', 180], + ['Финансовая отчетность', 90], +]; + +// Общие навыки +$commonSkills = [ + ['Техника безопасности', 365], + ['Обслуживание клиентов', 90], + ['Работа в 1С', 180], +]; + +$allSkills = array_merge($floristSkills, $cashierSkills, $commonSkills); + +foreach ($allSkills as $skillData) { + $skill = new EmployeeSkill(); + $skill->name = $skillData[0]; + $skill->lifespan = $skillData[1]; + $skill->save(); +} +``` + +--- + +### Обновление срока действия навыка + +```php +$skill = EmployeeSkill::findOne(['name' => 'Техника безопасности']); +$skill->lifespan = 180; // Изменить с 365 на 180 дней +$skill->save(); + +echo "Срок действия навыка обновлен на {$skill->lifespan} дней"; +``` + +--- + +## Использование в системе + +### Проверка актуальности навыков сотрудника + +```php +// Пример использования (предполагаемая структура) +$adminId = 15; +$today = date('Y-m-d'); + +// Получить все навыки сотрудника +$assignments = EmployeeSkillAssignment::find() + ->with(['skill']) + ->where(['admin_id' => $adminId]) + ->all(); + +$expiredSkills = []; +$expiringSkills = []; + +foreach ($assignments as $assignment) { + $expiresDate = date('Y-m-d', strtotime($assignment->acquired_at . ' + ' . $assignment->skill->lifespan . ' days')); + + if ($expiresDate < $today) { + $expiredSkills[] = $assignment->skill->name; + } elseif ($expiresDate <= date('Y-m-d', strtotime('+30 days'))) { + $expiringSkills[] = [ + 'name' => $assignment->skill->name, + 'expires' => $expiresDate + ]; + } +} + +if (!empty($expiredSkills)) { + echo "Истекшие навыки: " . implode(', ', $expiredSkills) . "\n"; +} + +if (!empty($expiringSkills)) { + echo "Истекают в ближайшие 30 дней:\n"; + foreach ($expiringSkills as $skill) { + echo "- {$skill['name']} (истекает {$skill['expires']})\n"; + } +} +``` + +--- + +## Связанные модели + +- **[EmployeePosition](./EmployeePosition.md)** — должности сотрудников +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeeSkillNeed.md b/erp24/docs/models/EmployeeSkillNeed.md new file mode 100644 index 00000000..6aa94570 --- /dev/null +++ b/erp24/docs/models/EmployeeSkillNeed.md @@ -0,0 +1,146 @@ +# Модель EmployeeSkillNeed + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkillNeed)) + Таблица БД + employee_skill_need + Свойства + id + int + name + string + skill_id + int + type_id + int + period + int + target_value + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeSkillNeed` описывает требования (критерии) для получения навыка. Определяет условия, которые должен выполнить сотрудник: период измерения, целевое значение и оператор сравнения. Используется для автоматической проверки соответствия сотрудника требованиям. + +**Файл модели:** `erp24/records/EmployeeSkillNeed.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_skill_need` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(200) | Название критерия | +| `skill_id` | INTEGER | ID навыка (FK → employee_skill.id) | +| `type_id` | INTEGER | ID типа критерия (FK → employee_skill_type.id) | +| `period` | INTEGER | Период измерения в днях | +| `target_value` | FLOAT | Целевое значение для сравнения | +| `binary_operator` | VARCHAR | Оператор сравнения (>, <, =, >=, <=) | + +--- + +## Описание полей + +### `period` — Период измерения + +Количество дней, за которые собирается статистика: +- `30` — месяц +- `90` — квартал +- `365` — год + +### `target_value` — Целевое значение + +Число, с которым сравнивается результат: +- `100` — количество продаж +- `0.05` — процент списаний (5%) +- `4.5` — средний рейтинг + +### `binary_operator` — Оператор сравнения + +- `>` — больше +- `>=` — больше или равно +- `<` — меньше +- `<=` — меньше или равно +- `=` — равно + +--- + +## Примеры использования + +### Создание критерия + +```php +$need = new EmployeeSkillNeed(); +$need->name = 'Выполнение плана продаж'; +$need->skill_id = $skillId; +$need->type_id = 1; +$need->period = 30; // За месяц +$need->target_value = 100; // 100% +$need->binary_operator = '>='; // Больше или равно +$need->save(); +``` + +### Проверка критерия + +```php +$need = EmployeeSkillNeed::findOne($id); +$actualValue = calculateEmployeeMetric($adminId, $need->period); + +$passed = false; +switch ($need->binary_operator) { + case '>': + $passed = $actualValue > $need->target_value; + break; + case '>=': + $passed = $actualValue >= $need->target_value; + break; + case '<': + $passed = $actualValue < $need->target_value; + break; + case '<=': + $passed = $actualValue <= $need->target_value; + break; + case '=': + $passed = $actualValue == $need->target_value; + break; +} + +echo $passed ? "Критерий выполнен" : "Критерий не выполнен"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 200 символов | +| `skill_id` | Обязательное, целое число | +| `type_id` | Обязательное, целое число | +| `period` | Обязательное, целое число | +| `target_value` | Обязательное, число | +| `binary_operator` | Обязательное, строка | + +--- + +## Связанные модели + +- **[EmployeeSkill](./EmployeeSkill.md)** — навыки +- **[EmployeeSkillType](./EmployeeSkillType.md)** — типы критериев +- **[EmployeeSkillStatus](./EmployeeSkillStatus.md)** — статус навыка + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeeSkillNeedSearch.md b/erp24/docs/models/EmployeeSkillNeedSearch.md new file mode 100644 index 00000000..1a0ca788 --- /dev/null +++ b/erp24/docs/models/EmployeeSkillNeedSearch.md @@ -0,0 +1,181 @@ +# Класс: EmployeeSkillNeedSearch + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkillNeedSearch)) + Таблица БД + ActiveRecord + Наследование + extends EmployeeSkillNeed +``` + +## Назначение +Search-модель для поиска и фильтрации требований к навыкам сотрудников в ERP24. Обеспечивает поиск по навыку, типу, периоду, целевому значению и бинарному оператору сравнения. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`EmployeeSkillNeed` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `skill_id`, `type_id`, `period` — integer +- `name`, `binary_operator` — safe +- `target_value` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос EmployeeSkillNeed::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, skill_id, type_id, period, target_value + - LIKE: name, binary_operator + +## Диаграмма структуры требований + +```mermaid +erDiagram + EmployeeSkillNeed { + int id PK + varchar name + int skill_id FK + int type_id FK + int period + decimal target_value + varchar binary_operator + } + + EmployeeSkill { + int id PK + varchar name + } + + EmployeeSkillType { + int id PK + varchar name + } + + EmployeeSkill ||--o{ EmployeeSkillNeed : "skill_id" + EmployeeSkillType ||--o{ EmployeeSkillNeed : "type_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new EmployeeSkillNeedSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по навыку +```php +$searchModel = new EmployeeSkillNeedSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillNeedSearch' => [ + 'skill_id' => 5, + ] +]); +``` + +### Поиск по типу +```php +$searchModel = new EmployeeSkillNeedSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillNeedSearch' => [ + 'type_id' => 2, + ] +]); +``` + +### Поиск по целевому значению +```php +$searchModel = new EmployeeSkillNeedSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillNeedSearch' => [ + 'target_value' => 80, + ] +]); +``` + +### Поиск по оператору сравнения +```php +$searchModel = new EmployeeSkillNeedSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillNeedSearch' => [ + 'binary_operator' => '>=', + ] +]); +``` + +### Поиск по периоду +```php +$searchModel = new EmployeeSkillNeedSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillNeedSearch' => [ + 'period' => 30, // дней + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'skill_id', + 'type_id', + 'binary_operator', + 'target_value', + 'period', + ], +]) ?> +``` + +## Связанные модели + +- [EmployeeSkillNeed](./EmployeeSkillNeed.md) — базовая модель требований +- [EmployeeSkill](./EmployeeSkill.md) — навыки +- [EmployeeSkillType](./EmployeeSkillType.md) — типы навыков + +## Особенности реализации + +1. **Система требований**: Требования к навыкам сотрудников +2. **Binary operator**: Оператор сравнения (>=, <=, =, >, <) +3. **Target value**: Целевое значение для сравнения +4. **Period**: Период оценки в днях +5. **LIKE-поиск**: По name и binary_operator +6. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/EmployeeSkillSearch.md b/erp24/docs/models/EmployeeSkillSearch.md new file mode 100644 index 00000000..f6892408 --- /dev/null +++ b/erp24/docs/models/EmployeeSkillSearch.md @@ -0,0 +1,138 @@ +# Класс: EmployeeSkillSearch + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkillSearch)) + Таблица БД + ActiveRecord + Наследование + extends EmployeeSkill +``` + +## Назначение +Search-модель для поиска и фильтрации навыков сотрудников в ERP24. Простая модель для поиска по ID и названию навыка. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`EmployeeSkill` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поисковым запросом. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос EmployeeSkill::find() +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id + - LIKE: name + +## Диаграмма связей + +```mermaid +erDiagram + EmployeeSkill { + int id PK + varchar name + } + + EmployeeSkillNeed { + int id PK + int skill_id FK + } + + EmployeePositionSkill { + int position_id FK + int skill_id FK + } + + EmployeeSkill ||--o{ EmployeeSkillNeed : "skill_id" + EmployeeSkill ||--o{ EmployeePositionSkill : "skill_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new EmployeeSkillSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new EmployeeSkillSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillSearch' => [ + 'name' => 'Составление букетов', + ] +]); +``` + +### Поиск по ID +```php +$searchModel = new EmployeeSkillSearch(); +$dataProvider = $searchModel->search([ + 'EmployeeSkillSearch' => [ + 'id' => 5, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + ], +]) ?> +``` + +## Связанные модели + +- [EmployeeSkill](./EmployeeSkill.md) — базовая модель навыка +- [EmployeeSkillNeed](./EmployeeSkillNeed.md) — требования к навыкам +- [EmployeePositionSkill](./EmployeePositionSkill.md) — навыки должностей + +## Особенности реализации + +1. **Минимальная модель**: Только id и name +2. **LIKE-поиск**: Для названия навыка +3. **Справочник**: Простой справочник навыков +4. **Стандартный Gii-шаблон**: Типичная Search-модель diff --git a/erp24/docs/models/EmployeeSkillStatus.md b/erp24/docs/models/EmployeeSkillStatus.md new file mode 100644 index 00000000..bb78dad6 --- /dev/null +++ b/erp24/docs/models/EmployeeSkillStatus.md @@ -0,0 +1,176 @@ +# Модель EmployeeSkillStatus + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkillStatus)) + Таблица БД + employee_skill_status + Свойства + admin_id + int + skill_id + int + status + int + created_at + string + closed_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeSkillStatus` фиксирует наличие навыка у конкретного сотрудника. Хранит дату получения и срок действия навыка. Используется для учёта квалификации персонала и контроля сроков переаттестации. + +**Файл модели:** `erp24/records/EmployeeSkillStatus.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_skill_status` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `admin_id` | INTEGER | ID сотрудника (FK → admin.id, часть PK) | +| `skill_id` | INTEGER | ID навыка (FK → employee_skill.id, часть PK) | +| `status` | INTEGER | Статус навыка | +| `created_at` | TIMESTAMP | Дата получения навыка | +| `closed_at` | TIMESTAMP | Дата окончания действия | + +--- + +## Описание полей + +### `status` — Статус навыка + +- `1` — Активен +- `0` — Истёк / требует обновления +- `-1` — Отозван + +### `closed_at` — Дата окончания + +Рассчитывается как `created_at + skill.lifespan` дней. После этой даты навык считается истёкшим. + +--- + +## Особенности + +- **Составной уникальный ключ** по `admin_id` и `skill_id` +- Один сотрудник может иметь много навыков +- Один навык может быть у многих сотрудников + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_skill_status }o--|| admin : "belongs_to" + employee_skill_status }o--|| employee_skill : "references" + + employee_skill_status { + int admin_id PK,FK + int skill_id PK,FK + int status + timestamp created_at + timestamp closed_at + } +``` + +--- + +## Примеры использования + +### Присвоение навыка сотруднику + +```php +$skill = EmployeeSkill::findOne($skillId); + +$status = new EmployeeSkillStatus(); +$status->admin_id = $adminId; +$status->skill_id = $skillId; +$status->status = 1; +$status->created_at = date('Y-m-d H:i:s'); +$status->closed_at = date('Y-m-d H:i:s', strtotime("+{$skill->lifespan} days")); +$status->save(); +``` + +### Проверка наличия навыка + +```php +$hasSkill = EmployeeSkillStatus::find() + ->where([ + 'admin_id' => $adminId, + 'skill_id' => $skillId, + 'status' => 1 + ]) + ->andWhere(['>', 'closed_at', date('Y-m-d')]) + ->exists(); + +echo $hasSkill ? "Навык активен" : "Навык отсутствует или истёк"; +``` + +### Получение всех навыков сотрудника + +```php +$skills = EmployeeSkillStatus::find() + ->alias('ess') + ->innerJoin('employee_skill es', 'es.id = ess.skill_id') + ->select(['es.name', 'ess.created_at', 'ess.closed_at', 'ess.status']) + ->where(['ess.admin_id' => $adminId]) + ->asArray() + ->all(); + +foreach ($skills as $skill) { + $expired = strtotime($skill['closed_at']) < time() ? ' (истёк)' : ''; + echo "{$skill['name']}{$expired}\n"; +} +``` + +### Продление навыка + +```php +$status = EmployeeSkillStatus::findOne([ + 'admin_id' => $adminId, + 'skill_id' => $skillId +]); + +$skill = EmployeeSkill::findOne($skillId); +$status->created_at = date('Y-m-d H:i:s'); +$status->closed_at = date('Y-m-d H:i:s', strtotime("+{$skill->lifespan} days")); +$status->status = 1; +$status->save(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `admin_id` | Обязательное, целое число | +| `skill_id` | Обязательное, целое число | +| `status` | Целое число | +| `created_at` | Обязательное, безопасное | +| `closed_at` | Обязательное, безопасное | +| `admin_id + skill_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники +- **[EmployeeSkill](./EmployeeSkill.md)** — справочник навыков +- **[EmployeeSkillNeed](./EmployeeSkillNeed.md)** — требования к навыкам + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EmployeeSkillType.md b/erp24/docs/models/EmployeeSkillType.md new file mode 100644 index 00000000..255e6b29 --- /dev/null +++ b/erp24/docs/models/EmployeeSkillType.md @@ -0,0 +1,108 @@ +# Модель EmployeeSkillType + + +## Mindmap + +```mermaid +mindmap + root((EmployeeSkillType)) + Таблица БД + employee_skill_type + Свойства + id + int + name + string + apply_type + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EmployeeSkillType` представляет справочник типов критериев для оценки навыков. Определяет способ получения значения критерия: ручной ввод или автоматический запрос к базе данных. Используется для настройки автоматизации проверки соответствия требованиям. + +**Файл модели:** `erp24/records/EmployeeSkillType.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `employee_skill_type` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(250) | Название типа критерия | +| `apply_type` | INTEGER | Тип получения значения | + +--- + +## Описание полей + +### `apply_type` — Тип получения значения + +- `0` — **Ручной** — значение вводится вручную администратором +- `1` — **Автоматический** — значение получается запросом к БД + +--- + +## Диаграмма связей + +```mermaid +erDiagram + employee_skill_type ||--o{ employee_skill_need : "categorizes" + + employee_skill_type { + int id PK + string name + int apply_type + } +``` + +--- + +## Примеры использования + +### Создание типа критерия + +```php +$type = new EmployeeSkillType(); +$type->name = 'Количество продаж'; +$type->apply_type = 1; // Автоматический запрос +$type->save(); +``` + +### Получение всех типов + +```php +$types = EmployeeSkillType::find()->all(); + +foreach ($types as $type) { + $method = $type->apply_type == 0 ? 'ручной' : 'автоматический'; + echo "{$type->name} ({$method})\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 250 символов | +| `apply_type` | Целое число | + +--- + +## Связанные модели + +- **[EmployeeSkillNeed](./EmployeeSkillNeed.md)** — требования к навыкам +- **[EmployeeSkill](./EmployeeSkill.md)** — навыки + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EntityType.md b/erp24/docs/models/EntityType.md new file mode 100644 index 00000000..4213b39d --- /dev/null +++ b/erp24/docs/models/EntityType.md @@ -0,0 +1,99 @@ +# Модель EntityType + + +## Mindmap + +```mermaid +mindmap + root((EntityType)) + Таблица БД + entity_type + Свойства + id + int + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `EntityType` представляет справочник типов сущностей системы. Хранит названия сущностей для использования в полиморфных связях и универсальных механизмах (файлы, комментарии, логи). Используется для стандартизации именования сущностей. + +**Файл модели:** `erp24/records/EntityType.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `entity_type` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(36) | Название типа сущности | + +--- + +## Примеры использования + +### Создание типа сущности + +```php +$type = new EntityType(); +$type->name = 'order'; +$type->save(); +``` + +### Получение всех типов + +```php +$types = EntityType::find()->all(); + +foreach ($types as $type) { + echo "{$type->id}: {$type->name}\n"; +} +``` + +### Получение по имени + +```php +$taskType = EntityType::findOne(['name' => 'task']); +echo "ID типа 'task': {$taskType->id}"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 36 символов | + +--- + +## Типичные типы сущностей + +| ID | Название | Описание | +|----|----------|----------| +| 1 | task | Задачи | +| 2 | order | Заказы | +| 3 | product | Товары | +| 4 | admin | Сотрудники | +| 5 | user | Клиенты | +| 6 | store | Магазины | + +--- + +## Связанные модели + +- **[Files](./Files.md)** — файлы +- **[Comment](./Comment.md)** — комментарии +- **[EmployeeBalance](./EmployeeBalance.md)** — баланс сотрудников + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/EqualizationRemains.md b/erp24/docs/models/EqualizationRemains.md new file mode 100644 index 00000000..96896c96 --- /dev/null +++ b/erp24/docs/models/EqualizationRemains.md @@ -0,0 +1,320 @@ +# Класс: EqualizationRemains + + +## Mindmap + +```mermaid +mindmap + root((EqualizationRemains)) + Таблица БД + equalization_remains + Свойства + id + int + shift_transfer_id + int + product_id + string + product_count + float + product_price + float + product_self_cost + float + Связи + Product + 1:1 Products1c + ProductReplacement + 1:1 Products1c + ProductPrice + 1:1 Prices + ProductReplacementPrice + 1:1 Prices + ProductPriceSelfCost + 1:1 SelfCostProductDynamic + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель выравнивания остатков при передаче смены в ERP24. Используется для учёта замен товаров при пересортице — когда фактически на складе есть другой товар вместо заявленного. Рассчитывает разницу в стоимости между недостающим товаром и товаром-заменой. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`equalization_remains` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +### TimestampBehavior +Автоматическое заполнение `created_at` и `updated_at`. + +### BlameableBehavior +Автоматическое заполнение `created_by` и `updated_by`. + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `shift_transfer_id` | int | ID записи передачи смены | +| `product_id` | varchar(255) | GUID товара с недостатком | +| `product_count` | decimal | Количество недостающего товара | +| `product_price` | decimal | Розничная цена недостающего товара | +| `product_self_cost` | decimal | Себестоимость недостающего товара | +| `product_replacement_id` | varchar(255) | GUID товара-замены | +| `product_replacement_count` | decimal | Количество товара-замены | +| `product_replacement_price` | decimal | Розничная цена товара-замены | +| `product_replacement_self_cost` | decimal | Себестоимость товара-замены | +| `balance` | decimal | Разница в сумме (розничные цены) | +| `balance_self_cost` | decimal | Разница в сумме (себестоимость) | +| `created_at` | timestamp | Дата создания | +| `updated_at` | timestamp | Дата обновления | +| `created_by` | int | Автор создания | +| `updated_by` | int | Автор обновления | + +## Связи (Relations) + +### getProduct() +Возвращает недостающий товар. +```php +public function getProduct(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Products1c::class, ['id' => 'product_id'])` + +### getProductReplacement() +Возвращает товар-замену. +```php +public function getProductReplacement(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Products1c::class, ['id' => 'product_replacement_id'])` + +### getProductPrice() +Возвращает цену недостающего товара. +```php +public function getProductPrice(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Prices::class, ['product_id' => 'product_id'])` + +### getProductReplacementPrice() +Возвращает цену товара-замены. +```php +public function getProductReplacementPrice(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(Prices::class, ['product_id' => 'product_replacement_id'])` + +### getProductPriceSelfCost() +Возвращает динамическую себестоимость недостающего товара на дату передачи смены. +```php +public function getProductPriceSelfCost(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasOne(SelfCostProductDynamic::class, ...)` + +### getProductReplacementPriceSelfCost() +Возвращает динамическую себестоимость товара-замены. +```php +public function getProductReplacementPriceSelfCost(): \yii\db\ActiveQuery +``` + +### getShiftTransfer() +Возвращает связанные передачи смен. +```php +public function getShiftTransfer(): \yii\db\ActiveQuery +``` +**Возвращает**: `hasMany(ShiftTransfer::class, ['id' => 'shift_transfer_id'])` + +### getCreatedBy() / getUpdatedBy() +Возвращают авторов создания и обновления. + +## Методы + +### updateData() +Обновляет данные выравнивания для передачи смены. + +```php +public static function updateData(array $equalizationRemains, int $shift_transfer_id): void +``` + +**Параметры**: +- `$equalizationRemains` (array) — массив данных выравнивания +- `$shift_transfer_id` (int) — ID передачи смены + +**Логика**: +1. Удаляет существующие записи для данной передачи смены +2. Парсит product_replacement_id (поддерживает GUID и формат "Название (арт. 12345)") +3. Создаёт новые записи выравнивания + +### setData() +Автоматически рассчитывает выравнивание на основе расхождений в остатках. + +```php +public static function setData(ShiftTransfer $shiftTransfer): void +``` + +**Логика**: +1. Получает товары с положительной разницей (излишки) из ShiftRemains +2. Получает товары с отрицательной разницей (недостача) из ShiftRemains +3. Для каждого излишка ищет возможные замены через Product1cReplacement +4. Рассчитывает разницу в стоимости +5. Создаёт записи выравнивания + +## Диаграмма расчёта выравнивания + +```mermaid +flowchart TD + A[Передача смены] --> B[ShiftRemains] + B --> C{fact_and_1c_diff} + C -->|> 0| D[Излишки - товары замены] + C -->|< 0| E[Недостача - отсутствующие товары] + D --> F[Product1cReplacement] + E --> F + F --> G{Есть связь замены?} + G -->|Да| H[Создать EqualizationRemains] + G -->|Нет| I[Пропустить] + H --> J[Рассчитать balance] + J --> K["balance = count * (price_orig - price_repl)"] +``` + +## Диаграмма связей + +```mermaid +erDiagram + EqualizationRemains { + int id PK + int shift_transfer_id FK + varchar product_id FK + decimal product_count + decimal product_price + decimal product_self_cost + varchar product_replacement_id FK + decimal product_replacement_count + decimal product_replacement_price + decimal product_replacement_self_cost + decimal balance + decimal balance_self_cost + timestamp created_at + timestamp updated_at + int created_by FK + int updated_by FK + } + + ShiftTransfer { + int id PK + date date + int store_id FK + } + + Products1c { + varchar id PK + varchar name + } + + ShiftRemains { + int id PK + int shift_transfer_id FK + varchar product_guid FK + decimal fact_and_1c_diff + } + + Product1cReplacement { + varchar guid FK + varchar guid_replacement FK + } + + EqualizationRemains }o--|| ShiftTransfer : "shift_transfer_id" + EqualizationRemains }o--|| Products1c : "product_id" + EqualizationRemains }o--|| Products1c : "product_replacement_id" + ShiftRemains }o--|| ShiftTransfer : "shift_transfer_id" + Product1cReplacement }o--|| Products1c : "guid" + Product1cReplacement }o--|| Products1c : "guid_replacement" +``` + +## Примеры использования + +### Автоматический расчёт выравнивания +```php +$shiftTransfer = ShiftTransfer::findOne($id); +EqualizationRemains::setData($shiftTransfer); +``` + +### Ручное обновление выравнивания +```php +$equalizationData = [ + [ + 'product_id' => 'rose-001', + 'product_count' => 5, + 'product_price' => 150, + 'product_self_cost' => 80, + 'product_replacement_id' => 'chrysanthemum-002', + 'product_replacement_count' => 5, + 'product_replacement_price' => 120, + 'product_replacement_self_cost' => 60, + 'balance' => 5 * (150 - 120), // 150 + 'balance_self_cost' => 5 * (80 - 60), // 100 + ] +]; + +EqualizationRemains::updateData($equalizationData, $shiftTransfer->id); +``` + +### Получение выравниваний для передачи смены +```php +$equalizations = EqualizationRemains::find() + ->where(['shift_transfer_id' => $shiftTransferId]) + ->with(['product', 'productReplacement']) + ->all(); + +foreach ($equalizations as $eq) { + echo "Недостача: {$eq->product->name} x {$eq->product_count}\n"; + echo "Замена: {$eq->productReplacement->name} x {$eq->product_replacement_count}\n"; + echo "Разница: {$eq->balance} руб.\n"; +} +``` + +### Расчёт общей разницы +```php +$totalBalance = EqualizationRemains::find() + ->where(['shift_transfer_id' => $shiftTransferId]) + ->sum('balance'); + +$totalSelfCostBalance = EqualizationRemains::find() + ->where(['shift_transfer_id' => $shiftTransferId]) + ->sum('balance_self_cost'); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `shift_transfer_id` | integer | +| `product_id` | string (max 255) | +| `product_count` | required, number | +| `product_price` | required, number | +| `product_self_cost` | required, number | +| `product_replacement_id` | string (max 255) | +| `product_replacement_count` | required, number | +| `product_replacement_price` | required, number | +| `product_replacement_self_cost` | required, number | +| `balance` | required, number | +| `balance_self_cost` | required, number | + +## Связанные модели + +- [ShiftTransfer](./ShiftTransfer.md) — передачи смен +- [ShiftRemains](./ShiftRemains.md) — остатки при передаче +- [Products1c](./Products1c.md) — товары +- [Product1cReplacement](./Product1cReplacement.md) — связи товаров-замен +- [Prices](./Prices.md) — цены товаров +- [SelfCostProductDynamic](./SelfCostProductDynamic.md) — динамическая себестоимость + +## Особенности реализации + +1. **Пересортица**: Модель решает проблему учёта, когда товар физически есть, но под другим наименованием +2. **Автоматический расчёт**: Метод setData() автоматически находит связи замен и создаёт записи +3. **Двойной расчёт разницы**: balance (розничные цены) и balance_self_cost (себестоимость) +4. **Интеграция с 1С**: Поддержка парсинга товаров по формату "Название (арт. XXX)" +5. **Аудит изменений**: TimestampBehavior и BlameableBehavior для трекинга diff --git a/erp24/docs/models/ErrorInfoErp.md b/erp24/docs/models/ErrorInfoErp.md new file mode 100644 index 00000000..4f6d61cb --- /dev/null +++ b/erp24/docs/models/ErrorInfoErp.md @@ -0,0 +1,219 @@ +# Класс: ErrorInfoErp + + +## Mindmap + +```mermaid +mindmap + root((ErrorInfoErp)) + Таблица БД + error_info_erp + Свойства + id + int + date + string + guid + string + error_field + string + error_field_text + string + created_at + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель для хранения информации об ошибках ERP-системы. Используется для сбора и анализа ошибок, возникающих в процессе работы системы, с категоризацией по источникам, типам и связью с конкретными сущностями через GUID. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`error_info_erp` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `source` | varchar(100) / null | Источник ошибки (модуль, компонент) | +| `category` | varchar(100) / null | Категория ошибки | +| `prefix` | varchar(100) / null | Префикс для группировки | +| `file` | varchar(200) / null | Путь к файлу, где произошла ошибка | +| `description` | varchar(200) / null | Краткое описание ошибки | +| `message` | text / null | Полное сообщение об ошибке | +| `date` | varchar(100) | Дата ошибки (строка) | +| `guid` | varchar(200) | GUID связанной сущности | +| `error_field` | varchar(100) | Поле, в котором произошла ошибка | +| `error_field_text` | varchar(100) | Текстовое описание поля с ошибкой | +| `created_at` | int | Timestamp создания записи | + +## Диаграмма связей + +```mermaid +erDiagram + ErrorInfoErp { + int id PK + varchar source + varchar category + varchar prefix + varchar file + varchar description + text message + varchar date + varchar guid + varchar error_field + varchar error_field_text + int created_at + } + + Products1c { + varchar id PK + } + + Orders { + varchar guid PK + } + + CreateChecks { + varchar guid PK + } + + ErrorInfoErp }o--o| Products1c : "guid (логическая)" + ErrorInfoErp }o--o| Orders : "guid (логическая)" + ErrorInfoErp }o--o| CreateChecks : "guid (логическая)" +``` + +## Примеры использования + +### Логирование ошибки валидации +```php +$error = new ErrorInfoErp(); +$error->source = 'api2'; +$error->category = 'validation'; +$error->prefix = 'ORDER'; +$error->file = 'controllers/OrderController.php'; +$error->description = 'Ошибка валидации заказа'; +$error->message = json_encode($model->errors); +$error->date = date('Y-m-d H:i:s'); +$error->guid = $order->guid; +$error->error_field = 'total_sum'; +$error->error_field_text = 'Сумма заказа'; +$error->created_at = time(); +$error->save(); +``` + +### Логирование ошибки интеграции с 1С +```php +$error = new ErrorInfoErp(); +$error->source = '1c_integration'; +$error->category = 'sync'; +$error->prefix = 'PRODUCT'; +$error->description = 'Товар не найден в 1С'; +$error->message = "Product GUID {$productGuid} not found in 1C database"; +$error->date = date('Y-m-d H:i:s'); +$error->guid = $productGuid; +$error->error_field = 'product_id'; +$error->error_field_text = 'ID товара'; +$error->created_at = time(); +$error->save(); +``` + +### Получение последних ошибок +```php +$recentErrors = ErrorInfoErp::find() + ->orderBy(['created_at' => SORT_DESC]) + ->limit(100) + ->all(); +``` + +### Поиск ошибок по категории +```php +$validationErrors = ErrorInfoErp::find() + ->where(['category' => 'validation']) + ->andWhere(['>=', 'created_at', strtotime('-24 hours')]) + ->all(); +``` + +### Поиск ошибок для конкретной сущности +```php +$entityErrors = ErrorInfoErp::find() + ->where(['guid' => $entityGuid]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Статистика ошибок по источникам +```php +$stats = ErrorInfoErp::find() + ->select(['source', 'COUNT(*) as count']) + ->where(['>=', 'created_at', strtotime('-7 days')]) + ->groupBy('source') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); +``` + +### Очистка старых ошибок +```php +$deleted = ErrorInfoErp::deleteAll([ + '<', 'created_at', strtotime('-30 days') +]); +echo "Удалено {$deleted} старых записей об ошибках"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `source` | string (max 100), nullable | +| `category` | string (max 100), nullable | +| `prefix` | string (max 100), nullable | +| `file` | string (max 200), nullable | +| `description` | string (max 200), nullable | +| `message` | string (text), nullable | +| `date` | required, string (max 100) | +| `guid` | string (max 200) | +| `error_field` | string (max 100) | +| `error_field_text` | string (max 100) | +| `created_at` | required, integer | + +## Типичные категории ошибок + +| Категория | Описание | +|-----------|----------| +| `validation` | Ошибки валидации данных | +| `sync` | Ошибки синхронизации с 1С | +| `api` | Ошибки внешних API | +| `database` | Ошибки базы данных | +| `business_logic` | Ошибки бизнес-логики | +| `integration` | Ошибки интеграций | + +## Типичные источники ошибок + +| Источник | Описание | +|----------|----------| +| `api1` | API первой версии | +| `api2` | API второй версии | +| `1c_integration` | Интеграция с 1С | +| `cron` | Фоновые задачи | +| `web` | Веб-интерфейс | + +## Связанные модели + +- [ErrorLog](./ErrorLog.md) — общий лог ошибок приложения +- [ApiIntegrationLogs](./ApiIntegrationLogs.md) — логи API интеграций + +## Особенности реализации + +1. **Привязка к сущностям**: Поле guid связывает ошибку с конкретной записью (заказ, товар, чек) +2. **Категоризация**: source, category, prefix позволяют группировать и фильтровать ошибки +3. **Контекст поля**: error_field и error_field_text указывают конкретное поле с ошибкой +4. **Timestamp**: created_at хранится как Unix timestamp для эффективных запросов +5. **Полнотекстовое сообщение**: message может содержать полный стек ошибки или JSON diff --git a/erp24/docs/models/ErrorInfoErpSearch.md b/erp24/docs/models/ErrorInfoErpSearch.md new file mode 100644 index 00000000..3e9eba31 --- /dev/null +++ b/erp24/docs/models/ErrorInfoErpSearch.md @@ -0,0 +1,158 @@ +# Класс: ErrorInfoErpSearch + + +## Mindmap + +```mermaid +mindmap + root((ErrorInfoErpSearch)) + Таблица БД + ActiveRecord + Наследование + extends ErrorInfoErp +``` + +## Назначение +Search-модель для поиска и фильтрации ошибок ERP-системы в ERP24. Обеспечивает поиск по источнику, описанию, файлу, GUID и категории с сортировкой по ID (новые первыми). + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ErrorInfoErp` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `created_at` — integer +- `source`, `description`, `prefix`, `guid`, `file`, `context`, `message`, `date` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с сортировкой по ID DESC. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ErrorInfoErp::find() с сортировкой по id DESC +2. Оборачивает в ActiveDataProvider +3. Загружает и валидирует параметры +4. Применяет фильтры: + - Точное совпадение: id, created_at + - LIKE: source, category, prefix, file, guid, description, message, date + +## Диаграмма мониторинга ошибок + +```mermaid +flowchart TD + A[Ошибка в системе] --> B[ErrorInfoErp запись] + B --> C[source, description, file] + C --> D[guid, context, message] + + E[ErrorInfoErpSearch] --> F[ORDER BY id DESC] + F --> G[Фильтр по source] + G --> H[Фильтр по category] + H --> I[LIKE по message] + I --> J[Новые ошибки первыми] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ErrorInfoErpSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по источнику ошибки +```php +$searchModel = new ErrorInfoErpSearch(); +$dataProvider = $searchModel->search([ + 'ErrorInfoErpSearch' => [ + 'source' => 'api', + ] +]); +``` + +### Поиск по файлу +```php +$searchModel = new ErrorInfoErpSearch(); +$dataProvider = $searchModel->search([ + 'ErrorInfoErpSearch' => [ + 'file' => 'SalesController.php', + ] +]); +``` + +### Поиск по GUID +```php +$searchModel = new ErrorInfoErpSearch(); +$dataProvider = $searchModel->search([ + 'ErrorInfoErpSearch' => [ + 'guid' => 'abc123-def456', + ] +]); +``` + +### Поиск по сообщению +```php +$searchModel = new ErrorInfoErpSearch(); +$dataProvider = $searchModel->search([ + 'ErrorInfoErpSearch' => [ + 'message' => 'Connection refused', + ] +]); +``` + +### GridView для мониторинга +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'date', + 'source', + 'prefix', + [ + 'attribute' => 'message', + 'format' => 'ntext', + 'contentOptions' => ['style' => 'max-width: 300px;'], + ], + 'file', + ], +]) ?> +``` + +## Связанные модели + +- [ErrorInfoErp](./ErrorInfoErp.md) — базовая модель ошибок +- [ApiErrorLog](./ApiErrorLog.md) — логи ошибок API + +## Особенности реализации + +1. **Сортировка по умолчанию**: ORDER BY id DESC — новые первыми +2. **LIKE-поиск**: По source, category, file, message и др. +3. **GUID tracking**: Поиск по уникальному идентификатору ошибки +4. **Context**: Поиск по контексту для отладки +5. **PostgreSQL ilike**: Возможно использование регистронезависимого поиска diff --git a/erp24/docs/models/ErrorLog.md b/erp24/docs/models/ErrorLog.md new file mode 100644 index 00000000..f79d12e1 --- /dev/null +++ b/erp24/docs/models/ErrorLog.md @@ -0,0 +1,173 @@ +# Класс: ErrorLog + + +## Mindmap + +```mermaid +mindmap + root((ErrorLog)) + Таблица БД + error_log + Наследование + extends ActiveRecord +``` + +## Назначение +Модель для хранения общего лога ошибок приложения ERP24. Используется для записи и анализа runtime-ошибок, исключений и предупреждений с информацией о месте возникновения (файл, строка, колонка) и контексте выполнения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`error_log` + +## Родительский класс +`yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `ip` | varchar | IP-адрес клиента | +| `file` | varchar | Путь к файлу, где произошла ошибка | +| `line` | int | Номер строки | +| `col` | int | Номер колонки (позиция в строке) | +| `level` | int/varchar | Уровень ошибки (error, warning, info) | +| `category` | varchar | Категория ошибки | +| `log_time` | timestamp | Время записи в лог | +| `prefix` | varchar | Префикс сообщения | +| `message` | text | Полное сообщение об ошибке | +| `context` | text | Контекст выполнения (JSON/сериализованные данные) | + +## Диаграмма структуры + +```mermaid +erDiagram + ErrorLog { + int id PK + varchar ip + varchar file + int line + int col + varchar level + varchar category + timestamp log_time + varchar prefix + text message + text context + } +``` + +## Уровни ошибок (level) + +| Уровень | Описание | +|---------|----------| +| `error` | Критическая ошибка, требующая внимания | +| `warning` | Предупреждение о потенциальной проблеме | +| `info` | Информационное сообщение | +| `debug` | Отладочная информация | +| `trace` | Детальная трассировка | + +## Примеры использования + +### Получение последних ошибок +```php +$errors = ErrorLog::find() + ->orderBy(['log_time' => SORT_DESC]) + ->limit(100) + ->all(); +``` + +### Поиск ошибок по файлу +```php +$fileErrors = ErrorLog::find() + ->where(['like', 'file', 'OrderController']) + ->orderBy(['log_time' => SORT_DESC]) + ->all(); +``` + +### Поиск ошибок по IP-адресу +```php +$ipErrors = ErrorLog::find() + ->where(['ip' => '192.168.1.100']) + ->andWhere(['>=', 'log_time', date('Y-m-d 00:00:00')]) + ->all(); +``` + +### Статистика ошибок по уровням +```php +$stats = ErrorLog::find() + ->select(['level', 'COUNT(*) as count']) + ->where(['>=', 'log_time', date('Y-m-d', strtotime('-7 days'))]) + ->groupBy('level') + ->asArray() + ->all(); +``` + +### Поиск ошибок по категории +```php +$dbErrors = ErrorLog::find() + ->where(['category' => 'yii\db\Exception']) + ->orderBy(['log_time' => SORT_DESC]) + ->limit(50) + ->all(); +``` + +### Анализ частых ошибок +```php +$frequentErrors = ErrorLog::find() + ->select(['file', 'line', 'COUNT(*) as count']) + ->where(['level' => 'error']) + ->groupBy(['file', 'line']) + ->orderBy(['count' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); +``` + +### Очистка старых записей +```php +$deleted = ErrorLog::deleteAll([ + '<', 'log_time', date('Y-m-d H:i:s', strtotime('-30 days')) +]); +``` + +## Конфигурация логгера Yii2 + +Для записи ошибок в эту таблицу используется DbTarget: + +```php +// config/web.php +'log' => [ + 'targets' => [ + [ + 'class' => 'yii\log\DbTarget', + 'levels' => ['error', 'warning'], + 'logTable' => 'error_log', + 'logVars' => ['_GET', '_POST', '_SESSION'], + ], + ], +], +``` + +## Валидация + +Модель не имеет явных правил валидации в коде, так как записи создаются автоматически системой логирования Yii2. + +## Связанные модели + +- [ErrorInfoErp](./ErrorInfoErp.md) — структурированные ошибки ERP с привязкой к сущностям +- [ApiIntegrationLogs](./ApiIntegrationLogs.md) — логи API интеграций + +## Особенности реализации + +1. **Минимальная модель**: Содержит только tableName(), данные записываются автоматически +2. **Интеграция с Yii2 Logger**: Работает как target для стандартного логгера +3. **Контекст выполнения**: Поле context может содержать $_GET, $_POST, $_SESSION и другие данные +4. **Позиция в коде**: Поля file, line, col позволяют точно локализовать место ошибки +5. **IP-трекинг**: Запись IP помогает отслеживать источник проблем + +## Примечание к документации + +В PHPDoc класса ошибочно указано `@package yii_app\records` под комментарием `Class CityStore` — это артефакт копирования. Фактически это класс ErrorLog. diff --git a/erp24/docs/models/ExportImport.md b/erp24/docs/models/ExportImport.md new file mode 100644 index 00000000..9b466585 --- /dev/null +++ b/erp24/docs/models/ExportImport.md @@ -0,0 +1,210 @@ +# Класс: ExportImport + + +## Mindmap + +```mermaid +mindmap + root((ExportImport)) + Таблица БД + export_import + Свойства + list_guid + string + entity_id + string + guid + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель маппинга идентификаторов для экспорта/импорта данных в ERP24. Связывает внутренние GUID системы ERP с идентификаторами внешних систем (динамических списков), обеспечивая синхронизацию сущностей между различными платформами. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`export_import` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `list_guid` | varchar(36) | GUID динамического списка (внешней системы) | +| `entity_id` | varchar(36) | Внутренний ID сущности во внешнем списке | +| `guid` | varchar(36) | GUID сущности в ERP (наш внутренний идентификатор) | + +## Индексы и ограничения + +- **UNIQUE**: (`list_guid`, `entity_id`) — уникальная комбинация внешнего списка и ID сущности + +## Диаграмма связей + +```mermaid +erDiagram + ExportImport { + varchar list_guid PK + varchar entity_id PK + varchar guid FK + } + + Products1c { + varchar id PK + } + + Users { + varchar guid PK + } + + Orders { + varchar guid PK + } + + ExternalSystem { + varchar list_guid PK + varchar name + } + + ExportImport }o--|| ExternalSystem : "list_guid" + ExportImport }o--o| Products1c : "guid (логическая)" + ExportImport }o--o| Users : "guid (логическая)" + ExportImport }o--o| Orders : "guid (логическая)" +``` + +## Концептуальная схема маппинга + +```mermaid +flowchart LR + subgraph ERP24["ERP24"] + E1["GUID: abc-123"] + E2["GUID: def-456"] + end + + subgraph Mapping["ExportImport"] + M1["list_guid: ext-sys-1
    entity_id: 1001
    guid: abc-123"] + M2["list_guid: ext-sys-1
    entity_id: 1002
    guid: def-456"] + end + + subgraph External["Внешняя система"] + X1["ID: 1001"] + X2["ID: 1002"] + end + + E1 --> M1 + M1 --> X1 + E2 --> M2 + M2 --> X2 +``` + +## Примеры использования + +### Создание связи для экспорта +```php +$mapping = new ExportImport(); +$mapping->list_guid = 'external-crm-list-001'; +$mapping->entity_id = '12345'; +$mapping->guid = $erpEntity->guid; +$mapping->save(); +``` + +### Поиск внутреннего GUID по внешнему ID +```php +$mapping = ExportImport::findOne([ + 'list_guid' => 'external-crm-list-001', + 'entity_id' => '12345' +]); + +if ($mapping) { + $erpGuid = $mapping->guid; + $product = Products1c::findOne(['id' => $erpGuid]); +} +``` + +### Поиск внешнего ID по внутреннему GUID +```php +$mapping = ExportImport::findOne([ + 'list_guid' => 'external-crm-list-001', + 'guid' => $erpEntity->guid +]); + +if ($mapping) { + $externalId = $mapping->entity_id; + // Использовать во внешнем API +} +``` + +### Массовый импорт с маппингом +```php +foreach ($externalEntities as $external) { + $mapping = ExportImport::findOne([ + 'list_guid' => $listGuid, + 'entity_id' => $external['id'] + ]); + + if ($mapping) { + // Обновление существующей записи + $erpEntity = Products1c::findOne(['id' => $mapping->guid]); + $erpEntity->name = $external['name']; + $erpEntity->save(); + } else { + // Создание новой записи + $erpEntity = new Products1c(); + $erpEntity->id = Yii::$app->security->generateRandomString(36); + $erpEntity->name = $external['name']; + $erpEntity->save(); + + // Создание маппинга + $newMapping = new ExportImport(); + $newMapping->list_guid = $listGuid; + $newMapping->entity_id = $external['id']; + $newMapping->guid = $erpEntity->id; + $newMapping->save(); + } +} +``` + +### Получение всех маппингов для внешнего списка +```php +$mappings = ExportImport::find() + ->where(['list_guid' => 'external-crm-list-001']) + ->indexBy('entity_id') + ->all(); + +// Быстрый доступ по entity_id +if (isset($mappings['12345'])) { + $guid = $mappings['12345']->guid; +} +``` + +### Удаление маппинга при удалении сущности +```php +ExportImport::deleteAll(['guid' => $deletedEntityGuid]); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `list_guid` | required, string (max 36) | +| `entity_id` | required, string (max 36) | +| `guid` | required, string (max 36) | +| (`list_guid`, `entity_id`) | unique | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары (логическая связь через guid) +- [Users](./Users.md) — клиенты (логическая связь через guid) +- [Orders](./Orders.md) — заказы (логическая связь через guid) + +## Особенности реализации + +1. **Универсальный маппинг**: Одна таблица для маппинга любых типов сущностей +2. **Множественные внешние системы**: list_guid позволяет работать с несколькими внешними системами одновременно +3. **Композитный ключ**: Уникальность обеспечивается комбинацией list_guid + entity_id +4. **Отсутствие первичного ключа ID**: Таблица использует составной ключ вместо auto-increment +5. **Гибкость**: Подходит для интеграции с CRM, маркетплейсами, 1С и другими системами diff --git a/erp24/docs/models/ExportImportIntegrations.md b/erp24/docs/models/ExportImportIntegrations.md new file mode 100644 index 00000000..6ee9ee5a --- /dev/null +++ b/erp24/docs/models/ExportImportIntegrations.md @@ -0,0 +1,90 @@ +# Модель ExportImportIntegrations + + +## Mindmap + +```mermaid +mindmap + root((ExportImportIntegrations)) + Таблица БД + export_import_integrations + Свойства + id + int + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ExportImportIntegrations` представляет справочник внешних интеграций для экспорта и импорта данных. Хранит названия систем, с которыми производится обмен данными. Используется в паре с `ExportImportTable` для маппинга идентификаторов. + +**Файл модели:** `erp24/records/ExportImportIntegrations.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `export_import_integrations` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | ID интеграции (первичный ключ) | +| `name` | VARCHAR(200) | Название интеграции | + +--- + +## Примеры использования + +### Создание интеграции + +```php +$integration = new ExportImportIntegrations(); +$integration->name = 'Яндекс.Маркет'; +$integration->save(); +``` + +### Получение всех интеграций + +```php +$integrations = ExportImportIntegrations::find()->all(); + +foreach ($integrations as $int) { + echo "{$int->id}: {$int->name}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 200 символов | + +--- + +## Типичные интеграции + +| ID | Название | +|----|----------| +| 1 | 1С | +| 2 | Wildberries | +| 3 | Ozon | +| 4 | AmoCRM | +| 5 | RetailCRM | +| 6 | iGooods | + +--- + +## Связанные модели + +- **[ExportImportTable](./ExportImportTable.md)** — маппинг ID + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ExportImportTable.md b/erp24/docs/models/ExportImportTable.md new file mode 100644 index 00000000..e4d7100c --- /dev/null +++ b/erp24/docs/models/ExportImportTable.md @@ -0,0 +1,607 @@ +# Class: ExportImportTable + +## 🧠 Mindmap: Модель ExportImportTable + +```mermaid +mindmap + root((ExportImportTable)) + Идентификация + entity тип сущности + entity_id внутренний ID + export_id внешний ID + Маппинг + export_val значение экспорта + Композитный ключ + entity entity_id export_id + Назначение + Связь с внешними системами + Синхронизация данных + Экспорт/Импорт +``` + +--- + +## Назначение + +Модель маппинга идентификаторов между внутренней системой ERP24 и внешними системами (1С, маркетплейсы и др.). Хранит соответствия между внутренними ID сущностей и их ID во внешних системах. + +Используется для синхронизации данных, экспорта в внешние системы и импорта из них. Позволяет отслеживать, какая запись в ERP24 соответствует какой записи в 1С или на маркетплейсе. Критически важна для двусторонней синхронизации и предотвращения дублирования данных. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`export_import_table` + +--- + +## Основные свойства + +### Идентификация сущности + +| Имя | Тип | Описание | +|-----|-----|----------| +| `entity` | string(55) | **Тип сущности** (products, orders, users и т.д., обязательное) | +| `entity_id` | int | **Внутренний ID** сущности в ERP24 (обязательное) | + +### Маппинг с внешней системой + +| Имя | Тип | Описание | +|-----|-----|----------| +| `export_id` | int | **ID внешней системы** (номер системы экспорта, обязательное) | +| `export_val` | string(250) | **Значение во внешней системе** (ID, GUID или другой идентификатор, обязательное) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'entity', // тип сущности + 'entity_id', // внутренний ID + 'export_id', // ID системы экспорта + 'export_val' // значение во внешней системе +] +``` + +### Типы данных +```php +['entity_id', 'export_id'] // integer +['entity'] // string max:55 +['export_val'] // string max:250 +``` + +### Уникальность +```php +['entity', 'entity_id', 'export_id'] // unique composite +// Комбинация должна быть уникальной: одна сущность может иметь +// разные ID в разных внешних системах, но в каждой системе только один +``` + +--- + +## Методы + +### tableName() +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `string` — имя таблицы +**Описание:** Возвращает имя таблицы базы данных для модели + +**Логика работы:** +Статический метод, который возвращает строку 'export_import_table' - имя таблицы в базе данных, с которой связана данная ActiveRecord модель. Используется Yii2 для построения SQL-запросов. + +**Пример:** +```php +$tableName = ExportImportTable::tableName(); // 'export_import_table' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив правил валидации +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации, которые применяются при вызове `validate()` или `save()`. Правила включают: +1. Все поля обязательные +2. entity_id и export_id должны быть integer +3. entity ограничена 55 символами +4. export_val ограничена 250 символами +5. Композитный уникальный ключ (entity, entity_id, export_id) + +**Пример:** +```php +$mapping = new ExportImportTable(); +$mapping->entity = 'products'; +$mapping->entity_id = 123; +$mapping->export_id = 1; // 1 = 1С +$mapping->export_val = 'uuid-from-1c-123'; +if ($mapping->validate()) { + $mapping->save(); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив меток атрибутов +**Описание:** Возвращает человекочитаемые названия для атрибутов модели + +**Логика работы:** +Возвращает ассоциативный массив с названиями атрибутов на английском языке. Используется в формах и сообщениях об ошибках валидации. + +**Пример:** +```php +$labels = (new ExportImportTable())->attributeLabels(); +echo $labels['entity']; // "Entity" +echo $labels['export_val']; // "Export Val" +``` + +--- + +## Примеры использования + +### Создание маппинга при экспорте в 1С + +```php +use yii_app\records\ExportImportTable; + +// При создании продукта в 1С +$product = Products::findOne(123); +$response = $this->export1C($product); + +// Сохраняем соответствие +$mapping = new ExportImportTable(); +$mapping->entity = 'products'; +$mapping->entity_id = $product->id; +$mapping->export_id = 1; // ID системы 1С +$mapping->export_val = $response['guid']; // GUID из 1С +$mapping->save(); + +echo "Mapping created: Product {$product->id} <=> 1C {$response['guid']}\n"; +``` + +### Поиск внешнего ID по внутреннему + +```php +// Найти GUID продукта в 1С +$productId = 123; +$exportId = 1; // 1С + +$mapping = ExportImportTable::findOne([ + 'entity' => 'products', + 'entity_id' => $productId, + 'export_id' => $exportId +]); + +if ($mapping) { + echo "Product {$productId} in 1C: {$mapping->export_val}\n"; + // Теперь можем обновить продукт в 1С + $this->update1CProduct($mapping->export_val, $data); +} else { + echo "Product not exported to 1C yet\n"; +} +``` + +### Поиск внутреннего ID по внешнему + +```php +// Получили обновление из 1С с GUID +$guid1C = 'uuid-from-1c-456'; +$exportId = 1; + +$mapping = ExportImportTable::find() + ->where([ + 'entity' => 'products', + 'export_val' => $guid1C, + 'export_id' => $exportId + ]) + ->one(); + +if ($mapping) { + // Обновляем локальный продукт + $product = Products::findOne($mapping->entity_id); + $product->name = $data['name']; + $product->price = $data['price']; + $product->save(); + + echo "Updated product #{$mapping->entity_id}\n"; +} else { + echo "Product not found, creating new\n"; + // Создаем новый продукт +} +``` + +### Экспорт в несколько систем + +```php +$orderId = 789; + +// Экспорт в 1С +$mapping1C = new ExportImportTable(); +$mapping1C->entity = 'orders'; +$mapping1C->entity_id = $orderId; +$mapping1C->export_id = 1; // 1С +$mapping1C->export_val = $response1C['id']; +$mapping1C->save(); + +// Экспорт на маркетплейс Wildberries +$mappingWB = new ExportImportTable(); +$mappingWB->entity = 'orders'; +$mappingWB->entity_id = $orderId; +$mappingWB->export_id = 2; // Wildberries +$mappingWB->export_val = $responseWB['orderId']; +$mappingWB->save(); + +echo "Order {$orderId} exported to 1C and Wildberries\n"; +``` + +### Получение всех маппингов для сущности + +```php +$entityType = 'products'; +$exportId = 1; // 1С + +$mappings = ExportImportTable::find() + ->where([ + 'entity' => $entityType, + 'export_id' => $exportId + ]) + ->all(); + +echo "Products exported to 1C:\n"; +foreach ($mappings as $mapping) { + echo "Product #{$mapping->entity_id} => {$mapping->export_val}\n"; +} + +echo "Total: " . count($mappings) . " products\n"; +``` + +### Проверка существования маппинга + +```php +function isExported($entity, $entityId, $exportId) { + return ExportImportTable::find() + ->where([ + 'entity' => $entity, + 'entity_id' => $entityId, + 'export_id' => $exportId + ]) + ->exists(); +} + +$productId = 123; +if (isExported('products', $productId, 1)) { + echo "Product already exported to 1C\n"; +} else { + echo "Product not exported yet, exporting...\n"; + $this->exportTo1C($productId); +} +``` + +### Обновление маппинга + +```php +// Если ID во внешней системе изменился (редкий случай) +$mapping = ExportImportTable::findOne([ + 'entity' => 'products', + 'entity_id' => 123, + 'export_id' => 1 +]); + +if ($mapping) { + $oldVal = $mapping->export_val; + $mapping->export_val = 'new-guid-from-1c'; + $mapping->save(); + + echo "Updated mapping: {$oldVal} => {$mapping->export_val}\n"; +} +``` + +### Удаление маппинга при удалении сущности + +```php +$productId = 123; + +// Удаляем продукт +$product = Products::findOne($productId); +$product->delete(); + +// Удаляем все маппинги этого продукта +$deleted = ExportImportTable::deleteAll([ + 'entity' => 'products', + 'entity_id' => $productId +]); + +echo "Deleted {$deleted} export mappings for product {$productId}\n"; +``` + +### Статистика экспорта + +```php +$stats = ExportImportTable::find() + ->select(['entity', 'export_id', 'COUNT(*) as count']) + ->groupBy(['entity', 'export_id']) + ->asArray() + ->all(); + +$exportSystems = [ + 1 => '1C', + 2 => 'Wildberries', + 3 => 'Ozon' +]; + +echo "Export statistics:\n"; +foreach ($stats as $stat) { + $systemName = $exportSystems[$stat['export_id']] ?? "Unknown ({$stat['export_id']})"; + echo "{$stat['entity']} => {$systemName}: {$stat['count']} records\n"; +} +``` + +### Массовый экспорт с маппингом + +```php +$products = Products::find() + ->where(['exported_to_1c' => 0]) + ->limit(100) + ->all(); + +$exported = 0; +$failed = 0; + +foreach ($products as $product) { + try { + // Экспорт в 1С + $response = $this->export1C($product); + + // Создание маппинга + $mapping = new ExportImportTable(); + $mapping->entity = 'products'; + $mapping->entity_id = $product->id; + $mapping->export_id = 1; + $mapping->export_val = $response['guid']; + $mapping->save(); + + // Обновление флага + $product->exported_to_1c = 1; + $product->save(); + + $exported++; + + } catch (\Exception $e) { + echo "Failed to export product {$product->id}: " . $e->getMessage() . "\n"; + $failed++; + } +} + +echo "Export completed: {$exported} success, {$failed} failed\n"; +``` + +### Синхронизация: поиск несоответствий + +```php +// Находим продукты без маппинга в 1С +$productsWithoutMapping = Products::find() + ->leftJoin( + ExportImportTable::tableName() . ' eit', + 'products.id = eit.entity_id + AND eit.entity = :entity + AND eit.export_id = :export_id', + [':entity' => 'products', ':export_id' => 1] + ) + ->where(['eit.entity_id' => null]) + ->all(); + +echo "Products not exported to 1C: " . count($productsWithoutMapping) . "\n"; +foreach ($productsWithoutMapping as $product) { + echo "- Product #{$product->id}: {$product->name}\n"; +} +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + ExportImportTable { + string entity PK "Entity type" + int entity_id PK "Internal ID" + int export_id PK "Export system ID" + string export_val "External ID/GUID" + } + + Products ||--o{ ExportImportTable : "has mappings" + Orders ||--o{ ExportImportTable : "has mappings" + Users ||--o{ ExportImportTable : "has mappings" +``` + +```mermaid +graph LR + A[ERP24 Products] -->|entity_id| B[ExportImportTable] + B -->|export_val| C[1C Products] + B -->|export_val| D[Wildberries] + B -->|export_val| E[Ozon] + + style B fill:#f9f,stroke:#333,stroke-width:2px +``` + +--- + +## Бизнес-логика + +### Назначение маппинга + +ExportImportTable решает проблему синхронизации данных между системами: + +**Без маппинга:** +- Невозможно определить, какая запись в ERP соответствует записи в 1С +- Риск дублирования при импорте +- Сложность обновления существующих записей + +**С маппингом:** +- Однозначное соответствие записей +- Двусторонняя синхронизация +- Предотвращение дублей +- Возможность обновления данных в обе стороны + +### Типы сущностей + +Примеры значений поля `entity`: + +- `products` — товары +- `orders` — заказы +- `users` — пользователи/клиенты +- `stores` — магазины +- `categories` — категории +- `prices` — цены +- `stock` — остатки + +### Системы экспорта (export_id) + +Примеры значений: + +- `1` — 1С (основная система учета) +- `2` — Wildberries (маркетплейс) +- `3` — Ozon (маркетплейс) +- `4` — Яндекс.Маркет +- `5` — Внешний склад +- и т.д. + +### Формат export_val + +Может содержать различные типы идентификаторов: + +- **GUID** (36 символов): `550e8400-e29b-41d4-a716-446655440000` +- **Числовой ID**: `123456` +- **Строковый код**: `PROD-2025-001` +- **Составной ключ**: `store_123_product_456` + +### Сценарии использования + +**1. Экспорт данных:** +``` +ERP24 Product (id=123) + → Экспорт в 1С + → Получение GUID + → Создание маппинга +``` + +**2. Импорт данных:** +``` +1С Product (guid=xxx) + → Поиск по export_val + → Находим entity_id=123 + → Обновляем Product +``` + +**3. Синхронизация:** +``` +Изменение в ERP24 + → Поиск маппинга + → Обновление в 1С по export_val +``` + +--- + +## Связи с другими моделями + +Модель ExportImportTable универсальна и логически связана с любыми сущностями через поле entity: + +- **Products** — через entity='products' +- **Orders** — через entity='orders' +- **Users** — через entity='users' +- **Store** — через entity='stores' +- и другие... + +**Примечание:** нет foreign key constraints, связи только логические через entity и entity_id + +--- + +## Индексы и производительность + +### Первичный ключ (композитный) + +```sql +PRIMARY KEY (entity, entity_id, export_id) +``` + +### Рекомендуемые индексы + +```sql +-- Поиск по внутреннему ID +CREATE INDEX idx_export_import_entity_id ON export_import_table(entity, entity_id); + +-- Поиск по внешнему ID +CREATE INDEX idx_export_import_export_val ON export_import_table(export_id, export_val); + +-- Поиск по типу сущности и системе +CREATE INDEX idx_export_import_entity_export ON export_import_table(entity, export_id); +``` + +### Оптимизация + +```php +// Плохо: N+1 запросов +$products = Products::find()->all(); +foreach ($products as $product) { + $mapping = ExportImportTable::findOne([ + 'entity' => 'products', + 'entity_id' => $product->id, + 'export_id' => 1 + ]); // +N запросов +} + +// Хорошо: один запрос с JOIN +$products = Products::find() + ->leftJoin( + ExportImportTable::tableName() . ' eit', + 'products.id = eit.entity_id + AND eit.entity = "products" + AND eit.export_id = 1' + ) + ->select(['products.*', 'eit.export_val as export_guid']) + ->asArray() + ->all(); +``` + +--- + +## Замечания + +1. **Композитный первичный ключ** — (entity, entity_id, export_id) +2. **Универсальность** — одна таблица для всех типов сущностей +3. **Все поля обязательные** — нет nullable полей +4. **export_val** — может хранить любые строковые идентификаторы до 250 символов +5. **Уникальность** — одна сущность может иметь только один ID в конкретной внешней системе +6. **Без foreign keys** — универсальная структура без жестких связей +7. **Двусторонний поиск** — можно искать как по internal ID, так и по external ID +8. **Требует очистки** — при удалении сущности нужно удалять маппинги +9. **Критична для синхронизации** — потеря данных может нарушить интеграции +10. **Бэкап** — рекомендуется регулярное резервное копирование + +--- + +## Связанные документы + +- [Products1c.md](./Products1c.md) — модель товаров из 1С +- [ApiLogs.md](./ApiLogs.md) — логирование API запросов +- [ApiCron.md](./ApiCron.md) — задачи синхронизации diff --git a/erp24/docs/models/Files.md b/erp24/docs/models/Files.md index 84e18610..f80dac29 100644 --- a/erp24/docs/models/Files.md +++ b/erp24/docs/models/Files.md @@ -1,5 +1,30 @@ # Class: Files + +## Mindmap + +```mermaid +mindmap + root((Files)) + Таблица БД + files + Свойства + id + int + url + string + file_type + string + created_at + string + entity_id + int + entity + string + Наследование + extends yiidbActiveRecord +``` + ## Назначение Универсальная модель файлов в системе ERP24. Обеспечивает хранение ссылок на файлы (изображения, документы, видео) с привязкой к различным сущностям системы через полиморфную связь `entity` + `entity_id`. diff --git a/erp24/docs/models/Firms.md b/erp24/docs/models/Firms.md new file mode 100644 index 00000000..c671d1d4 --- /dev/null +++ b/erp24/docs/models/Firms.md @@ -0,0 +1,273 @@ +# Класс: Firms + + +## Mindmap + +```mermaid +mindmap + root((Firms)) + Таблица БД + firms + Свойства + id + int + inn + string + group_id + int + post_id + int + name + string + name_full + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель юридических лиц (фирм/организаций) в ERP24. Хранит полную информацию об организациях: реквизиты, банковские данные, налоговую информацию. Используется для выставления счетов, формирования платёжных документов и интеграции с бухгалтерией. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`firms` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +### Основные данные + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `inn` | varchar(22) | ИНН организации | +| `group_id` | int | ID группы фирм | +| `post_id` | int | ID должности/поста | +| `name` | varchar(225) | Краткое название фирмы | +| `name_full` | varchar(255) | Полное название организации | +| `description` | text | Описание | +| `phone` | varchar(65) | Телефон | +| `adress` | text | Юридический адрес | +| `adress_fakt` | varchar(255) | Фактический адрес | +| `posit` | int | Позиция для сортировки | +| `date_add` | datetime | Дата и время добавления | + +### Банковские реквизиты + +| Поле | Тип | Описание | +|------|-----|----------| +| `ogrn` | varchar(25) | ОГРН организации | +| `bik` | varchar(12) | БИК банка | +| `kpp` | varchar(12) | КПП организации | +| `bank_kbk` | varchar(25) | КБК банка | +| `name_bank` | text | Название банка | +| `bank_city` | varchar(160) | Город банка | +| `rschet` | varchar(35) | Расчётный счёт | +| `rschet_bank` | varchar(35) | Корреспондентский счёт банка | + +### Налоговая информация + +| Поле | Тип | Описание | +|------|-----|----------| +| `nds` | int | Процент НДС (0 = без НДС) | +| `TaxStatus` | varchar(35) | Налоговый статус | +| `TaxKbk` | varchar(35) | КБК налога | +| `TaxReason` | varchar(35) | Основание налога | +| `Okato` | varchar(35) | Код ОКАТО | +| `TaxPeriod` | varchar(35) | Налоговый период | +| `TaxNumber` | varchar(35) | Номер налогового документа | +| `TaxDate` | varchar(35) | Дата налогового документа | +| `Oktmo` | varchar(35) | Код ОКТМО | + +### Финансовые данные + +| Поле | Тип | Описание | +|------|-----|----------| +| `saldo` | decimal | Текущее сальдо | +| `saldo_data` | datetime | Дата актуальности сальдо | +| `money_name_id` | int | ID статьи расхода по умолчанию | + +## Методы + +### getInn() +Возвращает статический массив ИНН с названиями фирм (захардкоженные данные). + +```php +public static function getInn(): array +``` + +**Возвращает**: Ассоциативный массив [ИНН => Название] + +**Пример**: +```php +$innList = Firms::getInn(); +// ['525911109960' => 'ИП Белов', '526003058422' => 'ИП Кузнецов', ...] +``` + +## Диаграмма связей + +```mermaid +erDiagram + Firms { + int id PK + varchar inn UK + int group_id FK + int post_id FK + varchar name + varchar name_full + text description + varchar phone + text adress + varchar adress_fakt + varchar ogrn + varchar bik + varchar kpp + varchar bank_kbk + text name_bank + varchar bank_city + varchar rschet + varchar rschet_bank + int nds + varchar TaxStatus + varchar TaxKbk + varchar TaxReason + varchar Okato + varchar TaxPeriod + varchar TaxNumber + varchar TaxDate + varchar Oktmo + decimal saldo + datetime saldo_data + int money_name_id FK + datetime date_add + int posit + } + + FirmsGroupPrefix { + int id PK + varchar name + varchar prefix + } + + MoneyName { + int id PK + varchar name + } + + ApiCronBuh { + int id PK + int inn FK + } + + Firms }o--o| FirmsGroupPrefix : "group_id" + Firms }o--o| MoneyName : "money_name_id" + ApiCronBuh }o--|| Firms : "inn (логическая)" +``` + +## Примеры использования + +### Получение фирмы по ИНН +```php +$firm = Firms::findOne(['inn' => '525911109960']); +echo $firm->name_full; +``` + +### Создание новой фирмы +```php +$firm = new Firms(); +$firm->inn = '7712345678'; +$firm->name = 'ООО Тест'; +$firm->name_full = 'Общество с ограниченной ответственностью "Тест"'; +$firm->ogrn = '1234567890123'; +$firm->kpp = '771201001'; +$firm->bik = '044525225'; +$firm->name_bank = 'ПАО Сбербанк'; +$firm->rschet = '40702810938000012345'; +$firm->rschet_bank = '30101810400000000225'; +$firm->adress = 'г. Москва, ул. Примерная, д. 1'; +$firm->adress_fakt = 'г. Москва, ул. Примерная, д. 1'; +$firm->nds = 20; +$firm->group_id = 1; +$firm->post_id = 1; +$firm->posit = 10; +$firm->saldo = 0; +$firm->saldo_data = date('Y-m-d H:i:s'); +$firm->date_add = date('Y-m-d H:i:s'); +// ... остальные обязательные поля +$firm->save(); +``` + +### Формирование списка для выбора +```php +$firmsList = ArrayHelper::map( + Firms::find()->orderBy(['posit' => SORT_ASC])->all(), + 'id', + 'name' +); +``` + +### Получение реквизитов для платёжки +```php +$firm = Firms::findOne($firmId); +$requisites = [ + 'ИНН' => $firm->inn, + 'КПП' => $firm->kpp, + 'ОГРН' => $firm->ogrn, + 'Банк' => $firm->name_bank, + 'БИК' => $firm->bik, + 'Р/с' => $firm->rschet, + 'К/с' => $firm->rschet_bank, +]; +``` + +### Фильтрация по НДС +```php +// Фирмы с НДС +$firmsWithNds = Firms::find() + ->where(['>', 'nds', 0]) + ->all(); + +// Фирмы без НДС +$firmsWithoutNds = Firms::find() + ->where(['nds' => 0]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `inn` | required, string (max 22) | +| `group_id` | required, integer | +| `post_id` | required, integer | +| `name` | required, string (max 225) | +| `name_full` | required, string (max 255) | +| `description` | required, string (text) | +| `phone` | required, string (max 65) | +| `adress` | required, string (text) | +| `adress_fakt` | required, string (max 255) | +| `ogrn` | required, string (max 25) | +| `bik` | required, string (max 12) | +| `kpp` | required, string (max 12) | +| `rschet` | required, string (max 35) | +| `nds` | integer | +| `saldo` | required, number | +| `posit` | required, integer | + +## Связанные модели + +- [FirmsGroupPrefix](./FirmsGroupPrefix.md) — группы фирм с префиксами +- [MoneyName](./MoneyName.md) — статьи расходов +- [ApiCronBuh](./ApiCronBuh.md) — бухгалтерские запросы по ИНН + +## Особенности реализации + +1. **Полные реквизиты**: Хранит все данные для формирования любых финансовых документов +2. **Налоговые поля**: Поддержка КБК, ОКАТО, ОКТМО для налоговых платежей +3. **НДС**: Поле nds определяет ставку НДС (0 = без НДС, 20 = 20%) +4. **Сальдо**: Отслеживание текущего баланса с датой актуальности +5. **Статические данные**: Метод getInn() содержит захардкоженный список ИП — возможно, требует рефакторинга diff --git a/erp24/docs/models/FirmsGroupPrefix.md b/erp24/docs/models/FirmsGroupPrefix.md new file mode 100644 index 00000000..408af3d0 --- /dev/null +++ b/erp24/docs/models/FirmsGroupPrefix.md @@ -0,0 +1,128 @@ +# Класс: FirmsGroupPrefix + + +## Mindmap + +```mermaid +mindmap + root((FirmsGroupPrefix)) + Таблица БД + firms_group_prefix + Свойства + id + int + name + string + prefix + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник групп фирм с префиксами в ERP24. Используется для категоризации юридических лиц и формирования уникальных идентификаторов документов с префиксами, специфичными для каждой группы. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`firms_group_prefix` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(100) | Название группы фирм | +| `prefix` | varchar(100) | Префикс для документов группы | + +## Диаграмма связей + +```mermaid +erDiagram + FirmsGroupPrefix { + int id PK + varchar name + varchar prefix + } + + Firms { + int id PK + int group_id FK + varchar name + } + + FirmsGroupPrefix ||--o{ Firms : "group_id" +``` + +## Примеры использования + +### Получение всех групп +```php +$groups = FirmsGroupPrefix::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Создание группы фирм +```php +$group = new FirmsGroupPrefix(); +$group->name = 'Розничные магазины'; +$group->prefix = 'RTL'; +$group->save(); +``` + +### Формирование номера документа с префиксом +```php +$firm = Firms::findOne($firmId); +$group = FirmsGroupPrefix::findOne($firm->group_id); + +$documentNumber = $group->prefix . '-' . str_pad($docId, 6, '0', STR_PAD_LEFT); +// Например: "RTL-000123" +``` + +### Получение списка для выбора +```php +$groupsList = ArrayHelper::map( + FirmsGroupPrefix::find()->all(), + 'id', + 'name' +); +``` + +### Поиск фирм по группе +```php +$group = FirmsGroupPrefix::findOne(['prefix' => 'RTL']); +$firms = Firms::find() + ->where(['group_id' => $group->id]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 100) | +| `prefix` | required, string (max 100) | + +## Связанные модели + +- [Firms](./Firms.md) — юридические лица, принадлежащие группе + +## Типичные группы (примеры) + +| Название | Префикс | Описание | +|----------|---------|----------| +| Розничные магазины | RTL | Точки розничной торговли | +| Оптовые поставщики | WHS | Оптовые операции | +| Интернет-магазины | ONL | Онлайн-продажи | +| Франчайзи | FRN | Франчайзинговые партнёры | + +## Особенности реализации + +1. **Префиксы документов**: Используются для визуального различения документов разных групп +2. **Группировка фирм**: Позволяет категоризировать юридические лица по типу деятельности +3. **Краткость**: Префикс обычно 3-4 символа для компактности в номерах документов diff --git a/erp24/docs/models/FotMetrics.md b/erp24/docs/models/FotMetrics.md new file mode 100644 index 00000000..1ea60b3f --- /dev/null +++ b/erp24/docs/models/FotMetrics.md @@ -0,0 +1,308 @@ +# Class: FotMetrics + +## Mindmap + +```mermaid +mindmap + root((FotMetrics)) + Таблица БД + Metrics наследник + Свойства + calculateDay + bool false + alias + array + Метрики + sales_sum_admin + Сумма продаж + day_payroll + Дневной ФОТ + admin_count + Кол-во админов + Связи + AdminPayrollDays + 1:N данные + Наследование + extends Metrics +``` + +## Назначение + +Класс FotMetrics наследует абстрактный класс Metrics и реализует расчёт метрик фонда оплаты труда (ФОТ) по магазинам с разбивкой по сменам в системе ERP24. Агрегирует данные из таблицы `admin_payroll_days` (начисления сотрудникам), вычисляет дневной ФОТ, сумму продаж сотрудников и количество работающих администраторов. Используется для анализа эффективности персонала и контроля расходов на оплату труда. + +## Пространство имён + +```php +namespace yii_app\records\metrics; +``` + +## Родительский класс + +```php +\yii_app\records\metrics\Metrics +``` + +## Использования (Dependencies) + +- `yii\db\Expression` - SQL выражения Yii2 +- `yii_app\records\AdminPayrollDays` - модель начислений сотрудникам +- `yii_app\records\metrics\Metrics` - базовый класс метрик + +## Свойства (Properties) + +### Защищённые флаги + +```php +protected bool $calculateDay = false; // Не рассчитываем дневные метрики (только смены) +``` + +### Защищённые массивы псевдонимов + +```php +protected array $alias = [ + 'sales_sum_admin', // Сумма продаж всех сотрудников за смену + 'day_payroll', // Дневной ФОТ (зарплата за смену) + 'admin_count' // Количество работающих администраторов +]; +``` + +## Методы + +### getQueryDataDay() + +**Описание:** Реализует абстрактный метод родителя. Возвращает false, так как метрики ФОТ рассчитываются только по сменам, без дневной агрегации. + +**Параметры:** Нет + +**Возвращает:** `bool` - всегда false + +**Логика работы:** +Метод просто возвращает false, указывая родительскому классу не создавать LEFT JOIN для дневных данных. + +**Пример:** +```php +$fotMetrics = new FotMetrics(); +$dayQuery = $fotMetrics->getQueryDataDay(); +var_dump($dayQuery); // bool(false) +``` + +--- + +### getQueryDataShifts() + +**Описание:** Реализует абстрактный метод родителя. Возвращает запрос для расчёта метрик ФОТ по сменам (shift_type = 1 или 2). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос к таблице admin_payroll_days с агрегацией по сменам + +**Логика работы:** +1. Создаёт запрос к таблице `admin_payroll_days` +2. Выполняет SELECT с агрегирующими функциями: + - `date` - дата смены + - `shift_type` (smena_type) - тип смены (1 или 2) + - `store_dynamic_id` - ID записи динамики магазина + - `store_id` - ID магазина + - `day_payroll` - SUM дневных начислений всех сотрудников + - `sales_sum_admin` - SUM суммы продаж всех сотрудников + - `admin_count` - COUNT количество администраторов +3. INNER JOIN с `city_store` для привязки к магазинам +4. INNER JOIN с `store_dynamic` для определения кластера: + - Условие: store_dynamic.store_id = city_store.id + - date_from <= admin_payroll_days.date + - date_to > admin_payroll_days.date +5. Фильтрует по: + - Диапазону дат (dateStart, dateEnd) + - Кластеру (если указан через $this->cluster) + - Магазину (если указан через $this->store) + - Типу смены (только 1 и 2, исключая 4) +6. Группирует по дате, типу смены, store_dynamic.id, city_store.id +7. Возвращает ActiveQuery + +**Вызовы сторонних методов:** +- `AdminPayrollDays::find()` - создание запроса +- `->select([...])` - выбор полей с агрегацией +- `->innerJoin('city_store', ...)` - соединение с магазинами +- `->innerJoin('store_dynamic', [...])` - соединение с кластерами +- `->andFilterWhere([...])` - условная фильтрация по кластеру и магазину +- `->andWhere([...])` - фильтрация по диапазону дат и типу смены +- `->addGroupBy([...])` - группировка результатов +- `new Expression()` - SQL выражения для форматирования дат + +**Пример результирующего запроса:** +```sql +SELECT + admin_payroll_days.date AS date, + admin_payroll_days.smena_type AS shift_type, + store_dynamic.id AS store_dynamic_id, + city_store.id AS store_id, + SUM(admin_payroll_days.day_payroll) AS day_payroll, + SUM(admin_payroll_days.sales_sum) AS sales_sum_admin, + COUNT(admin_payroll_days.admin_id) AS admin_count +FROM admin_payroll_days +INNER JOIN city_store ON admin_payroll_days.store_id = city_store.id +INNER JOIN store_dynamic ON store_dynamic.store_id = city_store.id + AND DATE_FORMAT(store_dynamic.date_from, '%Y-%m-%d') <= DATE_FORMAT(admin_payroll_days.date, '%Y-%m-%d') + AND DATE_FORMAT(store_dynamic.date_to, '%Y-%m-%d') > DATE_FORMAT(admin_payroll_days.date, '%Y-%m-%d') +WHERE store_dynamic.value_int = 1 -- Кластер (если указан) + AND city_store.id = 5 -- Магазин (если указан) + AND admin_payroll_days.smena_type IN (1, 2) + AND DATE_FORMAT(admin_payroll_days.date, '%Y-%m-%d') >= '2025-01-01' + AND DATE_FORMAT(admin_payroll_days.date, '%Y-%m-%d') <= '2025-01-31' +GROUP BY admin_payroll_days.date, + admin_payroll_days.smena_type, + store_dynamic.id, + city_store.id +``` + +## Связи (Relations) + +```mermaid +erDiagram + FOT_METRICS --|> METRICS : extends + FOT_METRICS --> ADMIN_PAYROLL_DAYS : reads from + FOT_METRICS --> CITY_STORE : joins with + FOT_METRICS --> STORE_DYNAMIC : joins with + FOT_METRICS --> RNP_INDEX : writes to + FOT_METRICS --> RNP_DATA : writes to + FOT_METRICS --> RNP_ALIAS : uses + + ADMIN_PAYROLL_DAYS { + int id PK + int admin_id FK + int store_id FK + date date + int smena_type + float day_payroll + float sales_sum + } +``` + +## Примеры использования + +### Расчёт метрик ФОТ за месяц + +```php +$metrics = new FotMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +$result = $metrics->insertData(); +echo $result; +// |Время поиска: 2.350 sec.|Время записи индексов: 0.320 sec.|Время записи данных: 1.150 sec.| +``` + +### Расчёт для конкретного кластера + +```php +$metrics = new FotMetrics(); +$metrics->dateStart = '2025-01-15'; +$metrics->dateEnd = '2025-01-15'; +$metrics->cluster = 2; // Только кластер 2 + +$metrics->insertData(); +``` + +### Расчёт для одного магазина за неделю + +```php +$metrics = new FotMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-07'; +$metrics->store = 12; // Магазин ID 12 + +$metrics->insertData(); +``` + +### Получение рассчитанных данных ФОТ + +```php +$metrics = new FotMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +// Получить данные дневной смены (shift_type = 1) +$dayShiftData = $metrics->getDataArray(1, 5, 1); + +foreach ($dayShiftData as $row) { + echo "{$row['date']} - {$row['alias']}: {$row['value']}\n"; +} +// Результат: +// 2025-01-01 - sales_sum_admin: 180000.00 +// 2025-01-01 - day_payroll: 15000.50 +// 2025-01-01 - admin_count: 8 +``` + +### Анализ эффективности персонала + +```php +$metrics = new FotMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; +$metrics->store = 5; + +$data = $metrics->getDataArray(null, 5, 1); + +// Группировка по датам +$byDate = []; +foreach ($data as $row) { + $byDate[$row['date']][$row['alias']] = $row['value']; +} + +// Расчёт выручки на сотрудника +foreach ($byDate as $date => $values) { + $revenuePerEmployee = $values['sales_sum_admin'] / $values['admin_count']; + $payrollPercent = ($values['day_payroll'] / $values['sales_sum_admin']) * 100; + + echo "{$date}:\n"; + echo " Выручка на сотрудника: " . number_format($revenuePerEmployee, 2) . " руб.\n"; + echo " ФОТ к выручке: " . number_format($payrollPercent, 2) . "%\n"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[FotMetrics::insertData] --> B[Валидация дат] + B --> C[getQueryDataCollection] + C --> D[getQueryDataShifts - смены 1,2] + C --> E[getQueryDataDay = false] + D --> F[AdminPayrollDays: SUM по сменам] + E --> G[Пропуск дневных данных] + F --> H[Формирование selectQuery] + H --> I[Batch 1000 записей] + I --> J[Расчёт индексов RnpIndex] + J --> K[Маппинг RnpAlias: sales_sum_admin, day_payroll, admin_count] + K --> L[RnpData::deleteAll старые] + L --> M[RnpData::batchInsert новые] + M --> N[Возврат статистики] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Metrics](./Metrics.md) | Abstract | Базовый класс метрик | +| [AdminPayrollDays](./AdminPayrollDays.md) | Model | Модель начислений сотрудникам | +| [SalesMetrics](./SalesMetrics.md) | Model | Метрики продаж | +| [WriteOffsMetrics](./WriteOffsMetrics.md) | Model | Метрики списаний | +| `FotMetricsJob` | Job | Фоновый расчёт метрик ФОТ | +| `RnpData` | Model | Хранение данных метрик | + +## Примечания + +1. **Только смены**: Метрики ФОТ рассчитываются только по сменам (1, 2), без дневной агрегации (4) +2. **sales_sum_admin**: Это сумма продаж, закреплённая за конкретными администраторами (не общая сумма магазина) +3. **admin_count**: COUNT считает количество записей в admin_payroll_days, то есть количество администраторов, работавших в эту смену +4. **Кластеризация**: Использует store_dynamic для определения принадлежности магазина к кластеру на конкретную дату +5. **Аналитика**: Данные используются для расчёта показателей "выручка на сотрудника", "процент ФОТ к выручке" + +--- + +**Связанная документация:** +- [Metrics](./Metrics.md) +- [SalesMetrics](./SalesMetrics.md) +- [WriteOffsMetrics](./WriteOffsMetrics.md) +- [AdminPayrollDays](./AdminPayrollDays.md) +- [Архитектура системы метрик](../architecture/metrics-system.md) diff --git a/erp24/docs/models/FunctionRegulations.md b/erp24/docs/models/FunctionRegulations.md new file mode 100644 index 00000000..b665f0e8 --- /dev/null +++ b/erp24/docs/models/FunctionRegulations.md @@ -0,0 +1,119 @@ +# Модель FunctionRegulations + + +## Mindmap + +```mermaid +mindmap + root((FunctionRegulations)) + Таблица БД + function_regulations + Свойства + id + int + function_id + int + regulation_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `FunctionRegulations` связывает функции компании с регламентами. Определяет, какие регламенты относятся к выполнению конкретной функции. Используется для организации базы знаний по бизнес-процессам. + +**Файл модели:** `erp24/records/FunctionRegulations.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `function_regulations` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `function_id` | INTEGER | ID функции (FK → company_functions.id) | +| `regulation_id` | INTEGER | ID регламента (FK → regulations.id) | + +--- + +## Особенности + +- Реализует связь **many-to-many** между функциями и регламентами +- Одна функция может иметь несколько регламентов +- Один регламент может относиться к нескольким функциям + +--- + +## Диаграмма связей + +```mermaid +erDiagram + function_regulations }o--|| company_functions : "function" + function_regulations }o--|| regulations : "regulation" + + function_regulations { + int id PK + int function_id FK + int regulation_id FK + } +``` + +--- + +## Примеры использования + +### Привязка регламента к функции + +```php +$link = new FunctionRegulations(); +$link->function_id = $functionId; +$link->regulation_id = $regulationId; +$link->save(); +``` + +### Получение регламентов функции + +```php +$regulationIds = FunctionRegulations::find() + ->select('regulation_id') + ->where(['function_id' => $functionId]) + ->column(); + +$regulations = Regulations::find() + ->where(['id' => $regulationIds]) + ->all(); +``` + +### Удаление связи + +```php +FunctionRegulations::deleteAll([ + 'function_id' => $functionId, + 'regulation_id' => $regulationId +]); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `function_id` | Обязательное, целое число | +| `regulation_id` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[CompanyFunctions](./CompanyFunctions.md)** — функции компании +- **[Regulations](./Regulations.md)** — регламенты + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/GradeGroup.md b/erp24/docs/models/GradeGroup.md new file mode 100644 index 00000000..54e71c87 --- /dev/null +++ b/erp24/docs/models/GradeGroup.md @@ -0,0 +1,156 @@ +# Модель GradeGroup + + +## Mindmap + +```mermaid +mindmap + root((GradeGroup)) + Таблица БД + grade_group + Свойства + group_id + int + grade_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `GradeGroup` представляет связующую таблицу между грейдами сотрудников (`grade`) и группами должностей (`admin_group`). Определяет, какие грейды применимы к каким должностям в системе оплаты труда. + +**Файл модели:** `erp24/records/GradeGroup.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `grade_group` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `group_id` | INTEGER | ID группы должностей (FK → `admin_group.id`, PK) | +| `grade_id` | INTEGER | ID грейда (FK → `grade.id`, PK) | + +**Составной первичный ключ:** `[group_id, grade_id]` + +--- + +## Методы модели + +### `tableName(): string` +Возвращает имя таблицы `'grade_group'`. + +### `rules(): array` +Определяет правила валидации: +- `group_id`, `grade_id` — обязательные, целые числа +- Комбинация `[group_id, grade_id]` — уникальная + +### `attributeLabels(): array` +Возвращает метки атрибутов: +- `group_id` → 'Group ID' +- `grade_id` → 'Grade ID' + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `group_id` | Обязательное, целое число, уникальное в комбинации с grade_id | +| `grade_id` | Обязательное, целое число, уникальное в комбинации с group_id | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + grade_group }o--|| admin_group : "belongs_to" + grade_group }o--|| grade : "belongs_to" + + grade_group { + int group_id PK_FK + int grade_id PK_FK + } + + admin_group { + int id PK + string name + } + + grade { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Связывание грейда с группой + +```php +$gradeGroup = new GradeGroup(); +$gradeGroup->group_id = 30; // GROUP_FLORIST_DAY +$gradeGroup->grade_id = 5; // ID грейда "Флорист 3 уровня" + +if ($gradeGroup->save()) { + echo "Грейд привязан к группе"; +} +``` + +--- + +### Получение грейдов для группы + +```php +$groupId = 30; // GROUP_FLORIST_DAY + +$gradeGroups = GradeGroup::find() + ->with(['grade']) + ->where(['group_id' => $groupId]) + ->all(); + +foreach ($gradeGroups as $gg) { + echo "Грейд: {$gg->grade->name}\n"; +} +``` + +--- + +### Проверка наличия связи + +```php +$exists = GradeGroup::find() + ->where(['group_id' => 30, 'grade_id' => 5]) + ->exists(); + +if ($exists) { + echo "Связь существует"; +} +``` + +--- + +### Удаление связи + +```php +GradeGroup::deleteAll(['group_id' => 30, 'grade_id' => 5]); +``` + +--- + +## Связанные модели + +- **[Grade](./Grade.md)** — грейды сотрудников +- **[AdminGroup](./AdminGroup.md)** — группы/должности сотрудников + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/GradePrice.md b/erp24/docs/models/GradePrice.md new file mode 100644 index 00000000..1cf626b6 --- /dev/null +++ b/erp24/docs/models/GradePrice.md @@ -0,0 +1,252 @@ +# Модель GradePrice + + +## Mindmap + +```mermaid +mindmap + root((GradePrice)) + Таблица БД + grade_price + Свойства + id + int + created_at + string + created_by + int + closed_at + string + grade_id + int + city_id + int + Связи + CreatedBy + 1:1 Admin + Grade + 1:1 Grade + City + 1:1 City + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `GradePrice` представляет цены (ставки оплаты) для грейдов сотрудников в зависимости от города. Хранит месячную и часовую ставку для каждого грейда с учетом временного периода действия. + +**Файл модели:** `erp24/records/GradePrice.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `grade_price` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `created_at` | TIMESTAMP | Время создания записи | +| `created_by` | INTEGER | ID сотрудника, создавшего запись (FK → `admin.id`) | +| `closed_at` | TIMESTAMP | Время окончания действия грейда (до 2100-01-01) | +| `grade_id` | INTEGER | ID грейда (FK → `grade.id`) | +| `city_id` | INTEGER | ID города (FK → `city.id_city`) | +| `price_month` | FLOAT | Месячная ставка оплаты в рублях | +| `price_hour` | FLOAT | Часовая ставка оплаты в рублях | + +--- + +## Методы модели + +### `tableName(): string` +Возвращает имя таблицы `'grade_price'`. + +### `rules(): array` +Определяет правила валидации: +- Все поля обязательны +- `created_at`, `closed_at` — безопасный тип (datetime) +- `created_by`, `grade_id`, `city_id` — целые числа +- `price_month`, `price_hour` — числа (float) + +### `attributeLabels(): array` +Возвращает метки атрибутов для форм. + +--- + +## Связи (Relations) + +### `getCreatedBy()` +Связь с сотрудником, создавшим запись. + +**Тип:** hasOne +**Модель:** `Admin` + +```php +$gradePrice = GradePrice::findOne(1); +echo $gradePrice->createdBy->name; +``` + +--- + +### `getGrade()` +Связь с грейдом. + +**Тип:** hasOne +**Модель:** `Grade` + +```php +$gradePrice = GradePrice::findOne(1); +echo $gradePrice->grade->name; +``` + +--- + +### `getCity()` +Связь с городом. + +**Тип:** hasOne +**Модель:** `City` + +```php +$gradePrice = GradePrice::findOne(1); +echo $gradePrice->city->name; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + grade_price }o--|| grade : "belongs_to" + grade_price }o--|| city : "belongs_to" + grade_price }o--|| admin : "created_by" + + grade_price { + int id PK + timestamp created_at + int created_by FK + timestamp closed_at + int grade_id FK + int city_id FK + float price_month + float price_hour + } + + grade { + int id PK + string name + } + + city { + int id_city PK + string name + } + + admin { + int id PK + string name + } +``` + +--- + +## Примеры использования + +### Создание цены для грейда + +```php +$gradePrice = new GradePrice(); +$gradePrice->created_at = date('Y-m-d H:i:s'); +$gradePrice->created_by = Yii::$app->user->id; +$gradePrice->closed_at = '2100-01-01 00:00:00'; // Бессрочно +$gradePrice->grade_id = 5; +$gradePrice->city_id = 1; // Москва +$gradePrice->price_month = 50000.00; +$gradePrice->price_hour = 250.00; + +if ($gradePrice->save()) { + echo "Цена грейда сохранена"; +} +``` + +--- + +### Получение актуальной цены грейда + +```php +$gradeId = 5; +$cityId = 1; + +$gradePrice = GradePrice::find() + ->where(['grade_id' => $gradeId, 'city_id' => $cityId]) + ->andWhere(['>=', 'closed_at', date('Y-m-d H:i:s')]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($gradePrice) { + echo "Месячная ставка: {$gradePrice->price_month} руб.\n"; + echo "Часовая ставка: {$gradePrice->price_hour} руб.\n"; +} +``` + +--- + +### Получение истории цен грейда + +```php +$gradeId = 5; +$cityId = 1; + +$history = GradePrice::find() + ->where(['grade_id' => $gradeId, 'city_id' => $cityId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($history as $price) { + echo "{$price->created_at}: {$price->price_month} руб/мес\n"; +} +``` + +--- + +### Закрытие действующей цены и установка новой + +```php +// Закрываем текущую цену +$currentPrice = GradePrice::find() + ->where(['grade_id' => 5, 'city_id' => 1]) + ->andWhere(['>=', 'closed_at', date('Y-m-d H:i:s')]) + ->one(); + +if ($currentPrice) { + $currentPrice->closed_at = date('Y-m-d H:i:s'); + $currentPrice->save(); +} + +// Создаем новую цену +$newPrice = new GradePrice(); +$newPrice->created_at = date('Y-m-d H:i:s'); +$newPrice->created_by = Yii::$app->user->id; +$newPrice->closed_at = '2100-01-01 00:00:00'; +$newPrice->grade_id = 5; +$newPrice->city_id = 1; +$newPrice->price_month = 55000.00; // Повышение +$newPrice->price_hour = 275.00; +$newPrice->save(); +``` + +--- + +## Связанные модели + +- **[Grade](./Grade.md)** — грейды сотрудников +- **[City](./City.md)** — города +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Holiday.md b/erp24/docs/models/Holiday.md new file mode 100644 index 00000000..8a530db8 --- /dev/null +++ b/erp24/docs/models/Holiday.md @@ -0,0 +1,234 @@ +# Класс: Holiday + + +## Mindmap + +```mermaid +mindmap + root((Holiday)) + Таблица БД + holiday + Свойства + id + string + date + string + name + string + Наследование + extends ActiveRecord +``` + +## Назначение +Справочник праздничных и особых дней в ERP24. Используется для учёта выходных, праздников и сокращённых рабочих дней при планировании графиков работы, расчёте зарплаты и формировании расписания сотрудников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`holiday` + +## Родительский класс +`yii\db\ActiveRecord` + +## Константы типов дней + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `DAY_HOLIDAY` | 1 | Праздничный день (выходной) | +| `DAY_REDUCED` | 2 | Сокращённый рабочий день | +| `DAY_WORK` | 3 | Рабочий день (перенос выходного) | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `date` | date | Дата праздника/особого дня (уникальная) | +| `name` | string | Название праздника | +| `day_type` | int | Тип дня (1=праздник, 2=сокращённый, 3=рабочий) | + +## Методы + +### dayType() +Возвращает список типов дней. + +```php +public static function dayType(): array +``` + +**Возвращает**: +```php +[ + 1 => 'Праздник', + 2 => 'Сокращённый день', + 3 => 'Рабочий день', +] +``` + +### isHoliday() +Проверяет, является ли день праздничным. + +```php +public function isHoliday(): bool +``` + +**Возвращает**: `true` если day_type === DAY_HOLIDAY + +## Диаграмма типов дней + +```mermaid +stateDiagram-v2 + [*] --> DAY_HOLIDAY: Праздник + [*] --> DAY_REDUCED: Сокращённый + [*] --> DAY_WORK: Рабочий + + DAY_HOLIDAY: day_type=1 + DAY_HOLIDAY: Нерабочий день + DAY_HOLIDAY: 100% отдых + + DAY_REDUCED: day_type=2 + DAY_REDUCED: Сокращённый день + DAY_REDUCED: На 1 час меньше + + DAY_WORK: day_type=3 + DAY_WORK: Рабочий день + DAY_WORK: Перенос выходного +``` + +## Диаграмма связей + +```mermaid +erDiagram + Holiday { + int id PK + date date UK + varchar name + int day_type + } + + TimetableV3 { + int id PK + date date + } + + TimetablePlanV3 { + int id PK + date date + } + + Holiday ||--o{ TimetableV3 : "date (логическая)" + Holiday ||--o{ TimetablePlanV3 : "date (логическая)" +``` + +## Примеры использования + +### Добавление праздника +```php +$holiday = new Holiday(); +$holiday->date = '2024-01-01'; +$holiday->name = 'Новый год'; +$holiday->day_type = Holiday::DAY_HOLIDAY; +$holiday->save(); +``` + +### Добавление сокращённого дня +```php +$holiday = new Holiday(); +$holiday->date = '2024-03-07'; +$holiday->name = 'Предпраздничный день'; +$holiday->day_type = Holiday::DAY_REDUCED; +$holiday->save(); +``` + +### Добавление переноса рабочего дня +```php +$holiday = new Holiday(); +$holiday->date = '2024-04-27'; // Суббота +$holiday->name = 'Перенос с 1 мая'; +$holiday->day_type = Holiday::DAY_WORK; +$holiday->save(); +``` + +### Проверка, является ли день праздником +```php +$holiday = Holiday::findOne(['date' => '2024-01-01']); +if ($holiday && $holiday->isHoliday()) { + echo "Это праздник: {$holiday->name}"; +} +``` + +### Получение праздников за год +```php +$holidays = Holiday::find() + ->where(['>=', 'date', '2024-01-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->orderBy(['date' => SORT_ASC]) + ->all(); +``` + +### Получение только выходных праздников +```php +$daysOff = Holiday::find() + ->where(['day_type' => Holiday::DAY_HOLIDAY]) + ->andWhere(['>=', 'date', date('Y-01-01')]) + ->all(); +``` + +### Проверка даты на особый день +```php +$specialDay = Holiday::findOne(['date' => '2024-05-09']); +if ($specialDay) { + $typeLabel = Holiday::dayType()[$specialDay->day_type]; + echo "{$specialDay->name} - {$typeLabel}"; +} +``` + +### Формирование списка для календаря +```php +$holidays = Holiday::find() + ->where(['>=', 'date', $startDate]) + ->andWhere(['<=', 'date', $endDate]) + ->indexBy('date') + ->asArray() + ->all(); + +// Использование в календаре +foreach ($calendarDays as $day) { + $dateStr = $day->format('Y-m-d'); + if (isset($holidays[$dateStr])) { + $day->setHolidayInfo($holidays[$dateStr]); + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `date` | required, date (Y-m-d), unique | +| `name` | required, string | +| `day_type` | required, in (1, 2, 3), default: 1 | + +## Связанные модели + +- [TimetableV3](./TimetableV3.md) — расписание (учёт праздников) +- [TimetablePlanV3](./TimetablePlanV3.md) — плановое расписание +- [AdminPayroll](./AdminPayroll.md) — расчёт зарплаты с учётом праздников + +## Особенности реализации + +1. **Уникальность дат**: Каждая дата может быть только один раз в справочнике +2. **Три типа дней**: Праздник (выходной), сокращённый, рабочий (перенос) +3. **Интеграция с расписанием**: Используется при планировании смен и расчёте рабочего времени +4. **Ежегодное обновление**: Справочник требует обновления на каждый календарный год + +## Производственный календарь + +Модель используется для хранения данных производственного календаря РФ: + +| Категория | Примеры | +|-----------|---------| +| Государственные праздники | Новый год, 23 февраля, 8 марта, 1 мая, 9 мая, 12 июня, 4 ноября | +| Сокращённые дни | Предпраздничные дни | +| Переносы | Рабочие субботы при длинных выходных | diff --git a/erp24/docs/models/ImageDocumentLink.md b/erp24/docs/models/ImageDocumentLink.md new file mode 100644 index 00000000..fbe07ed2 --- /dev/null +++ b/erp24/docs/models/ImageDocumentLink.md @@ -0,0 +1,267 @@ +# Класс: ImageDocumentLink + + +## Mindmap + +```mermaid +mindmap + root((ImageDocumentLink)) + Таблица БД + image_document_link + Свойства + id + int + document_group_id + int + document_id + int + document_item_id + int + created_admin_id + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель связи изображений с документами в ERP24. Обеспечивает привязку фотографий к различным типам документов (списания, накладные и др.) с поддержкой мягкого удаления и аудита изменений. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`image_document_link` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `document_group_id` | int | ID группы документов (тип документа) | +| `document_id` | int | ID основного документа | +| `document_item_id` | int | ID элемента документа (строка, позиция) | +| `image_id` | int / null | ID изображения из таблицы images | +| `created_admin_id` | int | ID сотрудника, создавшего связь | +| `created_at` | varchar(100) | Дата создания | +| `updated_at` | varchar(100) / null | Дата обновления | +| `deleted_at` | varchar(100) / null | Дата мягкого удаления | +| `active` | int | Флаг активности (1=активно, 0=удалено) | +| `deleted_admin_id` | int / null | ID сотрудника, удалившего связь | + +## Статические методы + +### deleteCurrentLinkImage() +Мягкое удаление связи изображения с документом. + +```php +public static function deleteCurrentLinkImage( + int $writeOffsErpId, + int $modelProductId, + int $documentGroupId, + int $adminId +): void +``` + +**Параметры**: +- `$writeOffsErpId` (int) — ID документа +- `$modelProductId` (int) — ID элемента документа +- `$documentGroupId` (int) — ID группы документов +- `$adminId` (int) — ID сотрудника, удаляющего связь + +**Логика**: Устанавливает `active = 0`, `deleted_at` и `deleted_admin_id` + +### getImages() +Получение изображений для документа. + +```php +public static function getImages( + int $documentGroupId, + $documentId = null, + $documentItemId = null +): array +``` + +**Параметры**: +- `$documentGroupId` (int) — ID группы документов +- `$documentId` (int|null) — ID документа (опционально) +- `$documentItemId` (int|null) — ID элемента (опционально) + +**Возвращает**: Массив активных связей с изображениями + +## Геттеры и сеттеры + +Модель реализует fluent interface для большинства свойств: + +| Метод | Описание | +|-------|----------| +| `getDocumentGroupId()` / `setDocumentGroupId()` | Группа документов | +| `getDocumentId()` / `setDocumentId()` | ID документа | +| `getDocumentItemId()` / `setDocumentItemId()` | ID элемента | +| `getImageId()` / `setImageId()` | ID изображения | +| `getCreatedAdminId()` / `setCreatedAdminId()` | Автор создания | +| `getCreatedAt()` / `setCreatedAt()` | Дата создания | +| `getUpdatedAt()` / `setUpdatedAt()` | Дата обновления | +| `getDeletedAt()` / `setDeletedAt()` | Дата удаления | +| `getActive()` / `setActive()` / `disableActive()` | Флаг активности | +| `getDeletedAdminId()` / `setDeletedAdminId()` | Кто удалил | + +## Диаграмма связей + +```mermaid +erDiagram + ImageDocumentLink { + int id PK + int document_group_id + int document_id FK + int document_item_id FK + int image_id FK + int created_admin_id FK + varchar created_at + varchar updated_at + varchar deleted_at + int active + int deleted_admin_id FK + } + + Images { + int id PK + varchar filename + varchar original_name + } + + WriteOffs { + int id PK + varchar guid + } + + WriteOffsProducts { + int id PK + varchar write_offs_id FK + } + + Admin { + int id PK + varchar name + } + + ImageDocumentLink }o--|| Images : "image_id" + ImageDocumentLink }o--|| WriteOffs : "document_id (пример)" + ImageDocumentLink }o--|| WriteOffsProducts : "document_item_id" + ImageDocumentLink }o--|| Admin : "created_admin_id" + ImageDocumentLink }o--o| Admin : "deleted_admin_id" +``` + +## Примеры использования + +### Создание связи изображения с документом +```php +$link = new ImageDocumentLink(); +$link->setDocumentGroupId(1) // 1 = списания + ->setDocumentId($writeOff->id) + ->setDocumentItemId($product->id) + ->setImageId($image->id) + ->setCreatedAdminId(Yii::$app->user->id) + ->setCreatedAt() + ->setActive(); +$link->save(); +``` + +### Получение изображений документа списания +```php +$links = ImageDocumentLink::getImages( + documentGroupId: 1, // Группа "Списания" + documentId: $writeOffId +); + +foreach ($links as $link) { + $image = Images::findOne($link->image_id); + echo $image->filename; +} +``` + +### Получение изображений конкретной позиции +```php +$links = ImageDocumentLink::getImages( + documentGroupId: 1, + documentId: $writeOffId, + documentItemId: $productId +); +``` + +### Мягкое удаление связи +```php +ImageDocumentLink::deleteCurrentLinkImage( + writeOffsErpId: $writeOff->id, + modelProductId: $product->id, + documentGroupId: 1, + adminId: Yii::$app->user->id +); +``` + +### Обновление связи +```php +$link = ImageDocumentLink::findOne($id); +$link->setImageId($newImageId) + ->setUpdatedAt(); +$link->save(); +``` + +### Восстановление удалённой связи +```php +$link = ImageDocumentLink::findOne([ + 'document_id' => $docId, + 'document_item_id' => $itemId, + 'active' => 0 +]); + +if ($link) { + $link->active = 1; + $link->deleted_at = null; + $link->deleted_admin_id = null; + $link->save(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `document_group_id` | required, integer | +| `document_id` | required, integer | +| `document_item_id` | required, integer | +| `image_id` | integer, nullable | +| `created_admin_id` | required, integer | +| `created_at` | required, string (max 100) | +| `updated_at` | string (max 100), nullable | +| `deleted_at` | string (max 100), nullable | +| `active` | integer | +| `deleted_admin_id` | integer, nullable | + +## Группы документов (document_group_id) + +| ID | Тип документа | +|----|---------------| +| 1 | Документы списания (WriteOffs) | +| 2 | Накладные (Waybill) | +| 3 | Инвентаризация | +| ... | Другие типы | + +## Связанные модели + +- [Images](./Images.md) — хранилище изображений +- [WriteOffs](./WriteOffs.md) — документы списания +- [WriteOffsProducts](./WriteOffsProducts.md) — товары в списании +- [Admin](./Admin.md) — сотрудники + +## Особенности реализации + +1. **Мягкое удаление**: Записи не удаляются физически, а помечаются `active = 0` +2. **Аудит**: Отслеживается кто создал и кто удалил связь +3. **Fluent Interface**: Сеттеры возвращают `$this` для цепочки вызовов +4. **Универсальность**: Одна таблица для связи изображений с любыми типами документов +5. **Даты как строки**: created_at, updated_at, deleted_at хранятся как varchar, не timestamp diff --git a/erp24/docs/models/Images.md b/erp24/docs/models/Images.md new file mode 100644 index 00000000..dc393bf2 --- /dev/null +++ b/erp24/docs/models/Images.md @@ -0,0 +1,260 @@ +# Класс: Images + + +## Mindmap + +```mermaid +mindmap + root((Images)) + Таблица БД + images + Свойства + id + int + original_name + string + created_at + int + updated_at + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель хранилища изображений в ERP24. Управляет загрузкой, хранением и оптимизацией изображений. Поддерживает автоматическую оптимизацию JPEG и PNG файлов при загрузке. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`images` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +### TimestampBehavior +Автоматическое заполнение `created_at` и `updated_at` при создании и обновлении. + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `original_name` | varchar(255) | Оригинальное название файла | +| `type` | varchar(255) / null | MIME-тип файла (image/jpeg, image/png) | +| `filename` | varchar(255) / null | Имя файла на диске (MD5 хеш) | +| `width` | int / null | Ширина изображения в пикселях | +| `height` | int / null | Высота изображения в пикселях | +| `size` | int / null | Размер файла в байтах | +| `created_at` | int | Unix timestamp создания | +| `updated_at` | int | Unix timestamp обновления | + +## Методы + +### loadImage() +Загружает изображение, сохраняет на диск и оптимизирует. + +```php +public function loadImage(UploadedFile $file_obj = null, $deleteTempFile = true): bool|int|null +``` + +**Параметры**: +- `$file_obj` (UploadedFile|null) — объект загруженного файла +- `$deleteTempFile` (bool) — удалять ли временный файл после копирования (default: true) + +**Логика**: +1. Получает размеры изображения через `getimagesize()` +2. Генерирует уникальное имя файла: `md5(originalName + random) + extension` +3. Создаёт поддиректорию по первым 2 символам имени файла +4. Сохраняет файл в `@uploads/images/{2chars}/{filename}` +5. Заполняет метаданные (размеры, тип, вес) +6. Оптимизирует JPEG через `jpegoptim` и PNG через `optipng` + +**Возвращает**: +- `int` — ID созданной записи при успехе +- `false` — при ошибке сохранения +- `null` — если файл не передан + +### isImageFile() +Проверяет, является ли файл изображением. + +```php +public static function isImageFile($file, $extension = null): bool +``` + +**Параметры**: +- `$file` (UploadedFile) — объект файла +- `$extension` (array|null) — допустимые расширения (jpeg, png, gif и т.д.) + +**Возвращает**: `true` если файл является изображением указанного типа + +### getRealName() +Получает полное имя файла с расширением. + +```php +public static function getRealName($file): string|false +``` + +**Параметры**: +- `$file` (int) — ID изображения + +**Возвращает**: `{original_name}.{extension}` или `false` + +## Структура хранения файлов + +``` +@uploads/images/ +├── a1/ +│ ├── a1b2c3d4...xyz.jpg +│ └── a1f5g6h7...abc.png +├── b2/ +│ └── b2c3d4e5...def.jpg +└── c3/ + └── c3d4e5f6...ghi.png +``` + +Файлы распределяются по директориям на основе первых 2 символов имени файла для оптимизации файловой системы. + +## Диаграмма связей + +```mermaid +erDiagram + Images { + int id PK + varchar original_name + varchar type + varchar filename + int width + int height + int size + int created_at + int updated_at + } + + ImageDocumentLink { + int id PK + int image_id FK + int document_id + } + + Products1c { + varchar id PK + int image_id FK + } + + Admin { + int id PK + int photo_id FK + } + + Images ||--o{ ImageDocumentLink : "image_id" + Images ||--o| Products1c : "image_id" + Images ||--o| Admin : "photo_id" +``` + +## Примеры использования + +### Загрузка изображения из формы +```php +$uploadedFile = UploadedFile::getInstance($model, 'photo'); +if ($uploadedFile) { + $image = new Images(); + $imageId = $image->loadImage($uploadedFile); + + if ($imageId) { + $model->image_id = $imageId; + $model->save(); + } +} +``` + +### Проверка типа файла перед загрузкой +```php +$uploadedFile = UploadedFile::getInstance($model, 'photo'); +if (Images::isImageFile($uploadedFile, ['jpeg', 'png'])) { + $image = new Images(); + $image->loadImage($uploadedFile); +} +``` + +### Получение URL изображения +```php +$image = Images::findOne($imageId); +if ($image) { + $dir = substr($image->filename, 0, 2); + $url = "/uploads/images/{$dir}/{$image->filename}"; +} +``` + +### Получение оригинального имени для скачивания +```php +$realName = Images::getRealName($imageId); +// "photo.jpg" +``` + +### Удаление изображения с файлом +```php +$image = Images::findOne($imageId); +if ($image) { + $dir = substr($image->filename, 0, 2); + $filePath = Yii::getAlias("@uploads/images/{$dir}/{$image->filename}"); + + if (file_exists($filePath)) { + unlink($filePath); + } + + $image->delete(); +} +``` + +### Получение изображений за период +```php +$recentImages = Images::find() + ->where(['>=', 'created_at', strtotime('-7 days')]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `original_name` | required, string (max 255) | +| `type` | string (max 255), nullable | +| `filename` | string (max 255), nullable | +| `width` | integer, nullable | +| `height` | integer, nullable | +| `size` | integer, nullable | +| `created_at` | required, integer | +| `updated_at` | required, integer | + +## Системные требования + +Для оптимизации изображений требуются утилиты: +- **jpegoptim** — оптимизация JPEG файлов +- **optipng** — оптимизация PNG файлов + +```bash +# Ubuntu/Debian +apt-get install jpegoptim optipng + +# CentOS/RHEL +yum install jpegoptim optipng +``` + +## Связанные модели + +- [ImageDocumentLink](./ImageDocumentLink.md) — связи изображений с документами +- [Products1c](./Products1c.md) — товары с изображениями +- [Admin](./Admin.md) — фотографии сотрудников + +## Особенности реализации + +1. **Хеширование имён**: Имена файлов генерируются через MD5 для уникальности и безопасности +2. **Распределённое хранение**: Файлы распределяются по поддиректориям для оптимизации ФС +3. **Автоматическая оптимизация**: JPEG и PNG оптимизируются при загрузке через внешние утилиты +4. **Метаданные**: Сохраняются размеры, вес и тип для информации о файле +5. **Progressive JPEG**: jpegoptim конвертирует в progressive формат для быстрой загрузки в браузере diff --git a/erp24/docs/models/Incoming.md b/erp24/docs/models/Incoming.md new file mode 100644 index 00000000..d8f09bd9 --- /dev/null +++ b/erp24/docs/models/Incoming.md @@ -0,0 +1,255 @@ +# Модель Incoming + + +## Mindmap + +```mermaid +mindmap + root((Incoming)) + Таблица БД + incoming + Свойства + id + string + status + int + store_id + string + number + string + date + string + counteragent_id + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Incoming` представляет документы поступления товаров от поставщиков. Является частью старой системы учёта поступлений, хранит информацию о накладных поставщиков и связана с системой 1С. + +**Файл модели:** `erp24/records/Incoming.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `incoming` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(36) | Первичный ключ (GUID документа) | +| `status` | INTEGER | Статус документа | +| `date_add` | TIMESTAMP | Дата добавления записи | +| `store_id` | VARCHAR(36) | GUID магазина (FK) | +| `number` | VARCHAR(125) | Номер накладной поставщика | +| `date` | TIMESTAMP | Дата документа поступления | +| `counteragent_id` | VARCHAR(36) | GUID контрагента-поставщика (FK) | +| `comment` | TEXT | Комментарий к поступлению | +| `is_discrepancies` | INTEGER | Флаг наличия расхождений (0/1) | +| `items` | TEXT | Позиции товаров (JSON) | +| `supplier_items` | TEXT | Позиции с расхождениями (JSON) | +| `payments` | TEXT | Платежи по документу (JSON) | +| `summ` | NUMERIC | Общая сумма поступления | + +--- + +## Константы + +Модель не содержит констант. + +--- + +## Методы модели + +Модель содержит только стандартные методы ActiveRecord: + +### `tableName(): string` + +Возвращает название таблицы БД. + +**Возвращает:** `'incoming'` + +### `rules(): array` + +Правила валидации полей модели. + +**Возвращает:** массив правил валидации + +### `attributeLabels(): array` + +Метки полей для форм. + +**Возвращает:** массив меток + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `id` | Обязательное, уникальное, макс. 36 символов | +| `store_id` | Обязательное, макс. 36 символов | +| `number` | Обязательное, макс. 125 символов | +| `date` | Обязательное, безопасное | +| `counteragent_id` | Обязательное, макс. 36 символов | +| `items` | Обязательное, текстовое | +| `supplier_items` | Обязательное, текстовое | +| `payments` | Обязательное, текстовое | +| `summ` | Обязательное, числовое | +| `status` | Целое число | +| `is_discrepancies` | Целое число (0 или 1) | +| `date_add` | Безопасное | +| `comment` | Текстовое | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + incoming }o--|| city_store : "belongs_to" + incoming }o--|| counteragent : "supplier" + incoming ||--o{ incoming_items : "has_many" + + incoming { + string id PK + integer status + string store_id FK + string number + timestamp date + string counteragent_id FK + text items + text supplier_items + numeric summ + integer is_discrepancies + } + + incoming_items { + int id PK + string incoming_id FK + string product_id FK + integer quantity + numeric price + numeric summ + } + + city_store { + string id PK + string name + } + + counteragent { + string id PK + string name + } +``` + +--- + +## Примеры использования + +### Получение документов поступления магазина + +```php +$incomings = Incoming::find() + ->where(['store_id' => $storeId]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($incomings as $incoming) { + echo "Накладная: {$incoming->number}\n"; + echo "Дата: {$incoming->date}\n"; + echo "Сумма: {$incoming->summ}\n"; + echo "Расхождения: " . ($incoming->is_discrepancies ? 'Да' : 'Нет') . "\n\n"; +} +``` + +### Создание нового документа поступления + +```php +$incoming = new Incoming(); +$incoming->id = DataHelper::createGuidMy(); +$incoming->store_id = $storeGuid; +$incoming->number = 'ПН-2024-001'; +$incoming->date = date('Y-m-d H:i:s'); +$incoming->counteragent_id = $supplierGuid; +$incoming->comment = 'Плановое поступление товаров'; +$incoming->is_discrepancies = 0; +$incoming->items = json_encode($items); +$incoming->supplier_items = json_encode($supplierItems); +$incoming->payments = json_encode($payments); +$incoming->summ = 150000.00; +$incoming->status = 1; +$incoming->date_add = date('Y-m-d H:i:s'); + +if ($incoming->save()) { + echo "Документ поступления создан: {$incoming->id}\n"; +} +``` + +### Поиск поступлений за период + +```php +$incomings = Incoming::find() + ->where(['>=', 'date', '2024-01-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->andWhere(['store_id' => $storeId]) + ->all(); + +$totalSumm = array_sum(array_column($incomings, 'summ')); +echo "Всего поступлений: " . count($incomings) . "\n"; +echo "Общая сумма: {$totalSumm}\n"; +``` + +### Работа с JSON-полями + +```php +$incoming = Incoming::findOne($id); + +// Декодирование товаров +$items = json_decode($incoming->items, true); +foreach ($items as $item) { + echo "Товар: {$item['name']}, Количество: {$item['quantity']}\n"; +} + +// Декодирование расхождений +if ($incoming->is_discrepancies) { + $discrepancies = json_decode($incoming->supplier_items, true); + echo "Обнаружено расхождений: " . count($discrepancies) . "\n"; +} + +// Декодирование платежей +$payments = json_decode($incoming->payments, true); +foreach ($payments as $payment) { + echo "Платёж: {$payment['amount']} руб., Дата: {$payment['date']}\n"; +} +``` + +--- + +## Связанные модели + +- **[IncomingItems](./IncomingItems.md)** — позиции товаров поступления (таблица-детализация) +- **[CityStore](./CityStore.md)** — справочник магазинов +- **Counteragent** — справочник контрагентов (поставщики) +- **[Products1c](./Products1c.md)** — справочник товаров (через items JSON) + +--- + +## Примечания + +1. Это **старая система** учёта поступлений, используется GUID в качестве первичного ключа +2. Поддерживает хранение товаров, расхождений и платежей в виде JSON +3. Флаг `is_discrepancies` указывает на наличие расхождений между заказанным и полученным +4. Поле `supplier_items` хранит информацию о товарах с расхождениями +5. Связана с системой 1С через GUID магазинов, контрагентов и товаров +6. Для детализации товаров используется отдельная таблица `incoming_items` +7. Поле `comment` не обязательно при валидации (закомментировано в правилах) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/IncomingItems.md b/erp24/docs/models/IncomingItems.md new file mode 100644 index 00000000..c2a32f04 --- /dev/null +++ b/erp24/docs/models/IncomingItems.md @@ -0,0 +1,226 @@ +# Модель IncomingItems + + +## Mindmap + +```mermaid +mindmap + root((IncomingItems)) + Таблица БД + incoming_items + Свойства + id + int + incoming_id + string + product_id + string + quantity + int + price + float + vat_rate + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `IncomingItems` представляет позиции товаров в документах поступления. Детализирует каждый товар, полученный от поставщика: количество, цену, НДС и сумму. + +**Файл модели:** `erp24/records/IncomingItems.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `incoming_items` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `incoming_id` | VARCHAR(36) | GUID документа поступления из таблицы `incoming` (FK) | +| `product_id` | VARCHAR(36) | GUID товара из таблицы `products_1c` (FK) | +| `color` | VARCHAR(36) | Цвет товара | +| `quantity` | INTEGER | Количество единиц товара | +| `price` | NUMERIC | Цена единицы товара | +| `vat_rate` | VARCHAR(36) | Ставка НДС (строковое описание) | +| `vat_amount` | NUMERIC | Величина НДС | +| `summ` | NUMERIC | Сумма позиции (обычно quantity * price) | + +--- + +## Константы + +Модель не содержит констант. + +--- + +## Методы модели + +Модель содержит только стандартные методы ActiveRecord: + +### `tableName(): string` + +Возвращает название таблицы БД. + +**Возвращает:** `'incoming_items'` + +### `rules(): array` + +Правила валидации полей модели. + +**Возвращает:** массив правил валидации + +### `attributeLabels(): array` + +Метки полей для форм. + +**Возвращает:** массив меток + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `incoming_id` | Обязательное, макс. 36 символов | +| `product_id` | Обязательное, макс. 36 символов | +| `quantity` | Обязательное, целое число, по умолчанию null | +| `price` | Обязательное, числовое | +| `vat_rate` | Обязательное, макс. 36 символов | +| `summ` | Обязательное, числовое | +| `color` | Макс. 36 символов | +| `vat_amount` | Числовое | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + incoming ||--o{ incoming_items : "has_many" + incoming_items }o--|| products_1c : "belongs_to" + + incoming { + string id PK + string store_id + string number + numeric summ + } + + incoming_items { + int id PK + string incoming_id FK + string product_id FK + string color + integer quantity + numeric price + string vat_rate + numeric vat_amount + numeric summ + } + + products_1c { + string id PK + string name + string tip + } +``` + +--- + +## Примеры использования + +### Создание позиции товара в поступлении + +```php +$item = new IncomingItems(); +$item->incoming_id = '550e8400-e29b-41d4-a716-446655440000'; +$item->product_id = '660e8400-e29b-41d4-a716-446655440001'; +$item->color = 'Белый'; +$item->quantity = 50; +$item->price = 120.00; +$item->vat_rate = '20%'; +$item->vat_amount = 1000.00; +$item->summ = 6000.00; + +if ($item->save()) { + echo "Позиция создана с ID: {$item->id}\n"; +} +``` + +### Получение позиций документа поступления + +```php +$items = IncomingItems::find() + ->where(['incoming_id' => $incomingId]) + ->all(); + +foreach ($items as $item) { + echo "Товар: {$item->product_id}\n"; + echo "Количество: {$item->quantity}\n"; + echo "Цена: {$item->price}\n"; + echo "НДС: {$item->vat_rate} ({$item->vat_amount} руб.)\n"; + echo "Сумма: {$item->summ}\n\n"; +} +``` + +### Расчёт итоговой суммы поступления + +```php +$totalSumm = IncomingItems::find() + ->where(['incoming_id' => $incomingId]) + ->sum('summ'); + +echo "Общая сумма поступления: {$totalSumm} руб.\n"; +``` + +### Подсчёт количества товаров + +```php +$totalQuantity = IncomingItems::find() + ->where(['incoming_id' => $incomingId]) + ->sum('quantity'); + +echo "Всего товаров: {$totalQuantity} шт.\n"; +``` + +### Получение товаров с НДС 20% + +```php +$items = IncomingItems::find() + ->where(['incoming_id' => $incomingId]) + ->andWhere(['vat_rate' => '20%']) + ->all(); + +foreach ($items as $item) { + echo "Товар: {$item->product_id}, Сумма НДС: {$item->vat_amount}\n"; +} +``` + +--- + +## Связанные модели + +- **[Incoming](./Incoming.md)** — документы поступления товаров +- **[Products1c](./Products1c.md)** — справочник товаров из 1С + +--- + +## Примечания + +1. Модель является детализацией документа `Incoming` +2. Поле `color` хранит текстовое описание цвета товара +3. Поле `vat_rate` хранит ставку НДС в виде строки (например, "20%", "10%", "Без НДС") +4. Поле `vat_amount` хранит абсолютную величину НДС в рублях +5. Поле `summ` обычно рассчитывается как `quantity * price` +6. Связана с системой 1С через GUID документа и товара +7. Используется для детального учёта поступлений от поставщиков + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/InfoItemsTableShop0.md b/erp24/docs/models/InfoItemsTableShop0.md new file mode 100644 index 00000000..7846df5a --- /dev/null +++ b/erp24/docs/models/InfoItemsTableShop0.md @@ -0,0 +1,306 @@ +# Класс: InfoItemsTableShop0 + + +## Mindmap + +```mermaid +mindmap + root((InfoItemsTableShop0)) + Таблица БД + info_items_table_shop_0 + Свойства + item_id + int + title + string + price + float + f_id + int + kod + string + id_1c + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель товаров интернет-магазина (legacy) в ERP24. Расширенная карточка товара с полным набором атрибутов для e-commerce: цены, SEO, изображения, размеры, теги, статусы и флаги промо-акций. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`info_items_table_shop_0` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +### Идентификация +| Поле | Тип | Описание | +|------|-----|----------| +| `item_id` | int | Первичный ключ | +| `f_id` | int | ID товара в сторонней программе | +| `kod` | varchar(32) | Штрих-код | +| `id_1c` | varchar(36) | GUID товара из 1С | + +### Основные данные +| Поле | Тип | Описание | +|------|-----|----------| +| `title` | text | Наименование товара | +| `description` | text | Описание | +| `description2` | text | Дополнительное описание | +| `content` | text | Полное содержимое | + +### Цены +| Поле | Тип | Описание | +|------|-----|----------| +| `price` | float | Цена продажи | +| `price_zakup` | float | Закупочная цена | +| `price_old` | float | Старая цена (для скидок) | +| `price_individ` | text | Индивидуальная цена | +| `price_m2` | text | Цена за м² | +| `m2_korobka` | float | м² в коробке | + +### Категории и теги +| Поле | Тип | Описание | +|------|-----|----------| +| `cat_items_id` | int | ID категории | +| `cat_id_ishod` | int | Исходный ID категории | +| `cat_items_id_dop` | text | Дополнительные категории | +| `cat_items_catalog_arr` | text | Массив каталогов | +| `group_arr` | text | Массив групп | +| `tag_arr` | text | Теги и свойства товара | +| `tags` | text | Теги | +| `tag_main` | int | Главный тег | + +### Изображения и медиа +| Поле | Тип | Описание | +|------|-----|----------| +| `image` | varchar(255) | Основное изображение | +| `image_sm` | varchar(255) | Миниатюра | +| `video` | text | Видео | + +### SEO +| Поле | Тип | Описание | +|------|-----|----------| +| `title_url` | varchar(255) | URL-заголовок | +| `title_from_url` | varchar(255) | Title из URL | +| `url` | text | URL товара | +| `seo_title` | text | SEO Title | +| `seo_description` | text | SEO Description | +| `seo_keywords` | text | SEO Keywords | + +### Размеры и вес +| Поле | Тип | Описание | +|------|-----|----------| +| `dlina` | int | Длина | +| `glubina` | int | Глубина | +| `visota` | int | Высота | +| `shirina` | int | Ширина | +| `massa` | float | Масса | +| `molar_volume` | float | Объём м³ | +| `razmer` | varchar(155) | Размер (текст) | + +### Флаги и статусы +| Поле | Тип | Описание | +|------|-----|----------| +| `status` | int | Статус | +| `visible` | text | Видимость | +| `show_in_catalog` | int | Показывать в каталоге | +| `instock` | text | В наличии | +| `archive` | text | В архиве | + +### Промо-флаги +| Поле | Тип | Описание | +|------|-----|----------| +| `sale` | text | Распродажа | +| `action` | text | Акция | +| `novinka` | text | Новинка | +| `hit` | text | Хит продаж | +| `exclusive` | text | Эксклюзив | +| `super_price` | text | Супер цена | +| `speed_delivery` | int | Быстрая доставка (матричные букеты) | + +### Счётчики +| Поле | Тип | Описание | +|------|-----|----------| +| `counter` | int | Счётчик просмотров | +| `counter_down` | int | Счётчик скачиваний | +| `counter_com` | int | Счётчик комментариев | +| `counter_poll` | int | Счётчик голосов | +| `avg_ball` | float | Средний балл | +| `order` | int | Порядок сортировки | +| `kol` | int | Количество | + +### Даты +| Поле | Тип | Описание | +|------|-----|----------| +| `data` | datetime | Дата создания | +| `data_edit` | datetime | Дата редактирования | +| `data_start` | datetime | Дата начала показа | +| `data_end` | datetime | Дата окончания показа | + +### Связи +| Поле | Тип | Описание | +|------|-----|----------| +| `proizvoditel_id` | int | ID производителя | +| `site_id` | int | ID сайта | +| `sites_arr` | text | Массив сайтов | +| `modul_id` | int | ID модуля | +| `city_arr` | text | Массив городов | +| `city_all` | int | Все города | +| `sostav` | int | Составной товар | +| `dop_items` | text | Дополнительные товары | +| `no_bonus` | int | Без бонусов | + +## Диаграмма связей + +```mermaid +erDiagram + InfoItemsTableShop0 { + int item_id PK + varchar id_1c FK + varchar kod + text title + float price + float price_old + int cat_items_id FK + varchar image + int status + text sale + text hit + } + + Products1c { + varchar id PK + varchar name + } + + Category { + int id PK + varchar name + } + + Products1c ||--o| InfoItemsTableShop0 : "id_1c" + Category ||--o{ InfoItemsTableShop0 : "cat_items_id" +``` + +## Примеры использования + +### Получение товара по ID +```php +$item = InfoItemsTableShop0::findOne($itemId); + +if ($item) { + echo "Товар: {$item->title}\n"; + echo "Цена: {$item->price} руб.\n"; + if ($item->price_old > $item->price) { + $discount = round((1 - $item->price / $item->price_old) * 100); + echo "Скидка: {$discount}%\n"; + } +} +``` + +### Товары в наличии +```php +$inStock = InfoItemsTableShop0::find() + ->where(['instock' => '1']) + ->andWhere(['show_in_catalog' => 1]) + ->andWhere(['status' => 1]) + ->orderBy(['order' => SORT_ASC]) + ->all(); +``` + +### Товары по категории +```php +$categoryItems = InfoItemsTableShop0::find() + ->where(['cat_items_id' => $categoryId]) + ->andWhere(['status' => 1]) + ->orderBy(['order' => SORT_ASC]) + ->all(); +``` + +### Хиты продаж +```php +$hits = InfoItemsTableShop0::find() + ->where(['hit' => '1']) + ->andWhere(['status' => 1]) + ->orderBy(['counter' => SORT_DESC]) + ->limit(10) + ->all(); +``` + +### Новинки +```php +$newItems = InfoItemsTableShop0::find() + ->where(['novinka' => '1']) + ->andWhere(['status' => 1]) + ->orderBy(['data' => SORT_DESC]) + ->limit(12) + ->all(); +``` + +### Распродажа +```php +$saleItems = InfoItemsTableShop0::find() + ->where(['sale' => '1']) + ->andWhere(['>', 'price_old', 0]) + ->andWhere(['status' => 1]) + ->orderBy(['order' => SORT_ASC]) + ->all(); +``` + +### Поиск по названию +```php +$results = InfoItemsTableShop0::find() + ->where(['like', 'title', $searchQuery]) + ->andWhere(['status' => 1]) + ->limit(20) + ->all(); +``` + +### Поиск по GUID 1С +```php +$item = InfoItemsTableShop0::find() + ->where(['id_1c' => $guid1c]) + ->one(); +``` + +### Быстрая доставка (матричные букеты) +```php +$fastDelivery = InfoItemsTableShop0::find() + ->where(['speed_delivery' => 1]) + ->andWhere(['status' => 1]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `item_id` | required, integer | +| `title` | required, string | +| `id_1c` | string (max 36) | +| `kod` | string (max 32) | +| `price` | number | +| `image` | string (max 255) | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары из 1С (id_1c) +- Категории (cat_items_id) + +## Особенности реализации + +1. **Legacy-модель**: Таблица из старой CMS интернет-магазина +2. **Множество полей**: ~100+ атрибутов товара +3. **Промо-флаги**: sale, hit, novinka, exclusive, action, super_price +4. **SEO-поля**: title_url, seo_title, seo_description, seo_keywords +5. **Интеграция с 1С**: id_1c для синхронизации +6. **Массивы в текстовых полях**: tag_arr, sites_arr, city_arr и др. +7. **Счётчики**: counter, avg_ball для аналитики +8. **Быстрая доставка**: speed_delivery для матричных букетов diff --git a/erp24/docs/models/InfoLog.md b/erp24/docs/models/InfoLog.md new file mode 100644 index 00000000..b00f42b5 --- /dev/null +++ b/erp24/docs/models/InfoLog.md @@ -0,0 +1,191 @@ +# Модель InfoLog + + +## Mindmap + +```mermaid +mindmap + root((InfoLog)) + Таблица БД + info_log + Свойства + id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `InfoLog` представляет системный журнал логирования для записи информационных сообщений, ошибок и отладочной информации. Хранит детальные данные о событиях в системе, включая местоположение в коде (файл, строка, колонка), уровень важности и контекст. Используется для мониторинга работы приложения и диагностики проблем. + +**Файл модели:** `erp24/records/InfoLog.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `info_log` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `line` | INTEGER | Номер строки в файле, где произошло событие | +| `col` | INTEGER | Номер колонки в строке | +| `level` | INTEGER | Уровень логирования (severity) | +| `category` | VARCHAR(255) | Категория лога (application, db, api и т.д.) | +| `log_time` | INTEGER | Unix timestamp времени события | +| `time_group` | INTEGER | Группировка по времени | +| `prefix` | VARCHAR(255) | Префикс сообщения | +| `file` | TEXT | Путь к файлу, где произошло событие | +| `message` | TEXT | Текст сообщения лога | +| `context` | TEXT | Дополнительный контекст (JSON/сериализованные данные) | +| `created_at` | TIMESTAMP | Дата и время создания записи | + +--- + +## Уровни логирования + +| Уровень | Описание | +|---------|----------| +| 1 | ERROR — критические ошибки | +| 2 | WARNING — предупреждения | +| 4 | INFO — информационные сообщения | +| 8 | TRACE — отладочная информация | +| 16 | PROFILE — профилирование производительности | + +--- + +## Методы модели + +### Геттеры и сеттеры + +| Метод | Описание | +|-------|----------| +| `getLine(): ?int` | Возвращает номер строки | +| `setLine(?int $line): object` | Устанавливает номер строки | +| `getCol(): ?int` | Возвращает номер колонки | +| `setCol(?int $col): void` | Устанавливает номер колонки | +| `getLevel(): ?int` | Возвращает уровень логирования | +| `setLevel(?int $level): void` | Устанавливает уровень логирования | +| `getCategory(): ?string` | Возвращает категорию | +| `setCategory(?string $category): object` | Устанавливает категорию | +| `getLogTime(): ?int` | Возвращает время лога | +| `setLogTime(): object` | Устанавливает текущее время (time()) | +| `getPrefix(): ?string` | Возвращает префикс | +| `setPrefix(?string $prefix): void` | Устанавливает префикс | +| `getMessage(): ?string` | Возвращает сообщение | +| `setMessage(?string $message): object` | Устанавливает сообщение | +| `getContext(): ?string` | Возвращает контекст | +| `setContext(?string $context): object` | Устанавливает контекст | +| `getFile(): ?string` | Возвращает путь к файлу | +| `setFile(?string $file): object` | Устанавливает путь к файлу | +| `setCreatedAt(): object` | Устанавливает текущую дату | + +--- + +## Примеры использования + +### Создание записи лога + +```php +$log = new InfoLog(); +$log->setCategory('api') + ->setLevel(4) // INFO + ->setMessage('API request processed successfully') + ->setFile(__FILE__) + ->setLine(__LINE__) + ->setLogTime() + ->setCreatedAt() + ->setContext(json_encode(['user_id' => 123, 'action' => 'create'])); +$log->save(); +``` + +### Получение ошибок за период + +```php +$errors = InfoLog::find() + ->where(['level' => 1]) // ERROR + ->andWhere(['>=', 'created_at', '2025-12-01']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($errors as $error) { + echo "{$error->created_at}: {$error->message}\n"; + echo " File: {$error->file}:{$error->line}\n"; +} +``` + +### Статистика по категориям + +```php +$stats = InfoLog::find() + ->select(['category', 'level', 'COUNT(*) as count']) + ->groupBy(['category', 'level']) + ->asArray() + ->all(); +``` + +### Поиск логов по контексту + +```php +$logs = InfoLog::find() + ->where(['like', 'context', 'user_id":123']) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(50) + ->all(); +``` + +### Очистка старых логов + +```php +$deleted = InfoLog::deleteAll([ + '<', 'created_at', date('Y-m-d', strtotime('-30 days')) +]); +echo "Удалено {$deleted} старых записей"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `line`, `col`, `level`, `log_time`, `time_group` | Целое число | +| `file`, `message`, `context`, `created_at` | Строка | +| `category`, `prefix` | Строка, макс. 255 символов | + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + info_log { + int id PK + int line + int col + int level + string category + int log_time + int time_group + string prefix + text file + text message + text context + timestamp created_at + } +``` + +--- + +## Связанные модели + +- **[ApiErrorLog](./ApiErrorLog.md)** — логи ошибок API +- **[ApiLogs](./ApiLogs.md)** — логи API запросов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/KikFeedbackCategory.md b/erp24/docs/models/KikFeedbackCategory.md new file mode 100644 index 00000000..4a10f8b7 --- /dev/null +++ b/erp24/docs/models/KikFeedbackCategory.md @@ -0,0 +1,152 @@ +# Класс: KikFeedbackCategory + + +## Mindmap + +```mermaid +mindmap + root((KikFeedbackCategory)) + Таблица БД + kik_feedback_category + Свойства + id + int + name + string + type + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник категорий обратной связи в системе контроля качества (КиК) ERP24. Используется для классификации обращений клиентов по типу (положительный, нейтральный, отрицательный). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`kik_feedback_category` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы типов категорий + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `TYPE_NEGATIVE` | 0 | Отрицательный отзыв (жалоба) | +| `TYPE_NEUTRAL` | 1 | Нейтральный отзыв | +| `TYPE_POSITIVE` | 2 | Положительный отзыв (благодарность) | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Название категории | +| `type` | int | Тип категории (0=негатив, 1=нейтрал, 2=позитив) | +| `active` | int | Флаг активности (0=удалено, 1=активно) | + +## Методы + +### typeLabels() +Возвращает список типов категорий с человекочитаемыми названиями. + +```php +public static function typeLabels(): array +``` + +**Возвращает**: +```php +[ + 0 => 'Отрицательный', + 1 => 'Нейтральный', + 2 => 'Положительный', +] +``` + +## Диаграмма связей + +```mermaid +erDiagram + KikFeedbackCategory { + int id PK + varchar name + int type + int active + } + + KikFeedbackSubcategory { + int id PK + int category_id FK + varchar name + } + + KikFeedbackRequest { + int id PK + int category FK + } + + KikFeedbackCategory ||--o{ KikFeedbackSubcategory : "category_id" + KikFeedbackCategory ||--o{ KikFeedbackRequest : "category" +``` + +## Примеры использования + +### Получение активных категорий +```php +$categories = KikFeedbackCategory::find() + ->where(['active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Создание категории +```php +$category = new KikFeedbackCategory(); +$category->name = 'Качество обслуживания'; +$category->type = KikFeedbackCategory::TYPE_NEGATIVE; +$category->active = 1; +$category->save(); +``` + +### Формирование списка для фильтра +```php +$categoriesList = ArrayHelper::map( + KikFeedbackCategory::find()->where(['active' => 1])->all(), + 'id', + function($model) { + $typeLabel = KikFeedbackCategory::typeLabels()[$model->type]; + return "{$model->name} ({$typeLabel})"; + } +); +``` + +### Фильтрация по типу +```php +$negativeCategories = KikFeedbackCategory::find() + ->where(['type' => KikFeedbackCategory::TYPE_NEGATIVE, 'active' => 1]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `type` | required, integer | +| `active` | required, integer | + +## Связанные модели + +- [KikFeedbackSubcategory](./KikFeedbackSubcategory.md) — подкатегории +- [KikFeedbackRequest](./KikFeedbackRequest.md) — обращения + +## Особенности реализации + +1. **Мягкое удаление**: Записи не удаляются физически, а помечаются `active = 0` +2. **Типизация отзывов**: Категории разделены на положительные, нейтральные и отрицательные +3. **Иерархическая структура**: Категории имеют подкатегории через KikFeedbackSubcategory diff --git a/erp24/docs/models/KikFeedbackRequest.md b/erp24/docs/models/KikFeedbackRequest.md new file mode 100644 index 00000000..a24f3f50 --- /dev/null +++ b/erp24/docs/models/KikFeedbackRequest.md @@ -0,0 +1,324 @@ +# Класс: KikFeedbackRequest + + +## Mindmap + +```mermaid +mindmap + root((KikFeedbackRequest)) + Таблица БД + kik_feedback_request + Свойства + id + int + name + string + phone + string + source + int + status + int + created_at + string + Связи + SourceEntity + 1:1 KikFeedbackSource + Store + 1:1 Products1c + CategoryEntity + 1:1 KikFeedbackCategory + SubcategoryEntity + 1:1 KikFeedbackSubcategory + ResponsibleEntity + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель обращений в отдел контроля качества (КиК) в ERP24. Полный workflow обработки клиентских обращений: от создания до закрытия с учётом времени нахождения в каждом статусе. Интегрирована с чеками продаж, заказами AmoCRM, магазинами. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`kik_feedback_request` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы статусов + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_DRAFT` | -1 | Черновик | +| `STATUS_NEW` | 1 | Новое обращение | +| `STATUS_IN_WORK` | 2 | В работе | +| `STATUS_WAITING` | 3 | Ожидает | +| `STATUS_MANAGEMENT_DECISION_WANTED` | 4 | Нужно решение руководства | +| `STATUS_RETURNED_IN_WORK` | 5 | Возвращено в работу | +| `STATUS_COMPLETE` | 6 | Выполнено | +| `STATUS_DELETED` | 7 | Удалено | + +## Константы групп доступа + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `GROUP_MANAGER_KIK` | 82 | Менеджер отдела контроля качества | +| `GROUP_HEAD_MANAGER_KIK` | 83 | Руководитель отдела КиК | +| `GROUP_ROP` | 3 | Руководитель отдела продаж | + +## Поля таблицы + +### Основные данные + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `number` | int | Номер обращения | +| `name` | varchar(255) | ФИО клиента | +| `phone` | varchar(40) | Телефон клиента | +| `client_info` | varchar(1000) / null | Дополнительная информация от клиента | +| `description` | text / null | Описание проблемы | + +### Связи с сущностями + +| Поле | Тип | Описание | +|------|-----|----------| +| `source` | int | ID источника (KikFeedbackSource) | +| `check_id` | varchar(40) / null | GUID чека | +| `order_id` | varchar(40) / null | Номер заказа в AmoCRM | +| `store_id` | varchar(36) / null | GUID магазина | +| `category` | int / null | ID категории | +| `subcategory` | int / null | ID подкатегории | +| `responsible` | int / null | ID ответственного сотрудника | +| `verdict_id` | int / null | ID решения | + +### Статусы и даты + +| Поле | Тип | Описание | +|------|-----|----------| +| `status` | int | Текущий статус | +| `created_at` | datetime | Дата создания | +| `closed_at` | datetime / null | Дата закрытия | +| `status_changed_at` | datetime | Дата смены статуса | +| `in_work_started_at` | datetime / null | Дата начала работы | +| `complete_at` | datetime / null | Дата выполнения | + +### Решение + +| Поле | Тип | Описание | +|------|-----|----------| +| `management_decision` | varchar(1000) / null | Решение руководства | +| `verdict_description` | varchar(1000) / null | Описание решения | +| `delete_reason` | text / null | Причина удаления | + +### Метрики времени (секунды) + +| Поле | Тип | Описание | +|------|-----|----------| +| `status_1_duration` | int | Время в статусе "Новое" | +| `status_2_duration` | int | Время в статусе "В работе" | +| `status_3_duration` | int | Время в статусе "Ожидает" | +| `status_4_duration` | int | Время в статусе "Нужно решение" | +| `status_5_duration` | int | Время в статусе "Возвращено" | +| `status_6_duration` | int | Время в статусе "Выполнено" | +| `status_7_duration` | int | Время в статусе "Удалено" | + +## Методы + +### columnStatuses() +Возвращает статусы для колоночного отображения (без черновика и удалённых). + +```php +public static function columnStatuses(): array +``` + +### allStatuses() +Возвращает все статусы включая черновик и удалённые. + +```php +public static function allStatuses(): array +``` + +## Связи (Relations) + +| Метод | Модель | Описание | +|-------|--------|----------| +| `getSourceEntity()` | KikFeedbackSource | Источник обращения | +| `getStore()` | Products1c | Магазин | +| `getCategoryEntity()` | KikFeedbackCategory | Категория | +| `getSubcategoryEntity()` | KikFeedbackSubcategory | Подкатегория | +| `getResponsibleEntity()` | Admin | Ответственный | +| `getVerdictEntity()` | KikFeedbackVerdict | Решение | +| `getFiles()` | Files[] | Прикреплённые файлы | +| `getComments()` | Comment[] | Комментарии | +| `getSale()` | Sales | Связанная продажа | + +## Диаграмма workflow + +```mermaid +stateDiagram-v2 + [*] --> DRAFT: Создание + DRAFT --> NEW: Публикация + + NEW --> IN_WORK: Взять в работу + IN_WORK --> WAITING: Ожидание ответа + IN_WORK --> MANAGEMENT_DECISION: Нужно решение + IN_WORK --> COMPLETE: Завершить + + WAITING --> IN_WORK: Получен ответ + MANAGEMENT_DECISION --> RETURNED_IN_WORK: Решение принято + RETURNED_IN_WORK --> COMPLETE: Завершить + + NEW --> DELETED: Удалить + IN_WORK --> DELETED: Удалить + COMPLETE --> [*] + DELETED --> [*] + + DRAFT: status=-1 + NEW: status=1 + IN_WORK: status=2 + WAITING: status=3 + MANAGEMENT_DECISION: status=4 + RETURNED_IN_WORK: status=5 + COMPLETE: status=6 + DELETED: status=7 +``` + +## Диаграмма связей + +```mermaid +erDiagram + KikFeedbackRequest { + int id PK + int number + varchar name + varchar phone + int source FK + varchar check_id FK + varchar order_id + varchar store_id FK + varchar client_info + text description + int category FK + int subcategory FK + int status + datetime created_at + datetime closed_at + int responsible FK + varchar management_decision + int verdict_id FK + varchar verdict_description + datetime in_work_started_at + datetime complete_at + text delete_reason + datetime status_changed_at + } + + KikFeedbackSource ||--o{ KikFeedbackRequest : "source" + KikFeedbackCategory ||--o{ KikFeedbackRequest : "category" + KikFeedbackSubcategory ||--o{ KikFeedbackRequest : "subcategory" + Admin ||--o{ KikFeedbackRequest : "responsible" + KikFeedbackVerdict ||--o{ KikFeedbackRequest : "verdict_id" + Sales ||--o{ KikFeedbackRequest : "check_id" + Store ||--o{ KikFeedbackRequest : "store_id" +``` + +## Примеры использования + +### Создание нового обращения +```php +$request = new KikFeedbackRequest(); +$request->name = 'Иван Петров'; +$request->phone = '79876543210'; +$request->source = 1; // Звонок +$request->check_id = $check->guid; +$request->store_id = $store->id; +$request->description = 'Букет завял на следующий день'; +$request->category = 3; // Качество товара +$request->status = KikFeedbackRequest::STATUS_NEW; +$request->created_at = date('Y-m-d H:i:s'); +$request->status_changed_at = date('Y-m-d H:i:s'); +$request->number = KikFeedbackRequest::find()->max('number') + 1; +$request->save(); +``` + +### Взятие в работу +```php +$request = KikFeedbackRequest::findOne($id); +$request->status = KikFeedbackRequest::STATUS_IN_WORK; +$request->responsible = Yii::$app->user->id; +$request->in_work_started_at = date('Y-m-d H:i:s'); +$request->status_changed_at = date('Y-m-d H:i:s'); +$request->save(); +``` + +### Завершение с решением +```php +$request = KikFeedbackRequest::findOne($id); +$request->status = KikFeedbackRequest::STATUS_COMPLETE; +$request->verdict_id = 2; // Компенсация +$request->verdict_description = 'Выдан купон на скидку 500 руб.'; +$request->closed_at = date('Y-m-d H:i:s'); +$request->complete_at = date('Y-m-d H:i:s'); +$request->status_changed_at = date('Y-m-d H:i:s'); +$request->save(); +``` + +### Канбан-доска по статусам +```php +$columns = []; +foreach (KikFeedbackRequest::columnStatuses() as $status => $label) { + $columns[$status] = KikFeedbackRequest::find() + ->where(['status' => $status]) + ->with(['sourceEntity', 'categoryEntity', 'responsibleEntity']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +} +``` + +### Статистика времени обработки +```php +$avgTimes = KikFeedbackRequest::find() + ->select([ + 'AVG(status_1_duration) as avg_new', + 'AVG(status_2_duration) as avg_in_work', + ]) + ->where(['status' => KikFeedbackRequest::STATUS_COMPLETE]) + ->asArray() + ->one(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `phone` | required, string (max 40) | +| `source` | required, integer | +| `created_at` | required | +| `status_changed_at` | required | +| `description` | string | +| `category`, `subcategory` | integer | + +## Связанные модели + +- [KikFeedbackSource](./KikFeedbackSource.md) — источники обращений +- [KikFeedbackCategory](./KikFeedbackCategory.md) — категории +- [KikFeedbackSubcategory](./KikFeedbackSubcategory.md) — подкатегории +- [KikFeedbackVerdict](./KikFeedbackVerdict.md) — решения +- [Admin](./Admin.md) — ответственные сотрудники +- [Sales](./Sales.md) — связанные продажи +- [Files](./Files.md) — прикреплённые файлы +- [Comment](./Comment.md) — комментарии + +## Особенности реализации + +1. **Полный workflow**: 7 статусов для полного цикла обработки обращения +2. **Метрики времени**: Автоматический подсчёт времени в каждом статусе +3. **Интеграция с продажами**: Связь с чеками через check_id +4. **Интеграция с AmoCRM**: Поле order_id для связи с заказами +5. **Иерархия категорий**: Двухуровневая система категория → подкатегория +6. **Комментарии и файлы**: Полиморфная связь через entity diff --git a/erp24/docs/models/KikFeedbackSource.md b/erp24/docs/models/KikFeedbackSource.md new file mode 100644 index 00000000..58a6f45e --- /dev/null +++ b/erp24/docs/models/KikFeedbackSource.md @@ -0,0 +1,132 @@ +# Класс: KikFeedbackSource + + +## Mindmap + +```mermaid +mindmap + root((KikFeedbackSource)) + Таблица БД + kik_feedback_source + Свойства + id + int + name + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник источников обращений в систему контроля качества (КиК) ERP24. Определяет канал, через который клиент обратился с отзывом или жалобой. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`kik_feedback_source` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Название источника | +| `active` | int | Флаг активности (0=удалено, 1=активно) | + +## Диаграмма связей + +```mermaid +erDiagram + KikFeedbackSource { + int id PK + varchar name + int active + } + + KikFeedbackRequest { + int id PK + int source FK + } + + KikFeedbackSource ||--o{ KikFeedbackRequest : "source" +``` + +## Типичные источники обращений + +| Название | Описание | +|----------|----------| +| Звонок | Клиент позвонил по телефону | +| Email | Письмо на электронную почту | +| Отзыв на сайте | Отзыв оставлен на сайте компании | +| Соцсети | Обращение через социальные сети | +| Яндекс.Карты | Отзыв на Яндекс.Картах | +| Google Maps | Отзыв на Google Maps | +| 2ГИС | Отзыв в 2ГИС | +| WhatsApp | Сообщение в WhatsApp | +| Telegram | Сообщение в Telegram | +| Личное обращение | Клиент пришёл в магазин | + +## Примеры использования + +### Получение активных источников +```php +$sources = KikFeedbackSource::find() + ->where(['active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Создание источника +```php +$source = new KikFeedbackSource(); +$source->name = 'Telegram'; +$source->active = 1; +$source->save(); +``` + +### Формирование списка для выбора +```php +$sourcesList = ArrayHelper::map( + KikFeedbackSource::find()->where(['active' => 1])->all(), + 'id', + 'name' +); +``` + +### Статистика обращений по источникам +```php +$stats = KikFeedbackRequest::find() + ->select(['source', 'COUNT(*) as count']) + ->groupBy('source') + ->asArray() + ->all(); + +$sources = ArrayHelper::index(KikFeedbackSource::find()->all(), 'id'); +foreach ($stats as $stat) { + $sourceName = $sources[$stat['source']]->name ?? 'Неизвестно'; + echo "{$sourceName}: {$stat['count']} обращений\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `active` | required, integer | + +## Связанные модели + +- [KikFeedbackRequest](./KikFeedbackRequest.md) — обращения из данного источника + +## Особенности реализации + +1. **Мягкое удаление**: Записи помечаются `active = 0` вместо физического удаления +2. **Расширяемость**: Можно добавлять новые источники без изменения кода +3. **Аналитика**: Позволяет отслеживать эффективность каналов обратной связи diff --git a/erp24/docs/models/KikFeedbackSubcategory.md b/erp24/docs/models/KikFeedbackSubcategory.md new file mode 100644 index 00000000..42d95174 --- /dev/null +++ b/erp24/docs/models/KikFeedbackSubcategory.md @@ -0,0 +1,155 @@ +# Класс: KikFeedbackSubcategory + + +## Mindmap + +```mermaid +mindmap + root((KikFeedbackSubcategory)) + Таблица БД + kik_feedback_subcategory + Свойства + id + int + name + string + category_id + int + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник подкатегорий обращений в систему контроля качества (КиК) ERP24. Обеспечивает второй уровень классификации обращений для детализации проблемы в рамках основной категории. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`kik_feedback_subcategory` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Название подкатегории | +| `category_id` | int | ID родительской категории | +| `active` | int | Флаг активности (0=удалено, 1=активно) | + +## Диаграмма связей + +```mermaid +erDiagram + KikFeedbackCategory { + int id PK + varchar name + int type + } + + KikFeedbackSubcategory { + int id PK + varchar name + int category_id FK + int active + } + + KikFeedbackRequest { + int id PK + int subcategory FK + } + + KikFeedbackCategory ||--o{ KikFeedbackSubcategory : "category_id" + KikFeedbackSubcategory ||--o{ KikFeedbackRequest : "subcategory" +``` + +## Примеры подкатегорий + +### Для категории "Качество товара" (отрицательная) + +| Подкатегория | Описание | +|--------------|----------| +| Быстро завял | Цветы завяли раньше срока | +| Сломанные стебли | Повреждения при доставке | +| Не свежие цветы | Визуально несвежий товар | +| Несоответствие фото | Букет не соответствует заказу | + +### Для категории "Обслуживание" (отрицательная) + +| Подкатегория | Описание | +|--------------|----------| +| Грубость персонала | Некорректное поведение | +| Долгое ожидание | Длительное обслуживание | +| Некомпетентность | Сотрудник не смог помочь | + +### Для категории "Благодарность" (положительная) + +| Подкатегория | Описание | +|--------------|----------| +| Отличный сервис | Высокое качество обслуживания | +| Красивый букет | Довольны составом букета | +| Быстрая доставка | Доставка раньше срока | + +## Примеры использования + +### Получение подкатегорий для категории +```php +$subcategories = KikFeedbackSubcategory::find() + ->where(['category_id' => $categoryId, 'active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Создание подкатегории +```php +$subcategory = new KikFeedbackSubcategory(); +$subcategory->name = 'Быстро завял'; +$subcategory->category_id = 3; // Категория "Качество товара" +$subcategory->active = 1; +$subcategory->save(); +``` + +### Формирование списка для зависимого выбора +```php +$subcategoriesByCategory = []; +$subcategories = KikFeedbackSubcategory::find() + ->where(['active' => 1]) + ->all(); + +foreach ($subcategories as $sub) { + $subcategoriesByCategory[$sub->category_id][$sub->id] = $sub->name; +} +// Использование в JavaScript для динамической загрузки +``` + +### Получение с родительской категорией +```php +$subcategory = KikFeedbackSubcategory::findOne($id); +$category = KikFeedbackCategory::findOne($subcategory->category_id); +echo "Категория: {$category->name}, Подкатегория: {$subcategory->name}"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `category_id` | integer | +| `active` | required, integer | + +## Связанные модели + +- [KikFeedbackCategory](./KikFeedbackCategory.md) — родительская категория +- [KikFeedbackRequest](./KikFeedbackRequest.md) — обращения с данной подкатегорией + +## Особенности реализации + +1. **Иерархическая структура**: Подкатегория привязана к категории через category_id +2. **Мягкое удаление**: Записи помечаются `active = 0` +3. **Зависимый выбор**: В UI подкатегории фильтруются по выбранной категории +4. **Детализация отчётности**: Позволяет анализировать типовые проблемы diff --git a/erp24/docs/models/KikFeedbackVerdict.md b/erp24/docs/models/KikFeedbackVerdict.md new file mode 100644 index 00000000..1b5b2dae --- /dev/null +++ b/erp24/docs/models/KikFeedbackVerdict.md @@ -0,0 +1,142 @@ +# Класс: KikFeedbackVerdict + + +## Mindmap + +```mermaid +mindmap + root((KikFeedbackVerdict)) + Таблица БД + kik_feedback_verdict + Свойства + id + int + name + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник решений (вердиктов) по обращениям в систему контроля качества (КиК) ERP24. Определяет стандартные типы решений, которые могут быть приняты по обращению клиента. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`kik_feedback_verdict` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Суть решения | +| `active` | int | Флаг активности (0=удалено, 1=активно) | + +## Диаграмма связей + +```mermaid +erDiagram + KikFeedbackVerdict { + int id PK + varchar name + int active + } + + KikFeedbackRequest { + int id PK + int verdict_id FK + varchar verdict_description + } + + KikFeedbackVerdict ||--o{ KikFeedbackRequest : "verdict_id" +``` + +## Типичные вердикты + +| Название | Описание | +|----------|----------| +| Компенсация | Выдан денежный возврат или товар | +| Скидка на следующий заказ | Предоставлен купон на скидку | +| Повторная доставка | Перевыпуск заказа за счёт компании | +| Извинения | Принесены извинения, претензия снята | +| Необоснованная жалоба | Факты не подтвердились | +| Устранение недостатка | Проблема решена на месте | +| Передано руководству | Требует решения на уровне руководства | +| Обучение персонала | Проведена работа с сотрудником | + +## Примеры использования + +### Получение активных вердиктов +```php +$verdicts = KikFeedbackVerdict::find() + ->where(['active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Создание вердикта +```php +$verdict = new KikFeedbackVerdict(); +$verdict->name = 'Частичная компенсация'; +$verdict->active = 1; +$verdict->save(); +``` + +### Формирование списка для выбора +```php +$verdictsList = ArrayHelper::map( + KikFeedbackVerdict::find()->where(['active' => 1])->all(), + 'id', + 'name' +); +``` + +### Применение вердикта к обращению +```php +$request = KikFeedbackRequest::findOne($requestId); +$request->verdict_id = 2; // Скидка +$request->verdict_description = 'Выдан промокод SORRY500 на 500 руб.'; +$request->status = KikFeedbackRequest::STATUS_COMPLETE; +$request->save(); +``` + +### Статистика по вердиктам +```php +$stats = KikFeedbackRequest::find() + ->select(['verdict_id', 'COUNT(*) as count']) + ->where(['NOT', ['verdict_id' => null]]) + ->groupBy('verdict_id') + ->asArray() + ->all(); + +$verdicts = ArrayHelper::index(KikFeedbackVerdict::find()->all(), 'id'); +foreach ($stats as $stat) { + $verdictName = $verdicts[$stat['verdict_id']]->name ?? 'Без вердикта'; + echo "{$verdictName}: {$stat['count']}\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `active` | required, integer | + +## Связанные модели + +- [KikFeedbackRequest](./KikFeedbackRequest.md) — обращения с данным вердиктом + +## Особенности реализации + +1. **Мягкое удаление**: Записи помечаются `active = 0` для сохранения истории +2. **Дополнительное описание**: В KikFeedbackRequest есть verdict_description для деталей +3. **Стандартизация**: Позволяет унифицировать типы решений для аналитики +4. **Расширяемость**: Можно добавлять новые типы вердиктов diff --git a/erp24/docs/models/KogortStopList.md b/erp24/docs/models/KogortStopList.md new file mode 100644 index 00000000..dcf43b0f --- /dev/null +++ b/erp24/docs/models/KogortStopList.md @@ -0,0 +1,276 @@ +# Модель KogortStopList + + +## Mindmap + +```mermaid +mindmap + root((KogortStopList)) + Таблица БД + kogort_stop_list + Свойства + id + int + phone + string + created_at + string + created_by + int + Связи + UpdatedBy + 1:1 Admin + CreatedBy + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `KogortStopList` представляет стоп-лист телефонных номеров для когортных рассылок. Хранит номера телефонов клиентов, которым не следует отправлять маркетинговые сообщения и звонки. Используется для исключения номеров из когортных кампаний по запросу клиентов или по техническим причинам. + +**Файл модели:** `erp24/records/KogortStopList.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `kogort_stop_list` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `phone` | VARCHAR(20) | Телефон клиента | +| `comment` | TEXT | Причина добавления в стоп-лист | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата последнего обновления | +| `created_by` | INTEGER | ID сотрудника, создавшего запись | +| `updated_by` | INTEGER | ID сотрудника, обновившего запись | + +--- + +## Описание полей + +### `phone` — Телефон + +Номер телефона клиента, добавленный в стоп-лист. + +**Ограничения:** максимум 20 символов + +**Формат:** международный формат телефона + +**Примеры:** `"+79001234567"`, `"89001234567"` + +### `comment` — Комментарий + +Причина добавления номера в стоп-лист или дополнительная информация. + +**Примеры:** +- `"Клиент попросил не звонить"` +- `"Номер недействителен"` +- `"Жалоба на спам"` + +### `created_by` / `updated_by` — Авторство + +Идентификаторы сотрудников, создавших и обновивших запись. + +**Автозаполнение:** BlameableBehavior + +--- + +## Behaviors + +Модель использует стандартные Yii2 behaviors для автозаполнения: + +```php +public function behaviors(): array +{ + return [ + [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new Expression('NOW()'), + ], + [ + 'class' => BlameableBehavior::class, + 'createdByAttribute' => 'created_by', + 'updatedByAttribute' => 'updated_by', + ], + ]; +} +``` + +--- + +## Методы модели + +### `getUpdatedBy(): ActiveQuery` + +Возвращает связь с сотрудником, обновившим запись. + +**Возвращает:** `ActiveQuery` — запрос к модели Admin + +**Пример:** +```php +$record = KogortStopList::findOne($id); +echo $record->updatedBy->name; // Имя сотрудника +``` + +--- + +### `getCreatedBy(): ActiveQuery` + +Возвращает связь с сотрудником, создавшим запись. + +**Возвращает:** `ActiveQuery` — запрос к модели Admin + +**Пример:** +```php +$record = KogortStopList::findOne($id); +echo $record->createdBy->name; // Имя сотрудника +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + kogort_stop_list }o--|| admin : "created_by" + kogort_stop_list }o--|| admin : "updated_by" + kogort_stop_list ||--o{ users : "excludes" + + kogort_stop_list { + int id PK + string phone + text comment + timestamp created_at + timestamp updated_at + int created_by FK + int updated_by FK + } + + admin { + int id PK + string name + } + + users { + string id PK + string phone + } +``` + +--- + +## Примеры использования + +### Добавление номера в стоп-лист + +```php +$stopList = new KogortStopList(); +$stopList->phone = '+79001234567'; +$stopList->comment = 'Клиент отказался от рассылок'; +$stopList->save(); // created_by и created_at заполнятся автоматически +``` + +### Проверка номера в стоп-листе + +```php +$isBlocked = KogortStopList::find() + ->where(['phone' => $phone]) + ->exists(); + +if ($isBlocked) { + // Не отправлять сообщение + return; +} +``` + +### Массовая проверка номеров + +```php +$phones = ['+79001234567', '+79001234568', '+79001234569']; + +$blockedPhones = KogortStopList::find() + ->select('phone') + ->where(['phone' => $phones]) + ->column(); + +$allowedPhones = array_diff($phones, $blockedPhones); +``` + +### Удаление из стоп-листа + +```php +KogortStopList::deleteAll(['phone' => $phone]); +``` + +### Получение записей с информацией об авторах + +```php +$stopList = KogortStopList::find() + ->with(['createdBy', 'updatedBy']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($stopList as $record) { + echo "Телефон: {$record->phone}\n"; + echo "Добавил: {$record->createdBy->name}\n"; + echo "Комментарий: {$record->comment}\n"; +} +``` + +### Поиск по комментарию + +```php +$records = KogortStopList::find() + ->where(['like', 'comment', 'жалоба']) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `phone` | Обязательное, макс. 20 символов | +| `comment` | Строка (TEXT) | +| `created_by` | Целое число, автозаполнение | +| `updated_by` | Целое число, автозаполнение | +| `created_at` | Автозаполнение (TimestampBehavior) | +| `updated_at` | Автозаполнение (TimestampBehavior) | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники (авторы записей) +- **[Users](./Users.md)** — клиенты (по телефону) +- **[SentKogort](./SentKogort.md)** — отправленные когорты + +--- + +## Интеграция с когортными рассылками + +При формировании когорты для рассылки необходимо исключать номера из стоп-листа: + +```php +// Получение номеров для когорты с учётом стоп-листа +$targetPhones = Users::find() + ->select('phone') + ->where(['>=', 'last_purchase_date', $targetDate]) + ->andWhere(['not in', 'phone', + KogortStopList::find()->select('phone') + ]) + ->column(); +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/LPTrackerApi.md b/erp24/docs/models/LPTrackerApi.md new file mode 100644 index 00000000..4a48ecd7 --- /dev/null +++ b/erp24/docs/models/LPTrackerApi.md @@ -0,0 +1,248 @@ +# Класс: LPTrackerApi + + +## Mindmap + +```mermaid +mindmap + root((LPTrackerApi)) + Таблица БД + ActiveRecord + Наследование + extends ActiveRecord +``` + +## Назначение +API-клиент для интеграции с сервисом LPTracker в ERP24. Обеспечивает авторизацию и взаимодействие с CRM-системой для управления лидами и звонками клиентам. + +## Пространство имён +`yii_app\records` + +## Родительский класс +Нет (обычный PHP-класс, не ActiveRecord) + +## Константы + +### Авторизация +```php +private const LOGIN = 'Zakaz-bazacvetov24@yandex.ru1'; +private const PASSWORD = 'B8-YY7d3K2ekNdK'; +public const SERVICE = 117605; +private const TIMEOUT = 10; +public const BASE_URI = 'https://direct.lptracker.ru'; +``` + +### Статусы ответа +```php +public const SUCCESS_STATUS = 'success'; // Успешный ответ +public const ERROR_STATUS = 'error'; // Ошибка +``` + +### ID статусов лидов +```php +public const NEW_LEAD = 2086013; // Новый лид +public const TO_CALL = 2140957; // Нужно позвонить +public const REASON_FOR_THE_CALL = 2391182; // Причина звонка +``` + +### Причины звонков +```php +public const MEMORABLE_DATE = 'Памятная дата'; +public const PURCHASE_EARLIER = 'Покупал ранее'; +``` + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$token` | string | Токен авторизации | +| `$client` | GuzzleHttp\Client | HTTP-клиент | + +## Методы + +### __construct() +**Описание:** Конструктор. Создаёт HTTP-клиент и выполняет авторизацию. + +**Исключения:** `Exception` — при ошибке подключения или авторизации + +### auth() (private) +**Описание:** Авторизация в API LPTracker. Получает токен для последующих запросов. + +**Логика:** +1. POST-запрос на `/login` с credentials +2. При успехе сохраняет token в свойство +3. При ошибке выбрасывает исключение + +### get($endpoint) +**Описание:** GET-запрос к API. + +**Параметры:** +- `$endpoint` (string) — эндпоинт API + +**Возвращает:** `array` — декодированный JSON-ответ + +### post($endpoint, $data = []) +**Описание:** POST-запрос к API. + +**Параметры:** +- `$endpoint` (string) — эндпоинт API +- `$data` (array) — данные для отправки + +**Возвращает:** `array` — декодированный JSON-ответ + +## Диаграмма взаимодействия + +```mermaid +sequenceDiagram + participant ERP as ERP24 + participant API as LPTrackerApi + participant LP as LPTracker + + ERP->>API: new LPTrackerApi() + API->>LP: POST /login + LP-->>API: token + API-->>ERP: готов к работе + + ERP->>API: get('/leads') + API->>LP: GET /leads (with token) + LP-->>API: leads data + API-->>ERP: array + + ERP->>API: post('/leads', data) + API->>LP: POST /leads (with token) + LP-->>API: result + API-->>ERP: array +``` + +## Диаграмма статусов лидов + +```mermaid +flowchart TD + A[NEW_LEAD
    Новый лид] --> B{Обработка} + B -->|Нужен звонок| C[TO_CALL
    Нужно позвонить] + B -->|Памятная дата| D[REASON_FOR_THE_CALL
    Памятная дата] + B -->|Покупал ранее| E[REASON_FOR_THE_CALL
    Покупал ранее] + + C --> F[Звонок выполнен] + D --> F + E --> F +``` + +## Примеры использования + +### Инициализация клиента +```php +try { + $lpTracker = new LPTrackerApi(); + echo "Подключение установлено"; +} catch (Exception $e) { + echo "Ошибка подключения: " . $e->getMessage(); +} +``` + +### Получение лидов +```php +$lpTracker = new LPTrackerApi(); +$leads = $lpTracker->get('/leads'); + +if ($leads['status'] == LPTrackerApi::SUCCESS_STATUS) { + foreach ($leads['result'] as $lead) { + echo "Лид: {$lead['name']} - {$lead['phone']}\n"; + } +} +``` + +### Создание лида +```php +$lpTracker = new LPTrackerApi(); + +$leadData = [ + 'name' => 'Иван Иванов', + 'phone' => '+79001234567', + 'email' => 'ivan@example.com', + 'status_id' => LPTrackerApi::NEW_LEAD, + 'reason' => LPTrackerApi::MEMORABLE_DATE +]; + +$result = $lpTracker->post('/leads', $leadData); + +if ($result['status'] == LPTrackerApi::SUCCESS_STATUS) { + echo "Лид создан: ID " . $result['result']['id']; +} +``` + +### Обновление статуса лида +```php +$lpTracker = new LPTrackerApi(); + +$updateData = [ + 'lead_id' => 12345, + 'status_id' => LPTrackerApi::TO_CALL +]; + +$result = $lpTracker->post('/leads/update', $updateData); +``` + +### Создание задачи на звонок +```php +$lpTracker = new LPTrackerApi(); + +$callTask = [ + 'lead_id' => 12345, + 'status_id' => LPTrackerApi::TO_CALL, + 'reason' => LPTrackerApi::PURCHASE_EARLIER, + 'comment' => 'Клиент покупал 3 месяца назад' +]; + +$lpTracker->post('/tasks/create', $callTask); +``` + +### Обработка ошибок +```php +$lpTracker = new LPTrackerApi(); + +$result = $lpTracker->get('/leads/12345'); + +if ($result['status'] == LPTrackerApi::ERROR_STATUS) { + echo "Ошибка: " . $result['message']; +} else { + $lead = $result['result']; + echo "Лид: {$lead['name']}"; +} +``` + +### Пакетное создание лидов +```php +$lpTracker = new LPTrackerApi(); + +$customers = [ + ['name' => 'Клиент 1', 'phone' => '+79001111111'], + ['name' => 'Клиент 2', 'phone' => '+79002222222'], +]; + +foreach ($customers as $customer) { + $customer['status_id'] = LPTrackerApi::NEW_LEAD; + + $result = $lpTracker->post('/leads', $customer); + + if ($result['status'] == LPTrackerApi::SUCCESS_STATUS) { + echo "Создан лид: {$customer['name']}\n"; + } +} +``` + +## Связанные модели + +- [Users](./Users.md) — клиенты (синхронизация лидов) +- [UsersMessageManagement](./UsersMessageManagement.md) — маркетинговые кампании +- [TrackEvent](./TrackEvent.md) — отслеживание событий + +## Особенности реализации + +1. **Не ActiveRecord**: Обычный PHP-класс для работы с API +2. **Guzzle HTTP**: Использует GuzzleHttp\Client для запросов +3. **Автоматическая авторизация**: Токен получается в конструкторе +4. **Таймаут**: 10 секунд на запрос +5. **Константы статусов**: NEW_LEAD, TO_CALL, REASON_FOR_THE_CALL +6. **Причины звонков**: MEMORABLE_DATE, PURCHASE_EARLIER +7. **JSON API**: Автоматическая сериализация/десериализация diff --git a/erp24/docs/models/LessonPollAnswers.md b/erp24/docs/models/LessonPollAnswers.md new file mode 100644 index 00000000..e2c9e791 --- /dev/null +++ b/erp24/docs/models/LessonPollAnswers.md @@ -0,0 +1,158 @@ +# Class: LessonPollAnswers + + +## Mindmap + +```mermaid +mindmap + root((LessonPollAnswers)) + Таблица БД + lesson_poll_answers + Свойства + id + int + poll_id + int + name + string + pos + int + is_correct + int + Связи + Picture + 1:1 Files + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель варианта ответа на вопрос теста в системе ERP24. Каждый вопрос (LessonsPoll) имеет несколько вариантов ответа, среди которых один или несколько правильных. Модель хранит текст варианта, флаг правильности и позицию для сортировки. Может иметь прикреплённую картинку для визуального представления варианта ответа. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`lesson_poll_answers` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `poll_id` | int | **FK** ID вопроса из таблицы lessons_poll | +| `name` | string(200) | **Текст варианта ответа** | +| `pos` | int | Позиция в списке вариантов (для сортировки) | +| `is_correct` | int | **Флаг правильности:** 0 - неправильный, 1 - правильный ответ | + +--- + +## Отношения (Relations) + +### getPicture() + +**Тип:** `hasOne` +**Модель:** `Files` +**Ключ:** `['entity_id' => 'id']` +**Условие:** `['entity' => 'lesson_poll_answer_picture']` +**Описание:** Картинка варианта ответа + +**Логика:** +Полиморфная связь с таблицей files для прикрепления картинки к варианту ответа. Используется когда вариант ответа визуальный (например, выбор правильного изображения товара, схемы, фотографии). Картинка опциональна — большинство вариантов текстовые. + +**Вызовы сторонних методов:** +- `Files::hasOne()` - построение связи с таблицей files +- `onCondition()` - фильтрация по типу сущности + +**Пример:** +```php +$answer = LessonPollAnswers::findOne($id); +if ($answer->picture) { + echo "{$answer->name}"; +} else { + echo $answer->name; +} +``` + +--- + +## Правила валидации + +```php +['poll_id'] // required +['poll_id', 'pos', 'is_correct'] // integer +['name'] // string, max: 200 +``` + +--- + +## Примеры использования + +### Создание варианта ответа + +```php +use yii_app\records\LessonPollAnswers; + +$answer = new LessonPollAnswers(); +$answer->poll_id = $pollId; +$answer->name = '10%'; +$answer->pos = 2; +$answer->is_correct = 1; // Правильный ответ +$answer->save(); +``` + +--- + +### Получение всех вариантов вопроса + +```php +$pollId = 5; + +$answers = LessonPollAnswers::find() + ->where(['poll_id' => $pollId]) + ->orderBy(['pos' => SORT_ASC]) + ->all(); + +foreach ($answers as $answer) { + $marker = $answer->is_correct ? '✓' : '✗'; + echo "{$marker} {$answer->name}\n"; +} +``` + +--- + +### Получение только правильных ответов + +```php +$correctAnswers = LessonPollAnswers::find() + ->where(['poll_id' => $pollId, 'is_correct' => 1]) + ->all(); + +echo "Правильные ответы:\n"; +foreach ($correctAnswers as $answer) { + echo "- {$answer->name}\n"; +} +``` + +--- + +## Связанные документы + +- [LessonsPoll.md](./LessonsPoll.md) — вопросы тестов +- [Lessons.md](./Lessons.md) — модель уроков +- [Files.md](./Files.md) — модель файлов diff --git a/erp24/docs/models/Lessons.md b/erp24/docs/models/Lessons.md index 93ea56a2..d85706d7 100644 --- a/erp24/docs/models/Lessons.md +++ b/erp24/docs/models/Lessons.md @@ -1,444 +1,668 @@ # Class: Lessons +## Mindmap: Модель Lessons + +```mermaid +mindmap + root((Lessons)) + Идентификация + id PK + name название + group_id FK группа + Содержание урока + description описание + content контент HTML + video_url видео + open_poll открытый вопрос + open_poll_require_picture требует фото + Настройки прохождения + min_percent % просмотра видео + max_attempts попыток + recommended_time рек. время мин + attempt_delay задержка мин + max_time макс время мин + obligatory_time обяз. срок дней + Опросы + shuffle_polls перемешивание + LessonsPoll вопросы + LessonPollAnswers ответы + Геймификация + success_ball баллы успех + fail_ball баллы неудача + Статус + status 0-неактивный 1-активный + date дата создания + pos позиция + Связи + LessonsGroup группа + Admin автор + Admin редактор + Files картинка +``` + +--- + ## Назначение -Модель Lessons представляет урок в системе корпоративного обучения ERP24. Уроки являются основной единицей образовательного контента, содержат видео, текстовый материал, тесты и настройки прохождения. Используются для онбординга новых сотрудников и повышения квалификации. +Модель урока в системе обучения сотрудников ERP24. Содержит информацию о видеоуроке, его содержимом, настройках прохождения, опросах и системе баллов. Уроки группируются в LessonsGroup и могут содержать как видеоконтент, так и текстовые материалы с тестовыми вопросами. + +Модель поддерживает различные форматы обучения: видеоуроки с контролем времени просмотра, закрытые вопросы с множественным выбором, открытые вопросы с возможностью прикрепления фото-отчета. Реализована геймификация с начислением/списанием баллов за результаты прохождения. + +--- ## Пространство имён -```php -namespace yii_app\records; -``` +`yii_app\records` + +--- ## Родительский класс +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`lessons` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `group_id` | int | **FK** ID группы уроков из таблицы lessons_group | +| `name` | string(250) | **Название урока** | +| `description` | string(250) | Описание урока (краткое резюме) | +| `pos` | int | **Позиция в группе** (порядок сортировки) | + +### Содержимое урока + +| Имя | Тип | Описание | +|-----|-----|----------| +| `content` | text | **HTML-контент урока** (текст, разметка, инструкции) | +| `video_url` | text | **URL видео** (embed-ссылка, обязательное поле) | + +### Открытый вопрос + +| Имя | Тип | Описание | +|-----|-----|----------| +| `open_poll` | string(250) | **Текст открытого вопроса** (null - вопроса нет) | +| `open_poll_require_picture` | int | **Требуется ли фото к ответу:** 0 - нет, 1 - да | + +### Настройки прохождения видео + +| Имя | Тип | Описание | +|-----|-----|----------| +| `min_percent` | int | **Минимальный процент просмотра видео** (0-100, для зачета) | +| `max_attempts` | int | **Максимальное количество попыток** прохождения за раз | +| `recommended_time` | int | **Рекомендованное время прохождения** (в минутах) | +| `attempt_delay` | int | **Задержка между попытками** (в минутах) | +| `max_time` | int | **Максимальное время прохождения** (в минутах) | +| `obligatory_time` | int | **Обязательный срок для прохождения** (в днях с момента назначения) | + +### Настройки опросов + +| Имя | Тип | Описание | +|-----|-----|----------| +| `shuffle_polls` | int | **Перемешивание вопросов и ответов:** 0 - по позициям, 1 - случайный порядок | + +### Геймификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `success_ball` | int | **Баллы за успешное прохождение** (начисляются при зачете) | +| `fail_ball` | int | **Баллы за неудачное прохождение** (списываются при провале) | + +### Статус и аудит + +| Имя | Тип | Описание | +|-----|-----|----------| +| `status` | int | **Статус активности:** 0 - неактивен (скрыт), 1 - активен (отображается) | +| `date` | datetime | Дата создания урока | +| `created_by` | int | **FK** ID сотрудника, создавшего урок | +| `edited_by` | int | **FK** ID сотрудника, последним редактировавшего урок | + +--- + +## Константы + +### Статусы урока + ```php -\yii\db\ActiveRecord +const STATUS_ACTIVE = 1; // Урок активен, отображается у учеников +const STATUS_INACTIVE = 0; // Урок неактивен, скрыт ``` -## Таблица БД +**Использование:** +```php +$lesson->status = Lessons::STATUS_ACTIVE; +``` + +--- + +## Отношения (Relations) + +### getLessonGroup() + +**Тип:** `hasOne` +**Модель:** `LessonsGroup` +**Ключ:** `['id' => 'group_id']` +**Описание:** Группа, к которой принадлежит урок + +**Логика:** +Связь один-к-одному с моделью LessonsGroup. Позволяет получить информацию о группе уроков, в которой находится данный урок: название группы, описание, настройки прохождения группы, систему баллов. +**Пример:** +```php +$lesson = Lessons::findOne($id); +$group = $lesson->lessonGroup; +echo "Урок '{$lesson->name}' относится к группе '{$group->name}'"; ``` -lessons + +--- + +### getPicture() + +**Тип:** `hasOne` +**Модель:** `Files` +**Ключ:** `['entity_id' => 'id']` +**Условие:** `['files.entity' => 'lesson_picture']` +**Описание:** Картинка-обложка урока + +**Логика:** +Связь с таблицей files через полиморфную связь. Фильтрует файлы по типу сущности 'lesson_picture'. Возвращает первый найденный файл (обычно один). Используется для отображения превью урока в списке. + +**Вызовы сторонних методов:** +- `Files::hasOne()` - построение ActiveQuery связи с таблицей files +- `onCondition()` - добавление дополнительного условия фильтрации по полю entity + +**Пример:** +```php +$lesson = Lessons::findOne($id)->with('picture'); +if ($lesson->picture) { + echo "{$lesson->name}"; +} ``` -## Использования (Dependencies) +--- + +### getAuthor() + +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'created_by']` +**Описание:** Сотрудник, создавший урок -- `Yii` - фреймворк Yii2 -- `LessonsGroup` - группа уроков (курс) -- `Files` - прикреплённые файлы (картинки) -- `Admin` - сотрудник-автор/редактор +**Логика:** +Связь с моделью Admin через поле created_by. Возвращает объект сотрудника-автора урока. Используется для отображения информации об авторе в интерфейсе, ведения аудита создания контента. -## Константы +**Вызовы сторонних методов:** +- `Admin::hasOne()` - построение ActiveQuery связи с таблицей admin +**Пример:** ```php -const STATUS_ACTIVE = 1; // Урок активен и виден ученикам -const STATUS_INACTIVE = 0; // Урок неактивен (черновик или архив) +$lesson = Lessons::findOne($id); +echo "Автор: {$lesson->author->name}"; +echo "Email: {$lesson->author->email}"; ``` -## Свойства (Properties) - -| Имя | Тип | Описание | Обязательное | По умолчанию | -|-----|-----|----------|--------------|--------------| -| `id` | `int` | Уникальный идентификатор урока (PRIMARY KEY) | Да | AUTO | -| `group_id` | `int` | ID группы уроков (курса) из lessons_group | Да | - | -| `name` | `string` | Название урока (до 250 символов) | Да | - | -| `description` | `string` | Описание урока для превью (до 250 символов) | Нет | - | -| `status` | `int` | Статус: 1 - активен, 0 - неактивен | Да | - | -| `created_by` | `int` | ID сотрудника, создавшего урок (FK → admin.id) | Да | - | -| `edited_by` | `int\|null` | ID сотрудника, последнего редактировавшего урок | Нет | null | -| `content` | `string` | HTML-контент урока (текстовый материал) | Нет | - | -| `video_url` | `string` | URL видеоматериала (YouTube, Vimeo и т.д.) | Да | - | -| `open_poll` | `string\|null` | Текст открытого вопроса (если null - вопроса нет) | Нет | null | -| `open_poll_require_picture` | `int` | Требуется ли картинка к ответу: 0 - нет, 1 - да | Нет | 0 | -| `date` | `string` | Дата создания/публикации урока | Да | - | -| `pos` | `int` | Позиция в списке уроков (для сортировки) | Да | - | -| `obligatory_time` | `int` | Обязательный срок прохождения в днях | Да | - | -| `shuffle_polls` | `int` | Перемешивать вопросы: 0 - нет, 1 - да | Нет | 0 | -| `min_percent` | `int` | Минимальный % просмотра видео для зачёта | Да | - | -| `max_attempts` | `int` | Максимальное количество попыток прохождения | Да | - | -| `recommended_time` | `int` | Рекомендованное время прохождения (минуты) | Да | - | -| `attempt_delay` | `int` | Задержка между попытками (минуты) | Да | - | -| `max_time` | `int` | Максимальное время прохождения (минуты) | Да | - | -| `success_ball` | `int` | Баллы за успешное прохождение | Да | - | -| `fail_ball` | `int` | Баллы за неудачное прохождение (списание) | Да | - | - -## Правила валидации (Rules) +--- + +### getEditor() + +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'edited_by']` +**Описание:** Сотрудник, последним редактировавший урок + +**Логика:** +Связь с моделью Admin через поле edited_by. Возвращает объект последнего редактора урока. Может быть NULL, если урок не редактировался после создания. Используется для аудита изменений. +**Вызовы сторонних методов:** +- `Admin::hasOne()` - построение ActiveQuery связи с таблицей admin + +**Пример:** ```php -public function rules() -{ - return [ - // Обязательные поля для создания урока - [['group_id', 'name', 'status', 'video_url', 'date', 'pos', 'obligatory_time', - 'min_percent', 'max_attempts', 'recommended_time', 'attempt_delay', - 'max_time', 'success_ball', 'fail_ball'], 'required'], - - // Целочисленные поля - [['group_id', 'open_poll_require_picture', 'pos', 'obligatory_time', - 'min_percent', 'max_attempts', 'recommended_time', 'attempt_delay', - 'max_time', 'success_ball', 'fail_ball', 'status', - 'created_by', 'edited_by', 'shuffle_polls'], 'integer'], - - // Текстовые поля без ограничения длины - [['content', 'video_url'], 'string'], - - // Безопасные поля - [['date'], 'safe'], - - // Ограничение длины строк - [['name', 'description', 'open_poll'], 'string', 'max' => 250], - ]; +$lesson = Lessons::findOne($id); +if ($lesson->editor) { + echo "Последний редактор: {$lesson->editor->name}"; +} else { + echo "Урок не редактировался"; } ``` -### Описание правил: +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'group_id', 'name', 'status', 'video_url', 'date', 'pos', + 'obligatory_time', 'min_percent', 'max_attempts', + 'recommended_time', 'attempt_delay', 'max_time', + 'success_ball', 'fail_ball' +] +``` -1. **required**: Основные параметры урока обязательны для корректного функционирования -2. **integer**: Все настройки времени, попыток и баллов - целые числа -3. **string**: Контент и URL видео могут быть произвольной длины -4. **string max=250**: Название и описание ограничены для отображения в UI +### Целочисленные поля -## Методы +```php +[ + 'group_id', 'open_poll_require_picture', 'pos', 'obligatory_time', + 'min_percent', 'max_attempts', 'recommended_time', 'attempt_delay', + 'max_time', 'success_ball', 'fail_ball', 'status', + 'created_by', 'edited_by', 'shuffle_polls' +] // integer +``` -### tableName() +### Текстовые поля -**Описание:** Возвращает имя таблицы в базе данных. +```php +['content', 'video_url'] // text (без ограничения длины) +['name', 'description', 'open_poll'] // string, max: 250 +``` -**Параметры:** Нет +### Дата -**Возвращает:** `string` - имя таблицы `'lessons'` +```php +['date'] // safe (datetime) +``` --- -### getLessonGroup() +## Примеры использования -**Описание:** Получает группу (курс), к которой принадлежит урок. Группы используются для организации уроков по темам/модулям. +### Создание нового урока -**Параметры:** Нет +```php +use yii_app\records\Lessons; +use yii_app\records\LessonsGroup; -**Возвращает:** `ActiveQuery` - запрос для получения связанной `LessonsGroup` +$lesson = new Lessons(); +$lesson->group_id = 5; +$lesson->name = 'Введение в продажи цветов'; +$lesson->description = 'Основы флористики и работы с клиентами'; +$lesson->content = '

    Урок 1

    В этом уроке вы узнаете...

    '; +$lesson->video_url = 'https://www.youtube.com/embed/VIDEO_ID'; +$lesson->status = Lessons::STATUS_ACTIVE; +$lesson->pos = 1; -**Логика работы:** -1. Создаёт связь hasOne с моделью LessonsGroup -2. Связывает `group_id` текущего урока с `id` в lessons_group +// Настройки прохождения +$lesson->min_percent = 80; // Нужно просмотреть минимум 80% видео +$lesson->max_attempts = 3; // Максимум 3 попытки +$lesson->recommended_time = 30; // Рекомендуется пройти за 30 минут +$lesson->attempt_delay = 60; // Задержка между попытками 1 час +$lesson->max_time = 60; // Максимум 1 час на прохождение +$lesson->obligatory_time = 7; // Обязательно пройти за 7 дней -**Вызовы сторонних методов:** -- `$this->hasOne(LessonsGroup::class, ['id' => 'group_id'])` - связь "один к одному" +// Геймификация +$lesson->success_ball = 10; // +10 баллов за успех +$lesson->fail_ball = -5; // -5 баллов за провал + +// Открытый вопрос +$lesson->open_poll = 'Опишите, как вы применили знания на практике'; +$lesson->open_poll_require_picture = 1; // Требуется фото + +// Настройки опросов +$lesson->shuffle_polls = 1; // Перемешивать вопросы + +// Аудит +$lesson->date = date('Y-m-d H:i:s'); +$lesson->created_by = Yii::$app->user->id; + +if ($lesson->save()) { + echo "Урок создан с ID: {$lesson->id}"; +} +``` + +--- + +### Получение урока с связанными данными -**Пример:** ```php -$lesson = Lessons::findOne(1); -$group = $lesson->lessonGroup; +// Загрузка с группой, автором и картинкой +$lesson = Lessons::find() + ->where(['id' => $lessonId]) + ->with(['lessonGroup', 'author', 'picture']) + ->one(); -echo "Урок: " . $lesson->name; -echo "Курс: " . $group->name; +if ($lesson) { + echo "Урок: {$lesson->name}\n"; + echo "Группа: {$lesson->lessonGroup->name}\n"; + echo "Автор: {$lesson->author->name}\n"; + + if ($lesson->picture) { + echo "Превью: {$lesson->picture->url}\n"; + } +} ``` --- -### getPicture() +### Получение всех активных уроков группы -**Описание:** Получает картинку-превью урока из системы хранения файлов. Использует полиморфную связь через модель Files. +```php +use yii_app\records\Lessons; -**Параметры:** Нет +$groupId = 5; -**Возвращает:** `ActiveQuery` - запрос для получения связанного `Files` +$lessons = Lessons::find() + ->where([ + 'group_id' => $groupId, + 'status' => Lessons::STATUS_ACTIVE + ]) + ->orderBy(['pos' => SORT_ASC]) + ->all(); -**Логика работы:** -1. Создаёт связь hasOne с моделью Files -2. Связывает `id` урока с `entity_id` в Files -3. Добавляет условие `entity = 'lesson_picture'` для фильтрации типа файла +foreach ($lessons as $lesson) { + echo "{$lesson->pos}. {$lesson->name}\n"; +} +``` -**Вызовы сторонних методов:** -- `$this->hasOne(Files::class, ['entity_id' => 'id'])` - связь с файлами -- `->onCondition(['files.entity' => 'lesson_picture'])` - фильтр по типу сущности +--- + +### Обновление статуса урока -**Пример:** ```php -$lesson = Lessons::findOne(1); -$picture = $lesson->picture; +$lesson = Lessons::findOne($lessonId); + +if ($lesson) { + // Деактивация урока + $lesson->status = Lessons::STATUS_INACTIVE; + $lesson->edited_by = Yii::$app->user->id; + $lesson->save(); -if ($picture) { - echo '' . $lesson->name . ''; + echo "Урок деактивирован"; } ``` --- -### getAuthor() +### Изменение настроек прохождения + +```php +$lesson = Lessons::findOne($lessonId); -**Описание:** Получает сотрудника, создавшего урок. Используется для отображения информации об авторе. +if ($lesson) { + // Усложнение урока + $lesson->min_percent = 90; // Повышаем процент просмотра + $lesson->max_attempts = 2; // Уменьшаем количество попыток + $lesson->obligatory_time = 3; // Сокращаем срок до 3 дней -**Параметры:** Нет + $lesson->edited_by = Yii::$app->user->id; + $lesson->save(); +} +``` -**Возвращает:** `ActiveQuery` - запрос для получения связанного `Admin` +--- -**Логика работы:** -1. Создаёт связь hasOne с моделью Admin -2. Связывает `created_by` урока с `id` в admin +### Проверка наличия открытого вопроса -**Пример:** ```php -$lesson = Lessons::findOne(1); -$author = $lesson->author; +$lesson = Lessons::findOne($lessonId); + +if ($lesson && $lesson->open_poll !== null) { + echo "Открытый вопрос: {$lesson->open_poll}\n"; -echo "Автор: " . $author->name; -echo "Создано: " . $lesson->date; + if ($lesson->open_poll_require_picture) { + echo "ВНИМАНИЕ: Требуется прикрепить фотографию к ответу\n"; + } +} ``` --- -### getEditor() +### Расчет времени на прохождение + +```php +$lesson = Lessons::findOne($lessonId); -**Описание:** Получает последнего редактора урока. Может быть null, если урок не редактировался после создания. +if ($lesson) { + echo "Рекомендованное время: {$lesson->recommended_time} минут\n"; + echo "Максимальное время: {$lesson->max_time} минут\n"; + echo "Обязательный срок: {$lesson->obligatory_time} дней\n"; -**Параметры:** Нет + // Расчет дедлайна + $assignedDate = new DateTime(); + $deadline = $assignedDate->modify("+{$lesson->obligatory_time} days"); + echo "Дедлайн: {$deadline->format('Y-m-d H:i:s')}\n"; +} +``` -**Возвращает:** `ActiveQuery` - запрос для получения связанного `Admin` +--- -**Логика работы:** -1. Создаёт связь hasOne с моделью Admin -2. Связывает `edited_by` урока с `id` в admin +### Статистика по урокам группы -**Пример:** ```php -$lesson = Lessons::findOne(1); -$editor = $lesson->editor; +$groupId = 5; + +// Общее количество уроков +$total = Lessons::find() + ->where(['group_id' => $groupId]) + ->count(); -if ($editor) { - echo "Последнее редактирование: " . $editor->name; +// Активные уроки +$active = Lessons::find() + ->where([ + 'group_id' => $groupId, + 'status' => Lessons::STATUS_ACTIVE + ]) + ->count(); + +// Уроки с открытыми вопросами +$withOpenPoll = Lessons::find() + ->where(['group_id' => $groupId]) + ->andWhere(['IS NOT', 'open_poll', null]) + ->count(); + +// Средний балл за успех +$avgSuccessBall = Lessons::find() + ->where(['group_id' => $groupId, 'status' => Lessons::STATUS_ACTIVE]) + ->average('success_ball'); + +echo "Всего уроков: {$total}\n"; +echo "Активных: {$active}\n"; +echo "С открытыми вопросами: {$withOpenPoll}\n"; +echo "Средний балл за успех: " . round($avgSuccessBall, 2) . "\n"; +``` + +--- + +### Копирование урока + +```php +$original = Lessons::findOne($lessonId); + +if ($original) { + $copy = new Lessons(); + $copy->attributes = $original->attributes; + + // Сброс автоинкремента + $copy->id = null; + + // Изменение названия + $copy->name = $original->name . ' (копия)'; + + // Новый автор + $copy->created_by = Yii::$app->user->id; + $copy->edited_by = null; + $copy->date = date('Y-m-d H:i:s'); + + // Деактивация копии + $copy->status = Lessons::STATUS_INACTIVE; + + if ($copy->save()) { + echo "Урок скопирован с ID: {$copy->id}"; + } } ``` -## Связи (Relations) +--- + +## Связи с другими моделями + +### Прямые связи + +- **LessonsGroup** — группа уроков (обязательная связь) +- **Admin** (author) — автор урока +- **Admin** (editor) — редактор урока +- **Files** (picture) — картинка-превью урока + +### Обратные связи + +- От **LessonsPoll** — вопросы теста урока (hasMany) +- От **LessonsPassed** — записи о прохождении урока (hasMany) + +--- + +## Диаграмма отношений ```mermaid erDiagram - LESSONS ||--|| LESSONS_GROUP : belongs_to - LESSONS ||--o| FILES : has_picture - LESSONS ||--|| ADMIN : created_by - LESSONS |o--o| ADMIN : edited_by - LESSONS ||--o{ LESSONS_POLL : has_questions - LESSONS ||--o{ LESSONS_PASSED : has_completions - - LESSONS { + Lessons ||--|| LessonsGroup : "belongs to" + Lessons ||--o| Admin : "created by" + Lessons ||--o| Admin : "edited by" + Lessons ||--o| Files : "has picture" + Lessons ||--o{ LessonsPoll : "has polls" + Lessons ||--o{ LessonsPassed : "has passes" + + Lessons { int id PK int group_id FK string name string description - int status - int created_by FK - int edited_by FK - string content - string video_url + text content + text video_url string open_poll int open_poll_require_picture - date date + int status int pos - int obligatory_time - int shuffle_polls + datetime date + int created_by FK + int edited_by FK int min_percent int max_attempts int recommended_time int attempt_delay int max_time + int obligatory_time + int shuffle_polls int success_ball int fail_ball } - LESSONS_GROUP { + LessonsGroup { int id PK string name + string description int status + int study_parallel + int pos } - FILES { + Admin { int id PK - string url - string entity - int entity_id + string name + string email } - ADMIN { + Files { int id PK - string name + int entity_id FK + string entity + string url } - LESSONS_POLL { + LessonsPoll { int id PK int lesson_id FK - string question + string name int pos + string type_option } - LESSONS_PASSED { - int id PK - int lesson_id FK + LessonsPassed { + int entity_id FK + string entity int admin_id FK int status - int attempt + int mistakes + int attempts } ``` -## Настройки прохождения - -### Временные параметры - -| Параметр | Единица | Описание | -|----------|---------|----------| -| `obligatory_time` | дни | Срок, в который сотрудник должен пройти урок | -| `recommended_time` | минуты | Рекомендуемое время на прохождение | -| `max_time` | минуты | Максимальное время (таймер) | -| `attempt_delay` | минуты | Пауза между попытками | - -### Параметры тестирования - -| Параметр | Описание | -|----------|----------| -| `min_percent` | Минимальный % просмотра видео (0-100) | -| `max_attempts` | Лимит попыток за сессию | -| `shuffle_polls` | Случайный порядок вопросов | - -### Баллы - -| Параметр | Описание | -|----------|----------| -| `success_ball` | Начисляется при успешном прохождении | -| `fail_ball` | Списывается при провале (положительное число) | - -## Примеры использования - -### Получение активных уроков курса - -```php -$lessons = Lessons::find() - ->where([ - 'group_id' => $groupId, - 'status' => Lessons::STATUS_ACTIVE, - ]) - ->orderBy(['pos' => SORT_ASC]) - ->all(); - -foreach ($lessons as $lesson) { - echo $lesson->pos . ". " . $lesson->name . "\n"; - echo " Срок: " . $lesson->obligatory_time . " дней\n"; - echo " Попыток: " . $lesson->max_attempts . "\n"; -} -``` +--- -### Создание нового урока +## Бизнес-логика -```php -$lesson = new Lessons(); -$lesson->group_id = $groupId; -$lesson->name = 'Основы работы с клиентами'; -$lesson->description = 'Базовый курс по общению с клиентами'; -$lesson->status = Lessons::STATUS_INACTIVE; // Сначала черновик -$lesson->created_by = Yii::$app->user->id; -$lesson->video_url = 'https://youtube.com/watch?v=...'; -$lesson->date = date('Y-m-d'); -$lesson->pos = 1; -$lesson->obligatory_time = 7; // 7 дней на прохождение -$lesson->min_percent = 80; // 80% видео -$lesson->max_attempts = 3; -$lesson->recommended_time = 30; -$lesson->attempt_delay = 60; // Час между попытками -$lesson->max_time = 45; -$lesson->success_ball = 10; -$lesson->fail_ball = 5; +### Процесс создания урока -if ($lesson->save()) { - echo "Урок создан: " . $lesson->id; -} -``` +1. Администратор создает урок в группе +2. Заполняет контент, загружает видео +3. Настраивает параметры прохождения +4. Добавляет тестовые вопросы (LessonsPoll) +5. При необходимости добавляет открытый вопрос +6. Активирует урок (status = 1) -### Получение урока с предзагрузкой +### Процесс прохождения урока -```php -$lesson = Lessons::find() - ->with(['lessonGroup', 'picture', 'author']) - ->where(['id' => $lessonId]) - ->one(); +1. Сотруднику назначается урок (LessonsPassed создается) +2. Сотрудник просматривает видео (проверка min_percent) +3. Сотрудник отвечает на закрытые вопросы +4. При наличии открытого вопроса - пишет ответ (+ фото) +5. Система проверяет результаты +6. Начисляются/списываются баллы +7. Статус в LessonsPassed обновляется -// Доступ без дополнительных запросов -echo "Курс: " . $lesson->lessonGroup->name; -echo "Автор: " . $lesson->author->name; -if ($lesson->picture) { - echo "Картинка: " . $lesson->picture->url; -} -``` +### Контроль времени -### Активация урока +- **recommended_time** — рекомендация, не блокирует +- **max_time** — жесткое ограничение времени сессии +- **obligatory_time** — срок в днях с момента назначения +- **attempt_delay** — пауза между попытками -```php -$lesson = Lessons::findOne($id); -$lesson->status = Lessons::STATUS_ACTIVE; -$lesson->edited_by = Yii::$app->user->id; -$lesson->save(); -``` +### Система баллов -## Поток данных +- **success_ball** — начисляются при успешном прохождении +- **fail_ball** — списываются при провале теста +- Баллы влияют на рейтинг сотрудника (AdminRating) -```mermaid -flowchart TD - A[Сотрудник открывает урок] --> B{Урок активен?} - B -->|Нет| C[403 Forbidden] - B -->|Да| D[Загрузка контента] - D --> E[Просмотр видео] - E --> F{min_percent достигнут?} - F -->|Нет| G[Продолжить просмотр] - F -->|Да| H[Открыть тест] - H --> I[LessonsPoll вопросы] - I --> J{Ответы корректны?} - J -->|Да| K[LessonsPassed + success_ball] - J -->|Нет| L{Попытки остались?} - L -->|Да| M[Ожидание attempt_delay] - M --> H - L -->|Нет| N[LessonsPassed failed - fail_ball] -``` - -## Связанные компоненты +### Перемешивание вопросов -| Компонент | Тип | Описание | -|-----------|-----|----------| -| [LessonService](../services/LessonService.md) | Service | Бизнес-логика прохождения уроков | -| [LessonPollService](../services/LessonPollService.md) | Service | Работа с тестами | -| LessonsController | Controller | Управление уроками | -| LessonsGroup | Model | Группы (курсы) | -| LessonsPoll | Model | Вопросы теста | -| LessonPollAnswers | Model | Варианты ответов | -| LessonsPassed | Model | Результаты прохождения | -| [Files](./Files.md) | Model | Прикреплённые файлы | +При `shuffle_polls = 1`: +- Вопросы показываются в случайном порядке +- Ответы в каждом вопросе также перемешиваются +- Каждая попытка имеет новую последовательность -## Workflow прохождения урока - -```mermaid -stateDiagram-v2 - [*] --> NotStarted: Урок назначен - NotStarted --> InProgress: Начало просмотра - InProgress --> VideoComplete: min_percent достигнут - VideoComplete --> Testing: Открыт тест - Testing --> Passed: Тест пройден - Testing --> Failed: Исчерпаны попытки - Testing --> Waiting: Нужна пауза - Waiting --> Testing: attempt_delay прошло - Passed --> [*]: success_ball начислены - Failed --> [*]: fail_ball списаны -``` +--- -## Примечания +## Замечания -1. **Полиморфная связь Files**: Картинки уроков хранятся в общей таблице files с entity='lesson_picture' -2. **Мягкая деактивация**: Уроки не удаляются, а переводятся в status=0 -3. **Гамификация**: Система баллов стимулирует прохождение уроков -4. **Открытые вопросы**: Поле `open_poll` позволяет добавить вопрос с развёрнутым ответом -5. **Аудит**: Поля `created_by` и `edited_by` отслеживают авторство +1. **video_url** — обязательное поле, даже если урок больше текстовый +2. **open_poll** — NULL означает отсутствие открытого вопроса +3. **pos** — определяет порядок в группе, используется для сортировки +4. **status** — неактивные уроки не видны ученикам, но сохраняют историю прохождения +5. **shuffle_polls** — применяется только к закрытым вопросам, не к открытому +6. **min_percent** — проверяется через JavaScript-плеер на фронтенде +7. **max_attempts** — ограничение на количество попыток "за раз", может быть сброшено +8. **obligatory_time** — используется для расчета дедлайна в LessonsPassed +9. Баллы могут быть отрицательными для fail_ball +10. **edited_by** может быть NULL, если урок создан но не редактировался --- -**Связанная документация:** -- [LessonService](../services/LessonService.md) -- [LessonPollService](../services/LessonPollService.md) -- [Модуль Lesson](../modules/lesson/README.md) -- [Files](./Files.md) +## Связанные документы + +- [LessonsGroup.md](./LessonsGroup.md) — группы уроков +- [LessonsPassed.md](./LessonsPassed.md) — прохождение уроков +- [LessonsPoll.md](./LessonsPoll.md) — опросы уроков +- [LessonPollAnswers.md](./LessonPollAnswers.md) — ответы на вопросы +- [Admin.md](./Admin.md) — модель сотрудников +- [Files.md](./Files.md) — модель файлов diff --git a/erp24/docs/models/LessonsGroup.md b/erp24/docs/models/LessonsGroup.md new file mode 100644 index 00000000..53bd2aaf --- /dev/null +++ b/erp24/docs/models/LessonsGroup.md @@ -0,0 +1,446 @@ +# Class: LessonsGroup + +## Mindmap: Модель LessonsGroup + +```mermaid +mindmap + root((LessonsGroup)) + Идентификация + id PK + name название + description описание + pos позиция + Статус + status 0-неактивный 1-активный + Режим прохождения + study_parallel 0-последовательно 1-параллельно + Время и сроки + recommended_time рек. срок дней + obligatory_time обяз. срок дней + Награды + success_score цветорубли за завершение + recommended_score бонус за срок + obligatory_score штраф за просрочку + Аудит + created_by автор + edited_by редактор + Связи + Lessons уроки группы + Files картинка + Admin автор + Admin редактор +``` + +--- + +## Назначение + +Модель группы уроков (курса) в системе обучения сотрудников ERP24. Группа объединяет несколько уроков в логический курс с общими настройками прохождения, системой баллов и дедлайнами. Позволяет организовать образовательный контент в иерархическую структуру. + +Группа определяет порядок прохождения уроков (последовательный или параллельный), сроки выполнения, систему вознаграждений и штрафов в цветорублях. Используется для онбординга сотрудников, обучения новым навыкам и повышения квалификации. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`lessons_group` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `name` | string(255) | **Название группы** (обязательное) | +| `description` | string(250) | Описание группы уроков | +| `status` | int | **Статус активности:** 0 - неактивен, 1 - активен (отображается у учеников) | +| `study_parallel` | int | **Режим прохождения:** 0 - последовательный, 1 - произвольный порядок | +| `recommended_time` | int | Рекомендованное время прохождения (в днях) | +| `success_score` | int | **Цветорубли за успешное прохождение** всех уроков группы | +| `recommended_score` | int | **Бонусные цветорубли** за прохождение в рекомендуемый срок | +| `obligatory_time` | int | Обязательный срок прохождения (в днях) | +| `obligatory_score` | int | **Потеря цветорублей** за непрохождение в обязательный срок | +| `pos` | int | Позиция в списке групп (для сортировки) | +| `created_by` | int | **FK** ID сотрудника, создавшего группу | +| `edited_by` | int | **FK** ID сотрудника, последним редактировавшего группу | + +--- + +## Константы + +```php +const STATUS_ACTIVE = 1; // Группа активна, отображается у учеников +const STATUS_INACTIVE = 0; // Группа неактивна, скрыта +``` + +--- + +## Отношения (Relations) + +### getPicture() + +**Тип:** `hasOne` +**Модель:** `Files` +**Ключ:** `['entity_id' => 'id']` +**Условие:** `['files.entity' => 'lesson_group_picture']` +**Описание:** Картинка-обложка группы уроков + +**Логика:** +Полиморфная связь с таблицей files. Используется для отображения визуального представления курса в списке доступных групп. Картинка помогает сотрудникам быстро идентифицировать курс. + +**Вызовы сторонних методов:** +- `Files::hasOne()` - построение ActiveQuery связи +- `onCondition()` - фильтрация по типу сущности + +**Пример:** +```php +$group = LessonsGroup::findOne($id); +if ($group->picture) { + echo "{$group->name}"; +} +``` + +--- + +### getAuthor() + +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'created_by']` +**Описание:** Сотрудник, создавший группу + +**Логика:** +Связь с моделью Admin для отслеживания автора курса. Используется в аудите создания образовательного контента. + +**Вызовы сторонних методов:** +- `Admin::hasOne()` - построение связи с таблицей admin + +**Пример:** +```php +$group = LessonsGroup::findOne($id); +echo "Автор курса: {$group->author->name}"; +``` + +--- + +### getEditor() + +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'edited_by']` +**Описание:** Последний редактор группы + +**Логика:** +Связь с моделью Admin для отслеживания последних изменений. Может быть NULL, если группа не редактировалась после создания. + +**Вызовы сторонних методов:** +- `Admin::hasOne()` - построение связи с таблицей admin + +**Пример:** +```php +$group = LessonsGroup::findOne($id); +if ($group->editor) { + echo "Последний редактор: {$group->editor->name}"; +} +``` + +--- + +### getLessons() + +**Тип:** `hasMany` +**Модель:** `Lessons` +**Ключ:** `['group_id' => 'id']` +**Условие:** `['status' => Lessons::STATUS_ACTIVE]` +**Описание:** Активные уроки группы + +**Логика:** +Связь один-ко-многим с моделью Lessons. Возвращает только активные уроки (status=1), скрывая черновики и архивные материалы. Используется для отображения списка доступных уроков курса. + +**Вызовы сторонних методов:** +- `Lessons::hasMany()` - построение связи один-ко-многим +- `onCondition()` - фильтрация по статусу урока + +**Пример:** +```php +$group = LessonsGroup::findOne($id); +$lessons = $group->lessons; + +echo "Курс '{$group->name}' содержит " . count($lessons) . " уроков:\n"; +foreach ($lessons as $index => $lesson) { + echo ($index + 1) . ". {$lesson->name}\n"; +} +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['name'] // Название обязательно +``` + +### Целочисленные поля +```php +[ + 'status', 'created_by', 'edited_by', 'study_parallel', + 'recommended_time', 'success_score', 'recommended_score', + 'obligatory_time', 'obligatory_score', 'pos' +] // integer +``` + +### Строковые поля +```php +['name'] // string, max: 255 +['description'] // string, max: 250 +``` + +--- + +## Примеры использования + +### Создание новой группы уроков + +```php +use yii_app\records\LessonsGroup; + +$group = new LessonsGroup(); +$group->name = 'Онбординг новых флористов'; +$group->description = 'Базовый курс для новичков'; +$group->status = LessonsGroup::STATUS_ACTIVE; +$group->study_parallel = 0; // Последовательное прохождение +$group->pos = 1; + +// Настройка сроков +$group->recommended_time = 14; // 2 недели рекомендация +$group->obligatory_time = 30; // 1 месяц обязательно + +// Система вознаграждений +$group->success_score = 500; // 500 цветорублей за завершение +$group->recommended_score = 100; // +100 за прохождение в срок +$group->obligatory_score = 200; // -200 за просрочку + +// Аудит +$group->created_by = Yii::$app->user->id; + +if ($group->save()) { + echo "Группа создана с ID: {$group->id}"; +} +``` + +--- + +### Получение группы с уроками + +```php +$group = LessonsGroup::find() + ->where(['id' => $groupId]) + ->with(['lessons', 'picture', 'author']) + ->one(); + +if ($group) { + echo "Курс: {$group->name}\n"; + echo "Описание: {$group->description}\n"; + echo "Автор: {$group->author->name}\n"; + echo "Количество уроков: " . count($group->lessons) . "\n"; + + if ($group->picture) { + echo "Обложка: {$group->picture->url}\n"; + } +} +``` + +--- + +### Получение всех активных групп + +```php +$activeGroups = LessonsGroup::find() + ->where(['status' => LessonsGroup::STATUS_ACTIVE]) + ->orderBy(['pos' => SORT_ASC]) + ->all(); + +foreach ($activeGroups as $group) { + echo "{$group->pos}. {$group->name}\n"; +} +``` + +--- + +### Расчет наград и штрафов + +```php +$group = LessonsGroup::findOne($groupId); + +// Базовая награда +echo "За прохождение курса: {$group->success_score} цветорублей\n"; + +// Бонус за срок +if ($group->recommended_score > 0) { + echo "Бонус за прохождение за {$group->recommended_time} дней: +{$group->recommended_score}\n"; +} + +// Штраф +if ($group->obligatory_score > 0) { + echo "Штраф за просрочку после {$group->obligatory_time} дней: -{$group->obligatory_score}\n"; +} + +// Максимальная награда +$maxReward = $group->success_score + $group->recommended_score; +echo "Максимально можно получить: {$maxReward} цветорублей\n"; +``` + +--- + +### Проверка режима прохождения + +```php +$group = LessonsGroup::findOne($groupId); + +if ($group->study_parallel) { + echo "Уроки можно проходить в любом порядке\n"; +} else { + echo "Уроки необходимо проходить последовательно\n"; + $lessons = $group->lessons; + echo "Сначала пройдите: {$lessons[0]->name}\n"; +} +``` + +--- + +### Статистика по группе + +```php +$group = LessonsGroup::findOne($groupId); +$lessons = $group->lessons; + +echo "Группа: {$group->name}\n"; +echo "Статус: " . ($group->status ? 'Активна' : 'Неактивна') . "\n"; +echo "Уроков в курсе: " . count($lessons) . "\n"; + +// Общее время +$totalTime = 0; +$totalBalls = 0; +foreach ($lessons as $lesson) { + $totalTime += $lesson->recommended_time; + $totalBalls += $lesson->success_ball; +} + +echo "Общее время прохождения: {$totalTime} минут (" . round($totalTime / 60, 1) . " часов)\n"; +echo "Баллов за все уроки: {$totalBalls}\n"; +echo "Цветорублей за группу: {$group->success_score}\n"; +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + LessonsGroup ||--o{ Lessons : "has lessons" + LessonsGroup ||--o| Files : "has picture" + LessonsGroup ||--o| Admin : "created by" + LessonsGroup ||--o| Admin : "edited by" + + LessonsGroup { + int id PK + string name + string description + int status + int study_parallel + int recommended_time + int success_score + int recommended_score + int obligatory_time + int obligatory_score + int pos + int created_by FK + int edited_by FK + } + + Lessons { + int id PK + int group_id FK + string name + int status + int pos + } + + Files { + int id PK + int entity_id FK + string entity + string url + } + + Admin { + int id PK + string name + } +``` + +--- + +## Бизнес-логика + +### Режимы прохождения + +**Последовательный (study_parallel = 0):** +- Уроки проходятся строго по порядку (pos) +- Следующий урок недоступен до завершения предыдущего +- Используется для программ с зависимыми темами + +**Параллельный (study_parallel = 1):** +- Все уроки доступны сразу +- Можно проходить в любом порядке +- Используется для независимых тем + +### Система вознаграждений + +1. **Базовая награда (success_score):** начисляется при завершении всех уроков группы +2. **Бонус за срок (recommended_score):** дополнительные цветорубли за прохождение за recommended_time дней +3. **Штраф (obligatory_score):** вычитается при непрохождении за obligatory_time дней + +### Сроки выполнения + +- **recommended_time** — мягкий дедлайн, даёт бонус +- **obligatory_time** — жёсткий дедлайн, после него штраф +- Обычно: `obligatory_time > recommended_time` + +--- + +## Замечания + +1. **name** — единственное обязательное поле +2. **status** — неактивные группы скрыты, но сохраняют историю прохождения +3. **study_parallel** — влияет на UI и доступность уроков +4. **pos** — определяет порядок в каталоге курсов +5. **success_score**, **recommended_score**, **obligatory_score** — в цветорублях +6. **recommended_time**, **obligatory_time** — в днях (не минутах, как в Lessons) +7. Цветорубли — внутренняя валюта мотивационной системы ERP24 +8. При удалении группы (если реализовано) нужно обрабатывать связанные Lessons +9. **getLessons()** возвращает только активные уроки, черновики скрыты +10. **edited_by** может быть NULL + +--- + +## Связанные документы + +- [Lessons.md](./Lessons.md) — уроки группы +- [LessonsPassed.md](./LessonsPassed.md) — прохождение уроков +- [Admin.md](./Admin.md) — модель сотрудников +- [Files.md](./Files.md) — модель файлов diff --git a/erp24/docs/models/LessonsPassed.md b/erp24/docs/models/LessonsPassed.md new file mode 100644 index 00000000..3ec0f288 --- /dev/null +++ b/erp24/docs/models/LessonsPassed.md @@ -0,0 +1,573 @@ +# Class: LessonsPassed + +## Mindmap: Модель LessonsPassed + +```mermaid +mindmap + root((LessonsPassed)) + Идентификация + entity lesson/lesson_group + entity_id FK урок или группа + admin_id FK сотрудник + Временные метки + created_at назначен + started_at начал + finished_at завершил + Прогресс + status 0-6 этапы + mistakes ошибки + attempts попытки + Открытый вопрос + open_poll_answer текст + open_poll_answer_comment комментарий + Статусы + 0-назначен + 1-прочитан + 2-провален + 3-пройден + 4-проверка откр вопроса + 5-откр вопрос провален + 6-откр вопрос пройден + Связи + Lessons урок + LessonsGroup группа + Admin сотрудник +``` + +--- + +## Назначение + +Модель результата прохождения урока или группы уроков сотрудником в системе ERP24. Отслеживает все этапы прохождения: от назначения до финального результата. Хранит историю попыток, количество ошибок, ответы на открытые вопросы и временные метки всех действий. + +Модель является центральной в системе обучения, связывая сотрудников с образовательным контентом и обеспечивая полный аудит процесса обучения. Поддерживает как автоматическую проверку тестов, так и ручную проверку открытых вопросов. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`lessons_passed` + +--- + +## Основные свойства + +### Полиморфная связь + +| Имя | Тип | Описание | +|-----|-----|----------| +| `entity` | string | **Тип сущности:** 'lesson' (урок) или 'lesson_group' (группа) | +| `entity_id` | int | **ID сущности** (урок или группа уроков) | +| `admin_id` | int | **FK** ID сотрудника, проходящего обучение | + +### Временные метки + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | datetime | **Время назначения** урока/группы сотруднику | +| `started_at` | datetime | Время начала прохождения (старт сессии) | +| `finished_at` | datetime | Время завершения прохождения | + +### Прогресс прохождения + +| Имя | Тип | Описание | +|-----|-----|----------| +| `status` | int | **Статус прохождения** (0-6, см. константы) | +| `mistakes` | int | **Количество ошибок** (неправильных ответов на вопросы) | +| `attempts` | int | **Количество попыток** прохождения теста | + +### Открытый вопрос + +| Имя | Тип | Описание | +|-----|-----|----------| +| `open_poll_answer` | string(1000) | Ответ сотрудника на открытый вопрос | +| `open_poll_answer_comment` | string(1000) | Комментарий проверяющего к ответу | + +--- + +## Константы + +### Статусы прохождения + +```php +const STATUS_ATTACHED = 0; // Назначен (сотрудник ещё не начинал) +const STATUS_READ = 1; // Прочитан (просмотрел контент) +const STATUS_PASS_FAIL = 2; // Провален (исчерпал попытки) +const STATUS_PASS_SUCCESS = 3; // Пройден успешно +const STATUS_NEED_OPEN_POLL_CHECK = 4; // Нужна проверка открытого вопроса +const STATUS_FAIL_OPEN_POLL_CHECK = 5; // Открытый вопрос не принят +const STATUS_PASS_OPEN_POLL_CHECK = 6; // Открытый вопрос принят +``` + +**Описание статусов:** + +- **STATUS_ATTACHED (0)** — Урок только что назначен сотруднику, он ещё не открывал его +- **STATUS_READ (1)** — Сотрудник открыл урок и просмотрел контент (видео/текст) +- **STATUS_PASS_FAIL (2)** — Сотрудник провалил тест (превысил лимит попыток или ошибок) +- **STATUS_PASS_SUCCESS (3)** — Сотрудник успешно прошёл тест (нет открытого вопроса или он уже проверен) +- **STATUS_NEED_OPEN_POLL_CHECK (4)** — Тест пройден, но требуется ручная проверка открытого вопроса +- **STATUS_FAIL_OPEN_POLL_CHECK (5)** — Проверяющий отклонил ответ на открытый вопрос +- **STATUS_PASS_OPEN_POLL_CHECK (6)** — Проверяющий принял ответ на открытый вопрос (финальный успех) + +--- + +## Отношения (Relations) + +### getLesson() + +**Тип:** `hasOne` +**Модель:** `Lessons` +**Ключ:** `['id' => 'entity_id']` +**Описание:** Урок (если entity='lesson') + +**Логика:** +Связь с моделью Lessons для получения данных урока. Работает только когда entity='lesson'. Позволяет получить информацию об уроке: название, контент, настройки прохождения, баллы. + +**Вызовы сторонних методов:** +- `Lessons::hasOne()` - построение связи с таблицей lessons + +**Пример:** +```php +$passed = LessonsPassed::findOne(['admin_id' => $adminId, 'entity' => 'lesson']); +if ($passed && $passed->lesson) { + echo "Урок: {$passed->lesson->name}\n"; + echo "Группа: {$passed->lesson->lessonGroup->name}\n"; +} +``` + +--- + +### getLessonGroup() + +**Тип:** `hasOne` +**Модель:** `LessonsGroup` +**Ключ:** `['id' => 'entity_id']` +**Описание:** Группа уроков (если entity='lesson_group') + +**Логика:** +Связь с моделью LessonsGroup для получения данных группы. Работает только когда entity='lesson_group'. Используется для трекинга прохождения целого курса. + +**Вызовы сторонних методов:** +- `LessonsGroup::hasOne()` - построение связи с таблицей lessons_group + +**Пример:** +```php +$passed = LessonsPassed::findOne(['admin_id' => $adminId, 'entity' => 'lesson_group']); +if ($passed && $passed->lessonGroup) { + echo "Курс: {$passed->lessonGroup->name}\n"; + echo "Количество уроков: " . count($passed->lessonGroup->lessons) . "\n"; +} +``` + +--- + +### getStudent() + +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'admin_id']` +**Описание:** Сотрудник, проходящий обучение + +**Логика:** +Связь с моделью Admin для получения информации о сотруднике-ученике. Используется для отображения списков прохождения, отчётов, статистики. + +**Вызовы сторонних методов:** +- `Admin::hasOne()` - построение связи с таблицей admin + +**Пример:** +```php +$passed = LessonsPassed::findOne($id); +echo "Сотрудник: {$passed->student->name}\n"; +echo "Email: {$passed->student->email}\n"; +echo "Должность: {$passed->student->position}\n"; +``` + +--- + +## Метод statusLabels() + +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `array` — массив соответствия кодов статусов и их названий + +**Описание:** +Возвращает человекочитаемые названия статусов прохождения на русском языке. Используется для отображения статуса в интерфейсе и отчётах. + +**Логика:** +Статический метод возвращает ассоциативный массив, где ключ - константа статуса (0-6), значение - текстовое описание на русском языке. Метод не принимает параметров и всегда возвращает полный список всех возможных статусов. + +**Возвращаемое значение:** +```php +[ + 0 => 'Назначен', + 1 => 'Прочитан', + 2 => 'Провален', + 3 => 'Пройден', + 4 => 'Нужна проверка открытого вопроса', + 5 => 'Открытый вопрос провален', + 6 => 'Открытый вопрос пройден' +] +``` + +**Пример:** +```php +$passed = LessonsPassed::findOne($id); +$statuses = LessonsPassed::statusLabels(); + +echo "Статус: " . $statuses[$passed->status] . "\n"; + +// Или напрямую +foreach (LessonsPassed::statusLabels() as $code => $label) { + echo "{$code}: {$label}\n"; +} +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['entity', 'entity_id', 'admin_id', 'created_at', 'status', 'mistakes', 'attempts'] +``` + +### Целочисленные поля +```php +['entity_id', 'admin_id', 'status', 'mistakes', 'attempts'] // integer +``` + +### Строковые поля +```php +['entity'] // string (enum: 'lesson', 'lesson_group') +['open_poll_answer', 'open_poll_answer_comment'] // string, max: 1000 +``` + +### Временные поля +```php +['started_at', 'finished_at'] // safe (datetime) +``` + +### Уникальность +```php +['entity', 'entity_id', 'admin_id'] // unique composite +// Один сотрудник не может иметь две записи для одного урока/группы +``` + +--- + +## Примеры использования + +### Назначение урока сотруднику + +```php +use yii_app\records\LessonsPassed; + +$passed = new LessonsPassed(); +$passed->entity = 'lesson'; +$passed->entity_id = $lessonId; +$passed->admin_id = $employeeId; +$passed->created_at = date('Y-m-d H:i:s'); +$passed->status = LessonsPassed::STATUS_ATTACHED; +$passed->mistakes = 0; +$passed->attempts = 0; + +if ($passed->save()) { + echo "Урок назначен сотруднику"; +} +``` + +--- + +### Начало прохождения урока + +```php +$passed = LessonsPassed::findOne([ + 'admin_id' => $adminId, + 'entity' => 'lesson', + 'entity_id' => $lessonId +]); + +if ($passed && $passed->status == LessonsPassed::STATUS_ATTACHED) { + $passed->status = LessonsPassed::STATUS_READ; + $passed->started_at = date('Y-m-d H:i:s'); + $passed->save(); + + echo "Начало прохождения зафиксировано"; +} +``` + +--- + +### Завершение теста с результатом + +```php +$passed = LessonsPassed::findOne([ + 'admin_id' => $adminId, + 'entity' => 'lesson', + 'entity_id' => $lessonId +]); + +$mistakesCount = 5; // Количество ошибок +$lesson = $passed->lesson; + +$passed->attempts++; +$passed->mistakes = $mistakesCount; + +// Проверка успешности +$maxMistakes = count($lesson->polls) * 0.2; // Допустимо 20% ошибок + +if ($mistakesCount <= $maxMistakes) { + // Проверяем наличие открытого вопроса + if ($lesson->open_poll) { + $passed->status = LessonsPassed::STATUS_NEED_OPEN_POLL_CHECK; + echo "Тест пройден. Ожидается проверка открытого вопроса."; + } else { + $passed->status = LessonsPassed::STATUS_PASS_SUCCESS; + $passed->finished_at = date('Y-m-d H:i:s'); + echo "Урок успешно пройден!"; + } +} else { + if ($passed->attempts >= $lesson->max_attempts) { + $passed->status = LessonsPassed::STATUS_PASS_FAIL; + $passed->finished_at = date('Y-m-d H:i:s'); + echo "Урок провален. Исчерпаны попытки."; + } else { + echo "Попытка {$passed->attempts} не удалась. Попробуйте ещё раз."; + } +} + +$passed->save(); +``` + +--- + +### Ответ на открытый вопрос + +```php +$passed = LessonsPassed::findOne($id); + +if ($passed->status == LessonsPassed::STATUS_NEED_OPEN_POLL_CHECK) { + $passed->open_poll_answer = $_POST['answer']; + // Статус остаётся STATUS_NEED_OPEN_POLL_CHECK + $passed->save(); + + echo "Ответ сохранён. Ожидайте проверки."; +} +``` + +--- + +### Проверка открытого вопроса (преподавателем) + +```php +$passed = LessonsPassed::findOne($id); + +if ($passed->status == LessonsPassed::STATUS_NEED_OPEN_POLL_CHECK) { + $isApproved = true; // Результат проверки + + if ($isApproved) { + $passed->status = LessonsPassed::STATUS_PASS_OPEN_POLL_CHECK; + $passed->open_poll_answer_comment = 'Отлично! Ответ принят.'; + $passed->finished_at = date('Y-m-d H:i:s'); + echo "Ответ принят. Урок пройден!"; + } else { + $passed->status = LessonsPassed::STATUS_FAIL_OPEN_POLL_CHECK; + $passed->open_poll_answer_comment = 'Ответ недостаточно полный. Попробуйте ещё раз.'; + echo "Ответ отклонён."; + } + + $passed->save(); +} +``` + +--- + +### Получение статистики сотрудника + +```php +$adminId = 123; + +// Все назначенные уроки +$total = LessonsPassed::find() + ->where(['admin_id' => $adminId, 'entity' => 'lesson']) + ->count(); + +// Пройденные уроки +$completed = LessonsPassed::find() + ->where(['admin_id' => $adminId, 'entity' => 'lesson']) + ->andWhere(['IN', 'status', [ + LessonsPassed::STATUS_PASS_SUCCESS, + LessonsPassed::STATUS_PASS_OPEN_POLL_CHECK + ]]) + ->count(); + +// Проваленные +$failed = LessonsPassed::find() + ->where([ + 'admin_id' => $adminId, + 'entity' => 'lesson', + 'status' => LessonsPassed::STATUS_PASS_FAIL + ]) + ->count(); + +// В процессе +$inProgress = $total - $completed - $failed; + +echo "Всего назначено: {$total}\n"; +echo "Пройдено: {$completed}\n"; +echo "Провалено: {$failed}\n"; +echo "В процессе: {$inProgress}\n"; +echo "Процент завершения: " . round(($completed / $total) * 100, 1) . "%\n"; +``` + +--- + +### Получение списка на проверку + +```php +// Уроки, ожидающие проверки открытых вопросов +$pending = LessonsPassed::find() + ->where(['status' => LessonsPassed::STATUS_NEED_OPEN_POLL_CHECK]) + ->with(['student', 'lesson']) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +echo "Ожидают проверки: " . count($pending) . " уроков\n\n"; + +foreach ($pending as $passed) { + echo "Сотрудник: {$passed->student->name}\n"; + echo "Урок: {$passed->lesson->name}\n"; + echo "Назначен: {$passed->created_at}\n"; + echo "Ответ: {$passed->open_poll_answer}\n"; + echo "---\n"; +} +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + LessonsPassed ||--o| Lessons : "belongs to (polymorphic)" + LessonsPassed ||--o| LessonsGroup : "belongs to (polymorphic)" + LessonsPassed ||--|| Admin : "belongs to student" + + LessonsPassed { + string entity PK + int entity_id PK + int admin_id PK + datetime created_at + datetime started_at + datetime finished_at + int status + int mistakes + int attempts + string open_poll_answer + string open_poll_answer_comment + } + + Lessons { + int id PK + string name + int group_id FK + string open_poll + } + + LessonsGroup { + int id PK + string name + } + + Admin { + int id PK + string name + string email + } +``` + +--- + +## Диаграмма состояний + +```mermaid +stateDiagram-v2 + [*] --> Attached: Урок назначен + Attached --> Read: Открыл урок + Read --> PassSuccess: Тест пройден (нет откр. вопроса) + Read --> NeedCheck: Тест пройден (есть откр. вопрос) + Read --> PassFail: Исчерпаны попытки + NeedCheck --> PassOpenCheck: Ответ принят + NeedCheck --> FailOpenCheck: Ответ отклонён + PassSuccess --> [*]: Завершение + PassOpenCheck --> [*]: Завершение + PassFail --> [*]: Завершение + FailOpenCheck --> Read: Новая попытка +``` + +--- + +## Бизнес-логика + +### Жизненный цикл прохождения + +1. **Назначение (STATUS_ATTACHED)** — администратор/система назначает урок +2. **Открытие (STATUS_READ)** — сотрудник начал просмотр материалов +3. **Попытки прохождения** — сотрудник проходит тест +4. **Ветвление:** + - **Без открытого вопроса:** STATUS_PASS_SUCCESS или STATUS_PASS_FAIL + - **С открытым вопросом:** STATUS_NEED_OPEN_POLL_CHECK → проверка → STATUS_PASS_OPEN_POLL_CHECK / STATUS_FAIL_OPEN_POLL_CHECK + +### Полиморфная связь + +Поле `entity` определяет тип: +- `'lesson'` — прохождение отдельного урока +- `'lesson_group'` — прохождение группы (курса) целиком + +### Подсчёт ошибок + +- **mistakes** — количество вопросов с неправильным набором ответов +- Не количество неверных ответов, а количество вопросов, где выбрано неверно +- Используется для расчёта процента успешности + +### Временные метки + +- **created_at** — дата назначения (обязательна) +- **started_at** — дата начала прохождения (NULL до старта) +- **finished_at** — дата завершения (NULL до финиша) + +--- + +## Замечания + +1. **Composite PK:** `(entity, entity_id, admin_id)` — один сотрудник не может дважды проходить один урок +2. **Полиморфная связь** через entity/entity_id +3. **status** — центральное поле, определяет все действия UI +4. **mistakes** и **attempts** — накапливаются при каждой попытке +5. **open_poll_answer** — может быть NULL, если нет открытого вопроса +6. **open_poll_answer_comment** — заполняется только проверяющим +7. При STATUS_FAIL_OPEN_POLL_CHECK сотрудник может переделать ответ +8. Финальные успешные статусы: STATUS_PASS_SUCCESS, STATUS_PASS_OPEN_POLL_CHECK +9. Финальный провальный статус: STATUS_PASS_FAIL +10. **started_at** и **finished_at** используются для подсчёта времени прохождения + +--- + +## Связанные документы + +- [Lessons.md](./Lessons.md) — модель уроков +- [LessonsGroup.md](./LessonsGroup.md) — модель групп уроков +- [LessonsPoll.md](./LessonsPoll.md) — вопросы тестов +- [Admin.md](./Admin.md) — модель сотрудников diff --git a/erp24/docs/models/LessonsPoll.md b/erp24/docs/models/LessonsPoll.md new file mode 100644 index 00000000..a8b2e551 --- /dev/null +++ b/erp24/docs/models/LessonsPoll.md @@ -0,0 +1,251 @@ +# Class: LessonsPoll + + +## Mindmap + +```mermaid +mindmap + root((LessonsPoll)) + Таблица БД + lessons_poll + Свойства + id + int + lesson_id + int + name + string + pos + int + type_option + string + date + string + Связи + Answers + 1:N LessonPollAnswers + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель вопроса тестирования для урока в системе ERP24. Каждый урок может содержать несколько вопросов для проверки усвоения материала. Вопрос имеет множество вариантов ответа (LessonPollAnswers), среди которых один или несколько правильных. + +Поддерживает два типа вопросов: с одним правильным ответом (radio) и с множественным выбором (checkbox). Вопросы могут быть упорядочены или перемешаны (зависит от настройки shuffle_polls в уроке). + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`lessons_poll` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `lesson_id` | int | **FK** ID урока из таблицы lessons | +| `name` | text | **Текст вопроса** (вопрос к ученику) | +| `pos` | int | **Позиция в опросе** (порядок сортировки) | +| `type_option` | string | **Тип вопроса:** radio (один ответ) или checkbox (множественный выбор) | +| `date` | datetime | Дата добавления вопроса | +| `admin_id` | int | **FK** ID сотрудника, добавившего вопрос | + +--- + +## Отношения (Relations) + +### getAnswers() + +**Тип:** `hasMany` +**Модель:** `LessonPollAnswers` +**Ключ:** `['poll_id' => 'id']` +**Описание:** Все варианты ответа на вопрос + +**Логика:** +Связь один-ко-многим с моделью LessonPollAnswers. Возвращает все варианты ответа для данного вопроса, включая правильные и неправильные. Используется для отображения вариантов при прохождении теста и проверки ответов сотрудника. + +**Вызовы сторонних методов:** +- `LessonPollAnswers::hasMany()` - построение связи с таблицей lesson_poll_answers + +**Пример:** +```php +$poll = LessonsPoll::findOne($id); +foreach ($poll->answers as $answer) { + echo "- {$answer->name}"; + if ($answer->is_correct) { + echo " ✓"; + } + echo "\n"; +} +``` + +--- + +## Метод getPollHasCorrect() + +**Тип:** `static` +**Параметры:** +- `$lesson_id` (int) — ID урока +- `$lessonsPoll` (array|null) — массив объектов LessonsPoll (если уже загружены) + +**Возвращает:** `array` — массив `[poll_id => bool]` где bool = true если есть правильные ответы + +**Описание:** +Статический метод проверки наличия правильных ответов в вопросах урока. Используется для валидации перед активацией урока — каждый вопрос должен иметь хотя бы один правильный ответ, иначе тест невозможно пройти. + +**Логика работы:** +1. Если $lessonsPoll не передан, загружает все вопросы урока с answers через with() +2. Сортирует вопросы по pos (ASC) +3. Для каждого вопроса проверяет массив answers +4. Ищет хотя бы один ответ с is_correct = 1 +5. Формирует результирующий массив: ключ — ID вопроса, значение — true/false +6. Возвращает массив для всех вопросов урока + +**Вызовы сторонних методов:** +- `LessonsPoll::find()->where()->with('answers')->orderBy()->all()` - загрузка вопросов с ответами +- `foreach` по вопросам и ответам для проверки + +**Пример:** +```php +$lessonId = 5; +$pollsCorrectness = LessonsPoll::getPollHasCorrect($lessonId); + +foreach ($pollsCorrectness as $pollId => $hasCorrect) { + if (!$hasCorrect) { + echo "ОШИБКА: Вопрос #{$pollId} не имеет правильных ответов!\n"; + } +} + +// Или с уже загруженными вопросами +$polls = LessonsPoll::find()->where(['lesson_id' => $lessonId])->with('answers')->all(); +$correctness = LessonsPoll::getPollHasCorrect($lessonId, $polls); +``` + +--- + +## Правила валидации + +```php +['lesson_id', 'name', 'pos', 'date', 'admin_id'] // required +['lesson_id', 'pos', 'admin_id'] // integer +['name', 'type_option'] // string (text) +['date'] // safe (datetime) +``` + +--- + +## Примеры использования + +### Создание вопроса с ответами + +```php +use yii_app\records\LessonsPoll; +use yii_app\records\LessonPollAnswers; + +// Создание вопроса +$poll = new LessonsPoll(); +$poll->lesson_id = $lessonId; +$poll->name = 'Какой процент бонусов начисляется при покупке?'; +$poll->pos = 1; +$poll->type_option = 'radio'; // Один правильный ответ +$poll->date = date('Y-m-d H:i:s'); +$poll->admin_id = Yii::$app->user->id; +$poll->save(); + +// Добавление вариантов ответа +$answers = [ + ['name' => '5%', 'is_correct' => 0], + ['name' => '10%', 'is_correct' => 1], // Правильный ответ + ['name' => '15%', 'is_correct' => 0], + ['name' => '20%', 'is_correct' => 0], +]; + +foreach ($answers as $index => $answerData) { + $answer = new LessonPollAnswers(); + $answer->poll_id = $poll->id; + $answer->name = $answerData['name']; + $answer->is_correct = $answerData['is_correct']; + $answer->pos = $index + 1; + $answer->save(); +} +``` + +--- + +### Получение вопросов урока + +```php +$lessonId = 5; + +$polls = LessonsPoll::find() + ->where(['lesson_id' => $lessonId]) + ->with('answers') + ->orderBy(['pos' => SORT_ASC]) + ->all(); + +foreach ($polls as $poll) { + echo "{$poll->pos}. {$poll->name}\n"; + echo "Тип: " . ($poll->type_option == 'radio' ? 'Один ответ' : 'Множественный выбор') . "\n"; + + foreach ($poll->answers as $answer) { + echo " [{$answer->pos}] {$answer->name}\n"; + } + echo "\n"; +} +``` + +--- + +### Проверка ответов сотрудника + +```php +$pollId = 10; +$selectedAnswerIds = [1, 3]; // ID выбранных ответов + +$poll = LessonsPoll::findOne($pollId); +$correctAnswerIds = []; + +foreach ($poll->answers as $answer) { + if ($answer->is_correct) { + $correctAnswerIds[] = $answer->id; + } +} + +// Сравнение массивов +sort($selectedAnswerIds); +sort($correctAnswerIds); + +$isCorrect = ($selectedAnswerIds === $correctAnswerIds); + +if ($isCorrect) { + echo "Ответ верный!\n"; +} else { + echo "Ответ неверный.\n"; + echo "Правильные ответы: " . implode(', ', $correctAnswerIds) . "\n"; +} +``` + +--- + +## Связанные документы + +- [Lessons.md](./Lessons.md) — модель уроков +- [LessonPollAnswers.md](./LessonPollAnswers.md) — варианты ответов +- [LessonsPassed.md](./LessonsPassed.md) — прохождение уроков +- [Admin.md](./Admin.md) — модель сотрудников diff --git a/erp24/docs/models/MODEL_INVENTORY.md b/erp24/docs/models/MODEL_INVENTORY.md index 0212832d..5bd81812 100644 --- a/erp24/docs/models/MODEL_INVENTORY.md +++ b/erp24/docs/models/MODEL_INVENTORY.md @@ -1,5 +1,32 @@ # Инвентаризация моделей ERP24 +## Mindmap + +```mermaid +mindmap + root((Модели ERP24)) + Статистика + 389 моделей + 18 категорий + Ключевые домены + HR 35 моделей + Admin + AdminGroup + Продажи 15 моделей + Sales + SalesProducts + Товары 25 моделей + Products1c + Balances + Клиенты 15 моделей + Users + UsersBonus + Приоритеты + Критические 15 + Высокие 30 + Средние 100 +``` + ## Обзор **Всего моделей ActiveRecord:** 389 @@ -13,8 +40,8 @@ ### 1. Сотрудники и HR (35 моделей) #### Основные модели сотрудников -- **Admin** — основная модель сотрудника -- **AdminGroup** — группы сотрудников (должности) +- **[Admin](./Admin.md)** — основная модель сотрудника ✅ +- **[AdminGroup](./AdminGroup.md)** — группы сотрудников (должности) ✅ - **AdminStores** — привязка сотрудников к магазинам - **AdminCheckin** — отметки о входе/выходе - **AdminDevice** — устройства сотрудников @@ -69,8 +96,8 @@ ### 2. Продажи и чеки (15 моделей) #### Продажи -- **Sales** — чеки продаж (GUID, дата, сумма, скидка, оплата) -- **SalesProducts** — товары в чеках +- **[Sales](./SalesProducts.md)** — чеки продаж (GUID, дата, сумма, скидка, оплата) ✅ +- **[SalesProducts](./SalesProducts.md)** — товары в чеках ✅ - **SalesItems** — позиции продаж - **SalesHistory** — история продаж - **SalesProductsHistory** — история товаров в продажах @@ -97,7 +124,7 @@ ### 3. Товары и номенклатура (25 моделей) #### Товары 1С -- **Products1c** — номенклатура из 1С (GUID, название, артикул) +- **[Products1c](./Products1c.md)** — номенклатура из 1С (GUID, название, артикул) ✅ - **Products1cNomenclature** — номенклатура товаров - **Products1cNomenclatureActuality** — актуальность номенклатуры - **Products1cOptions** — опции товаров @@ -105,7 +132,7 @@ - **Products1cAdditionalCharacteristics** — дополнительные характеристики #### Классификация и свойства -- **ProductsClass** — классы товаров +- **[ProductsClass](./ProductsClass.md)** — классы товаров ✅ - **ProductsCatProperty** — свойства категорий - **ProductsPropertyValue** — значения свойств товаров - **ProductsVarieties** — разновидности товаров @@ -115,13 +142,13 @@ - **Product1cReplacementLog** — логи замен товаров #### Остатки и балансы -- **Balances** — остатки товаров +- **[Balances](./Balances.md)** — остатки товаров ✅ - **StoreBalance** — балансы магазинов - **StoreProductsFact** — фактические остатки магазинов - **ShiftRemains** — остатки на смену #### Цены -- **Prices** — цены товаров +- **[Prices](./Prices.md)** — цены товаров ✅ - **PricesDynamic** — динамические цены - **PricesZakup** — закупочные цены - **PricesRegion** — региональные цены @@ -142,7 +169,7 @@ ### 4. Магазины и локации (15 моделей) #### Магазины -- **CityStore** — магазины (ID, название, адрес, GPS) +- **[CityStore](./CityStore.md)** — магазины (ID, название, адрес, GPS) ✅ - **StoreCityList** — список городов магазинов - **StoreType** — типы магазинов - **StoresTypeList** — список типов магазинов @@ -168,7 +195,7 @@ ### 5. Заказы и маркетплейсы (25 моделей) #### Заказы магазинов -- **StoreOrders** — заказы магазинов +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов ✅ - **StoreOrdersItem** — позиции заказов - **StoreOrdersFields** — поля заказов - **StoreOrdersFieldsData** — данные полей заказов @@ -182,12 +209,12 @@ - **OrderStoreSort** — сортировка заказов магазинов #### AMO CRM заказы -- **OrdersAmo** — заказы из AmoCRM +- **[OrdersAmo](./OrdersAmo.md)** — заказы из AmoCRM ✅ - **OrdersStatus** — статусы заказов - **OrdersUnion** — объединение заказов #### Маркетплейсы -- **MarketplaceOrders** — заказы маркетплейсов +- **[MarketplaceOrders](./MarketplaceOrders.md)** — заказы маркетплейсов ✅ - **MarketplaceOrderItems** — товары в заказах маркетплейсов - **MarketplaceOrderDelivery** — доставка заказов маркетплейсов - **MarketplaceStatus** — статусы маркетплейсов @@ -206,20 +233,20 @@ ### 6. Клиенты и бонусная система (15 моделей) #### Клиенты -- **Users** — клиенты (телефон, имя, баланс, покупки) +- **[Users](./Users.md)** — клиенты (телефон, имя, баланс, покупки) ✅ - **UsersEvents** — события клиентов - **UsersPhones** — телефоны клиентов - **UsersStopList** — стоп-лист клиентов #### Бонусы -- **UsersBonus** — бонусы клиентов +- **[UsersBonus](./UsersBonus.md)** — бонусы клиентов ✅ - **BonusLevels** — уровни бонусов - **UsersBonusLevels** — уровни бонусов клиентов - **UserBonusSendToTgLogs** — логи отправки бонусов в Telegram - **AdminBonusConversion** — конверсия бонусов администраторов #### Telegram -- **UsersTelegram** — Telegram клиентов +- **[UsersTelegram](./UsersTelegram.md)** — Telegram клиентов ✅ - **UsersTelegramMessage** — сообщения Telegram - **UsersTelegramLog** — логи Telegram - **TgSubscription** — подписки Telegram @@ -243,7 +270,7 @@ ### 7. Задачи и задания (15 моделей) #### Задачи -- **Task** — задачи (название, описание, статус, дедлайн) +- **[Task](./Task.md)** — задачи (название, описание, статус, дедлайн) ✅ - **TaskUsers** — исполнители задач - **TaskViewers** — наблюдатели задач - **TaskLogs** — логи задач @@ -270,7 +297,7 @@ ### 8. Матрица и ассортимент (15 моделей) #### Матрица товаров -- **MatrixErp** — матрица ERP +- **[MatrixErp](./MatrixErp.md)** — матрица ERP ✅ - **MatrixErpProperty** — свойства матрицы - **MatrixErpPropertyDynamic** — динамические свойства матрицы - **MatrixErpMedia** — медиа матрицы @@ -286,14 +313,14 @@ ### 9. График работы и смены (20 моделей) #### График работы -- **Timetable** — расписание +- **[Timetable](./Timetable.md)** — расписание ✅ - **TimetableV3** — расписание v3 - **TimetablePlan** — план расписания - **TimetablePlanV3** — план расписания v3 - **TimetableFact** — факт расписания - **TimetableFactV3** — факт расписания v3 - **TimetableFactModel** — модель факта расписания -- **TimetableShift** — смены расписания +- **[TimetableShift](./TimetableShift.md)** — смены расписания ✅ - **TimetableWorkbot** — бот расписания #### Смены и передачи @@ -307,7 +334,7 @@ #### Списания - **WriteOffs** — списания - **WriteOffsProducts** — товары списаний -- **WriteOffsErp** — списания ERP +- **[WriteOffsErp](./WriteOffsErp.md)** — списания ERP ✅ - **WriteOffsProductsErp** — товары списаний ERP - **WriteOffsErpCauseDict** — справочник причин списаний @@ -423,7 +450,7 @@ - **Holiday** — праздники #### Кластеры -- **Cluster** — кластеры +- **[Cluster](./Cluster.md)** — кластеры ✅ - **ClusterAdmin** — администраторы кластеров - **ClusterCalendar** — календарь кластеров - **ClusterCalendarCategoryDict** — категории календаря кластеров diff --git a/erp24/docs/models/MarketplaceFlowwowEmails.md b/erp24/docs/models/MarketplaceFlowwowEmails.md new file mode 100644 index 00000000..32a8019e --- /dev/null +++ b/erp24/docs/models/MarketplaceFlowwowEmails.md @@ -0,0 +1,194 @@ +# Класс: MarketplaceFlowwowEmails + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceFlowwowEmails)) + Таблица БД + marketplace_flowwow_emails + Свойства + id + int + subject + string + from + string + to + string + date + string + body + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель для хранения и обработки email-писем от маркетплейса Flowwow в ERP24. Используется для автоматического парсинга уведомлений о заказах, поступающих на email, и их последующей обработки в системе. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_flowwow_emails` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `subject` | varchar(255) | Тема письма | +| `email_status` | int / null | Статус обработки (0=не обработано, 1=разобрано, 2=не разобрано) | +| `from` | varchar(255) | Email отправителя | +| `to` | varchar(255) | Email получателя | +| `date` | datetime | Дата письма | +| `body` | text | Тело письма (HTML/text) | +| `created_at` | datetime / null | Дата создания записи в БД | + +## Статусы обработки (email_status) + +| Значение | Описание | +|----------|----------| +| `0` | Не обработано (по умолчанию) | +| `1` | Успешно разобрано (создан заказ) | +| `2` | Не удалось разобрать | + +## Диаграмма потока обработки + +```mermaid +flowchart TD + A[Email от Flowwow] --> B[IMAP/POP3 сервер] + B --> C[Cron задача] + C --> D[Сохранение в БД] + D --> E{email_status} + E -->|0| F[Парсинг письма] + F --> G{Успешно?} + G -->|Да| H[Создание заказа] + H --> I[email_status = 1] + G -->|Нет| J[email_status = 2] + J --> K[Ручная обработка] +``` + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceFlowwowEmails { + int id PK + varchar subject + int email_status + varchar from + varchar to + datetime date + text body + datetime created_at + } + + MarketplaceOrders { + int id PK + varchar source + varchar external_id + } + + MarketplaceFlowwowEmails ||--o| MarketplaceOrders : "парсинг создаёт" +``` + +## Примеры использования + +### Сохранение нового письма +```php +$email = new MarketplaceFlowwowEmails(); +$email->subject = 'Новый заказ #12345 от клиента'; +$email->from = 'noreply@flowwow.com'; +$email->to = 'orders@company.ru'; +$email->date = '2024-01-15 14:30:00'; +$email->body = '...

    Детали заказа...

    ...'; +$email->email_status = 0; +$email->created_at = date('Y-m-d H:i:s'); +$email->save(); +``` + +### Получение необработанных писем +```php +$unprocessedEmails = MarketplaceFlowwowEmails::find() + ->where(['email_status' => 0]) + ->orderBy(['date' => SORT_ASC]) + ->all(); +``` + +### Обработка письма +```php +$email = MarketplaceFlowwowEmails::findOne($id); +try { + $orderData = $this->parseFlowwowEmail($email->body); + $this->createMarketplaceOrder($orderData); + + $email->email_status = 1; // Успешно + $email->save(); +} catch (\Exception $e) { + $email->email_status = 2; // Ошибка + $email->save(); + + Yii::error("Failed to parse Flowwow email #{$email->id}: " . $e->getMessage()); +} +``` + +### Получение статистики обработки +```php +$stats = MarketplaceFlowwowEmails::find() + ->select(['email_status', 'COUNT(*) as count']) + ->groupBy('email_status') + ->asArray() + ->all(); +``` + +### Поиск писем за период +```php +$emails = MarketplaceFlowwowEmails::find() + ->where(['>=', 'date', '2024-01-01']) + ->andWhere(['<=', 'date', '2024-01-31']) + ->orderBy(['date' => SORT_DESC]) + ->all(); +``` + +### Повторная обработка ошибочных +```php +$failedEmails = MarketplaceFlowwowEmails::find() + ->where(['email_status' => 2]) + ->all(); + +foreach ($failedEmails as $email) { + // Сброс статуса для повторной обработки + $email->email_status = 0; + $email->save(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `subject` | required, string (max 255) | +| `from` | required, string (max 255) | +| `to` | required, string (max 255) | +| `date` | required, safe | +| `body` | required, string | +| `email_status` | integer, default: 0 | +| `created_at` | safe | + +## Связанные модели + +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейсов (создаются из писем) + +## Особенности реализации + +1. **Email интеграция**: Письма получаются через IMAP/POP3 cron-задачей +2. **Парсинг HTML**: Тело письма содержит HTML, требующий парсинга +3. **Отложенная обработка**: Письма сначала сохраняются, затем обрабатываются +4. **Отслеживание статуса**: Три статуса для контроля процесса обработки +5. **История переписки**: Все письма сохраняются для аудита и отладки diff --git a/erp24/docs/models/MarketplaceFlowwowEmailsSearch.md b/erp24/docs/models/MarketplaceFlowwowEmailsSearch.md new file mode 100644 index 00000000..558ebc69 --- /dev/null +++ b/erp24/docs/models/MarketplaceFlowwowEmailsSearch.md @@ -0,0 +1,149 @@ +# Класс: MarketplaceFlowwowEmailsSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceFlowwowEmailsSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceFlowwowEmails +``` + +## Назначение +Search-модель для поиска и фильтрации email-писем от маркетплейса Flowwow в ERP24. Обеспечивает поиск по теме, отправителю, получателю, телу письма и статусу с поддержкой кастомного formName. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceFlowwowEmails` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `email_status` — integer +- `subject`, `from`, `to`, `date`, `body`, `created_at`, `email_status` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы для load() + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MarketplaceFlowwowEmails::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с возможным кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, date, created_at, email_status + - ilike: subject, from, to, body + +## Диаграмма интеграции + +```mermaid +flowchart TD + A[Flowwow] -->|Email| B[Входящее письмо] + B --> C[MarketplaceFlowwowEmails] + C --> D[subject, from, to, body] + + E[MarketplaceFlowwowEmailsSearch] --> F[ilike по subject] + F --> G[Фильтр по email_status] + G --> H[Результаты поиска] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceFlowwowEmailsSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск с кастомным formName +```php +$searchModel = new MarketplaceFlowwowEmailsSearch(); +$dataProvider = $searchModel->search($params, 'FlowwowEmail'); +``` + +### Поиск по теме +```php +$searchModel = new MarketplaceFlowwowEmailsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceFlowwowEmailsSearch' => [ + 'subject' => 'Новый заказ', + ] +]); +``` + +### Поиск по статусу +```php +$searchModel = new MarketplaceFlowwowEmailsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceFlowwowEmailsSearch' => [ + 'email_status' => 1, // Обработан + ] +]); +``` + +### Поиск по отправителю +```php +$searchModel = new MarketplaceFlowwowEmailsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceFlowwowEmailsSearch' => [ + 'from' => 'noreply@flowwow.com', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'date', + 'subject', + 'from', + 'to', + 'email_status', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceFlowwowEmails](./MarketplaceFlowwowEmails.md) — базовая модель писем +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейса + +## Особенности реализации + +1. **Кастомный formName**: Параметр $formName в методе search() +2. **ilike поиск**: Регистронезависимый поиск для PostgreSQL +3. **Email-специфика**: Поля subject, from, to, body +4. **Status tracking**: Фильтрация по email_status +5. **Flowwow интеграция**: Специфично для маркетплейса Flowwow diff --git a/erp24/docs/models/MarketplaceOrder1cStatuses.md b/erp24/docs/models/MarketplaceOrder1cStatuses.md new file mode 100644 index 00000000..a98b78ce --- /dev/null +++ b/erp24/docs/models/MarketplaceOrder1cStatuses.md @@ -0,0 +1,262 @@ +# Класс: MarketplaceOrder1cStatuses + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrder1cStatuses)) + Таблица БД + marketplace_order_1c_statuses + Свойства + id + int + marketplace_id + int + allowed_reserve + int + allowed_closing + int + cancelled_order + int + successful_order + int + Связи + RelationsFrom + 1:N MarketplaceOrder1cStatusesRelations + RelationsTo + 1:N MarketplaceOrder1cStatusesRelations + NextStatuses + 1:N MarketplaceOrder1cStatuses + PrevStatuses + 1:N MarketplaceOrder1cStatuses + OrderStatus + 1:1 MarketplaceOrderStatusTypes + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник статусов заказов маркетплейсов для синхронизации с 1С в ERP24. Определяет машину состояний заказа с настройками разрешений на операции (резервирование, закрытие, редактирование) и инструкциями для пользователей. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_order_1c_statuses` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы маркетплейсов + +| Константа | Значение (GUID) | Маркетплейс | +|-----------|-----------------|-------------| +| `GUID_FLOWWOW` | `08202503-2554-0637-52ce-100057714437` | Flowwow | +| `GUID_YANDEXMARKET` | `08202503-2554-0637-05c5-100007297950` | Яндекс.Маркет | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `marketplace_id` | int | ID маркетплейса (1=Flowwow, 2=Яндекс.Маркет) | +| `status_id` | varchar(100) | Артикул/код статуса | +| `status` | varchar(100) | Название статуса | +| `status_instruction` | text | Инструкция к статусу для пользователя | +| `posit` | int | Порядок отображения статуса (default: 0) | +| `allowed_reserve` | int | Разрешено резервирование (0/1, default: 0) | +| `allowed_closing` | int | Разрешено закрытие заказа (0/1, default: 0) | +| `allowed_editing` | int | Разрешено редактирование (0/1) | +| `cancelled_order` | int | Флаг отменённого заказа (0/1) | +| `successful_order` | int | Флаг успешно завершённого заказа (0/1) | +| `order_status_id` | int / null | FK на основной статус в МП | +| `order_substatus_id` | int / null | FK на субстатус в МП | + +## Методы + +### guid2id() +**Описание:** Возвращает маппинг GUID маркетплейса → числовой ID. + +**Возвращает:** `array` — ассоциативный массив [GUID => ID] + +```php +public static function guid2id(): array +``` + +### id2guid() +**Описание:** Возвращает маппинг числового ID маркетплейса → GUID. + +**Возвращает:** `array` — ассоциативный массив [ID => GUID] + +```php +public static function id2guid(): array +``` + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getRelationsFrom()` | hasMany | MarketplaceOrder1cStatusesRelations | Переходы ИЗ данного статуса | +| `getRelationsTo()` | hasMany | MarketplaceOrder1cStatusesRelations | Переходы В данный статус | +| `getNextStatuses()` | hasMany via | MarketplaceOrder1cStatuses | Следующие возможные статусы | +| `getPrevStatuses()` | hasMany via | MarketplaceOrder1cStatuses | Предыдущие статусы | +| `getOrderStatus()` | hasOne | MarketplaceOrderStatusTypes | Основной статус заказа МП | +| `getOrderSubstatus()` | hasOne | MarketplaceOrderStatusTypes | Субстатус заказа МП | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrder1cStatuses { + int id PK + int marketplace_id + varchar status_id + varchar status + text status_instruction + int posit + int allowed_reserve + int allowed_closing + int allowed_editing + int cancelled_order + int successful_order + int order_status_id FK + int order_substatus_id FK + } + + MarketplaceOrder1cStatusesRelations { + int id PK + int status_id_from FK + int status_id_to FK + varchar button_text + int order + } + + MarketplaceOrderStatusTypes { + int id PK + varchar code + varchar name + } + + MarketplaceOrder1cStatuses ||--o{ MarketplaceOrder1cStatusesRelations : "status_id_from" + MarketplaceOrder1cStatuses ||--o{ MarketplaceOrder1cStatusesRelations : "status_id_to" + MarketplaceOrder1cStatuses }o--|| MarketplaceOrderStatusTypes : "order_status_id" + MarketplaceOrder1cStatuses }o--|| MarketplaceOrderStatusTypes : "order_substatus_id" +``` + +## Диаграмма машины состояний + +```mermaid +flowchart TD + subgraph Flowwow + F1[Новый] --> F2[В обработке] + F2 --> F3[Готов к отправке] + F3 --> F4[Доставлен] + F2 --> F5[Отменён] + end + + subgraph YandexMarket + Y1[Новый] --> Y2[В сборке] + Y2 --> Y3[Готов к выдаче] + Y3 --> Y4[Получен] + Y1 --> Y5[Отменён] + Y2 --> Y5 + end +``` + +## Примеры использования + +### Получение статусов для маркетплейса +```php +$flowwowStatuses = MarketplaceOrder1cStatuses::find() + ->where(['marketplace_id' => 1]) // Flowwow + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Получение следующих возможных статусов +```php +$currentStatus = MarketplaceOrder1cStatuses::findOne($currentStatusId); +$nextStatuses = $currentStatus->nextStatuses; + +foreach ($nextStatuses as $nextStatus) { + echo "Можно перейти в: {$nextStatus->status}\n"; +} +``` + +### Проверка разрешений на операции +```php +$status = MarketplaceOrder1cStatuses::findOne($statusId); + +if ($status->allowed_reserve) { + // Можно резервировать товар +} + +if ($status->allowed_closing) { + // Можно закрыть заказ +} + +if ($status->allowed_editing) { + // Можно редактировать заказ +} +``` + +### Получение ID маркетплейса по GUID +```php +$guid = '08202503-2554-0637-52ce-100057714437'; +$marketplaceId = MarketplaceOrder1cStatuses::guid2id()[$guid] ?? null; +// $marketplaceId = 1 (Flowwow) +``` + +### Проверка финального статуса +```php +$status = MarketplaceOrder1cStatuses::findOne($statusId); + +if ($status->successful_order) { + // Заказ успешно завершён +} elseif ($status->cancelled_order) { + // Заказ отменён +} +``` + +### Формирование списка для UI +```php +$statuses = MarketplaceOrder1cStatuses::find() + ->where(['marketplace_id' => $marketplaceId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statuses as $status) { + echo ""; + if ($status->status_instruction) { + echo "{$status->status_instruction}"; + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `marketplace_id` | required, integer | +| `status_id` | required, string (max 100) | +| `status` | required, string (max 100) | +| `status_instruction` | required, string | +| `posit` | integer, default: 0 | +| `allowed_reserve` | integer, default: 0 | +| `allowed_closing` | integer, default: 0 | + +## Связанные модели + +- [MarketplaceOrder1cStatusesRelations](./MarketplaceOrder1cStatusesRelations.md) — переходы между статусами +- [MarketplaceOrderStatusTypes](./MarketplaceOrderStatusTypes.md) — типы статусов маркетплейсов +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейсов + +## Особенности реализации + +1. **Машина состояний**: Статусы связаны через таблицу relations для определения допустимых переходов +2. **Двусторонний маппинг**: guid2id() и id2guid() для конвертации идентификаторов маркетплейсов +3. **Настраиваемые разрешения**: Три флага (reserve/closing/editing) для гибкого управления операциями +4. **Финальные статусы**: Флаги successful_order и cancelled_order для определения завершения workflow +5. **Инструкции**: Поле status_instruction содержит подсказки для операторов +6. **Интеграция с 1С**: Артикул status_id используется для синхронизации с 1С diff --git a/erp24/docs/models/MarketplaceOrder1cStatusesRelations.md b/erp24/docs/models/MarketplaceOrder1cStatusesRelations.md new file mode 100644 index 00000000..131058b7 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrder1cStatusesRelations.md @@ -0,0 +1,216 @@ +# Класс: MarketplaceOrder1cStatusesRelations + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrder1cStatusesRelations)) + Таблица БД + marketplace_order_1c_statuses_relations + Свойства + id + int + order + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель связей (переходов) между статусами заказов маркетплейсов в ERP24. Определяет граф допустимых переходов между статусами с текстами кнопок и порядком отображения для реализации машины состояний заказа. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_order_1c_statuses_relations` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `status_id_from` | int / null | FK на исходный статус (откуда переход) | +| `status_id_to` | int / null | FK на целевой статус (куда переход) | +| `description` | text / null | Описание перехода для 1С | +| `button_text` | varchar(255) / null | Текст кнопки в UI | +| `order` | int | Порядок отображения перехода | +| `created_at` | datetime / null | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrder1cStatuses { + int id PK + varchar status + int marketplace_id + } + + MarketplaceOrder1cStatusesRelations { + int id PK + int status_id_from FK + int status_id_to FK + text description + varchar button_text + int order + datetime created_at + datetime updated_at + } + + MarketplaceOrder1cStatuses ||--o{ MarketplaceOrder1cStatusesRelations : "status_id_from" + MarketplaceOrder1cStatuses ||--o{ MarketplaceOrder1cStatusesRelations : "status_id_to" +``` + +## Диаграмма графа переходов + +```mermaid +flowchart LR + subgraph "Машина состояний заказа" + NEW[Новый] -->|Взять в работу| PROCESSING[В обработке] + PROCESSING -->|Собрать| READY[Готов] + PROCESSING -->|Отменить| CANCELLED[Отменён] + READY -->|Доставить| DELIVERED[Доставлен] + READY -->|Отменить| CANCELLED + end +``` + +## Методы + +### beforeSave($insert) +**Описание:** Автоматически вычисляет порядок перехода (order) для новых записей. + +**Логика работы:** +1. Если `status_id_from` указан, но `order` не задан +2. Находит максимальный order для данного status_id_from +3. Устанавливает order = max + 1 + +**Параметры:** +- `$insert` (bool) — true при создании новой записи + +**Возвращает:** `bool` — результат валидации + +```php +public function beforeSave($insert): bool +``` + +## Примеры использования + +### Создание перехода между статусами +```php +$relation = new MarketplaceOrder1cStatusesRelations(); +$relation->status_id_from = 1; // Новый +$relation->status_id_to = 2; // В обработке +$relation->button_text = 'Взять в работу'; +$relation->description = 'Переход заказа в обработку'; +// order вычислится автоматически в beforeSave +$relation->save(); +``` + +### Получение всех переходов из статуса +```php +$transitions = MarketplaceOrder1cStatusesRelations::find() + ->where(['status_id_from' => $currentStatusId]) + ->orderBy(['order' => SORT_ASC]) + ->all(); + +foreach ($transitions as $t) { + echo "\n"; +} +``` + +### Проверка допустимости перехода +```php +$isAllowed = MarketplaceOrder1cStatusesRelations::find() + ->where([ + 'status_id_from' => $currentStatusId, + 'status_id_to' => $targetStatusId + ]) + ->exists(); + +if (!$isAllowed) { + throw new \Exception('Переход в данный статус недопустим'); +} +``` + +### Получение кнопок действий для UI +```php +$actions = MarketplaceOrder1cStatusesRelations::find() + ->where(['status_id_from' => $order->status_id]) + ->orderBy(['order' => SORT_ASC]) + ->all(); + +foreach ($actions as $action) { + echo Html::button($action->button_text, [ + 'data-target-status' => $action->status_id_to, + 'class' => 'btn btn-primary' + ]); +} +``` + +### Построение графа переходов +```php +$allRelations = MarketplaceOrder1cStatusesRelations::find() + ->with(['statusFrom', 'statusTo']) + ->all(); + +$graph = []; +foreach ($allRelations as $rel) { + $graph[$rel->status_id_from][] = [ + 'to' => $rel->status_id_to, + 'button' => $rel->button_text, + 'order' => $rel->order, + ]; +} +``` + +### Изменение порядка переходов +```php +$relations = MarketplaceOrder1cStatusesRelations::find() + ->where(['status_id_from' => $statusId]) + ->orderBy(['order' => SORT_ASC]) + ->all(); + +$newOrder = 1; +foreach ($reorderedIds as $id) { + $rel = MarketplaceOrder1cStatusesRelations::findOne($id); + $rel->order = $newOrder++; + $rel->save(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `status_id_from` | integer | +| `status_id_to` | integer | +| `description` | string | +| `button_text` | string (max 255) | +| `order` | integer | +| `created_at`, `updated_at` | safe | + +### Уникальные ограничения + +| Поля | Описание | +|------|----------| +| `[status_id_from, status_id_to]` | Только один переход между двумя статусами | +| `[status_id_from, order]` | Уникальный порядок в рамках одного исходного статуса | + +## Связанные модели + +- [MarketplaceOrder1cStatuses](./MarketplaceOrder1cStatuses.md) — статусы заказов (связь через status_id_from и status_id_to) + +## Особенности реализации + +1. **Граф переходов**: Таблица реализует ориентированный граф допустимых переходов между статусами +2. **Автоинкремент order**: При создании записи order вычисляется автоматически как max+1 +3. **Составные уникальные ключи**: Предотвращают дублирование переходов и порядков +4. **UI интеграция**: Поле button_text хранит текст кнопки для отображения в интерфейсе +5. **1С интеграция**: Поле description содержит описание для синхронизации с 1С +6. **Временные метки**: created_at и updated_at для аудита изменений diff --git a/erp24/docs/models/MarketplaceOrderDelivery.md b/erp24/docs/models/MarketplaceOrderDelivery.md new file mode 100644 index 00000000..6a52bc2b --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderDelivery.md @@ -0,0 +1,565 @@ +# Модель MarketplaceOrderDelivery + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderDelivery)) + Таблица БД + marketplace_order_delivery + Свойства + id + int + order_id + int + type + string + service_name + string + partner_type + string + country + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MarketplaceOrderDelivery` хранит информацию о доставке заказов маркетплейсов (Яндекс.Маркет, Flowwow). Содержит детальные данные об адресе доставки, временных интервалах, службе доставки, информации о курьере и геокоординатах. Используется для планирования логистики и контроля доставки. + +**Файл модели:** `erp24/records/MarketplaceOrderDelivery.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `marketplace_order_delivery` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | ID записи (первичный ключ) | +| `order_id` | INTEGER | Ссылка на заказ из marketplace_orders (обязательное) | + +### Тип и служба доставки + +| Поле | Тип | Описание | +|------|-----|----------| +| `type` | VARCHAR(32) | Тип доставки: DELIVERY, PICKUP (обязательное) | +| `service_name` | VARCHAR(64) | Название службы доставки (обязательное) | +| `partner_type` | VARCHAR(32) | Тип партнера доставки: SHOP, YANDEX_MARKET (обязательное) | + +### Адрес доставки + +| Поле | Тип | Описание | +|------|-----|----------| +| `country` | VARCHAR(64) | Страна (обязательное) | +| `city` | VARCHAR(128) | Город (обязательное) | +| `street` | VARCHAR(255) | Улица (обязательное) | +| `house` | VARCHAR(16) | Дом (обязательное) | +| `apartment` | VARCHAR(16) | Квартира | +| `postcode` | VARCHAR(16) | Индекс | + +### Геолокация + +| Поле | Тип | Описание | +|------|-----|----------| +| `latitude` | FLOAT | Широта (обязательное) | +| `longitude` | FLOAT | Долгота (обязательное) | + +### Временной интервал доставки + +| Поле | Тип | Описание | +|------|-----|----------| +| `delivery_start` | TIMESTAMP | Начало периода доставки | +| `delivery_end` | TIMESTAMP | Конец периода доставки | + +### Информация о курьере + +| Поле | Тип | Описание | +|------|-----|----------| +| `courier_full_name` | VARCHAR(255) | ФИО курьера | +| `courier_phone` | VARCHAR(32) | Телефон курьера | +| `courier_extension` | VARCHAR(16) | Дополнительный номер курьера | +| `courier_vehicle_number` | VARCHAR(32) | Номер автомобиля курьера | +| `courier_vehicle_description` | VARCHAR(255) | Описание автомобиля курьера | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'order_id', 'type', 'service_name', 'partner_type', + 'country', 'city', 'street', 'house', + 'latitude', 'longitude' +], 'required' +``` + +### Значения по умолчанию + +```php +[ + 'postcode', 'apartment', 'delivery_start', 'delivery_end', + 'courier_full_name', 'courier_phone', 'courier_extension', + 'courier_vehicle_number', 'courier_vehicle_description' +], 'default', 'value' => null +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `order_id` | Целочисленное значение с поддержкой NULL | +| `number` | `latitude`, `longitude` | Числа с плавающей точкой | +| `safe` | `delivery_start`, `delivery_end` | Дата/время без валидации | +| `string`, max=32 | `type`, `partner_type`, `courier_phone`, `courier_vehicle_number` | Короткие строки | +| `string`, max=64 | `service_name`, `country` | Средние строки | +| `string`, max=16 | `postcode`, `house`, `apartment`, `courier_extension` | Очень короткие строки | +| `string`, max=128 | `city` | Город | +| `string`, max=255 | `street`, `courier_full_name`, `courier_vehicle_description` | Длинные строки | + +--- + +## Типы доставки (type) + +| Значение | Описание | +|----------|----------| +| `DELIVERY` | Доставка курьером на адрес | +| `PICKUP` | Самовывоз из пункта выдачи | + +--- + +## Типы партнеров доставки (partner_type) + +| Значение | Описание | +|----------|----------| +| `SHOP` | Собственная служба доставки магазина | +| `YANDEX_MARKET` | Служба доставки Яндекс.Маркета | +| `COURIER_SERVICE` | Сторонняя курьерская служба | + +--- + +## Примеры служб доставки (service_name) + +- **Собственная доставка** - доставка магазином +- **Яндекс.Доставка** - служба Яндекса +- **СДЭК** - СДЭК +- **Boxberry** - Boxberry +- **DPD** - DPD +- **IML** - IML +- **PickPoint** - PickPoint + +--- + +## Форматирование адреса + +### Полный адрес одной строкой + +```php +function getFullAddress($delivery) { + $parts = [ + $delivery->country, + $delivery->city, + $delivery->street, + 'д. ' . $delivery->house + ]; + + if (!empty($delivery->apartment)) { + $parts[] = 'кв. ' . $delivery->apartment; + } + + if (!empty($delivery->postcode)) { + $parts[] = $delivery->postcode; + } + + return implode(', ', array_filter($parts)); +} +``` + +### Короткий адрес + +```php +function getShortAddress($delivery) { + $address = "{$delivery->city}, {$delivery->street}, {$delivery->house}"; + + if (!empty($delivery->apartment)) { + $address .= ", кв. {$delivery->apartment}"; + } + + return $address; +} +``` + +--- + +## Примеры использования + +### Получение данных доставки для заказа + +```php +$delivery = MarketplaceOrderDelivery::findOne(['order_id' => $orderId]); + +if ($delivery) { + echo "Адрес: {$delivery->city}, {$delivery->street}, {$delivery->house}\n"; + echo "Доставка: {$delivery->delivery_start} - {$delivery->delivery_end}\n"; + echo "Служба доставки: {$delivery->service_name}\n"; +} +``` + +### Создание записи доставки при синхронизации + +```php +$delivery = new MarketplaceOrderDelivery(); +$delivery->order_id = $orderId; +$delivery->type = 'DELIVERY'; +$delivery->service_name = $apiData['serviceName']; +$delivery->partner_type = $apiData['partnerType']; +$delivery->country = $apiData['address']['country']; +$delivery->postcode = $apiData['address']['postcode'] ?? null; +$delivery->city = $apiData['address']['city']; +$delivery->street = $apiData['address']['street']; +$delivery->house = $apiData['address']['house']; +$delivery->apartment = $apiData['address']['apartment'] ?? null; +$delivery->latitude = $apiData['address']['gps']['latitude']; +$delivery->longitude = $apiData['address']['gps']['longitude']; +$delivery->delivery_start = $apiData['dates']['fromDate'] ?? null; +$delivery->delivery_end = $apiData['dates']['toDate'] ?? null; +$delivery->courier_full_name = $apiData['courier']['fullName'] ?? null; +$delivery->courier_phone = $apiData['courier']['phone'] ?? null; +$delivery->courier_vehicle_number = $apiData['courier']['vehicleNumber'] ?? null; + +if ($delivery->save()) { + echo "Информация о доставке сохранена"; +} +``` + +### Получение заказов с доставкой сегодня + +```php +$today = date('Y-m-d'); + +$todayDeliveries = MarketplaceOrderDelivery::find() + ->where(['>=', 'delivery_start', $today . ' 00:00:00']) + ->andWhere(['<', 'delivery_start', date('Y-m-d', strtotime('+1 day')) . ' 00:00:00']) + ->all(); + +echo "Доставок сегодня: " . count($todayDeliveries); +``` + +### Получение доставок в определенном районе + +```php +// Поиск по координатам (прямоугольная область) +$deliveries = MarketplaceOrderDelivery::find() + ->where(['>=', 'latitude', 55.70]) + ->andWhere(['<=', 'latitude', 55.80]) + ->andWhere(['>=', 'longitude', 37.50]) + ->andWhere(['<=', 'longitude', 37.60]) + ->all(); + +echo "Доставок в районе: " . count($deliveries); +``` + +### Получение доставок по городу + +```php +$deliveriesByCity = MarketplaceOrderDelivery::find() + ->where(['city' => 'Москва']) + ->andWhere(['type' => 'DELIVERY']) + ->all(); + +foreach ($deliveriesByCity as $delivery) { + echo "Заказ #{$delivery->order_id}: {$delivery->street}, {$delivery->house}\n"; +} +``` + +### Формирование списка доставок для курьера + +```php +$courierDeliveries = MarketplaceOrderDelivery::find() + ->select([ + 'marketplace_order_delivery.*', + 'marketplace_orders.marketplace_order_id', + 'marketplace_orders.total' + ]) + ->leftJoin('marketplace_orders', 'marketplace_orders.id = marketplace_order_delivery.order_id') + ->where(['courier_full_name' => $courierName]) + ->andWhere(['>=', 'delivery_start', date('Y-m-d 00:00:00')]) + ->andWhere(['<', 'delivery_start', date('Y-m-d 23:59:59')]) + ->orderBy(['delivery_start' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($courierDeliveries as $delivery) { + echo "Заказ {$delivery['marketplace_order_id']}: "; + echo "{$delivery['city']}, {$delivery['street']}, {$delivery['house']} "; + echo "({$delivery['delivery_start']})\n"; +} +``` + +### Статистика по типам доставки + +```php +$deliveryStats = MarketplaceOrderDelivery::find() + ->select(['type', 'COUNT(*) as count']) + ->where(['>=', 'delivery_start', $startDate]) + ->groupBy('type') + ->asArray() + ->all(); + +foreach ($deliveryStats as $stat) { + echo "Тип '{$stat['type']}': {$stat['count']} доставок\n"; +} +``` + +### Поиск по номеру телефона курьера + +```php +$deliveries = MarketplaceOrderDelivery::find() + ->where(['courier_phone' => $phone]) + ->all(); + +echo "Доставок курьера: " . count($deliveries); +``` + +### Обновление информации о курьере + +```php +$delivery = MarketplaceOrderDelivery::findOne(['order_id' => $orderId]); + +if ($delivery) { + $delivery->courier_full_name = 'Иванов Иван Иванович'; + $delivery->courier_phone = '+79001234567'; + $delivery->courier_vehicle_number = 'А123БВ777'; + $delivery->courier_vehicle_description = 'Белый Hyundai Solaris'; + + if ($delivery->save()) { + echo "Информация о курьере обновлена"; + } +} +``` + +### Расчет расстояния между точками (Haversine formula) + +```php +function calculateDistance($lat1, $lon1, $lat2, $lon2) { + $earthRadius = 6371; // км + + $dLat = deg2rad($lat2 - $lat1); + $dLon = deg2rad($lon2 - $lon1); + + $a = sin($dLat/2) * sin($dLat/2) + + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * + sin($dLon/2) * sin($dLon/2); + + $c = 2 * atan2(sqrt($a), sqrt(1-$a)); + + return $earthRadius * $c; +} + +// Поиск доставок в радиусе 5 км от склада +$warehouseLat = 55.751244; +$warehouseLon = 37.618423; +$radius = 5; // км + +$allDeliveries = MarketplaceOrderDelivery::find()->all(); +$nearbyDeliveries = []; + +foreach ($allDeliveries as $delivery) { + $distance = calculateDistance( + $warehouseLat, + $warehouseLon, + $delivery->latitude, + $delivery->longitude + ); + + if ($distance <= $radius) { + $nearbyDeliveries[] = [ + 'delivery' => $delivery, + 'distance' => round($distance, 2) + ]; + } +} + +usort($nearbyDeliveries, function($a, $b) { + return $a['distance'] <=> $b['distance']; +}); +``` + +### Группировка доставок по часам + +```php +$deliveriesByHour = MarketplaceOrderDelivery::find() + ->select([ + "DATE_TRUNC('hour', delivery_start) as delivery_hour", + 'COUNT(*) as count' + ]) + ->where(['>=', 'delivery_start', date('Y-m-d 00:00:00')]) + ->andWhere(['<', 'delivery_start', date('Y-m-d 23:59:59')]) + ->groupBy('delivery_hour') + ->orderBy('delivery_hour') + ->asArray() + ->all(); + +foreach ($deliveriesByHour as $row) { + $hour = date('H:i', strtotime($row['delivery_hour'])); + echo "{$hour}: {$row['count']} доставок\n"; +} +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **MarketplaceOrders** - заказы маркетплейсов (через `order_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + marketplace_order_delivery ||--|| marketplace_orders : "delivery_for" + + marketplace_order_delivery { + int id PK + int order_id FK,UK + string type + string service_name + string partner_type + string country + string postcode + string city + string street + string house + string apartment + float latitude + float longitude + timestamp delivery_start + timestamp delivery_end + string courier_full_name + string courier_phone + string courier_extension + string courier_vehicle_number + string courier_vehicle_description + } + + marketplace_orders { + int id PK + string marketplace_order_id + int status_id + int store_id + float total + } +``` + +--- + +## Структура адреса + +```mermaid +flowchart LR + A[Страна] --> B[Индекс] + B --> C[Город] + C --> D[Улица] + D --> E[Дом] + E --> F[Квартира] + + style A fill:#ffcccc + style C fill:#ccffcc + style D fill:#ccccff + style E fill:#ffffcc + style F fill:#ffccff +``` + +--- + +## Процесс обработки доставки + +```mermaid +stateDiagram-v2 + [*] --> ПолученоИзAPI: Синхронизация + ПолученоИзAPI --> Сохранено: Валидация успешна + Сохранено --> НазначенКурьер: Назначение курьера + НазначенКурьер --> ВПути: Курьер выехал + ВПути --> Доставлено: Успешная доставка + Доставлено --> [*] + + НазначенКурьер --> ИзмененоВремя: Перенос доставки + ИзмененоВремя --> НазначенКурьер: Обновление +``` + +--- + +## Визуализация временного интервала + +```mermaid +gantt + title Интервал доставки заказа + dateFormat YYYY-MM-DD HH:mm + section Доставка + Начало интервала (delivery_start) :milestone, m1, 2025-12-11 14:00, 0m + Период доставки :active, task1, 2025-12-11 14:00, 2h + Конец интервала (delivery_end) :milestone, m2, 2025-12-11 16:00, 0m +``` + +--- + +## Пример полного набора данных доставки + +```php +[ + 'id' => 456, + 'order_id' => 789, + 'type' => 'DELIVERY', + 'service_name' => 'Собственная доставка', + 'partner_type' => 'SHOP', + 'country' => 'Россия', + 'postcode' => '119021', + 'city' => 'Москва', + 'street' => 'ул. Льва Толстого', + 'house' => '16', + 'apartment' => '42', + 'latitude' => 55.733771, + 'longitude' => 37.588144, + 'delivery_start' => '2025-12-11 14:00:00', + 'delivery_end' => '2025-12-11 16:00:00', + 'courier_full_name' => 'Петров Петр Петрович', + 'courier_phone' => '+79031234567', + 'courier_extension' => null, + 'courier_vehicle_number' => 'В456КЛ199', + 'courier_vehicle_description' => 'Белый Ford Transit' +] +``` + +**Полный адрес:** +Россия, 119021, Москва, ул. Льва Толстого, д. 16, кв. 42 + +**Интервал доставки:** +11 декабря 2025, с 14:00 до 16:00 + +**Курьер:** +Петров Петр Петрович, +79031234567, Белый Ford Transit (В456КЛ199) + +--- + +## Связанные модели + +- **[MarketplaceOrders](./MarketplaceOrders.md)** - заказы маркетплейсов +- **[MarketplaceOrderItems](./MarketplaceOrderItems.md)** - товары заказов +- **[MarketplaceStore](./MarketplaceStore.md)** - склады маркетплейсов +- **[OrdersUnion](./OrdersUnion.md)** - объединение заказов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MarketplaceOrderDeliverySearch.md b/erp24/docs/models/MarketplaceOrderDeliverySearch.md new file mode 100644 index 00000000..2bb25c1b --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderDeliverySearch.md @@ -0,0 +1,181 @@ +# Класс: MarketplaceOrderDeliverySearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderDeliverySearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceOrderDelivery +``` + +## Назначение +Search-модель для поиска и фильтрации данных доставки заказов маркетплейса в ERP24. Обеспечивает поиск по адресу, курьеру, координатам и временным интервалам доставки. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceOrderDelivery` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `order_id` — integer +- `type`, `service_name`, `partner_type`, `country`, `postcode`, `city`, `street`, `house`, `apartment`, `delivery_start`, `delivery_end`, `courier_full_name`, `courier_phone`, `courier_extension`, `courier_vehicle_number`, `courier_vehicle_description` — safe +- `latitude`, `longitude` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MarketplaceOrderDelivery::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с возможным кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, order_id, latitude, longitude, delivery_start, delivery_end + - ilike: type, service_name, partner_type, country, postcode, city, street, house, apartment, courier_* + +## Диаграмма структуры доставки + +```mermaid +erDiagram + MarketplaceOrderDelivery { + int id PK + int order_id FK + varchar type + varchar service_name + varchar city + varchar street + varchar house + decimal latitude + decimal longitude + datetime delivery_start + datetime delivery_end + varchar courier_full_name + varchar courier_phone + } + + MarketplaceOrders { + int id PK + } + + MarketplaceOrders ||--|| MarketplaceOrderDelivery : "order_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceOrderDeliverySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по городу +```php +$searchModel = new MarketplaceOrderDeliverySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderDeliverySearch' => [ + 'city' => 'Москва', + ] +]); +``` + +### Поиск по курьеру +```php +$searchModel = new MarketplaceOrderDeliverySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderDeliverySearch' => [ + 'courier_full_name' => 'Иванов', + ] +]); +``` + +### Поиск по заказу +```php +$searchModel = new MarketplaceOrderDeliverySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderDeliverySearch' => [ + 'order_id' => 12345, + ] +]); +``` + +### Поиск по типу доставки +```php +$searchModel = new MarketplaceOrderDeliverySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderDeliverySearch' => [ + 'type' => 'courier', + ] +]); +``` + +### Поиск по службе доставки +```php +$searchModel = new MarketplaceOrderDeliverySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderDeliverySearch' => [ + 'service_name' => 'Яндекс.Доставка', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'order_id', + 'type', + 'city', + 'street', + 'courier_full_name', + 'delivery_start', + 'delivery_end', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceOrderDelivery](./MarketplaceOrderDelivery.md) — базовая модель доставки +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейса + +## Особенности реализации + +1. **Полный адрес**: country, postcode, city, street, house, apartment +2. **Геоданные**: latitude, longitude для координат +3. **Курьер**: courier_full_name, courier_phone, courier_vehicle_* +4. **Временной интервал**: delivery_start, delivery_end +5. **ilike поиск**: Регистронезависимый для всех текстовых полей +6. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MarketplaceOrderItems.md b/erp24/docs/models/MarketplaceOrderItems.md new file mode 100644 index 00000000..b7cfc207 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderItems.md @@ -0,0 +1,560 @@ +# Модель MarketplaceOrderItems + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderItems)) + Таблица БД + marketplace_order_items + Свойства + id + int + order_id + int + external_item_id + int + offer_id + string + offer_name + string + price + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MarketplaceOrderItems` представляет позиции (товары) в заказах маркетплейсов (Яндекс.Маркет, Flowwow). Хранит детальную информацию о каждом товаре в заказе: артикул, название, цены, скидки, количество, НДС и субсидии. Используется для учета товарооборота с маркетплейсами и формирования отчетности. + +**Файл модели:** `erp24/records/MarketplaceOrderItems.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `marketplace_order_items` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `order_id` | INTEGER | Ссылка на заказ из marketplace_orders (обязательное) | +| `external_item_id` | INTEGER | ID элемента заказа из API маркетплейса (обязательное) | + +### Информация о товаре + +| Поле | Тип | Описание | +|------|-----|----------| +| `offer_id` | VARCHAR(64) | ID предложения (артикул товара) (обязательное) | +| `offer_name` | VARCHAR(255) | Название предложения (обязательное) | +| `shop_sku` | VARCHAR(64) | SKU магазина (обязательное) | +| `count` | INTEGER | Количество товара в заказе (обязательное) | + +### Цены и скидки + +| Поле | Тип | Описание | +|------|-----|----------| +| `price` | FLOAT | Цена товара для продавца (обязательное) | +| `buyer_price` | FLOAT | Цена для покупателя после скидок (обязательное) | +| `buyer_price_before_discount` | FLOAT | Цена для покупателя до скидки (обязательное) | +| `price_before_discount` | FLOAT | Цена продавца до скидки (обязательное) | +| `subsidy` | FLOAT | Субсидия от маркетплейса (обязательное) | + +### Налогообложение + +| Поле | Тип | Описание | +|------|-----|----------| +| `vat` | VARCHAR(32) | Ставка НДС (VAT_10, VAT_20, NO_VAT) (обязательное) | + +### Промо и субсидии + +| Поле | Тип | Описание | +|------|-----|----------| +| `promos` | TEXT (JSON) | JSON с информацией о промо-скидках | +| `subsidies` | TEXT (JSON) | JSON с информацией о субсидиях | + +### Склад + +| Поле | Тип | Описание | +|------|-----|----------| +| `partner_warehouse_id` | VARCHAR(64) | ID склада партнера (обязательное) | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'order_id', 'external_item_id', 'offer_id', 'offer_name', + 'price', 'buyer_price', 'buyer_price_before_discount', + 'price_before_discount', 'count', 'vat', 'shop_sku', + 'subsidy', 'partner_warehouse_id' +], 'required' +``` + +### Значения по умолчанию + +```php +['promos', 'subsidies'], 'default', 'value' => null +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `order_id`, `external_item_id`, `count` | Целочисленные значения с поддержкой NULL | +| `number` | `price`, `buyer_price`, `buyer_price_before_discount`, `price_before_discount`, `subsidy` | Числа с плавающей точкой | +| `string` | `promos`, `subsidies` | JSON текст | +| `string`, max=64 | `offer_id`, `shop_sku`, `partner_warehouse_id` | Короткие идентификаторы | +| `string`, max=255 | `offer_name` | Название товара | +| `string`, max=32 | `vat` | Код ставки НДС | + +--- + +## Ставки НДС (vat) + +Поле `vat` может принимать следующие значения: + +| Значение | Описание | Процент | +|----------|----------|---------| +| `NO_VAT` | Без НДС | 0% | +| `VAT_0` | НДС 0% | 0% | +| `VAT_10` | НДС 10% | 10% | +| `VAT_10_110` | НДС 10/110 | 10/110 | +| `VAT_20` | НДС 20% | 20% | +| `VAT_20_120` | НДС 20/120 | 20/120 | + +--- + +## Структура promos (промо-акции) + +Поле `promos` содержит JSON-массив с информацией о применённых промо-акциях: + +```json +[ + { + "type": "MARKET_PROMOCODE", + "discount": 500, + "subsidy": 250, + "shopPromoId": "PROMO2025" + }, + { + "type": "MARKET_DEAL", + "discount": 300, + "subsidy": 150 + } +] +``` + +**Поля объекта:** +- `type` - тип промо-акции +- `discount` - размер скидки +- `subsidy` - субсидия от маркетплейса +- `shopPromoId` - ID промо-акции магазина (опционально) + +--- + +## Структура subsidies (субсидии) + +Поле `subsidies` содержит JSON-массив с детальной информацией о субсидиях: + +```json +[ + { + "type": "SUBSIDY_TYPE_1", + "amount": 250 + }, + { + "type": "SUBSIDY_TYPE_2", + "amount": 150 + } +] +``` + +**Поля объекта:** +- `type` - тип субсидии +- `amount` - размер субсидии + +--- + +## Расчетные показатели + +### Итоговая стоимость позиции + +```php +$totalPrice = $item->buyer_price * $item->count; +``` + +### Скидка на позицию + +```php +$discount = ($item->buyer_price_before_discount - $item->buyer_price) * $item->count; +``` + +### Процент скидки + +```php +$discountPercent = (1 - $item->buyer_price / $item->buyer_price_before_discount) * 100; +``` + +### Сумма НДС + +```php +function calculateVAT($item) { + switch ($item->vat) { + case 'VAT_20': + return $item->price * $item->count * 0.20; + case 'VAT_10': + return $item->price * $item->count * 0.10; + case 'VAT_20_120': + return $item->price * $item->count * (20 / 120); + case 'VAT_10_110': + return $item->price * $item->count * (10 / 110); + default: + return 0; + } +} +``` + +--- + +## Примеры использования + +### Получение всех позиций заказа + +```php +$items = MarketplaceOrderItems::find() + ->where(['order_id' => $orderId]) + ->all(); + +foreach ($items as $item) { + echo "{$item->offer_name}: {$item->count} шт. × {$item->buyer_price} руб.\n"; +} +``` + +### Создание новой позиции при синхронизации с API + +```php +$item = new MarketplaceOrderItems(); +$item->order_id = $orderId; +$item->external_item_id = $apiItemData['id']; +$item->offer_id = $apiItemData['offerId']; +$item->offer_name = $apiItemData['offerName']; +$item->shop_sku = $apiItemData['shopSku']; +$item->count = $apiItemData['count']; +$item->price = $apiItemData['prices'][0]['value']; +$item->buyer_price = $apiItemData['buyerPrice']; +$item->buyer_price_before_discount = $apiItemData['buyerPriceBeforeDiscount']; +$item->price_before_discount = $apiItemData['priceBeforeDiscount']; +$item->vat = $apiItemData['vat']; +$item->subsidy = $apiItemData['subsidy']; +$item->partner_warehouse_id = $apiItemData['warehouseId']; +$item->promos = json_encode($apiItemData['promos'] ?? []); +$item->subsidies = json_encode($apiItemData['subsidies'] ?? []); + +if ($item->save()) { + echo "Позиция добавлена"; +} +``` + +### Расчет итоговой суммы заказа + +```php +$totalAmount = MarketplaceOrderItems::find() + ->where(['order_id' => $orderId]) + ->sum('buyer_price * count'); + +echo "Итого по заказу: {$totalAmount} руб."; +``` + +### Получение товаров с максимальной скидкой + +```php +$itemsWithDiscount = MarketplaceOrderItems::find() + ->select([ + '*', + '(buyer_price_before_discount - buyer_price) as discount_amount', + '((1 - buyer_price / buyer_price_before_discount) * 100) as discount_percent' + ]) + ->where(['order_id' => $orderId]) + ->andWhere('buyer_price < buyer_price_before_discount') + ->orderBy('discount_percent DESC') + ->all(); + +foreach ($itemsWithDiscount as $item) { + echo "{$item->offer_name}: скидка {$item->discount_percent}%\n"; +} +``` + +### Подсчет количества товаров в заказе + +```php +$totalCount = MarketplaceOrderItems::find() + ->where(['order_id' => $orderId]) + ->sum('count'); + +echo "Всего товаров: {$totalCount} шт."; +``` + +### Работа с промо-акциями + +```php +$items = MarketplaceOrderItems::find() + ->where(['order_id' => $orderId]) + ->all(); + +foreach ($items as $item) { + if (!empty($item->promos)) { + $promos = json_decode($item->promos, true); + + echo "Товар: {$item->offer_name}\n"; + echo "Применённые промо:\n"; + + foreach ($promos as $promo) { + echo " - {$promo['type']}: скидка {$promo['discount']} руб.\n"; + } + } +} +``` + +### Фильтрация по артикулу + +```php +$items = MarketplaceOrderItems::find() + ->where(['offer_id' => $offerId]) + ->all(); + +echo "Найдено заказов с артикулом {$offerId}: " . count($items); +``` + +### Статистика продаж по товару + +```php +$stats = MarketplaceOrderItems::find() + ->select([ + 'offer_id', + 'offer_name', + 'SUM(count) as total_count', + 'SUM(buyer_price * count) as total_amount', + 'COUNT(DISTINCT order_id) as order_count' + ]) + ->where(['>=', 'created_at', $startDate]) + ->groupBy(['offer_id', 'offer_name']) + ->orderBy('total_amount DESC') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['offer_name']}: "; + echo "{$stat['total_count']} шт. на сумму {$stat['total_amount']} руб. "; + echo "({$stat['order_count']} заказов)\n"; +} +``` + +### Расчет субсидий + +```php +$items = MarketplaceOrderItems::find() + ->where(['order_id' => $orderId]) + ->all(); + +$totalSubsidy = 0; +foreach ($items as $item) { + $itemSubsidy = $item->subsidy * $item->count; + $totalSubsidy += $itemSubsidy; + + if (!empty($item->subsidies)) { + $subsidies = json_decode($item->subsidies, true); + echo "Товар: {$item->offer_name}\n"; + echo "Детализация субсидий:\n"; + foreach ($subsidies as $subsidy) { + echo " - {$subsidy['type']}: {$subsidy['amount']} руб.\n"; + } + } +} + +echo "Общая субсидия по заказу: {$totalSubsidy} руб."; +``` + +### Получение товаров с определенной ставкой НДС + +```php +$itemsVAT20 = MarketplaceOrderItems::find() + ->where(['vat' => 'VAT_20']) + ->andWhere(['>=', 'created_at', $startDate]) + ->all(); + +$totalVAT = 0; +foreach ($itemsVAT20 as $item) { + $itemTotal = $item->price * $item->count; + $itemVAT = $itemTotal * 0.20; + $totalVAT += $itemVAT; +} + +echo "Общая сумма НДС 20%: {$totalVAT} руб."; +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **MarketplaceOrders** - заказы маркетплейсов (через `order_id`) +- **Products1c** - товары из 1С (через `offer_id` = `articule`) +- **MarketplaceStore** - склады маркетплейсов (через `partner_warehouse_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + marketplace_order_items }o--|| marketplace_orders : "belongs_to" + marketplace_order_items }o--|| products_1c : "product" + marketplace_order_items }o--|| marketplace_store : "warehouse" + + marketplace_order_items { + int id PK + int order_id FK + int external_item_id + string offer_id FK + string offer_name + float price + float buyer_price + float buyer_price_before_discount + float price_before_discount + int count + string vat + string shop_sku + float subsidy + string partner_warehouse_id FK + text promos + text subsidies + } + + marketplace_orders { + int id PK + string marketplace_order_id + int status_id + float total + string warehouse_guid + } + + products_1c { + string guid PK + string articule UK + string name + float price + } + + marketplace_store { + int id PK + string warehouse_guid UK + int store_id + int warehouse_id + } +``` + +--- + +## Структура ценообразования + +```mermaid +flowchart TD + A[Базовая цена price_before_discount] --> B[Применение промо-акций] + B --> C[buyer_price_before_discount] + C --> D[Применение скидок маркетплейса] + D --> E[buyer_price - финальная цена] + E --> F[Субсидия subsidy] + F --> G[price - цена для продавца] + + style A fill:#ffcccc + style E fill:#ccffcc + style G fill:#ccccff +``` + +--- + +## Процесс обработки позиции заказа + +```mermaid +stateDiagram-v2 + [*] --> Получено: Синхронизация с API + Получено --> Валидация: Проверка данных + Валидация --> Сохранено: Данные корректны + Валидация --> Ошибка: Данные некорректны + Сохранено --> СвязьСТоваром: Поиск в Products1c + СвязьСТоваром --> Готово: Товар найден + СвязьСТоваром --> НовыйТовар: Товар не найден + НовыйТовар --> Готово: Создан товар + Готово --> [*] + Ошибка --> [*] +``` + +--- + +## Пример полного набора данных позиции + +```php +[ + 'id' => 12345, + 'order_id' => 789, + 'external_item_id' => 987654321, + 'offer_id' => 'ROSE-PINK-50', + 'offer_name' => 'Роза кустовая розовая 50см', + 'price' => 45.50, + 'buyer_price' => 50.00, + 'buyer_price_before_discount' => 65.00, + 'price_before_discount' => 55.00, + 'count' => 25, + 'vat' => 'VAT_20', + 'shop_sku' => 'SKU-ROSE-PINK', + 'subsidy' => 4.50, + 'partner_warehouse_id' => 'WH-12345', + 'promos' => json_encode([ + [ + 'type' => 'MARKET_PROMOCODE', + 'discount' => 15.00, + 'subsidy' => 7.50, + 'shopPromoId' => 'SPRING2025' + ] + ]), + 'subsidies' => json_encode([ + [ + 'type' => 'YANDEX_SUBSIDY', + 'amount' => 4.50 + ] + ]) +] +``` + +**Расчеты для примера:** +- Стоимость позиции: 50.00 × 25 = 1,250 руб. +- Скидка покупателя: (65.00 - 50.00) × 25 = 375 руб. +- Процент скидки: (1 - 50/65) × 100 = 23.08% +- Доход продавца: 45.50 × 25 = 1,137.50 руб. +- НДС 20%: 1,137.50 × 0.20 = 227.50 руб. +- Субсидия: 4.50 × 25 = 112.50 руб. + +--- + +## Связанные модели + +- **[MarketplaceOrders](./MarketplaceOrders.md)** - заказы маркетплейсов +- **[MarketplaceOrderDelivery](./MarketplaceOrderDelivery.md)** - доставка заказов +- **[MarketplaceStore](./MarketplaceStore.md)** - склады маркетплейсов +- **[Products1c](./Products1c.md)** - товары 1С +- **[OrdersUnion](./OrdersUnion.md)** - объединение заказов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MarketplaceOrderItemsSearch.md b/erp24/docs/models/MarketplaceOrderItemsSearch.md new file mode 100644 index 00000000..010325b6 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderItemsSearch.md @@ -0,0 +1,172 @@ +# Класс: MarketplaceOrderItemsSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderItemsSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceOrderItems +``` + +## Назначение +Search-модель для поиска и фильтрации позиций заказов маркетплейса в ERP24. Обеспечивает поиск по товару, цене, количеству, SKU и промо-акциям. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceOrderItems` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `order_id`, `external_item_id`, `count` — integer +- `offer_id`, `offer_name`, `vat`, `shop_sku`, `partner_warehouse_id`, `promos`, `subsidies` — safe +- `price`, `buyer_price`, `buyer_price_before_discount`, `price_before_discount`, `subsidy` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MarketplaceOrderItems::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, order_id, external_item_id, price, buyer_price, buyer_price_before_discount, price_before_discount, count, subsidy + - ilike: offer_id, offer_name, vat, shop_sku, partner_warehouse_id, promos, subsidies + +## Диаграмма ценообразования + +```mermaid +flowchart TD + A[price_before_discount] --> B{Скидка?} + B -->|Да| C[price] + B -->|Нет| C + + D[buyer_price_before_discount] --> E{Скидка покупателя?} + E -->|Да| F[buyer_price] + E -->|Нет| F + + G[subsidy] --> H[Субсидия маркетплейса] + C --> I[Итоговая цена] + F --> I + H --> I +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceOrderItemsSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по заказу +```php +$searchModel = new MarketplaceOrderItemsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderItemsSearch' => [ + 'order_id' => 12345, + ] +]); +``` + +### Поиск по названию товара +```php +$searchModel = new MarketplaceOrderItemsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderItemsSearch' => [ + 'offer_name' => 'Букет роз', + ] +]); +``` + +### Поиск по SKU +```php +$searchModel = new MarketplaceOrderItemsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderItemsSearch' => [ + 'shop_sku' => 'FLOWER-001', + ] +]); +``` + +### Поиск по цене +```php +$searchModel = new MarketplaceOrderItemsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderItemsSearch' => [ + 'price' => 2500, + ] +]); +``` + +### Поиск с промо-акцией +```php +$searchModel = new MarketplaceOrderItemsSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderItemsSearch' => [ + 'promos' => 'SALE20', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'order_id', + 'offer_name', + 'shop_sku', + 'count', + 'price', + 'buyer_price', + 'subsidy', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceOrderItems](./MarketplaceOrderItems.md) — базовая модель позиций +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейса + +## Особенности реализации + +1. **Ценовая структура**: price, buyer_price, price_before_discount, buyer_price_before_discount +2. **Субсидии**: subsidy, subsidies для маркетплейса +3. **Промо**: promos для скидок и акций +4. **SKU mapping**: shop_sku, offer_id для идентификации +5. **ilike поиск**: Регистронезависимый для текстовых полей +6. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MarketplaceOrderStatusHistory.md b/erp24/docs/models/MarketplaceOrderStatusHistory.md new file mode 100644 index 00000000..e38ca44c --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderStatusHistory.md @@ -0,0 +1,231 @@ +# Класс: MarketplaceOrderStatusHistory + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderStatusHistory)) + Таблица БД + marketplace_order_status_history + Свойства + id + int + order_id + int + status_id + int + substatus_id + int + active + int + date_from + string + Связи + Status + 1:1 MarketplaceOrderStatusTypes + Substatus + 1:1 MarketplaceOrderStatusTypes + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории изменений статусов заказов маркетплейсов в ERP24. Хранит полную хронологию переходов между статусами с временными метками и информацией об инициаторе изменения для аудита и аналитики. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_order_status_history` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `order_id` | int | FK на заказ маркетплейса | +| `status_id` | int | FK на основной статус | +| `substatus_id` | int | FK на субстатус | +| `active` | int | Флаг активности записи (1=текущий статус, default: 1) | +| `date_from` | datetime | Дата и время начала действия статуса | +| `date_end` | datetime / null | Дата и время окончания действия статуса | +| `initiator` | text / null | Инициатор изменения (имя пользователя или система) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getStatus()` | hasOne | MarketplaceOrderStatusTypes | Основной статус | +| `getSubstatus()` | hasOne | MarketplaceOrderStatusTypes | Субстатус | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrders { + int id PK + varchar external_id + int status_id FK + } + + MarketplaceOrderStatusHistory { + int id PK + int order_id FK + int status_id FK + int substatus_id FK + int active + datetime date_from + datetime date_end + text initiator + } + + MarketplaceOrderStatusTypes { + int id PK + varchar code + varchar name + } + + MarketplaceOrders ||--o{ MarketplaceOrderStatusHistory : "order_id" + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrderStatusHistory : "status_id" + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrderStatusHistory : "substatus_id" +``` + +## Диаграмма временной шкалы + +```mermaid +gantt + title История статусов заказа #12345 + dateFormat YYYY-MM-DD HH:mm + section Статусы + Новый :done, s1, 2024-01-15 10:00, 30m + В обработке :done, s2, 2024-01-15 10:30, 2h + Готов к выдаче :done, s3, 2024-01-15 12:30, 3h + Доставлен :active, s4, 2024-01-15 15:30, 1h +``` + +## Примеры использования + +### Запись нового статуса в историю +```php +// Закрываем предыдущий активный статус +$previousHistory = MarketplaceOrderStatusHistory::find() + ->where(['order_id' => $orderId, 'active' => 1]) + ->one(); + +if ($previousHistory) { + $previousHistory->active = 0; + $previousHistory->date_end = date('Y-m-d H:i:s'); + $previousHistory->save(); +} + +// Создаём новую запись +$history = new MarketplaceOrderStatusHistory(); +$history->order_id = $orderId; +$history->status_id = $newStatusId; +$history->substatus_id = $newSubstatusId; +$history->date_from = date('Y-m-d H:i:s'); +$history->initiator = Yii::$app->user->identity->username ?? 'system'; +$history->active = 1; +$history->save(); +``` + +### Получение полной истории заказа +```php +$history = MarketplaceOrderStatusHistory::find() + ->where(['order_id' => $orderId]) + ->with(['status', 'substatus']) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $record) { + $duration = $record->date_end + ? strtotime($record->date_end) - strtotime($record->date_from) + : time() - strtotime($record->date_from); + + echo "{$record->status->name}: " . gmdate('H:i:s', $duration) . "\n"; + echo "Инициатор: {$record->initiator}\n"; +} +``` + +### Получение текущего статуса +```php +$currentStatus = MarketplaceOrderStatusHistory::find() + ->where(['order_id' => $orderId, 'active' => 1]) + ->with(['status', 'substatus']) + ->one(); + +echo "Текущий статус: {$currentStatus->status->name}"; +if ($currentStatus->substatus) { + echo " ({$currentStatus->substatus->name})"; +} +``` + +### Расчёт времени в каждом статусе +```php +$history = MarketplaceOrderStatusHistory::find() + ->where(['order_id' => $orderId]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +$durations = []; +foreach ($history as $record) { + $end = $record->date_end ?? date('Y-m-d H:i:s'); + $duration = strtotime($end) - strtotime($record->date_from); + $durations[$record->status_id] = ($durations[$record->status_id] ?? 0) + $duration; +} +``` + +### Аналитика по статусам +```php +// Среднее время в статусе "В обработке" +$avgProcessingTime = MarketplaceOrderStatusHistory::find() + ->select(['AVG(EXTRACT(EPOCH FROM (date_end - date_from))) as avg_seconds']) + ->where(['status_id' => $processingStatusId]) + ->andWhere(['not', ['date_end' => null]]) + ->scalar(); + +echo "Среднее время обработки: " . gmdate('H:i:s', $avgProcessingTime); +``` + +### Поиск заказов, застрявших в статусе +```php +$stuckOrders = MarketplaceOrderStatusHistory::find() + ->where(['active' => 1, 'status_id' => $statusId]) + ->andWhere(['<', 'date_from', date('Y-m-d H:i:s', strtotime('-2 hours'))]) + ->with(['order']) + ->all(); + +foreach ($stuckOrders as $history) { + echo "Заказ #{$history->order_id} в статусе более 2 часов\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `order_id` | required, integer | +| `status_id` | required, integer | +| `substatus_id` | required, integer | +| `date_from` | required, safe | +| `date_end` | safe, default: null | +| `initiator` | string, default: null | +| `active` | integer, default: 1 | + +## Связанные модели + +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы маркетплейсов +- [MarketplaceOrderStatusTypes](./MarketplaceOrderStatusTypes.md) — справочник типов статусов + +## Особенности реализации + +1. **Temporal pattern**: Записи имеют date_from/date_end для построения временной шкалы +2. **Активный статус**: Флаг active=1 указывает на текущий статус заказа +3. **Аудит**: Поле initiator хранит информацию об инициаторе изменения +4. **Двухуровневые статусы**: Поддержка основного статуса (status_id) и субстатуса (substatus_id) +5. **Аналитика**: Возможность расчёта времени пребывания в каждом статусе +6. **Полная история**: Все изменения сохраняются для аудита и отчётности diff --git a/erp24/docs/models/MarketplaceOrderStatusHistorySearch.md b/erp24/docs/models/MarketplaceOrderStatusHistorySearch.md new file mode 100644 index 00000000..222a2580 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderStatusHistorySearch.md @@ -0,0 +1,196 @@ +# Класс: MarketplaceOrderStatusHistorySearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderStatusHistorySearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceOrderStatusHistory +``` + +## Назначение +Search-модель для поиска и фильтрации истории статусов заказов маркетплейса в ERP24. Расширенная модель с поддержкой поиска по связанным таблицам статусов через алиасы. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceOrderStatusHistory` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$status_code` | string | Код статуса для поиска по связанной таблице | +| `$substatus_code` | string | Код подстатуса для поиска по связанной таблице | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `order_id`, `status_id`, `substatus_id`, `active` — integer +- `date_from`, `date_end`, `initiator`, `status_code`, `substatus_code` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к статусам. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с joinWith status (алиас statusAlias) и substatus (алиас substatusAlias) +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, order_id, status_id, substatus_id, active, date_from, date_end + - ilike: initiator, statusAlias.code, substatusAlias.code + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrderStatusHistory { + int id PK + int order_id FK + int status_id FK + int substatus_id FK + int active + datetime date_from + datetime date_end + varchar initiator + } + + MarketplaceOrderStatusTypes { + int id PK + varchar code + varchar name + } + + MarketplaceOrders { + int id PK + } + + MarketplaceOrderStatusHistory }o--|| MarketplaceOrderStatusTypes : "status_id" + MarketplaceOrderStatusHistory }o--o| MarketplaceOrderStatusTypes : "substatus_id" + MarketplaceOrders ||--o{ MarketplaceOrderStatusHistory : "order_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceOrderStatusHistorySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по заказу +```php +$searchModel = new MarketplaceOrderStatusHistorySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusHistorySearch' => [ + 'order_id' => 12345, + ] +]); +``` + +### Поиск по коду статуса +```php +$searchModel = new MarketplaceOrderStatusHistorySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusHistorySearch' => [ + 'status_code' => 'PROCESSING', + ] +]); +``` + +### Поиск по коду подстатуса +```php +$searchModel = new MarketplaceOrderStatusHistorySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusHistorySearch' => [ + 'substatus_code' => 'READY_TO_SHIP', + ] +]); +``` + +### Поиск по инициатору +```php +$searchModel = new MarketplaceOrderStatusHistorySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusHistorySearch' => [ + 'initiator' => 'MARKETPLACE', + ] +]); +``` + +### Поиск активных статусов +```php +$searchModel = new MarketplaceOrderStatusHistorySearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusHistorySearch' => [ + 'active' => 1, + ] +]); +``` + +### GridView с кодами статусов +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'order_id', + [ + 'attribute' => 'status_code', + 'value' => 'status.code', + ], + [ + 'attribute' => 'substatus_code', + 'value' => 'substatus.code', + ], + 'date_from', + 'date_end', + 'initiator', + 'active', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceOrderStatusHistory](./MarketplaceOrderStatusHistory.md) — базовая модель истории +- [MarketplaceOrderStatusTypes](./MarketplaceOrderStatusTypes.md) — типы статусов +- [MarketplaceOrders](./MarketplaceOrders.md) — заказы + +## Особенности реализации + +1. **JOIN со связями**: joinWith status и substatus с алиасами +2. **Дополнительные свойства**: status_code, substatus_code для поиска по коду +3. **Алиасы таблиц**: statusAlias, substatusAlias для избежания конфликтов +4. **ilike по связям**: Поиск по statusAlias.code и substatusAlias.code +5. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MarketplaceOrderStatusTypes.md b/erp24/docs/models/MarketplaceOrderStatusTypes.md new file mode 100644 index 00000000..ba1c0762 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderStatusTypes.md @@ -0,0 +1,197 @@ +# Класс: MarketplaceOrderStatusTypes + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderStatusTypes)) + Таблица БД + marketplace_order_status_types + Свойства + id + int + code + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов статусов и субстатусов заказов маркетплейсов в ERP24. Унифицированный справочник кодов статусов для интеграции с различными маркетплейсами (Flowwow, Яндекс.Маркет и др.). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_order_status_types` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы статусов + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `CANSELLED_CODE` | `CANCELLED` | Заказ отменён | +| `READY_CODE` | `READY_TO_SHIP` | Готов к отправке | +| `DELIVERED_CODE` | `DELIVERED` | Доставлен (самовывоз) | +| `DELIVERY_SERVICE_DELIVERED_CODE` | `DELIVERY_SERVICE_DELIVERED` | Доставлен курьером | + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: false | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `code` | varchar(64) | Уникальный код статуса | +| `name` | varchar(255) / null | Человекочитаемое название статуса | +| `description` | varchar(255) / null | Описание статуса | +| `created_at` | datetime / null | Дата создания (автоматически) | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrderStatusTypes { + int id PK + varchar code UK + varchar name + varchar description + datetime created_at + } + + MarketplaceOrder1cStatuses { + int id PK + int order_status_id FK + int order_substatus_id FK + } + + MarketplaceOrderStatusHistory { + int id PK + int status_id FK + int substatus_id FK + } + + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrder1cStatuses : "order_status_id" + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrder1cStatuses : "order_substatus_id" + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrderStatusHistory : "status_id" + MarketplaceOrderStatusTypes ||--o{ MarketplaceOrderStatusHistory : "substatus_id" +``` + +## Диаграмма типичного workflow + +```mermaid +flowchart TD + NEW[PENDING
    Ожидает подтверждения] --> PROCESSING[PROCESSING
    В обработке] + PROCESSING --> READY[READY_TO_SHIP
    Готов к отправке] + READY --> SHIPPED[SHIPPED
    Отправлен] + SHIPPED --> DELIVERED[DELIVERED
    Доставлен] + + NEW --> CANCELLED[CANCELLED
    Отменён] + PROCESSING --> CANCELLED + + SHIPPED --> DELIVERY_SERVICE_DELIVERED[DELIVERY_SERVICE_DELIVERED
    Доставлен курьером] +``` + +## Примеры использования + +### Получение статуса по коду +```php +$cancelledStatus = MarketplaceOrderStatusTypes::find() + ->where(['code' => MarketplaceOrderStatusTypes::CANSELLED_CODE]) + ->one(); +``` + +### Создание нового типа статуса +```php +$status = new MarketplaceOrderStatusTypes(); +$status->code = 'PARTIALLY_DELIVERED'; +$status->name = 'Частично доставлен'; +$status->description = 'Часть товаров из заказа доставлена'; +$status->save(); +// created_at заполнится автоматически +``` + +### Формирование списка для выбора +```php +$statusList = ArrayHelper::map( + MarketplaceOrderStatusTypes::find()->orderBy(['name' => SORT_ASC])->all(), + 'id', + 'name' +); +``` + +### Проверка финального статуса +```php +$order = MarketplaceOrders::findOne($orderId); +$statusHistory = $order->currentStatusHistory; + +$isCancelled = $statusHistory->status->code === MarketplaceOrderStatusTypes::CANSELLED_CODE; +$isDelivered = in_array($statusHistory->status->code, [ + MarketplaceOrderStatusTypes::DELIVERED_CODE, + MarketplaceOrderStatusTypes::DELIVERY_SERVICE_DELIVERED_CODE, +]); + +if ($isDelivered) { + echo "Заказ успешно доставлен"; +} elseif ($isCancelled) { + echo "Заказ отменён"; +} +``` + +### Маппинг статусов маркетплейса на внутренние +```php +$externalToInternalMap = [ + 'flowwow_new' => 'PENDING', + 'flowwow_accepted' => 'PROCESSING', + 'flowwow_ready' => 'READY_TO_SHIP', + 'flowwow_delivered' => 'DELIVERED', + 'flowwow_cancelled' => 'CANCELLED', +]; + +$internalCode = $externalToInternalMap[$externalStatus] ?? 'UNKNOWN'; +$status = MarketplaceOrderStatusTypes::find() + ->where(['code' => $internalCode]) + ->one(); +``` + +### Получение всех заказов в определённом статусе +```php +$readyOrderIds = MarketplaceOrderStatusHistory::find() + ->select('order_id') + ->innerJoin( + 'marketplace_order_status_types t', + 'marketplace_order_status_history.status_id = t.id' + ) + ->where(['t.code' => MarketplaceOrderStatusTypes::READY_CODE, 'active' => 1]) + ->column(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `code` | required, string (max 64), unique | +| `name` | string (max 255), default: null | +| `description` | string (max 255), default: null | +| `created_at` | safe (автоматически) | + +## Связанные модели + +- [MarketplaceOrder1cStatuses](./MarketplaceOrder1cStatuses.md) — статусы для 1С (order_status_id, order_substatus_id) +- [MarketplaceOrderStatusHistory](./MarketplaceOrderStatusHistory.md) — история статусов заказов + +## Особенности реализации + +1. **Уникальный код**: Поле code имеет уникальный индекс для идентификации статуса +2. **Константы**: Ключевые статусы определены как константы класса для типобезопасности +3. **TimestampBehavior**: Автоматическое заполнение created_at при создании +4. **Универсальность**: Используется и для основных статусов, и для субстатусов +5. **Интеграция с маркетплейсами**: Коды унифицированы для работы с разными площадками +6. **Двухуровневая система**: Статусы могут использоваться как основные или как субстатусы diff --git a/erp24/docs/models/MarketplaceOrderStatusTypesSearch.md b/erp24/docs/models/MarketplaceOrderStatusTypesSearch.md new file mode 100644 index 00000000..a584dbfa --- /dev/null +++ b/erp24/docs/models/MarketplaceOrderStatusTypesSearch.md @@ -0,0 +1,127 @@ +# Класс: MarketplaceOrderStatusTypesSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrderStatusTypesSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceOrderStatusTypes +``` + +## Назначение +Search-модель для поиска и фильтрации типов статусов заказов маркетплейса в ERP24. Простая модель для поиска по коду, названию и описанию статуса. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceOrderStatusTypes` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `code`, `name`, `description` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MarketplaceOrderStatusTypes::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с кастомным formName +4. Применяет фильтры: + - Точное совпадение: id + - ilike: code, name, description + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceOrderStatusTypesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по коду +```php +$searchModel = new MarketplaceOrderStatusTypesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusTypesSearch' => [ + 'code' => 'PROCESSING', + ] +]); +``` + +### Поиск по названию +```php +$searchModel = new MarketplaceOrderStatusTypesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusTypesSearch' => [ + 'name' => 'В обработке', + ] +]); +``` + +### Поиск по описанию +```php +$searchModel = new MarketplaceOrderStatusTypesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrderStatusTypesSearch' => [ + 'description' => 'ожидает', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'code', + 'name', + 'description', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceOrderStatusTypes](./MarketplaceOrderStatusTypes.md) — базовая модель типов статусов +- [MarketplaceOrderStatusHistory](./MarketplaceOrderStatusHistory.md) — история статусов + +## Особенности реализации + +1. **Справочник статусов**: Типы статусов заказов маркетплейса +2. **ilike поиск**: Регистронезависимый для code, name, description +3. **Кастомный formName**: Поддержка различных форм +4. **Простая модель**: Минимальный набор полей diff --git a/erp24/docs/models/MarketplaceOrders.md b/erp24/docs/models/MarketplaceOrders.md index 8be39a22..61817b81 100644 --- a/erp24/docs/models/MarketplaceOrders.md +++ b/erp24/docs/models/MarketplaceOrders.md @@ -1,5 +1,41 @@ # Model: MarketplaceOrders + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrders)) + Таблица БД + marketplace_orders + Свойства + id + int + marketplace_order_id + string + status_id + int + substatus_id + int + creation_date + string + updated_at + string + Связи + Store + 1:1 CityStore + Mpstore + 1:1 MarketplaceStore + Status + 1:1 MarketplaceOrderStatusTypes + Substatus + 1:1 MarketplaceOrderStatusTypes + Items + 1:N MarketplaceOrderItems + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель заказов с маркетплейсов (Flowwow, Яндекс.Маркет). Хранит информацию о заказах, их статусах, интеграции с 1С и Telegram-уведомлениях. diff --git a/erp24/docs/models/MarketplaceOrdersSearch.md b/erp24/docs/models/MarketplaceOrdersSearch.md new file mode 100644 index 00000000..eb59dae4 --- /dev/null +++ b/erp24/docs/models/MarketplaceOrdersSearch.md @@ -0,0 +1,212 @@ +# Класс: MarketplaceOrdersSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceOrdersSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplaceOrders +``` + +## Назначение +Search-модель для поиска и фильтрации заказов маркетплейса в ERP24. Расширенная модель с поддержкой поиска по связанным таблицам: магазин, статус, подстатус с использованием алиасов для избежания конфликтов имён. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplaceOrders` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$store_name` | string | Название магазина для поиска по связанной таблице marketplace_store | +| `$status_code` | string | Код статуса для поиска по связанной таблице (алиас statusAlias) | +| `$substatus_code` | string | Код подстатуса для поиска по связанной таблице (алиас substatusAlias) | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `store_id`, `status_id`, `substatus_id`, `cancel_requested`, `status_1c` — integer +- `marketplace_order_id`, `warehouse_guid`, `creation_date`, `updated_at`, `tax_system`, `payment_type`, `payment_method`, `raw_data`, `guid`, `marketplace_name`, `store_name`, `status_code`, `substatus_code` — safe +- `total`, `delivery_total`, `buyer_total_before_discount` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к магазину, статусу и подстатусу. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с eager loading status1C +2. Выполняет joinWith для store, status (алиас statusAlias), substatus (алиас substatusAlias) +3. Оборачивает в ActiveDataProvider +4. Загружает параметры с кастомным formName +5. Применяет фильтры: + - Точное совпадение: id, store_id, status_id, substatus_id, creation_date, updated_at, total, delivery_total, buyer_total_before_discount, cancel_requested, status_1c, marketplace_name + - ilike: marketplace_order_id, warehouse_guid, tax_system, payment_type, payment_method, raw_data, guid + - ilike по связям: marketplace_store.name, statusAlias.code, substatusAlias.code + +## Диаграмма связей + +```mermaid +erDiagram + MarketplaceOrders { + int id PK + int store_id FK + int status_id FK + int substatus_id FK + varchar marketplace_order_id + varchar marketplace_name + varchar warehouse_guid + datetime creation_date + decimal total + decimal delivery_total + int cancel_requested + int status_1c + } + + MarketplaceStore { + int id PK + varchar name + } + + MarketplaceOrderStatusTypes { + int id PK + varchar code + varchar name + } + + MarketplaceOrders }o--|| MarketplaceStore : "store_id" + MarketplaceOrders }o--|| MarketplaceOrderStatusTypes : "status_id" + MarketplaceOrders }o--o| MarketplaceOrderStatusTypes : "substatus_id" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplaceOrdersSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по маркетплейсу +```php +$searchModel = new MarketplaceOrdersSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrdersSearch' => [ + 'marketplace_name' => 'yandex', + ] +]); +``` + +### Поиск по названию магазина +```php +$searchModel = new MarketplaceOrdersSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrdersSearch' => [ + 'store_name' => 'Цветочный', + ] +]); +``` + +### Поиск по коду статуса +```php +$searchModel = new MarketplaceOrdersSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrdersSearch' => [ + 'status_code' => 'PROCESSING', + ] +]); +``` + +### Поиск по внешнему ID заказа +```php +$searchModel = new MarketplaceOrdersSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrdersSearch' => [ + 'marketplace_order_id' => '12345678', + ] +]); +``` + +### Поиск заказов с отменой +```php +$searchModel = new MarketplaceOrdersSearch(); +$dataProvider = $searchModel->search([ + 'MarketplaceOrdersSearch' => [ + 'cancel_requested' => 1, + ] +]); +``` + +### GridView с кодами статусов +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'marketplace_order_id', + 'marketplace_name', + [ + 'attribute' => 'store_name', + 'value' => 'store.name', + ], + [ + 'attribute' => 'status_code', + 'value' => 'status.code', + ], + [ + 'attribute' => 'substatus_code', + 'value' => 'substatus.code', + ], + 'total', + 'creation_date', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplaceOrders](./MarketplaceOrders.md) — базовая модель заказов +- [MarketplaceStore](./MarketplaceStore.md) — магазины маркетплейса +- [MarketplaceOrderStatusTypes](./MarketplaceOrderStatusTypes.md) — типы статусов +- [MarketplaceOrderItems](./MarketplaceOrderItems.md) — позиции заказа +- [MarketplaceOrderDelivery](./MarketplaceOrderDelivery.md) — доставка заказа + +## Особенности реализации + +1. **Множественные JOIN**: joinWith для store, status, substatus +2. **Алиасы таблиц**: statusAlias, substatusAlias для избежания конфликтов имён +3. **Eager loading**: with('status1C') для оптимизации +4. **Дополнительные свойства**: store_name, status_code, substatus_code для фильтрации по связям +5. **Поддержка нескольких маркетплейсов**: marketplace_name для фильтрации (yandex, ozon и др.) +6. **Статус 1С**: status_1c для интеграции с 1С +7. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MarketplacePrices.md b/erp24/docs/models/MarketplacePrices.md new file mode 100644 index 00000000..a738cb34 --- /dev/null +++ b/erp24/docs/models/MarketplacePrices.md @@ -0,0 +1,563 @@ +# Модель MarketplacePrices + + +## Mindmap + +```mermaid +mindmap + root((MarketplacePrices)) + Таблица БД + marketplace_prices + Свойства + id + int + matrix_erp_id + int + marketplace_id + int + marketplace_alias + string + price + float + created_at + string + Связи + History + 1:N MarketplacePricesLog + MatrixErp + 1:1 MatrixErp + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MarketplacePrices` управляет ценами товаров для маркетплейсов (Яндекс.Маркет, Flowwow). Хранит текущие и старые цены товаров, автоматически определяет алиас маркетплейса и отслеживает историю изменений через связанную модель. Поддерживает механизм скидок через поле старой цены. + +**Файл модели:** `erp24/records/MarketplacePrices.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `marketplace_prices` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | ID записи (первичный ключ) | +| `matrix_erp_id` | INTEGER | ID товара из матрицы (обязательное) | +| `marketplace_id` | INTEGER | ID маркетплейса: 1 - Яндекс.Маркет, 2 - Flowwow (обязательное) | +| `marketplace_alias` | VARCHAR(255) | Алиас маркетплейса: YM, FW (обязательное) | + +### Цены + +| Поле | Тип | Описание | +|------|-----|----------| +| `price` | FLOAT | Текущая цена товара для маркетплейса (обязательное) | +| `old_price` | FLOAT | Старая цена для отображения скидки | + +### Метаданные + +| Поле | Тип | Описание | +|------|-----|----------| +| `created_at` | TIMESTAMP | Дата и время создания записи (обязательное) | +| `updated_at` | TIMESTAMP | Дата и время последнего обновления | + +--- + +## Константы маркетплейсов + +| marketplace_id | marketplace_alias | Название | +|----------------|-------------------|----------| +| 1 | YM | Яндекс.Маркет | +| 2 | FW | Flowwow | + +--- + +## Behaviors (поведения) + +### TimestampBehavior + +Автоматически устанавливает временные метки при создании и обновлении записи. + +```php +public function behaviors() +{ + return [ + 'ts' => [ + 'class' => \yii\behaviors\TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new \yii\db\Expression('NOW()'), + ], + ]; +} +``` + +**Логика работы:** +- При создании (`INSERT`) - автоматически заполняет `created_at` и `updated_at` +- При обновлении (`UPDATE`) - автоматически обновляет только `updated_at` +- Использует функцию PostgreSQL `NOW()` для получения текущего времени + +--- + +## Методы + +### `beforeSave($insert)` + +Автоматически заполняет `marketplace_alias` перед сохранением записи. + +```php +public function beforeSave($insert) +{ + if (parent::beforeSave($insert)) { + // Автоматически заполняем marketplace_alias на основе marketplace_id + if ($this->marketplace_id == 1) { + $this->marketplace_alias = 'YM'; + } elseif ($this->marketplace_id == 2) { + $this->marketplace_alias = 'FW'; + } + return true; + } + return false; +} +``` + +**Параметры:** +- `$insert` (bool) - true если это новая запись, false если обновление + +**Возвращает:** `bool` - true если сохранение можно продолжить, false для отмены + +**Логика работы:** +1. Вызывает родительский `beforeSave()` для выполнения базовой валидации +2. Проверяет значение `marketplace_id` +3. Устанавливает соответствующий `marketplace_alias`: + - `marketplace_id = 1` → `marketplace_alias = 'YM'` + - `marketplace_id = 2` → `marketplace_alias = 'FW'` +4. Возвращает `true` для продолжения сохранения + +**Вызовы сторонних методов:** +- `parent::beforeSave($insert)` - базовая валидация от ActiveRecord + +**Примечание:** Это происходит автоматически при каждом вызове `save()`, пользователю не нужно вручную заполнять `marketplace_alias`. + +--- + +### `getHistory()` + +Получает историю изменений цен для данной записи. + +```php +public function getHistory() +{ + return $this->hasMany(MarketplacePricesLog::class, ['marketplace_prices_id' => 'id']) + ->orderBy(['changed_at' => SORT_DESC, 'id' => SORT_DESC]); +} +``` + +**Возвращает:** `\yii\db\ActiveQuery` - запрос для получения связанных записей истории + +**Тип связи:** `hasMany` - одна цена может иметь много записей в истории + +**Связанная модель:** `MarketplacePricesLog` + +**Сортировка:** По дате изменения (`changed_at`) и ID, от новых к старым + +**Использование:** +```php +$price = MarketplacePrices::findOne($id); +$history = $price->history; // Получить всю историю +``` + +--- + +### `getMatrixErp()` + +Получает информацию о товаре из матрицы ERP. + +```php +public function getMatrixErp() +{ + return $this->hasOne(MatrixErp::class, ['id' => 'matrix_erp_id']); +} +``` + +**Возвращает:** `\yii\db\ActiveQuery` - запрос для получения связанного товара + +**Тип связи:** `hasOne` - цена принадлежит одному товару + +**Связанная модель:** `MatrixErp` + +**Использование:** +```php +$price = MarketplacePrices::findOne($id); +$product = $price->matrixErp; // Получить товар +echo $product->name; +``` + +--- + +## Правила валидации + +### Значения по умолчанию + +```php +['old_price', 'updated_at'], 'default', 'value' => null +``` + +### Обязательные поля + +```php +['matrix_erp_id', 'marketplace_id', 'marketplace_alias', 'price'], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `matrix_erp_id`, `marketplace_id` | Целочисленные значения с поддержкой NULL | +| `number` | `price`, `old_price` | Числа с плавающей точкой | +| `safe` | `created_at`, `updated_at` | Дата/время без валидации | +| `string`, max=255 | `marketplace_alias` | Алиас маркетплейса | + +### Уникальность + +```php +['matrix_erp_id', 'marketplace_id'], 'unique', + 'targetAttribute' => ['matrix_erp_id', 'marketplace_id'], + 'message' => 'Цена для этого товара и маркетплейса уже существует.' +``` + +Это правило гарантирует, что для каждого товара на каждом маркетплейсе может быть только одна текущая цена. + +--- + +## Примеры использования + +### Создание новой цены для товара + +```php +$price = new MarketplacePrices(); +$price->matrix_erp_id = 123; // ID товара из матрицы +$price->marketplace_id = 1; // Яндекс.Маркет +// marketplace_alias заполнится автоматически как 'YM' +$price->price = 2500; +$price->old_price = 3000; // Показываем скидку + +if ($price->save()) { + echo "Цена добавлена"; + echo "Алиас: {$price->marketplace_alias}"; // 'YM' + echo "Создано: {$price->created_at}"; +} +``` + +### Обновление цены с сохранением в истории + +```php +$price = MarketplacePrices::findOne([ + 'matrix_erp_id' => $productId, + 'marketplace_id' => 1 // Яндекс.Маркет +]); + +if ($price) { + // Сохраняем старую цену для истории + $oldPrice = $price->price; + + // Устанавливаем новую цену + $price->old_price = $oldPrice; // Старая цена для скидки + $price->price = 2200; // Новая цена + + if ($price->save()) { + echo "Цена обновлена: {$oldPrice} → {$price->price}"; + echo "Обновлено: {$price->updated_at}"; + } +} +``` + +### Получение цены товара для конкретного маркетплейса + +```php +// Для Яндекс.Маркета +$ymPrice = MarketplacePrices::findOne([ + 'matrix_erp_id' => $productId, + 'marketplace_id' => 1 +]); + +// Для Flowwow +$fwPrice = MarketplacePrices::findOne([ + 'matrix_erp_id' => $productId, + 'marketplace_id' => 2 +]); + +if ($ymPrice) { + echo "Цена на ЯМ: {$ymPrice->price} руб."; + if ($ymPrice->old_price) { + $discount = round((1 - $ymPrice->price / $ymPrice->old_price) * 100); + echo " (скидка {$discount}%)"; + } +} +``` + +### Массовое добавление цен + +```php +$products = [ + ['matrix_erp_id' => 100, 'price' => 2500, 'old_price' => 3000], + ['matrix_erp_id' => 101, 'price' => 1800, 'old_price' => null], + ['matrix_erp_id' => 102, 'price' => 3500, 'old_price' => 4000], +]; + +foreach ($products as $productData) { + $price = new MarketplacePrices(); + $price->matrix_erp_id = $productData['matrix_erp_id']; + $price->marketplace_id = 1; // Яндекс.Маркет + $price->price = $productData['price']; + $price->old_price = $productData['old_price']; + $price->save(); +} +``` + +### Получение истории изменений цены + +```php +$price = MarketplacePrices::findOne($priceId); + +if ($price) { + echo "Текущая цена: {$price->price} руб.\n"; + echo "История изменений:\n"; + + foreach ($price->history as $log) { + echo " {$log->changed_at}: {$log->old_price} → {$log->new_price} руб.\n"; + } +} +``` + +### Получение товара с ценой + +```php +$price = MarketplacePrices::find() + ->where(['matrix_erp_id' => $productId, 'marketplace_id' => 1]) + ->with('matrixErp') // Eager loading + ->one(); + +if ($price) { + echo "Товар: {$price->matrixErp->name}\n"; + echo "Цена: {$price->price} руб.\n"; + echo "Маркетплейс: {$price->marketplace_alias}\n"; +} +``` + +### Получение всех цен товара + +```php +$prices = MarketplacePrices::find() + ->where(['matrix_erp_id' => $productId]) + ->all(); + +echo "Цены товара на маркетплейсах:\n"; +foreach ($prices as $price) { + $marketplace = $price->marketplace_id == 1 ? 'Яндекс.Маркет' : 'Flowwow'; + echo " {$marketplace}: {$price->price} руб.\n"; +} +``` + +### Синхронизация цен между маркетплейсами + +```php +// Получаем базовую цену товара +$basePrice = 2500; + +// Создаем цены для обоих маркетплейсов +$marketplaces = [ + ['id' => 1, 'coefficient' => 1.0], // ЯМ - без наценки + ['id' => 2, 'coefficient' => 1.1], // FW - наценка 10% +]; + +foreach ($marketplaces as $mp) { + $price = MarketplacePrices::findOne([ + 'matrix_erp_id' => $productId, + 'marketplace_id' => $mp['id'] + ]); + + if (!$price) { + $price = new MarketplacePrices(); + $price->matrix_erp_id = $productId; + $price->marketplace_id = $mp['id']; + } + + $price->price = $basePrice * $mp['coefficient']; + $price->save(); +} +``` + +### Поиск товаров со скидками + +```php +$discountedPrices = MarketplacePrices::find() + ->where(['marketplace_id' => 1]) + ->andWhere('old_price IS NOT NULL') + ->andWhere('old_price > price') + ->all(); + +echo "Товары со скидками на Яндекс.Маркете:\n"; +foreach ($discountedPrices as $price) { + $discount = round((1 - $price->price / $price->old_price) * 100); + echo " Товар #{$price->matrix_erp_id}: {$price->old_price} → {$price->price} руб. (-{$discount}%)\n"; +} +``` + +### Статистика цен + +```php +$stats = MarketplacePrices::find() + ->select([ + 'marketplace_id', + 'COUNT(*) as product_count', + 'AVG(price) as avg_price', + 'MIN(price) as min_price', + 'MAX(price) as max_price' + ]) + ->groupBy('marketplace_id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $mp = $stat['marketplace_id'] == 1 ? 'Яндекс.Маркет' : 'Flowwow'; + echo "{$mp}:\n"; + echo " Товаров: {$stat['product_count']}\n"; + echo " Средняя цена: " . round($stat['avg_price'], 2) . " руб.\n"; + echo " Мин. цена: {$stat['min_price']} руб.\n"; + echo " Макс. цена: {$stat['max_price']} руб.\n"; +} +``` + +--- + +## Связь с другими моделями + +Модель напрямую связана с: + +- **MatrixErp** - товары матрицы ERP (через `matrix_erp_id`) +- **MarketplacePricesLog** - история изменений цен (через `id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + marketplace_prices }o--|| matrix_erp : "product" + marketplace_prices ||--o{ marketplace_prices_log : "history" + + marketplace_prices { + int id PK + int matrix_erp_id FK,UK + int marketplace_id UK + string marketplace_alias + float price + float old_price + timestamp created_at + timestamp updated_at + } + + matrix_erp { + int id PK + string name + string articule + float base_price + } + + marketplace_prices_log { + int id PK + int marketplace_prices_id FK + float old_price + float new_price + timestamp changed_at + int changed_by + } +``` + +--- + +## Механизм автоматического заполнения алиаса + +```mermaid +flowchart TD + A[Создание/обновление записи] --> B[save вызывается] + B --> C[beforeSave срабатывает] + C --> D{marketplace_id = 1?} + D -->|Да| E[marketplace_alias = 'YM'] + D -->|Нет| F{marketplace_id = 2?} + F -->|Да| G[marketplace_alias = 'FW'] + F -->|Нет| H[marketplace_alias не изменяется] + E --> I[Сохранение в БД] + G --> I + H --> I + I --> J[Конец] +``` + +--- + +## Процесс обновления цены с историей + +```mermaid +sequenceDiagram + participant User + participant MarketplacePrices + participant DB + participant MarketplacePricesLog + + User->>MarketplacePrices: Загрузить цену + DB->>MarketplacePrices: Текущая цена = 2500 + User->>MarketplacePrices: Установить новую цену = 2200 + MarketplacePrices->>MarketplacePrices: beforeSave() + MarketplacePrices->>DB: UPDATE marketplace_prices + DB-->>MarketplacePrices: OK + MarketplacePrices->>MarketplacePricesLog: Создать запись истории + MarketplacePricesLog->>DB: INSERT (2500 → 2200) + DB-->>MarketplacePricesLog: OK + MarketplacePrices-->>User: Цена обновлена +``` + +--- + +## Пример полного набора данных + +```php +[ + 'id' => 789, + 'matrix_erp_id' => 456, + 'marketplace_id' => 1, + 'marketplace_alias' => 'YM', // Заполнено автоматически + 'price' => 2200.00, + 'old_price' => 2800.00, + 'created_at' => '2025-11-01 10:00:00', + 'updated_at' => '2025-12-11 15:30:00' +] +``` + +**Интерпретация:** +- Товар с ID 456 из матрицы +- Продается на Яндекс.Маркете (YM) +- Текущая цена: 2,200 руб. +- Старая цена (зачеркнутая): 2,800 руб. +- Скидка: 600 руб. (21.4%) +- Запись создана: 1 ноября 2025 +- Последнее обновление: 11 декабря 2025 + +--- + +## Связанные модели + +- **[MatrixErp](./MatrixErp.md)** - товары матрицы ERP +- **MarketplacePricesLog** - история изменений цен +- **[MarketplaceStore](./MarketplaceStore.md)** - магазины маркетплейсов +- **[MarketplaceOrderItems](./MarketplaceOrderItems.md)** - товары в заказах + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MarketplacePricesLog.md b/erp24/docs/models/MarketplacePricesLog.md new file mode 100644 index 00000000..7cc71145 --- /dev/null +++ b/erp24/docs/models/MarketplacePricesLog.md @@ -0,0 +1,259 @@ +# Класс: MarketplacePricesLog + + +## Mindmap + +```mermaid +mindmap + root((MarketplacePricesLog)) + Таблица БД + marketplace_prices_log + Свойства + id + int + marketplace_prices_id + int + action + int + changed_at + string + Связи + Price + 1:1 MarketplacePrices + MatrixErp + 1:1 MatrixErp + ChangedByUser + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель журнала изменений цен на маркетплейсах в ERP24. Хранит историю всех изменений цен товаров с фиксацией значений до и после изменения, а также информацией о пользователе, внёсшем изменение. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_prices_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | Описание | +|-----------|--------------|----------| +| `TimestampBehavior` | createdAt/updatedAt: `changed_at` | Автоматическая метка времени изменения | +| `BlameableBehavior` | createdBy/updatedBy: `changed_by` | Автоматическая фиксация пользователя | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `marketplace_prices_id` | int | FK на запись цены (marketplace_prices.id) | +| `action` | int | Тип действия: 1=создание, 2=обновление | +| `changed_at` | datetime | Дата и время изменения (автоматически) | +| `changed_by` | int / null | ID пользователя, внёсшего изменение (автоматически) | +| `price_before` | float / null | Цена продажи ДО изменения | +| `price_after` | float / null | Цена продажи ПОСЛЕ изменения | +| `old_price_before` | float / null | Старая цена (зачёркнутая) ДО изменения | +| `old_price_after` | float / null | Старая цена (зачёркнутая) ПОСЛЕ изменения | + +## Константы действий + +| Значение | Описание | +|----------|----------| +| `1` | Создание записи цены (create) | +| `2` | Обновление записи цены (update) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getPrice()` | hasOne | MarketplacePrices | Запись цены | +| `getMatrixErp()` | hasOne via | MatrixErp | Матричный букет через price | +| `getChangedByUser()` | hasOne | Admin | Пользователь, изменивший цену | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplacePricesLog { + int id PK + int marketplace_prices_id FK + int action + datetime changed_at + int changed_by FK + float price_before + float price_after + float old_price_before + float old_price_after + } + + MarketplacePrices { + int id PK + int matrix_erp_id FK + float price + float old_price + } + + MatrixErp { + int id PK + varchar name + } + + Admin { + int id PK + varchar username + } + + MarketplacePrices ||--o{ MarketplacePricesLog : "marketplace_prices_id" + Admin ||--o{ MarketplacePricesLog : "changed_by" + MatrixErp ||--o{ MarketplacePrices : "matrix_erp_id" +``` + +## Диаграмма потока данных + +```mermaid +flowchart TD + A[Изменение цены в UI] --> B[MarketplacePrices::save] + B --> C{Новая запись?} + C -->|Да| D[action = 1
    price_before = null] + C -->|Нет| E[action = 2
    price_before = old value] + D --> F[Создание MarketplacePricesLog] + E --> F + F --> G[TimestampBehavior
    changed_at = NOW] + G --> H[BlameableBehavior
    changed_by = user_id] + H --> I[Сохранение в БД] +``` + +## Примеры использования + +### Логирование изменения цены +```php +// При обновлении цены в MarketplacePrices +$price = MarketplacePrices::findOne($priceId); +$oldPrice = $price->price; +$oldOldPrice = $price->old_price; + +$price->price = 1500.00; +$price->old_price = 2000.00; + +if ($price->save()) { + $log = new MarketplacePricesLog(); + $log->marketplace_prices_id = $price->id; + $log->action = 2; // update + $log->price_before = $oldPrice; + $log->price_after = $price->price; + $log->old_price_before = $oldOldPrice; + $log->old_price_after = $price->old_price; + // changed_at и changed_by заполнятся автоматически + $log->save(); +} +``` + +### Получение истории изменений цены +```php +$history = MarketplacePricesLog::find() + ->where(['marketplace_prices_id' => $priceId]) + ->with(['changedByUser']) + ->orderBy(['changed_at' => SORT_DESC]) + ->all(); + +foreach ($history as $log) { + $action = $log->action === 1 ? 'Создана' : 'Изменена'; + $user = $log->changedByUser->username ?? 'Система'; + + echo "{$log->changed_at}: {$action} пользователем {$user}\n"; + echo "Цена: {$log->price_before} → {$log->price_after}\n"; + echo "Старая цена: {$log->old_price_before} → {$log->old_price_after}\n\n"; +} +``` + +### Аналитика изменений цен за период +```php +$stats = MarketplacePricesLog::find() + ->select([ + 'COUNT(*) as total_changes', + 'COUNT(DISTINCT marketplace_prices_id) as unique_prices', + 'COUNT(DISTINCT changed_by) as unique_users', + ]) + ->where(['>=', 'changed_at', '2024-01-01']) + ->andWhere(['<=', 'changed_at', '2024-01-31']) + ->asArray() + ->one(); +``` + +### Поиск значительных изменений цен +```php +// Изменения более чем на 20% +$significantChanges = MarketplacePricesLog::find() + ->where(['action' => 2]) + ->andWhere(['not', ['price_before' => null]]) + ->andWhere(['not', ['price_after' => null]]) + ->andWhere('ABS(price_after - price_before) / price_before > 0.2') + ->with(['price.matrixErp', 'changedByUser']) + ->all(); + +foreach ($significantChanges as $log) { + $percent = round(($log->price_after - $log->price_before) / $log->price_before * 100, 1); + echo "{$log->price->matrixErp->name}: {$percent}% изменение\n"; +} +``` + +### Откат изменения цены +```php +$lastChange = MarketplacePricesLog::find() + ->where(['marketplace_prices_id' => $priceId]) + ->orderBy(['changed_at' => SORT_DESC]) + ->one(); + +if ($lastChange && $lastChange->action === 2) { + $price = $lastChange->price; + $price->price = $lastChange->price_before; + $price->old_price = $lastChange->old_price_before; + $price->save(); + + // Логируем откат как новое изменение +} +``` + +### Отчёт по изменениям пользователя +```php +$userChanges = MarketplacePricesLog::find() + ->where(['changed_by' => $userId]) + ->andWhere(['>=', 'changed_at', date('Y-m-01')]) + ->count(); + +echo "Пользователь внёс {$userChanges} изменений цен в этом месяце"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `marketplace_prices_id` | required, integer | +| `action` | required, integer | +| `changed_at` | safe (автоматически) | +| `changed_by` | integer, default: null (автоматически) | +| `price_before` | number, default: null | +| `price_after` | number, default: null | +| `old_price_before` | number, default: null | +| `old_price_after` | number, default: null | + +## Связанные модели + +- [MarketplacePrices](./MarketplacePrices.md) — текущие цены на маркетплейсах +- [MatrixErp](./MatrixErp.md) — матричные букеты +- [Admin](./Admin.md) — пользователи системы + +## Особенности реализации + +1. **Автоматический аудит**: TimestampBehavior и BlameableBehavior заполняют changed_at и changed_by +2. **Полная история**: Сохраняются значения до и после для возможности отката +3. **Два типа действий**: Разделение на создание (1) и обновление (2) +4. **Двойная цена**: Отслеживается и основная цена (price), и старая/зачёркнутая (old_price) +5. **Связь через via**: Доступ к MatrixErp через промежуточную связь price +6. **Аналитика**: Возможность анализа частоты и масштаба изменений цен diff --git a/erp24/docs/models/MarketplacePricesLogSearch.md b/erp24/docs/models/MarketplacePricesLogSearch.md new file mode 100644 index 00000000..86660b50 --- /dev/null +++ b/erp24/docs/models/MarketplacePricesLogSearch.md @@ -0,0 +1,229 @@ +# Класс: MarketplacePricesLogSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplacePricesLogSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplacePricesLog +``` + +## Назначение +Search-модель для поиска и фильтрации истории изменений цен на маркетплейсах в ERP24. Расширенная модель с JOIN к матрице ERP и таблице администраторов, поддержкой кастомной сортировки по артикулу и имени изменившего пользователя. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplacePricesLog` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$articule` | string | Артикул из MatrixErp для поиска | +| `$changed_by_name` | string | Имя администратора, изменившего цену | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `marketplace_prices_id`, `action`, `changed_by` — integer +- `changed_at`, `articule`, `changed_by_name` — safe +- `price_before`, `price_after`, `old_price_before`, `old_price_after` — number + +### attributeLabels() +**Описание:** Метки атрибутов с дополнениями для связанных полей. + +**Возвращает:** `array` — метки атрибутов + +**Дополнительные метки:** +- `articule` — "Артикул" +- `changed_by_name` — "Кем изменено" + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с множественными JOIN и кастомной сортировкой. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с алиасом `l` для основной таблицы +2. Выполняет innerJoinWith для price (алиас p) и price.matrixErp (алиас me) +3. Выполняет leftJoin для admin (алиас u) по changed_by +4. Загружает связи с помощью with() +5. Устанавливает сортировку по умолчанию: changed_at DESC, id DESC +6. Добавляет кастомные атрибуты сортировки: articule, changed_by_name +7. Применяет фильтры: + - Точное совпадение: id, marketplace_prices_id, action, changed_by, price_before, price_after, old_price_before, old_price_after, changed_at + - ilike: me.articule, u.name + +## Диаграмма связей + +```mermaid +erDiagram + MarketplacePricesLog { + int id PK + int marketplace_prices_id FK + int action + int changed_by FK + datetime changed_at + decimal price_before + decimal price_after + decimal old_price_before + decimal old_price_after + } + + MarketplacePrices { + int id PK + int matrix_erp_id FK + decimal price + decimal old_price + } + + MatrixErp { + int id PK + varchar articule + varchar name + } + + Admin { + int id PK + varchar name + } + + MarketplacePricesLog }o--|| MarketplacePrices : "marketplace_prices_id" + MarketplacePricesLog }o--o| Admin : "changed_by" + MarketplacePrices }o--|| MatrixErp : "matrix_erp_id" +``` + +## Диаграмма изменения цен + +```mermaid +flowchart TD + A[Изменение цены] --> B{Тип действия} + B -->|Создание| C[action = 1] + B -->|Обновление| D[action = 2] + B -->|Удаление| E[action = 3] + + C --> F[price_after = новая цена] + D --> G[price_before → price_after] + E --> H[price_before = старая цена] + + I[old_price_before] --> J[Старая цена до] + K[old_price_after] --> L[Старая цена после] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplacePricesLogSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по артикулу +```php +$searchModel = new MarketplacePricesLogSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesLogSearch' => [ + 'articule' => 'FLOWER-001', + ] +]); +``` + +### Поиск по имени изменившего +```php +$searchModel = new MarketplacePricesLogSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesLogSearch' => [ + 'changed_by_name' => 'Иванов', + ] +]); +``` + +### Поиск по типу действия +```php +$searchModel = new MarketplacePricesLogSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesLogSearch' => [ + 'action' => 2, // Обновление + ] +]); +``` + +### Поиск по ID цены +```php +$searchModel = new MarketplacePricesLogSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesLogSearch' => [ + 'marketplace_prices_id' => 12345, + ] +]); +``` + +### GridView с сортировкой +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'articule', + 'value' => 'price.matrixErp.articule', + ], + [ + 'attribute' => 'changed_by_name', + 'value' => 'changedByUser.name', + ], + 'price_before', + 'price_after', + 'old_price_before', + 'old_price_after', + 'changed_at', + 'action', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplacePricesLog](./MarketplacePricesLog.md) — базовая модель лога цен +- [MarketplacePrices](./MarketplacePrices.md) — цены на маркетплейсах +- [MatrixErp](./MatrixErp.md) — матрица ERP товаров +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Множественные JOIN**: innerJoinWith для price и matrixErp, leftJoin для admin +2. **Алиасы таблиц**: l, p, me, u для всех таблиц +3. **Сортировка по умолчанию**: changed_at DESC, id DESC (новые изменения сверху) +4. **Кастомная сортировка**: Добавлены атрибуты articule и changed_by_name +5. **Eager loading**: with() для оптимизации загрузки связей +6. **Аудит изменений**: price_before/after, old_price_before/after для полной истории +7. **Типы действий**: action указывает на тип изменения (создание/обновление/удаление) diff --git a/erp24/docs/models/MarketplacePricesSearch.md b/erp24/docs/models/MarketplacePricesSearch.md new file mode 100644 index 00000000..e133be7a --- /dev/null +++ b/erp24/docs/models/MarketplacePricesSearch.md @@ -0,0 +1,219 @@ +# Класс: MarketplacePricesSearch + + +## Mindmap + +```mermaid +mindmap + root((MarketplacePricesSearch)) + Таблица БД + ActiveRecord + Наследование + extends MarketplacePrices +``` + +## Назначение +Search-модель для поиска и фильтрации цен товаров на маркетплейсах в ERP24. Расширенная модель с JOIN к матрице ERP для поиска по артикулу и наименованию товара. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MarketplacePrices` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$articule` | string | Артикул из MatrixErp | +| `$product_name` | string | Наименование товара из MatrixErp | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `matrix_erp_id`, `marketplace_id` — integer +- `marketplace_alias`, `created_at`, `updated_at` — safe +- `price`, `old_price` — number +- `articule`, `product_name` — safe + +### attributeLabels() +**Описание:** Метки атрибутов с дополнениями для связанных полей. + +**Возвращает:** `array` — метки атрибутов + +**Дополнительные метки:** +- `articule` — "Артикул" +- `product_name` — "Наименование" + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к матрице ERP и кастомной сортировкой. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с алиасом `mp` для основной таблицы +2. Выполняет innerJoinWith для matrixErp (алиас me) +3. Загружает связь matrixErp с помощью with() +4. Добавляет кастомные атрибуты сортировки: articule (me.articule), product_name (me.name) +5. Применяет фильтры: + - Точное совпадение: id, matrix_erp_id, marketplace_id, price, old_price, created_at, updated_at + - ilike: marketplace_alias, me.articule, me.name + +## Диаграмма связей + +```mermaid +erDiagram + MarketplacePrices { + int id PK + int matrix_erp_id FK + int marketplace_id + varchar marketplace_alias + decimal price + decimal old_price + datetime created_at + datetime updated_at + } + + MatrixErp { + int id PK + varchar articule + varchar name + } + + MarketplacePrices }o--|| MatrixErp : "matrix_erp_id" +``` + +## Диаграмма ценообразования + +```mermaid +flowchart LR + A[MatrixErp] --> B[MarketplacePrices] + B --> C{Маркетплейс} + C -->|Яндекс| D[marketplace_id = 1] + C -->|Ozon| E[marketplace_id = 2] + C -->|Wildberries| F[marketplace_id = 3] + C -->|Flowwow| G[marketplace_id = 4] + + B --> H[price - текущая цена] + B --> I[old_price - зачёркнутая цена] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MarketplacePricesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по артикулу +```php +$searchModel = new MarketplacePricesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesSearch' => [ + 'articule' => 'FLOWER-001', + ] +]); +``` + +### Поиск по наименованию +```php +$searchModel = new MarketplacePricesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesSearch' => [ + 'product_name' => 'Букет роз', + ] +]); +``` + +### Поиск по маркетплейсу +```php +$searchModel = new MarketplacePricesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesSearch' => [ + 'marketplace_id' => 1, // Яндекс Маркет + ] +]); +``` + +### Поиск по алиасу маркетплейса +```php +$searchModel = new MarketplacePricesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesSearch' => [ + 'marketplace_alias' => 'yandex', + ] +]); +``` + +### Поиск по цене +```php +$searchModel = new MarketplacePricesSearch(); +$dataProvider = $searchModel->search([ + 'MarketplacePricesSearch' => [ + 'price' => 2500, + ] +]); +``` + +### GridView с сортировкой по артикулу +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'articule', + 'value' => 'matrixErp.articule', + ], + [ + 'attribute' => 'product_name', + 'value' => 'matrixErp.name', + ], + 'marketplace_alias', + 'price', + 'old_price', + 'created_at', + 'updated_at', + ], +]) ?> +``` + +## Связанные модели + +- [MarketplacePrices](./MarketplacePrices.md) — базовая модель цен +- [MatrixErp](./MatrixErp.md) — матрица ERP товаров +- [MarketplacePricesLog](./MarketplacePricesLog.md) — история изменений цен + +## Особенности реализации + +1. **JOIN к матрице ERP**: innerJoinWith для доступа к артикулу и наименованию +2. **Алиасы таблиц**: mp для MarketplacePrices, me для MatrixErp +3. **Кастомная сортировка**: Добавлены атрибуты articule и product_name +4. **Eager loading**: with(['matrixErp']) для оптимизации +5. **Двойная цена**: price (текущая) и old_price (зачёркнутая/старая) +6. **Мультимаркетплейс**: marketplace_id и marketplace_alias для различных площадок +7. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MarketplacePriority.md b/erp24/docs/models/MarketplacePriority.md new file mode 100644 index 00000000..efb7c634 --- /dev/null +++ b/erp24/docs/models/MarketplacePriority.md @@ -0,0 +1,260 @@ +# Класс: MarketplacePriority + + +## Mindmap + +```mermaid +mindmap + root((MarketplacePriority)) + Таблица БД + marketplace_priority + Свойства + id + int + guid + string + reminder_koef + float + minimal_quantity + int + created_at + string + created_by + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель приоритетов товаров на маркетплейсах в ERP24. Хранит настройки приоритизации матричных букетов для управления доступностью товаров на маркетплейсах на основе коэффициента остатка и минимального количества. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`marketplace_priority` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: `updated_at` | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(36) | GUID матричного букета | +| `reminder_koef` | float | Коэффициент остатка (множитель для расчёта доступности) | +| `minimal_quantity` | int | Минимальное количество для отображения на маркетплейсе | +| `created_at` | datetime | Дата создания записи (автоматически) | +| `created_by` | int / null | ID пользователя, создавшего запись | +| `updated_at` | datetime / null | Дата обновления записи (автоматически) | +| `updated_by` | int / null | ID пользователя, обновившего запись | + +## Диаграмма связей + +```mermaid +erDiagram + MarketplacePriority { + int id PK + varchar guid FK + float reminder_koef + int minimal_quantity + datetime created_at + int created_by FK + datetime updated_at + int updated_by FK + } + + MatrixErp { + varchar guid PK + varchar name + int quantity + } + + Admin { + int id PK + varchar username + } + + MatrixErp ||--o| MarketplacePriority : "guid" + Admin ||--o{ MarketplacePriority : "created_by" + Admin ||--o{ MarketplacePriority : "updated_by" +``` + +## Диаграмма логики приоритизации + +```mermaid +flowchart TD + A[Запрос доступности товара] --> B{Есть запись в
    MarketplacePriority?} + B -->|Нет| C[Использовать
    стандартные настройки] + B -->|Да| D[Загрузить настройки] + D --> E[Получить остаток товара] + E --> F{остаток >= minimal_quantity?} + F -->|Нет| G[Товар недоступен
    на маркетплейсе] + F -->|Да| H[Рассчитать доступное кол-во] + H --> I[available = остаток × reminder_koef] + I --> J[Товар доступен
    с количеством = available] + C --> K[Стандартный расчёт] +``` + +## Методы + +### beforeSave($insert) +**Описание:** Автоматически заполняет поля created_by и updated_by текущим пользователем. + +**Логика работы:** +- При создании записи: заполняет created_by и updated_by +- При обновлении: заполняет только updated_by + +**Параметры:** +- `$insert` (bool) — true при создании новой записи + +**Возвращает:** `bool` — результат родительского beforeSave + +```php +public function beforeSave($insert): bool +``` + +## Примеры использования + +### Создание настройки приоритета +```php +$priority = new MarketplacePriority(); +$priority->guid = '12345678-1234-1234-1234-123456789abc'; +$priority->reminder_koef = 0.8; // 80% от остатка +$priority->minimal_quantity = 5; // Минимум 5 штук +$priority->save(); +// created_at, updated_at заполнятся автоматически +// created_by, updated_by заполнятся в beforeSave +``` + +### Получение настроек для букета +```php +$priority = MarketplacePriority::find() + ->where(['guid' => $bouquetGuid]) + ->one(); + +if ($priority) { + $availableQuantity = floor($currentStock * $priority->reminder_koef); + $isAvailable = $currentStock >= $priority->minimal_quantity; +} +``` + +### Расчёт доступного количества для маркетплейса +```php +function getMarketplaceAvailability($bouquetGuid, $currentStock) +{ + $priority = MarketplacePriority::find() + ->where(['guid' => $bouquetGuid]) + ->one(); + + if (!$priority) { + // Стандартные настройки + return [ + 'available' => true, + 'quantity' => $currentStock, + ]; + } + + if ($currentStock < $priority->minimal_quantity) { + return [ + 'available' => false, + 'quantity' => 0, + ]; + } + + return [ + 'available' => true, + 'quantity' => floor($currentStock * $priority->reminder_koef), + ]; +} +``` + +### Массовое обновление коэффициентов +```php +// Уменьшить доступность всех товаров на 10% +MarketplacePriority::updateAll( + ['reminder_koef' => new Expression('reminder_koef * 0.9')], + ['>', 'reminder_koef', 0.1] +); +``` + +### Получение товаров с особыми настройками +```php +$prioritizedItems = MarketplacePriority::find() + ->where(['<', 'reminder_koef', 1.0]) // Товары с ограниченной доступностью + ->orWhere(['>', 'minimal_quantity', 1]) + ->all(); + +foreach ($prioritizedItems as $item) { + echo "GUID: {$item->guid}\n"; + echo "Коэффициент: {$item->reminder_koef}\n"; + echo "Минимум: {$item->minimal_quantity}\n\n"; +} +``` + +### Аудит изменений настроек +```php +$recentChanges = MarketplacePriority::find() + ->where(['>=', 'updated_at', date('Y-m-d', strtotime('-7 days'))]) + ->with(['updatedByUser']) + ->orderBy(['updated_at' => SORT_DESC]) + ->all(); + +foreach ($recentChanges as $item) { + echo "{$item->updated_at}: изменён {$item->guid}\n"; + echo "Пользователь: {$item->updatedByUser->username}\n"; +} +``` + +### Синхронизация с маркетплейсом +```php +$priorities = MarketplacePriority::find()->indexBy('guid')->all(); + +foreach ($bouquets as $bouquet) { + $priority = $priorities[$bouquet->guid] ?? null; + + $availableQty = $priority + ? floor($bouquet->stock * $priority->reminder_koef) + : $bouquet->stock; + + $minQty = $priority ? $priority->minimal_quantity : 1; + + if ($bouquet->stock >= $minQty) { + $api->updateStock($bouquet->external_id, $availableQty); + } else { + $api->setOutOfStock($bouquet->external_id); + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | required, string (max 36) | +| `reminder_koef` | required, number | +| `minimal_quantity` | required, integer | +| `created_by` | integer, default: null | +| `updated_by` | integer, default: null | +| `created_at`, `updated_at` | safe (автоматически) | + +## Связанные модели + +- [MatrixErp](./MatrixErp.md) — матричные букеты (связь по guid) +- [Admin](./Admin.md) — пользователи (created_by, updated_by) + +## Особенности реализации + +1. **Коэффициент остатка**: reminder_koef определяет, какую долю реального остатка показывать на маркетплейсе +2. **Минимальный порог**: minimal_quantity — порог, ниже которого товар скрывается с маркетплейса +3. **Полный аудит**: TimestampBehavior + ручное заполнение created_by/updated_by в beforeSave +4. **Связь по GUID**: Привязка к матричным букетам через GUID для синхронизации с 1С +5. **Гибкое управление**: Позволяет индивидуально настраивать доступность каждого товара +6. **Защита от перепродажи**: Помогает избежать ситуации, когда товар продан на маркетплейсе, но отсутствует на складе diff --git a/erp24/docs/models/MarketplaceStatus.md b/erp24/docs/models/MarketplaceStatus.md new file mode 100644 index 00000000..f6cf4be4 --- /dev/null +++ b/erp24/docs/models/MarketplaceStatus.md @@ -0,0 +1,488 @@ +# Модель MarketplaceStatus + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceStatus)) + Таблица БД + marketplace_status + Свойства + id + int + alias + string + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MarketplaceStatus` представляет справочник статусов заказов маркетплейсов (Яндекс.Маркет, Flowwow). Хранит соответствие между алиасами статусов из API маркетплейсов и их русскоязычными названиями. Используется для отображения статусов в понятном виде в интерфейсе системы. + +**Файл модели:** `erp24/records/MarketplaceStatus.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `marketplace_status` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `alias` | VARCHAR(100) | Алиас статуса из API (обязательное) | +| `name` | VARCHAR(100) | Наименование статуса на русском (обязательное) | + +--- + +## Правила валидации + +### Обязательные поля + +```php +['alias', 'name'], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `string`, max=100 | `alias`, `name` | Максимум 100 символов | + +--- + +## Типовые статусы + +### Яндекс.Маркет + +| Alias | Название | Описание | +|-------|----------|----------| +| `PLACING` | Размещается | Заказ только создается в системе | +| `UNPAID` | Не оплачен | Ожидание оплаты | +| `RESERVED` | Зарезервирован | Товар зарезервирован | +| `PROCESSING` | В обработке | Заказ обрабатывается | +| `PICKUP` | Готов к выдаче | Готов к самовывозу | +| `DELIVERY` | Передан в доставку | Отправлен курьеру | +| `DELIVERED` | Доставлен | Успешно доставлен | +| `CANCELLED` | Отменен | Заказ отменен | +| `RETURNED` | Возвращен | Заказ возвращен | +| `LOST` | Утерян | Заказ утерян при доставке | + +### Flowwow + +| Alias | Название | Описание | +|-------|----------|----------| +| `NEW` | Новый | Новый заказ | +| `CONFIRMED` | Подтвержден | Магазин подтвердил заказ | +| `PREPARING` | Готовится | Заказ собирается | +| `READY` | Готов | Заказ готов к отправке | +| `IN_DELIVERY` | В доставке | Заказ у курьера | +| `COMPLETED` | Выполнен | Заказ успешно доставлен | +| `CANCELLED` | Отменен | Заказ отменен | + +--- + +## Примеры использования + +### Получение всех статусов + +```php +$statuses = MarketplaceStatus::find()->all(); + +foreach ($statuses as $status) { + echo "{$status->alias}: {$status->name}\n"; +} +``` + +### Получение названия статуса по алиасу + +```php +$status = MarketplaceStatus::findOne(['alias' => 'DELIVERED']); + +if ($status) { + echo "Статус: {$status->name}"; +} else { + echo "Статус не найден"; +} +``` + +### Создание нового статуса + +```php +$status = new MarketplaceStatus(); +$status->alias = 'PROCESSING'; +$status->name = 'В обработке'; + +if ($status->save()) { + echo "Статус добавлен"; +} +``` + +### Обновление названия статуса + +```php +$status = MarketplaceStatus::findOne(['alias' => 'DELIVERED']); + +if ($status) { + $status->name = 'Успешно доставлен'; + $status->save(); +} +``` + +### Получение списка для выпадающего списка + +```php +$statusList = MarketplaceStatus::find() + ->select(['id', 'name']) + ->indexBy('id') + ->column(); + +// Результат: [1 => 'Новый', 2 => 'В обработке', ...] +``` + +### Получение списка алиасов + +```php +$aliasList = MarketplaceStatus::find() + ->select(['alias', 'name']) + ->indexBy('alias') + ->column(); + +// Результат: ['NEW' => 'Новый', 'PROCESSING' => 'В обработке', ...] +``` + +### Массовое добавление статусов + +```php +$statuses = [ + ['alias' => 'NEW', 'name' => 'Новый'], + ['alias' => 'CONFIRMED', 'name' => 'Подтвержден'], + ['alias' => 'PREPARING', 'name' => 'Готовится'], + ['alias' => 'READY', 'name' => 'Готов'], + ['alias' => 'IN_DELIVERY', 'name' => 'В доставке'], + ['alias' => 'COMPLETED', 'name' => 'Выполнен'], + ['alias' => 'CANCELLED', 'name' => 'Отменен'], +]; + +foreach ($statuses as $statusData) { + $existing = MarketplaceStatus::findOne(['alias' => $statusData['alias']]); + + if (!$existing) { + $status = new MarketplaceStatus(); + $status->alias = $statusData['alias']; + $status->name = $statusData['name']; + $status->save(); + } +} +``` + +### Получение статистики заказов по статусам + +```php +$stats = Yii::$app->db->createCommand(" + SELECT + ms.name, + COUNT(mo.id) as order_count, + SUM(mo.total) as total_amount + FROM marketplace_orders mo + JOIN marketplace_status ms ON mo.status_id = ms.id + GROUP BY ms.id, ms.name + ORDER BY order_count DESC +")->queryAll(); + +foreach ($stats as $row) { + echo "{$row['name']}: {$row['order_count']} заказов на сумму {$row['total_amount']} руб.\n"; +} +``` + +### Проверка существования статуса по алиасу + +```php +function statusExists($alias) { + return MarketplaceStatus::find() + ->where(['alias' => $alias]) + ->exists(); +} + +if (statusExists('DELIVERED')) { + echo "Статус существует"; +} +``` + +### Синхронизация статусов с API маркетплейса + +```php +// Получаем статусы из API +$apiStatuses = [ + ['code' => 'PLACING', 'name' => 'Размещается'], + ['code' => 'PROCESSING', 'name' => 'В обработке'], + ['code' => 'DELIVERED', 'name' => 'Доставлен'], + // ... другие статусы +]; + +foreach ($apiStatuses as $apiStatus) { + $status = MarketplaceStatus::findOne(['alias' => $apiStatus['code']]); + + if ($status) { + // Обновляем название + $status->name = $apiStatus['name']; + $status->save(); + } else { + // Создаем новый статус + $status = new MarketplaceStatus(); + $status->alias = $apiStatus['code']; + $status->name = $apiStatus['name']; + $status->save(); + } +} +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **MarketplaceOrders** - заказы маркетплейсов (через `status_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + marketplace_status ||--o{ marketplace_orders : "has_orders" + + marketplace_status { + int id PK + string alias UK + string name + } + + marketplace_orders { + int id PK + string marketplace_order_id + int status_id FK + int store_id + float total + } +``` + +--- + +## Жизненный цикл заказа через статусы + +```mermaid +stateDiagram-v2 + direction LR + + [*] --> NEW: Новый заказ + NEW --> CONFIRMED: Подтверждение + CONFIRMED --> PREPARING: Начало сборки + PREPARING --> READY: Готов + READY --> IN_DELIVERY: Передача курьеру + IN_DELIVERY --> COMPLETED: Доставка + COMPLETED --> [*] + + NEW --> CANCELLED: Отмена + CONFIRMED --> CANCELLED: Отмена + PREPARING --> CANCELLED: Отмена + READY --> CANCELLED: Отмена + IN_DELIVERY --> RETURNED: Возврат + CANCELLED --> [*] + RETURNED --> [*] +``` + +--- + +## Типовая последовательность статусов + +### Для Яндекс.Маркет + +```mermaid +flowchart LR + A[PLACING] --> B[RESERVED] + B --> C[PROCESSING] + C --> D[DELIVERY] + D --> E[DELIVERED] + + style A fill:#ffcccc + style E fill:#ccffcc +``` + +### Для Flowwow + +```mermaid +flowchart LR + A[NEW] --> B[CONFIRMED] + B --> C[PREPARING] + C --> D[READY] + D --> E[IN_DELIVERY] + E --> F[COMPLETED] + + style A fill:#ffcccc + style F fill:#ccffcc +``` + +--- + +## Пример полного справочника статусов + +```php +// Типичный набор статусов для маркетплейсов +$standardStatuses = [ + // Начальные статусы + [ + 'alias' => 'NEW', + 'name' => 'Новый', + 'group' => 'initial' + ], + [ + 'alias' => 'PLACING', + 'name' => 'Размещается', + 'group' => 'initial' + ], + [ + 'alias' => 'UNPAID', + 'name' => 'Не оплачен', + 'group' => 'initial' + ], + + // Рабочие статусы + [ + 'alias' => 'RESERVED', + 'name' => 'Зарезервирован', + 'group' => 'processing' + ], + [ + 'alias' => 'CONFIRMED', + 'name' => 'Подтвержден', + 'group' => 'processing' + ], + [ + 'alias' => 'PROCESSING', + 'name' => 'В обработке', + 'group' => 'processing' + ], + [ + 'alias' => 'PREPARING', + 'name' => 'Готовится', + 'group' => 'processing' + ], + + // Готовность + [ + 'alias' => 'READY', + 'name' => 'Готов', + 'group' => 'ready' + ], + [ + 'alias' => 'PICKUP', + 'name' => 'Готов к выдаче', + 'group' => 'ready' + ], + + // Доставка + [ + 'alias' => 'DELIVERY', + 'name' => 'Передан в доставку', + 'group' => 'delivery' + ], + [ + 'alias' => 'IN_DELIVERY', + 'name' => 'В доставке', + 'group' => 'delivery' + ], + + // Финальные статусы + [ + 'alias' => 'DELIVERED', + 'name' => 'Доставлен', + 'group' => 'final' + ], + [ + 'alias' => 'COMPLETED', + 'name' => 'Выполнен', + 'group' => 'final' + ], + [ + 'alias' => 'CANCELLED', + 'name' => 'Отменен', + 'group' => 'final' + ], + [ + 'alias' => 'RETURNED', + 'name' => 'Возвращен', + 'group' => 'final' + ], + [ + 'alias' => 'LOST', + 'name' => 'Утерян', + 'group' => 'final' + ], +]; +``` + +--- + +## Вспомогательные методы + +### Получение ID статуса по алиасу + +```php +public static function getIdByAlias($alias) +{ + $status = self::findOne(['alias' => $alias]); + return $status ? $status->id : null; +} + +// Использование +$statusId = MarketplaceStatus::getIdByAlias('DELIVERED'); +``` + +### Получение названия по алиасу + +```php +public static function getNameByAlias($alias) +{ + $status = self::findOne(['alias' => $alias]); + return $status ? $status->name : $alias; +} + +// Использование +$statusName = MarketplaceStatus::getNameByAlias('PROCESSING'); +echo $statusName; // "В обработке" +``` + +### Проверка финального статуса + +```php +public static function isFinalStatus($alias) +{ + $finalStatuses = ['DELIVERED', 'COMPLETED', 'CANCELLED', 'RETURNED', 'LOST']; + return in_array($alias, $finalStatuses); +} + +// Использование +if (MarketplaceStatus::isFinalStatus('DELIVERED')) { + echo "Заказ завершен"; +} +``` + +--- + +## Связанные модели + +- **[MarketplaceOrders](./MarketplaceOrders.md)** - заказы маркетплейсов +- **[OrdersStatus](./OrdersStatus.md)** - статусы заказов AMO +- **[OrdersUnion](./OrdersUnion.md)** - объединение заказов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MarketplaceStore.md b/erp24/docs/models/MarketplaceStore.md new file mode 100644 index 00000000..687d89f6 --- /dev/null +++ b/erp24/docs/models/MarketplaceStore.md @@ -0,0 +1,650 @@ +# Модель MarketplaceStore + + +## Mindmap + +```mermaid +mindmap + root((MarketplaceStore)) + Таблица БД + marketplace_store + Свойства + id + int + store_id + int + guid + string + warehouse_id + int + warehouse_guid + string + firm + string + Связи + Store + 1:1 CityStore + CreatedBy + 1:1 Admin + UpdatedBy + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MarketplaceStore` управляет связью между магазинами ERP24 и складами маркетплейсов (Яндекс.Маркет, Flowwow). Хранит соответствия между внутренними магазинами компании и складами/аккаунтами на маркетплейсах, включая GUID, юридическое лицо, email аккаунта и статус активности фида. Используется для маршрутизации заказов и управления ассортиментом. + +**Файл модели:** `erp24/records/MarketplaceStore.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `marketplace_store` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Константы + +### Идентификаторы маркетплейсов + +```php +const FLOWWOW_WAREHOUSE_ID = 1; +const YANDEX_WAREHOUSE_ID = 2; +``` + +--- + +## Поля таблицы + +### Идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `store_id` | INTEGER | ID магазина в ERP24 (обязательное, FK → CityStore) | +| `guid` | TEXT | GUID магазина из 1С (обязательное) | +| `warehouse_id` | INTEGER | ID маркетплейса: 1 - Flowwow, 2 - Яндекс.Маркет (обязательное) | +| `warehouse_guid` | TEXT | GUID склада в маркетплейсе (обязательное) | + +### Юридическая информация + +| Поле | Тип | Описание | +|------|-----|----------| +| `firm` | TEXT | Юридическое лицо, к которому относится магазин (обязательное) | + +### Настройки + +| Поле | Тип | Описание | +|------|-----|----------| +| `account_email` | TEXT | Email аккаунта, привязанного к магазину (уникальное) | +| `is_feed_active` | INTEGER | Активность фида (0 - неактивен, 1 - активен) | + +### Метаданные + +| Поле | Тип | Описание | +|------|-----|----------| +| `created_at` | TIMESTAMP | Дата и время создания записи (автоматически) | +| `created_by` | INTEGER | ID сотрудника, создавшего запись (автоматически) | +| `updated_at` | TIMESTAMP | Дата и время последнего обновления (автоматически) | +| `updated_by` | INTEGER | ID сотрудника, обновившего запись (автоматически) | + +--- + +## Behaviors (поведения) + +### TimestampBehavior + +Автоматически устанавливает временные метки. + +```php +[ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new Expression('NOW()') +] +``` + +**Логика работы:** +- При создании записи заполняет `created_at` и `updated_at` +- При обновлении записи обновляет только `updated_at` +- Использует функцию PostgreSQL `NOW()` для текущего времени + +--- + +### BlameableBehavior + +Автоматически сохраняет информацию о пользователе. + +```php +[ + 'class' => BlameableBehavior::class, + 'createdByAttribute' => 'created_by', + 'updatedByAttribute' => 'updated_by', +] +``` + +**Логика работы:** +- При создании записи заполняет `created_by` ID текущего пользователя +- При обновлении записи заполняет `updated_by` ID текущего пользователя +- Использует `Yii::$app->user->id` для получения ID + +--- + +## Правила валидации + +### Обязательные поля + +```php +['store_id', 'guid', 'warehouse_id', 'warehouse_guid', 'firm'], 'required' +``` + +### Внешние ключи + +```php +['store_id'], 'exist', + 'targetClass' => CityStore::class, + 'targetAttribute' => 'id' +``` + +Проверяет, что `store_id` существует в таблице `CityStore`. + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `store_id`, `created_by`, `is_feed_active`, `updated_by`, `warehouse_id` | Целочисленные значения | +| `string` | `name`, `guid`, `firm`, `warehouse_guid`, `account_email` | Текстовые поля | +| `datetime` | `created_at`, `updated_at` | Формат: Y-m-d H:i:s | + +### Уникальность + +```php +['account_email'], 'unique', + 'message' => 'Этот email уже используется.' +``` + +Гарантирует, что один email не может быть привязан к нескольким магазинам. + +--- + +## Методы + +### `getWarehouseId()` + +Возвращает массив доступных маркетплейсов. + +```php +public static function getWarehouseId() +{ + return array( + '1' => 'Flowwow', + '2' => 'Яндекс Маркет', + ); +} +``` + +**Возвращает:** `array` - ассоциативный массив [ID => Название] + +**Использование:** +```php +$marketplaces = MarketplaceStore::getWarehouseId(); +// ['1' => 'Flowwow', '2' => 'Яндекс Маркет'] +``` + +--- + +### `getWarehouseGuidByAccountEmail($accountEmail)` + +Получает warehouse_guid по email аккаунта. + +```php +public static function getWarehouseGuidByAccountEmail($accountEmail) +{ + $model = self::findOne(['account_email' => $accountEmail]); + return $model ? $model->warehouse_guid : null; +} +``` + +**Параметры:** +- `$accountEmail` (string) - Email аккаунта маркетплейса + +**Возвращает:** `string|null` - GUID склада или null, если не найдено + +**Логика работы:** +1. Ищет запись по `account_email` +2. Если найдена - возвращает `warehouse_guid` +3. Если не найдена - возвращает `null` + +**Вызовы сторонних методов:** +- `self::findOne()` - поиск записи в БД + +**Использование:** +```php +$warehouseGuid = MarketplaceStore::getWarehouseGuidByAccountEmail('store@example.com'); + +if ($warehouseGuid) { + echo "GUID склада: {$warehouseGuid}"; +} +``` + +--- + +### `getStore()` + +Получает связанный объект магазина. + +```php +public function getStore() +{ + return $this->hasOne(CityStore::class, ['id' => 'store_id']); +} +``` + +**Возвращает:** `\yii\db\ActiveQuery` - запрос для получения магазина + +**Тип связи:** `hasOne` - одна запись связана с одним магазином + +**Связанная модель:** `CityStore` + +**Использование:** +```php +$marketplaceStore = MarketplaceStore::findOne($id); +$store = $marketplaceStore->store; + +echo "Магазин: {$store->name}"; +echo "Адрес: {$store->address}"; +``` + +--- + +### `getCreatedBy()` + +Получает информацию о сотруднике, создавшем запись. + +```php +public function getCreatedBy() +{ + return $this->hasOne(Admin::class, ['id' => 'created_by']); +} +``` + +**Возвращает:** `\yii\db\ActiveQuery` - запрос для получения сотрудника + +**Тип связи:** `hasOne` - одна запись создана одним сотрудником + +**Связанная модель:** `Admin` + +**Использование:** +```php +$marketplaceStore = MarketplaceStore::findOne($id); +$creator = $marketplaceStore->createdBy; + +echo "Создал: {$creator->name}"; +echo "Дата: {$marketplaceStore->created_at}"; +``` + +--- + +### `getUpdatedBy()` + +Получает информацию о сотруднике, последним обновившем запись. + +```php +public function getUpdatedBy() +{ + return $this->hasOne(Admin::class, ['id' => 'updated_by']); +} +``` + +**Возвращает:** `\yii\db\ActiveQuery` - запрос для получения сотрудника + +**Тип связи:** `hasOne` - одна запись обновлена одним сотрудником + +**Связанная модель:** `Admin` + +**Использование:** +```php +$marketplaceStore = MarketplaceStore::findOne($id); +$updater = $marketplaceStore->updatedBy; + +echo "Обновил: {$updater->name}"; +echo "Дата: {$marketplaceStore->updated_at}"; +``` + +--- + +## Примеры использования + +### Создание новой связки магазин-склад + +```php +$marketplaceStore = new MarketplaceStore(); +$marketplaceStore->store_id = 5; +$marketplaceStore->guid = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'; +$marketplaceStore->warehouse_id = MarketplaceStore::YANDEX_WAREHOUSE_ID; +$marketplaceStore->warehouse_guid = 'ym-warehouse-12345'; +$marketplaceStore->firm = 'ООО "Цветочная компания"'; +$marketplaceStore->account_email = 'store@example.com'; +$marketplaceStore->is_feed_active = 1; + +// created_at, created_by заполнятся автоматически +if ($marketplaceStore->save()) { + echo "Связка создана"; +} +``` + +### Получение всех магазинов на Яндекс.Маркете + +```php +$yandexStores = MarketplaceStore::find() + ->where(['warehouse_id' => MarketplaceStore::YANDEX_WAREHOUSE_ID]) + ->with('store') // Eager loading + ->all(); + +foreach ($yandexStores as $ms) { + echo "Магазин: {$ms->store->name}, Email: {$ms->account_email}\n"; +} +``` + +### Получение всех активных фидов + +```php +$activeStores = MarketplaceStore::find() + ->where(['is_feed_active' => 1]) + ->all(); + +echo "Активных фидов: " . count($activeStores); +``` + +### Поиск склада по GUID магазина + +```php +$marketplaceStore = MarketplaceStore::findOne(['guid' => $storeGuid]); + +if ($marketplaceStore) { + $marketplace = MarketplaceStore::getWarehouseId()[$marketplaceStore->warehouse_id]; + echo "Маркетплейс: {$marketplace}"; + echo "Warehouse GUID: {$marketplaceStore->warehouse_guid}"; +} +``` + +### Получение GUID склада по email + +```php +$warehouseGuid = MarketplaceStore::getWarehouseGuidByAccountEmail('store@example.com'); + +if ($warehouseGuid) { + echo "GUID склада: {$warehouseGuid}"; +} else { + echo "Магазин с таким email не найден"; +} +``` + +### Обновление email аккаунта + +```php +$marketplaceStore = MarketplaceStore::findOne($id); + +if ($marketplaceStore) { + $marketplaceStore->account_email = 'newemail@example.com'; + + // updated_at и updated_by заполнятся автоматически + if ($marketplaceStore->save()) { + echo "Email обновлен"; + } +} +``` + +### Активация/деактивация фида + +```php +$marketplaceStore = MarketplaceStore::findOne($id); + +if ($marketplaceStore) { + $marketplaceStore->is_feed_active = 1; // Активировать + // или + // $marketplaceStore->is_feed_active = 0; // Деактивировать + + $marketplaceStore->save(); +} +``` + +### Получение статистики по маркетплейсам + +```php +$stats = MarketplaceStore::find() + ->select(['warehouse_id', 'COUNT(*) as count']) + ->groupBy('warehouse_id') + ->asArray() + ->all(); + +$marketplaces = MarketplaceStore::getWarehouseId(); + +foreach ($stats as $stat) { + $mpName = $marketplaces[$stat['warehouse_id']]; + echo "{$mpName}: {$stat['count']} магазинов\n"; +} +``` + +### Получение всех магазинов с информацией + +```php +$stores = MarketplaceStore::find() + ->with(['store', 'createdBy', 'updatedBy']) + ->all(); + +foreach ($stores as $ms) { + $mp = MarketplaceStore::getWarehouseId()[$ms->warehouse_id]; + + echo "Магазин: {$ms->store->name}\n"; + echo "Маркетплейс: {$mp}\n"; + echo "Email: {$ms->account_email}\n"; + echo "Фид: " . ($ms->is_feed_active ? 'активен' : 'неактивен') . "\n"; + echo "Создал: {$ms->createdBy->name} ({$ms->created_at})\n"; + if ($ms->updated_by) { + echo "Обновил: {$ms->updatedBy->name} ({$ms->updated_at})\n"; + } + echo "---\n"; +} +``` + +### Поиск магазинов по юрлицу + +```php +$storesByFirm = MarketplaceStore::find() + ->where(['firm' => 'ООО "Цветочная компания"']) + ->all(); + +echo "Магазинов по юрлицу: " . count($storesByFirm); +``` + +### Получение магазинов конкретного store_id + +```php +$marketplaceStores = MarketplaceStore::find() + ->where(['store_id' => $storeId]) + ->all(); + +$marketplaces = MarketplaceStore::getWarehouseId(); + +echo "Магазин работает с маркетплейсами:\n"; +foreach ($marketplaceStores as $ms) { + echo "- {$marketplaces[$ms->warehouse_id]}\n"; +} +``` + +### Проверка существования email + +```php +$emailExists = MarketplaceStore::find() + ->where(['account_email' => $email]) + ->exists(); + +if ($emailExists) { + echo "Email уже используется"; +} +``` + +### Массовая активация фидов для маркетплейса + +```php +MarketplaceStore::updateAll( + ['is_feed_active' => 1], + ['warehouse_id' => MarketplaceStore::FLOWWOW_WAREHOUSE_ID] +); + +echo "Все фиды Flowwow активированы"; +``` + +--- + +## Связь с другими моделями + +Модель напрямую связана с: + +- **CityStore** - магазины ERP24 (через `store_id`) +- **Admin** - сотрудники (через `created_by`, `updated_by`) + +Модель логически связана с: + +- **MarketplaceOrders** - заказы маркетплейсов (через `warehouse_guid`) +- **MarketplaceOrderDelivery** - доставка заказов + +--- + +## Диаграмма связей + +```mermaid +erDiagram + marketplace_store }o--|| city_store : "store" + marketplace_store }o--|| admin : "created_by" + marketplace_store }o--|| admin : "updated_by" + marketplace_store ||--o{ marketplace_orders : "warehouse" + + marketplace_store { + int id PK + int store_id FK + text guid + int warehouse_id + text warehouse_guid UK + text firm + int created_at + int created_by FK + int updated_at + int updated_by FK + text account_email UK + int is_feed_active + } + + city_store { + int id PK + string name + string address + string guid + } + + admin { + int id PK + string name + string email + } + + marketplace_orders { + int id PK + string marketplace_order_id + string warehouse_guid FK + int status_id + float total + } +``` + +--- + +## Процесс создания связки + +```mermaid +flowchart TD + A[Начало: Создание связки] --> B[Выбрать магазин ERP24] + B --> C[Выбрать маркетплейс] + C --> D[Ввести GUID магазина из 1С] + D --> E[Ввести GUID склада маркетплейса] + E --> F[Указать юрлицо] + F --> G[Указать email аккаунта] + G --> H[Установить статус фида] + H --> I[save вызывается] + I --> J[beforeSave: заполнение метаданных] + J --> K[TimestampBehavior: created_at, updated_at] + K --> L[BlameableBehavior: created_by] + L --> M[Валидация данных] + M --> N{Валидация успешна?} + N -->|Да| O[Сохранение в БД] + N -->|Нет| P[Ошибка валидации] + O --> Q[Конец: Успех] + P --> Q[Конец: Ошибка] +``` + +--- + +## Структура маркетплейсов + +```mermaid +graph LR + A[ERP24 Магазин] --> B[MarketplaceStore] + B --> C[Flowwow
    warehouse_id=1] + B --> D[Яндекс.Маркет
    warehouse_id=2] + + C --> E[Склад FW
    warehouse_guid] + D --> F[Склад YM
    warehouse_guid] + + style A fill:#ffcccc + style C fill:#ccffcc + style D fill:#ccccff +``` + +--- + +## Пример полного набора данных + +```php +[ + 'id' => 15, + 'store_id' => 7, + 'guid' => 'e7f8a9b0-1234-5678-90ab-cdef12345678', + 'warehouse_id' => 2, // Яндекс.Маркет + 'warehouse_guid' => 'ym-wh-98765', + 'firm' => 'ООО "Цветочная Долина"', + 'created_at' => '2025-10-15 09:30:00', + 'created_by' => 12, + 'updated_at' => '2025-12-11 14:20:00', + 'updated_by' => 18, + 'account_email' => 'dolina@flowers.ru', + 'is_feed_active' => 1 +] +``` + +**Интерпретация:** +- Магазин с ID 7 из ERP24 +- Подключен к Яндекс.Маркету (warehouse_id = 2) +- GUID магазина в 1С: e7f8a9b0-1234-5678-90ab-cdef12345678 +- GUID склада на ЯМ: ym-wh-98765 +- Юрлицо: ООО "Цветочная Долина" +- Email: dolina@flowers.ru +- Фид активен +- Создано: 15 октября 2025 сотрудником ID 12 +- Обновлено: 11 декабря 2025 сотрудником ID 18 + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** - магазины ERP24 +- **[Admin](./Admin.md)** - сотрудники +- **[MarketplaceOrders](./MarketplaceOrders.md)** - заказы маркетплейсов +- **[MarketplaceOrderDelivery](./MarketplaceOrderDelivery.md)** - доставка заказов +- **[MarketplacePrices](./MarketplacePrices.md)** - цены товаров + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MatrixBouquetActuality.md b/erp24/docs/models/MatrixBouquetActuality.md new file mode 100644 index 00000000..c6b05c96 --- /dev/null +++ b/erp24/docs/models/MatrixBouquetActuality.md @@ -0,0 +1,222 @@ +# Класс: MatrixBouquetActuality + + +## Mindmap + +```mermaid +mindmap + root((MatrixBouquetActuality)) + Таблица БД + matrix_bouquet_actuality + Свойства + id + int + guid + string + bouquet_id + int + date_from + string + created_at + string + created_by + int + Связи + Bouquet + 1:1 BouquetComposition + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель актуальности матричных букетов в ERP24. Хранит периоды активности букетов с возможностью временного архивирования, обеспечивая контроль актуальности товаров в матрице ассортимента. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`matrix_bouquet_actuality` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(255) | GUID товара из 1С | +| `bouquet_id` | int | FK на букет из bouquet_composition | +| `date_from` | datetime | Дата и время начала активности | +| `date_to` | datetime / null | Дата и время окончания активности | +| `is_archive` | int / null | Признак архивного товара (0=активен, 1=архив, default: 0) | +| `created_at` | datetime | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | +| `created_by` | int | ID пользователя-создателя | +| `updated_by` | int / null | ID пользователя, обновившего запись | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getBouquet()` | hasOne | BouquetComposition | Композиция букета | + +## Диаграмма связей + +```mermaid +erDiagram + MatrixBouquetActuality { + int id PK + varchar guid FK + int bouquet_id FK + datetime date_from + datetime date_to + int is_archive + datetime created_at + datetime updated_at + int created_by FK + int updated_by FK + } + + Products1c { + varchar guid PK + varchar name + } + + BouquetComposition { + int id PK + varchar name + } + + Admin { + int id PK + varchar username + } + + Products1c ||--o{ MatrixBouquetActuality : "guid" + BouquetComposition ||--o{ MatrixBouquetActuality : "bouquet_id" + Admin ||--o{ MatrixBouquetActuality : "created_by" + Admin ||--o{ MatrixBouquetActuality : "updated_by" +``` + +## Диаграмма жизненного цикла + +```mermaid +flowchart TD + A[Создание букета] --> B[Период активности
    date_from - date_to] + B --> C{Текущая дата} + C -->|до date_from| D[Запланирован] + C -->|в периоде| E{is_archive?} + C -->|после date_to| F[Истёк срок] + E -->|0| G[Активен в матрице] + E -->|1| H[В архиве] + F --> I[Автоматическое скрытие] + H --> I +``` + +## Примеры использования + +### Создание записи актуальности +```php +$actuality = new MatrixBouquetActuality(); +$actuality->guid = '12345678-1234-1234-1234-123456789abc'; +$actuality->bouquet_id = $bouquet->id; +$actuality->date_from = '2024-03-01 00:00:00'; +$actuality->date_to = '2024-12-31 23:59:59'; +$actuality->is_archive = 0; +$actuality->created_at = date('Y-m-d H:i:s'); +$actuality->created_by = Yii::$app->user->id; +$actuality->save(); +``` + +### Получение активных букетов на дату +```php +$date = date('Y-m-d H:i:s'); + +$activeBouquets = MatrixBouquetActuality::find() + ->where(['is_archive' => 0]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $date] + ]) + ->with(['bouquet']) + ->all(); +``` + +### Архивирование букета +```php +$actuality = MatrixBouquetActuality::find() + ->where(['guid' => $productGuid]) + ->one(); + +$actuality->is_archive = 1; +$actuality->updated_at = date('Y-m-d H:i:s'); +$actuality->updated_by = Yii::$app->user->id; +$actuality->save(); +``` + +### Продление периода активности +```php +$actuality = MatrixBouquetActuality::findOne($id); +$actuality->date_to = '2025-06-30 23:59:59'; +$actuality->updated_at = date('Y-m-d H:i:s'); +$actuality->updated_by = Yii::$app->user->id; +$actuality->save(); +``` + +### Получение архивных товаров +```php +$archivedItems = MatrixBouquetActuality::find() + ->where(['is_archive' => 1]) + ->with(['bouquet']) + ->orderBy(['updated_at' => SORT_DESC]) + ->all(); +``` + +### Проверка актуальности товара +```php +function isProductActual($guid) +{ + $now = date('Y-m-d H:i:s'); + + return MatrixBouquetActuality::find() + ->where(['guid' => $guid, 'is_archive' => 0]) + ->andWhere(['<=', 'date_from', $now]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $now] + ]) + ->exists(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | required, string (max 255) | +| `bouquet_id` | required, integer | +| `date_from` | required, safe | +| `date_to` | safe, default: null | +| `is_archive` | integer, default: 0 | +| `created_at` | required, safe | +| `created_by` | required, integer | +| `updated_at`, `updated_by` | safe, default: null | + +## Связанные модели + +- [BouquetComposition](./BouquetComposition.md) — композиции букетов +- [Products1c](./Products1c.md) — товары из 1С (связь по guid) +- [Admin](./Admin.md) — пользователи системы + +## Особенности реализации + +1. **Temporal pattern**: Поля date_from и date_to определяют период активности +2. **Архивирование**: Флаг is_archive позволяет временно скрыть товар без удаления периода +3. **Интеграция с 1С**: Связь с товарами через GUID для синхронизации +4. **Полный аудит**: Хранятся данные о создателе и редакторе записи +5. **Бессрочная активность**: date_to = null означает неограниченный период +6. **Двойная привязка**: К товару 1С (guid) и к композиции букета (bouquet_id) diff --git a/erp24/docs/models/MatrixBouquetActualitySearch.md b/erp24/docs/models/MatrixBouquetActualitySearch.md new file mode 100644 index 00000000..77bbfd53 --- /dev/null +++ b/erp24/docs/models/MatrixBouquetActualitySearch.md @@ -0,0 +1,201 @@ +# Класс: MatrixBouquetActualitySearch + + +## Mindmap + +```mermaid +mindmap + root((MatrixBouquetActualitySearch)) + Таблица БД + ActiveRecord + Наследование + extends MatrixBouquetActuality +``` + +## Назначение +Search-модель для поиска и фильтрации актуальности букетов в матрице ERP24. Позволяет управлять периодами актуальности букетов с возможностью поиска по названию связанного букета. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MatrixBouquetActuality` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$bouquet_name` | string | Название букета для поиска по связанной таблице | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `bouquet_id`, `is_archive`, `created_by`, `updated_by` — integer +- `guid`, `bouquet_name`, `date_from`, `date_to`, `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к таблице букетов. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с joinWith для связи bouquet +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, bouquet_id, date_from, date_to, is_archive, created_at, updated_at, created_by, updated_by + - ilike: guid, bouquet.name + +## Диаграмма связей + +```mermaid +erDiagram + MatrixBouquetActuality { + int id PK + varchar guid + int bouquet_id FK + date date_from + date date_to + int is_archive + datetime created_at + datetime updated_at + int created_by FK + int updated_by FK + } + + Bouquet { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + MatrixBouquetActuality }o--|| Bouquet : "bouquet_id" + MatrixBouquetActuality }o--o| Admin : "created_by" + MatrixBouquetActuality }o--o| Admin : "updated_by" +``` + +## Диаграмма жизненного цикла актуальности + +```mermaid +stateDiagram-v2 + [*] --> Активный: Создание + Активный --> Активный: date_from <= today <= date_to + Активный --> Истёк: today > date_to + Активный --> Архив: is_archive = 1 + Истёк --> Архив: Архивирование + Архив --> [*] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MatrixBouquetActualitySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию букета +```php +$searchModel = new MatrixBouquetActualitySearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetActualitySearch' => [ + 'bouquet_name' => 'Розы', + ] +]); +``` + +### Поиск активных периодов +```php +$searchModel = new MatrixBouquetActualitySearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetActualitySearch' => [ + 'is_archive' => 0, + ] +]); +``` + +### Поиск по дате начала +```php +$searchModel = new MatrixBouquetActualitySearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetActualitySearch' => [ + 'date_from' => '2024-01-01', + ] +]); +``` + +### Поиск по GUID +```php +$searchModel = new MatrixBouquetActualitySearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetActualitySearch' => [ + 'guid' => 'abc-123', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'bouquet_name', + 'value' => 'bouquet.name', + ], + 'date_from', + 'date_to', + [ + 'attribute' => 'is_archive', + 'value' => function($model) { + return $model->is_archive ? 'Да' : 'Нет'; + }, + ], + 'created_at', + ], +]) ?> +``` + +## Связанные модели + +- [MatrixBouquetActuality](./MatrixBouquetActuality.md) — базовая модель актуальности +- [Bouquet](./Bouquet.md) — букеты +- [Admin](./Admin.md) — администраторы (создатель/редактор) + +## Особенности реализации + +1. **Периоды актуальности**: date_from и date_to определяют период действия +2. **Архивирование**: is_archive для мягкого удаления +3. **Аудит**: created_by, updated_by, created_at, updated_at +4. **GUID**: Уникальный идентификатор для внешних систем +5. **JOIN к букетам**: joinWith для поиска по названию букета +6. **ilike поиск**: Регистронезависимый для guid и bouquet.name +7. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MatrixBouquetForecast.md b/erp24/docs/models/MatrixBouquetForecast.md new file mode 100644 index 00000000..d9fc11bd --- /dev/null +++ b/erp24/docs/models/MatrixBouquetForecast.md @@ -0,0 +1,245 @@ +# Класс: MatrixBouquetForecast + + +## Mindmap + +```mermaid +mindmap + root((MatrixBouquetForecast)) + Таблица БД + matrix_bouquet_forecast + Свойства + id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель прогнозов продаж матричных букетов в ERP24. Хранит плановые показатели продаж букетов по месяцам с детализацией по типам магазинов (S/M/L/XL), маркетплейсам и интернет-магазину для планирования закупок и производства. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`matrix_bouquet_forecast` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(255) / null | GUID букета | +| `name` | varchar(255) / null | Наименование букета | +| `group` | varchar(255) / null | Матрица/группа букета | +| `year` | int / null | Год прогноза | +| `month` | int / null | Месяц прогноза (1-12) | +| `s_store` | int / null | Прогноз для магазинов типа S (малые) | +| `m_store` | int / null | Прогноз для магазинов типа M (средние) | +| `l_store` | int / null | Прогноз для магазинов типа L (большие) | +| `xl_store` | int / null | Прогноз для магазинов типа XL (флагманы) | +| `marketplace` | int / null | Прогноз для маркетплейсов | +| `internet` | int / null | Прогноз для интернет-магазина | +| `created_at` | datetime / null | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | + +## Диаграмма структуры прогнозов + +```mermaid +erDiagram + MatrixBouquetForecast { + int id PK + varchar guid FK + varchar name + varchar group + int year + int month + int s_store + int m_store + int l_store + int xl_store + int marketplace + int internet + datetime created_at + datetime updated_at + } + + MatrixErp { + varchar guid PK + varchar name + } + + MatrixErp ||--o{ MatrixBouquetForecast : "guid" +``` + +## Диаграмма распределения прогнозов + +```mermaid +flowchart TB + subgraph "Прогноз букета" + A[Общий прогноз
    на месяц] + end + + subgraph "Офлайн магазины" + B1[S Store
    Малые] + B2[M Store
    Средние] + B3[L Store
    Большие] + B4[XL Store
    Флагманы] + end + + subgraph "Онлайн каналы" + C1[Marketplace
    Flowwow, Яндекс] + C2[Internet
    Собственный сайт] + end + + A --> B1 + A --> B2 + A --> B3 + A --> B4 + A --> C1 + A --> C2 +``` + +## Примеры использования + +### Создание прогноза на месяц +```php +$forecast = new MatrixBouquetForecast(); +$forecast->guid = '12345678-1234-1234-1234-123456789abc'; +$forecast->name = 'Букет "Весенний"'; +$forecast->group = 'Сезонные'; +$forecast->year = 2024; +$forecast->month = 3; +$forecast->s_store = 50; +$forecast->m_store = 100; +$forecast->l_store = 150; +$forecast->xl_store = 200; +$forecast->marketplace = 80; +$forecast->internet = 120; +$forecast->created_at = date('Y-m-d H:i:s'); +$forecast->save(); +``` + +### Получение прогнозов на месяц +```php +$forecasts = MatrixBouquetForecast::find() + ->where(['year' => 2024, 'month' => 3]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($forecasts as $forecast) { + $total = $forecast->s_store + $forecast->m_store + + $forecast->l_store + $forecast->xl_store + + $forecast->marketplace + $forecast->internet; + + echo "{$forecast->name}: итого {$total} шт.\n"; +} +``` + +### Агрегация прогнозов по каналам +```php +$monthlyTotals = MatrixBouquetForecast::find() + ->select([ + 'year', + 'month', + 'SUM(s_store) as total_s', + 'SUM(m_store) as total_m', + 'SUM(l_store) as total_l', + 'SUM(xl_store) as total_xl', + 'SUM(marketplace) as total_marketplace', + 'SUM(internet) as total_internet', + ]) + ->groupBy(['year', 'month']) + ->orderBy(['year' => SORT_ASC, 'month' => SORT_ASC]) + ->asArray() + ->all(); +``` + +### Получение прогноза для конкретного букета +```php +$bouquetForecasts = MatrixBouquetForecast::find() + ->where(['guid' => $bouquetGuid, 'year' => 2024]) + ->orderBy(['month' => SORT_ASC]) + ->all(); + +// Годовой итог +$yearlyTotal = array_sum(array_map(function($f) { + return $f->s_store + $f->m_store + $f->l_store + $f->xl_store + + $f->marketplace + $f->internet; +}, $bouquetForecasts)); +``` + +### Сравнение с фактическими продажами +```php +$forecast = MatrixBouquetForecast::find() + ->where(['guid' => $guid, 'year' => $year, 'month' => $month]) + ->one(); + +$actualSales = Sales::find() + ->where(['product_guid' => $guid]) + ->andWhere(['>=', 'date', "$year-$month-01"]) + ->andWhere(['<', 'date', date('Y-m-01', strtotime("+1 month", strtotime("$year-$month-01")))]) + ->count(); + +$forecastTotal = $forecast->s_store + $forecast->m_store + + $forecast->l_store + $forecast->xl_store + + $forecast->marketplace + $forecast->internet; + +$accuracy = $forecastTotal > 0 + ? round($actualSales / $forecastTotal * 100, 1) + : 0; + +echo "Точность прогноза: {$accuracy}%"; +``` + +### Импорт прогнозов из Excel +```php +foreach ($excelRows as $row) { + $forecast = MatrixBouquetForecast::find() + ->where(['guid' => $row['guid'], 'year' => $row['year'], 'month' => $row['month']]) + ->one() ?? new MatrixBouquetForecast(); + + $forecast->guid = $row['guid']; + $forecast->name = $row['name']; + $forecast->group = $row['group']; + $forecast->year = $row['year']; + $forecast->month = $row['month']; + $forecast->s_store = $row['s_store'] ?? 0; + $forecast->m_store = $row['m_store'] ?? 0; + $forecast->l_store = $row['l_store'] ?? 0; + $forecast->xl_store = $row['xl_store'] ?? 0; + $forecast->marketplace = $row['marketplace'] ?? 0; + $forecast->internet = $row['internet'] ?? 0; + $forecast->updated_at = date('Y-m-d H:i:s'); + $forecast->save(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | string (max 255), default: null | +| `name` | string (max 255), default: null | +| `group` | string (max 255), default: null | +| `year` | integer, default: null | +| `month` | integer, default: null | +| `s_store`, `m_store`, `l_store`, `xl_store` | integer, default: null | +| `marketplace`, `internet` | integer, default: null | +| `created_at`, `updated_at` | safe, default: null | + +## Связанные модели + +- [MatrixErp](./MatrixErp.md) — матричные букеты (связь по guid) + +## Особенности реализации + +1. **Многоканальное планирование**: Отдельные прогнозы для 6 каналов продаж +2. **Типизация магазинов**: S/M/L/XL — размеры офлайн-магазинов +3. **Онлайн каналы**: Отдельно marketplace (агрегаторы) и internet (собственный сайт) +4. **Помесячная детализация**: Прогнозы хранятся по месяцам для сезонного планирования +5. **Денормализация**: Поля name и group дублируются для удобства отчётности +6. **Nullable поля**: Все прогнозные значения могут быть null (не заполнено) diff --git a/erp24/docs/models/MatrixBouquetForecastSearch.md b/erp24/docs/models/MatrixBouquetForecastSearch.md new file mode 100644 index 00000000..ae184d69 --- /dev/null +++ b/erp24/docs/models/MatrixBouquetForecastSearch.md @@ -0,0 +1,198 @@ +# Класс: MatrixBouquetForecastSearch + + +## Mindmap + +```mermaid +mindmap + root((MatrixBouquetForecastSearch)) + Таблица БД + ActiveRecord + Наследование + extends MatrixBouquetForecast +``` + +## Назначение +Search-модель для поиска и фильтрации прогнозов по букетам в матрице ERP24. Обеспечивает поиск по году, месяцу, названию, группе и прогнозным данным для разных типов магазинов и каналов продаж. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MatrixBouquetForecast` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `year`, `month`, `s_store`, `m_store`, `l_store`, `xl_store`, `marketplace`, `internet` — integer +- `guid`, `name`, `group`, `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MatrixBouquetForecast::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, year, month, s_store, m_store, l_store, xl_store, marketplace, internet, created_at, updated_at + - ilike: guid, name, group + +## Диаграмма структуры прогноза + +```mermaid +erDiagram + MatrixBouquetForecast { + int id PK + varchar guid + varchar name + varchar group + int year + int month + int s_store + int m_store + int l_store + int xl_store + int marketplace + int internet + datetime created_at + datetime updated_at + } +``` + +## Диаграмма каналов продаж + +```mermaid +flowchart TD + A[MatrixBouquetForecast] --> B{Каналы продаж} + + B --> C[Офлайн магазины] + C --> D[s_store - маленькие S] + C --> E[m_store - средние M] + C --> F[l_store - большие L] + C --> G[xl_store - очень большие XL] + + B --> H[Онлайн каналы] + H --> I[marketplace - маркетплейсы] + H --> J[internet - интернет-магазин] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MatrixBouquetForecastSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по году и месяцу +```php +$searchModel = new MatrixBouquetForecastSearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetForecastSearch' => [ + 'year' => 2024, + 'month' => 3, // Март + ] +]); +``` + +### Поиск по названию +```php +$searchModel = new MatrixBouquetForecastSearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetForecastSearch' => [ + 'name' => 'Букет роз', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new MatrixBouquetForecastSearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetForecastSearch' => [ + 'group' => 'Праздничные', + ] +]); +``` + +### Поиск по прогнозу для крупных магазинов +```php +$searchModel = new MatrixBouquetForecastSearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetForecastSearch' => [ + 'l_store' => 100, // Прогноз 100 штук для L-магазинов + ] +]); +``` + +### Поиск по каналу маркетплейс +```php +$searchModel = new MatrixBouquetForecastSearch(); +$dataProvider = $searchModel->search([ + 'MatrixBouquetForecastSearch' => [ + 'marketplace' => 50, // Прогноз 50 штук для маркетплейсов + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'group', + 'year', + 'month', + 's_store', + 'm_store', + 'l_store', + 'xl_store', + 'marketplace', + 'internet', + ], +]) ?> +``` + +## Связанные модели + +- [MatrixBouquetForecast](./MatrixBouquetForecast.md) — базовая модель прогнозов +- [Bouquet](./Bouquet.md) — букеты +- [CityStore](./CityStore.md) — магазины (типы S/M/L/XL) + +## Особенности реализации + +1. **Временная привязка**: year + month для периода прогноза +2. **Размеры магазинов**: s_store, m_store, l_store, xl_store для разных форматов +3. **Онлайн каналы**: marketplace и internet для прогнозов онлайн-продаж +4. **Группировка**: group для категоризации букетов +5. **GUID**: Уникальный идентификатор для внешних систем +6. **ilike поиск**: Регистронезависимый для guid, name, group +7. **Кастомный formName**: Поддержка различных форм diff --git a/erp24/docs/models/MatrixErp.md b/erp24/docs/models/MatrixErp.md new file mode 100644 index 00000000..c26f2462 --- /dev/null +++ b/erp24/docs/models/MatrixErp.md @@ -0,0 +1,363 @@ +# Модель MatrixErp + + +## Mindmap + +```mermaid +mindmap + root((MatrixErp)) + Таблица БД + matrix_erp + Свойства + id + int + guid + string + active + int + date_from + string + date_to + string + Связи + Price + 1:1 Prices + MatrixProperty + 1:1 MatrixErpProperty + MatrixMedia + 1:N MatrixErpMedia + MarketplacePrices + 1:N MarketplacePrices + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MatrixErp` представляет матрицу товаров (букетов) ERP-системы. Матрица определяет ассортимент букетов с их комплектацией, ценами и медиа-материалами. Используется для управления каталогом стандартных букетов с фиксированной комплектацией. + +**Файл модели:** `erp24/records/MatrixErp.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `matrix_erp` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | Уникальный GUID из 1С | +| `parent_id` | VARCHAR(100) | ID родительской группы | +| `group_name` | VARCHAR(100) | Категория букета | +| `category_id` | INTEGER | ID категории | +| `code` | VARCHAR(100) | Код товара | +| `name` | VARCHAR(255) | Название букета | +| `components` | TEXT | Комплектация (JSON-формат) | +| `articule` | VARCHAR(100) | Артикул товара | +| `active` | INTEGER | Активность записи (0/1) | +| `date_from` | VARCHAR(100) | Дата начала действия | +| `date_to` | VARCHAR(100) | Дата окончания действия | +| `created_admin_id` | INTEGER | ID сотрудника, создавшего запись | +| `created_at` | INTEGER | Дата создания (timestamp) | +| `updated_admin_id` | INTEGER | ID сотрудника, изменившего запись | +| `updated_at` | INTEGER | Дата изменения (timestamp) | +| `deleted_at` | VARCHAR | Дата удаления | +| `deleted_by` | INTEGER | ID сотрудника, удалившего запись | +| `is_feed_active` | INTEGER | Активность в фиде выгрузки (0/1) | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Модель использует `TimestampBehavior` для автоматического заполнения полей дат: + +```php +public function behaviors() +{ + return [ + [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new Expression('NOW()'), + ], + ]; +} +``` + +**Автоматически заполняемые поля:** +- `created_at` — при создании записи +- `updated_at` — при обновлении записи + +--- + +## Свойства модели + +### `$componentsArray` + +Публичный массив для хранения разобранной комплектации букета. + +```php +public array $componentsArray = []; +``` + +**Назначение:** Хранение расшифрованных данных комплектации после вызова `setComponentsArray()`. + +--- + +## Методы модели + +### `setComponentsArray(): void` + +Разбирает JSON-строку комплектации и заполняет свойство `$componentsArray` данными о товарах. + +**Логика работы:** +1. Получает JSON из поля `components` через `DataHelper::getValuesFromJson()` +2. Если массив не пустой, вызывает `getProducts()` для получения названий товаров +3. Результат сохраняется в `$componentsArray` + +```php +$matrix = MatrixErp::findOne($id); +$matrix->setComponentsArray(); +// $matrix->componentsArray = [ +// ['name' => 'Роза красная 50см', 'quantity' => 11], +// ['name' => 'Гипсофила', 'quantity' => 3], +// ] +``` + +**Вызываемые методы:** +- `DataHelper::getValuesFromJson()` — парсинг JSON +- `self::getProducts()` — получение данных о товарах + +### `getProducts(array $componentsArray): array` (static) + +Преобразует массив GUID товаров с количествами в массив с названиями. + +**Параметры:** +- `$componentsArray` — ассоциативный массив `['guid_товара' => количество]` + +**Возвращает:** +Массив объектов с полями `name` и `quantity`. + +**Логика работы:** +1. Получает данные о товарах из `Products1c::getProducts1c()` +2. Формирует массив с названиями и количествами +3. Если товар не найден, ставит название '-' + +```php +$components = ['abc-123-guid' => 5, 'def-456-guid' => 3]; +$products = MatrixErp::getProducts($components); +// [ +// ['name' => 'Роза красная', 'quantity' => 5], +// ['name' => 'Упаковка', 'quantity' => 3], +// ] +``` + +**Вызываемые методы:** +- `Products1c::getProducts1c()` — получение данных о товарах по GUID + +--- + +## Связи (Relations) + +### `getPrice()` + +Цена товара матрицы. + +```php +$price = $matrix->price; // Prices +``` + +**Тип:** hasOne +**Связанная модель:** `Prices` +**FK:** `product_id` → `guid` + +### `getMatrixProperty()` + +Дополнительные свойства матрицы. + +```php +$property = $matrix->matrixProperty; // MatrixErpProperty +``` + +**Тип:** hasOne +**Связанная модель:** `MatrixErpProperty` +**FK:** `guid` → `guid` + +### `getMatrixMedia()` + +Медиа-файлы (изображения) матрицы. + +```php +$media = $matrix->matrixMedia; // MatrixErpMedia[] +``` + +**Тип:** hasMany +**Связанная модель:** `MatrixErpMedia` +**FK:** `guid` → `guid` + +### `getMarketplacePrices()` + +Цены для маркетплейсов. + +```php +$marketPrices = $matrix->marketplacePrices; // MarketplacePrices[] +``` + +**Тип:** hasMany +**Связанная модель:** `MarketplacePrices` +**FK:** `matrix_erp_id` → `id` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + matrix_erp ||--o| prices : "has_price" + matrix_erp ||--o| matrix_erp_property : "has_property" + matrix_erp ||--o{ matrix_erp_media : "has_media" + matrix_erp ||--o{ marketplace_prices : "marketplace_prices" + matrix_erp }o--|| products_1c : "components" + + matrix_erp { + int id PK + string guid UK + string parent_id + string group_name + string name + text components + int active + string date_from + string date_to + int is_feed_active + } + + matrix_erp_property { + string guid FK + string properties + } + + matrix_erp_media { + string guid FK + string url + int sort + } + + prices { + string product_id FK + float price + } +``` + +--- + +## Примеры использования + +### Получение активной матрицы + +```php +$matrix = MatrixErp::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['or', + ['>=', 'date_to', date('Y-m-d')], + ['date_to' => null] + ]) + ->all(); +``` + +### Получение матрицы с комплектацией + +```php +$matrix = MatrixErp::findOne(['guid' => $guid]); +$matrix->setComponentsArray(); + +foreach ($matrix->componentsArray as $component) { + echo $component['name'] . ': ' . $component['quantity'] . ' шт.' . PHP_EOL; +} +``` + +### Получение матрицы с медиа и ценой + +```php +$matrix = MatrixErp::find() + ->where(['id' => $id]) + ->with(['matrixMedia', 'price', 'matrixProperty']) + ->one(); + +// Основное изображение +$mainImage = $matrix->matrixMedia[0]->url ?? null; + +// Цена +$price = $matrix->price->price ?? 0; +``` + +### Поиск по категории + +```php +$bouquets = MatrixErp::find() + ->where(['group_name' => 'Букеты до 3000']) + ->andWhere(['active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Получение цен для маркетплейса + +```php +$matrix = MatrixErp::findOne($id); +foreach ($matrix->marketplacePrices as $mpPrice) { + echo "Маркетплейс: {$mpPrice->marketplace_id}, Цена: {$mpPrice->price}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `guid` | Обязательное, макс. 100 символов | +| `date_from` | Обязательное, макс. 100 символов | +| `name` | Макс. 255 символов | +| `category_id`, `active`, `created_admin_id`, `updated_admin_id`, `is_feed_active` | Целое число | +| `components`, `deleted_at` | Строка (TEXT) | +| `parent_id`, `group_name`, `code`, `articule`, `date_to` | Макс. 100 символов | + +--- + +## Структура поля `components` + +Поле `components` содержит JSON-объект с комплектацией букета: + +```json +{ + "abc12345-guid-product1": 5, + "def67890-guid-product2": 3, + "ghi11111-guid-product3": 1 +} +``` + +**Формат:** `{ "GUID_товара": количество }` + +Для получения расшифрованной комплектации используйте метод `setComponentsArray()`. + +--- + +## Связанные модели + +- **[Prices](./Prices.md)** — цены товаров +- **MatrixErpProperty** — дополнительные свойства матрицы +- **MatrixErpMedia** — медиа-файлы (изображения букетов) +- **[MarketplacePrices](./MarketplaceOrders.md)** — цены для маркетплейсов +- **[Products1c](./Products1c.md)** — товары из 1С (для комплектации) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MatrixErpMedia.md b/erp24/docs/models/MatrixErpMedia.md new file mode 100644 index 00000000..4b6bbf5a --- /dev/null +++ b/erp24/docs/models/MatrixErpMedia.md @@ -0,0 +1,506 @@ +# Модель MatrixErpMedia + + +## Mindmap + +```mermaid +mindmap + root((MatrixErpMedia)) + Таблица БД + matrix_erp_media + Свойства + id + int + guid + string + name + string + date + string + Связи + File + 1:1 Files + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MatrixErpMedia` представляет медиа-файлы (изображения, видео) матрицы букетов ERP-системы. Хранит информацию о файлах, связанных с букетами из матрицы, включая порядок отображения фотографий. Используется для управления галереей изображений букетов в каталоге и на карточках товаров. + +**Файл модели:** `erp24/records/MatrixErpMedia.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `matrix_erp_media` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | GUID букета из матрицы (связь с `matrix_erp`) | +| `name` | VARCHAR(100) | Название медиа-файла | +| `description` | TEXT | Описание медиа-файла (nullable) | +| `file_id` | INTEGER | ID файла из таблицы `files` (nullable) | +| `date` | VARCHAR(100) | Дата создания/загрузки | +| `foto_order` | INTEGER | Порядок отображения фотографии (nullable) | +| `created_admin_id` | INTEGER | ID администратора, создавшего запись (nullable) | +| `created_at` | VARCHAR(100) | Дата создания записи (nullable) | +| `updated_admin_id` | INTEGER | ID администратора, обновившего запись (nullable) | +| `updated_at` | VARCHAR(100) | Дата обновления записи (nullable) | + +--- + +## Дополнительные свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$mediaFile` | mixed | Временное свойство для загрузки файла | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'matrix_erp_media'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = MatrixErpMedia::tableName(); +// 'matrix_erp_media' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает обязательные поля: `guid`, `name`, `date` +2. Проверяет типы данных: + - TEXT для `description` + - INTEGER для ID полей + - VARCHAR(100) для строковых полей +3. Разрешает безопасное присвоение для `mediaFiles` и `id` + +**Правила валидации:** +- **required**: `guid`, `name`, `date` +- **string (TEXT)**: `description` +- **safe**: `mediaFiles`, `id` +- **integer**: `file_id`, `foto_order`, `created_admin_id`, `updated_admin_id` +- **string (max 100)**: `guid`, `name`, `date`, `created_at`, `updated_at` + +**Пример:** +```php +$media = new MatrixErpMedia(); +$media->guid = 'matrix-букет-guid'; +$media->name = 'Фото букета основное'; +$media->date = date('Y-m-d'); +$media->file_id = 123; +$media->foto_order = 1; + +if ($media->validate()) { + $media->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет английские названия полей для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `guid` → "Guid" +- `name` → "Name" +- `description` → "Description" +- `file_id` → "File ID" +- `foto_order` → "Foto Order" +- `date` → "Date" +- `created_admin_id` → "Created Admin ID" +- `created_at` → "Created At" +- `updated_admin_id` → "Updated Admin ID" +- `updated_at` → "Updated At" + +--- + +### `getFile(): ActiveQuery` + +Возвращает связь с файлом из таблицы Files. + +**Возвращает:** ActiveQuery для связанной модели `Files` + +**Тип связи:** hasOne (многие к одному) + +**Логика работы:** +1. Устанавливает связь через внешний ключ `file_id` → `id` +2. Добавляет условие фильтрации: `entity LIKE 'matrix_media%'` +3. Возвращает только файлы, относящиеся к медиа матрицы + +**Пример:** +```php +$media = MatrixErpMedia::findOne($id); +$file = $media->file; + +if ($file) { + echo "URL файла: {$file->url}"; +} +``` + +**Связанные таблицы:** +- **Текущая таблица:** `matrix_erp_media` +- **Связанная таблица:** `files` +- **Внешний ключ:** `file_id` (FK) → `id` (PK) +- **Условие:** `entity LIKE 'matrix_media%'` + +--- + +## Связи (Relations) + +### `getFile()` + +Файл медиа. + +```php +$file = $media->file; // Files +``` + +**Тип:** hasOne +**Связанная модель:** `Files` +**FK:** `file_id` → `id` +**Условие:** `entity LIKE 'matrix_media%'` +**Описание:** Возвращает объект файла (изображение или видео) + +--- + +## Примеры использования + +### Создание медиа-записи для букета + +```php +$media = new MatrixErpMedia(); +$media->guid = 'букет-guid-123'; +$media->name = 'Главное фото'; +$media->description = 'Основное изображение букета для каталога'; +$media->file_id = 456; // ID из таблицы files +$media->date = date('Y-m-d'); +$media->foto_order = 1; // Первое фото +$media->created_admin_id = Yii::$app->user->id; + +if ($media->save()) { + echo "Медиа добавлено"; +} +``` + +### Получение всех медиа букета с сортировкой + +```php +$mediaList = MatrixErpMedia::find() + ->where(['guid' => $bucketGuid]) + ->orderBy(['foto_order' => SORT_ASC]) + ->all(); + +foreach ($mediaList as $media) { + $file = $media->file; + echo "{$media->name}"; +} +``` + +### Обновление порядка отображения фотографий + +```php +$photoOrder = [15, 12, 18, 20]; // Новый порядок ID медиа + +foreach ($photoOrder as $index => $mediaId) { + $media = MatrixErpMedia::findOne($mediaId); + if ($media) { + $media->foto_order = $index + 1; + $media->save(); + } +} +``` + +### Получение главного изображения букета + +```php +$mainPhoto = MatrixErpMedia::find() + ->where(['guid' => $bucketGuid]) + ->orderBy(['foto_order' => SORT_ASC]) + ->one(); + +if ($mainPhoto && $mainPhoto->file) { + echo "URL главного фото: {$mainPhoto->file->url}"; +} +``` + +### Добавление галереи изображений + +```php +$images = [ + ['name' => 'Вид спереди', 'file_id' => 101], + ['name' => 'Вид сбоку', 'file_id' => 102], + ['name' => 'Вид сверху', 'file_id' => 103], +]; + +$order = 1; +foreach ($images as $imageData) { + $media = new MatrixErpMedia(); + $media->guid = $bucketGuid; + $media->name = $imageData['name']; + $media->file_id = $imageData['file_id']; + $media->date = date('Y-m-d'); + $media->foto_order = $order++; + $media->save(); +} +``` + +### Удаление всех медиа букета + +```php +MatrixErpMedia::deleteAll(['guid' => $bucketGuid]); +``` + +### Получение медиа с информацией о файле + +```php +$mediaList = MatrixErpMedia::find() + ->where(['guid' => $bucketGuid]) + ->with('file') + ->orderBy(['foto_order' => SORT_ASC]) + ->all(); + +foreach ($mediaList as $media) { + if ($media->file) { + echo "{$media->name}: {$media->file->url} ({$media->file->size} bytes)" . PHP_EOL; + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `guid` | Обязательное, макс. 100 символов | +| `name` | Обязательное, макс. 100 символов | +| `date` | Обязательное, макс. 100 символов | +| `description` | TEXT, nullable | +| `file_id` | Целое число, nullable | +| `foto_order` | Целое число, nullable | +| `created_admin_id` | Целое число, nullable | +| `updated_admin_id` | Целое число, nullable | +| `created_at` | Макс. 100 символов, nullable | +| `updated_at` | Макс. 100 символов, nullable | +| `mediaFiles` | Безопасное присвоение (safe) | + +--- + +## Связанные модели + +- **[MatrixErp](./MatrixErp.md)** — матрица букетов (связь по `guid`) +- **[Files](./Files.md)** — таблица файлов (связь по `file_id`) +- **[MatrixErpProperty](./MatrixErpProperty.md)** — свойства матрицы (связаны через `guid`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + matrix_erp ||--o{ matrix_erp_media : "has_media" + matrix_erp_media }o--|| files : "links_to_file" + + matrix_erp { + int id PK + string guid UK + string name + } + + matrix_erp_media { + int id PK + string guid FK + string name + int file_id FK + int foto_order + } + + files { + int id PK + string url + string entity + int size + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[MatrixErp Букет] -->|guid| B[MatrixErpMedia] + B --> C[Информация о медиа] + C --> D[name] + C --> E[description] + C --> F[foto_order] + B -->|file_id| G[Files] + G --> H[URL файла] + F --> I[Сортировка галереи] + I --> J[Отображение в каталоге] + H --> J +``` + +--- + +## Особенности реализации + +### Поле foto_order (порядок фото) + +Поле `foto_order` определяет последовательность отображения изображений в галерее: +- **Меньшее значение** = **выше в списке** (первое фото) +- Используется для сортировки: `ORDER BY foto_order ASC` +- Главное фото обычно имеет `foto_order = 1` + +### Связь с Files через entity + +Связь с таблицей `files` фильтруется по полю `entity`: +```php +->andWhere(['like', 'entity', 'matrix_media']) +``` + +Это позволяет: +- Изолировать файлы матрицы от других сущностей +- Использовать одну таблицу files для разных типов + +### Временное свойство mediaFile + +Свойство `$mediaFile` используется для загрузки файлов через формы: +```php +public $mediaFile; +``` + +Это виртуальное свойство, не хранящееся в БД, используется при обработке загрузки файлов. + +--- + +## Использование в MatrixErp + +Модель связана с основной моделью матрицы: + +```php +// В MatrixErp +public function getMatrixMedia() +{ + return $this->hasMany(MatrixErpMedia::class, ['guid' => 'guid']); +} + +// Использование +$matrix = MatrixErp::findOne($id); +foreach ($matrix->matrixMedia as $media) { + echo $media->name . ': ' . $media->file->url . PHP_EOL; +} +``` + +--- + +## Использование в MatrixErpProperty + +Модель часто используется совместно с `MatrixErpProperty`: + +```php +// В MatrixErpProperty +public function getMediaFiles() { + return $this->hasMany(MatrixErpMedia::class, ['guid' => 'guid']) + ->orderBy(['foto_order' => SORT_ASC]); +} + +// Использование +$property = MatrixErpProperty::findOne(['guid' => $guid]); +$mediaFiles = $property->mediaFiles; +``` + +--- + +## Сценарии использования + +### 1. Загрузка и сохранение изображений букета + +```php +// Контроллер +$model = MatrixErp::findOne($id); +$uploadedFiles = UploadedFile::getInstancesByName('photos'); + +foreach ($uploadedFiles as $index => $file) { + // Сохраняем файл + $fileModel = new Files(); + $fileModel->entity = 'matrix_media'; + $fileModel->url = '/uploads/' . $file->name; + $fileModel->save(); + + // Создаем запись медиа + $media = new MatrixErpMedia(); + $media->guid = $model->guid; + $media->name = "Фото " . ($index + 1); + $media->file_id = $fileModel->id; + $media->date = date('Y-m-d'); + $media->foto_order = $index + 1; + $media->save(); +} +``` + +### 2. Реорганизация порядка фотографий + +```php +// JSON от клиента: [{'id': 15, 'order': 1}, {'id': 12, 'order': 2}, ...] +$newOrder = json_decode($_POST['photo_order'], true); + +foreach ($newOrder as $item) { + $media = MatrixErpMedia::findOne($item['id']); + if ($media) { + $media->foto_order = $item['order']; + $media->save(); + } +} +``` + +### 3. Отображение галереи в каталоге + +```php +$matrix = MatrixErp::find() + ->where(['id' => $id]) + ->with(['matrixMedia' => function($query) { + $query->orderBy(['foto_order' => SORT_ASC]); + }]) + ->one(); + +echo ""; +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MatrixErpProperty.md b/erp24/docs/models/MatrixErpProperty.md new file mode 100644 index 00000000..df2438b4 --- /dev/null +++ b/erp24/docs/models/MatrixErpProperty.md @@ -0,0 +1,595 @@ +# Модель MatrixErpProperty + + +## Mindmap + +```mermaid +mindmap + root((MatrixErpProperty)) + Таблица БД + matrix_erp_property + Свойства + id + int + guid + string + date + string + Связи + MediaFiles + 1:N MatrixErpMedia + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MatrixErpProperty` представляет дополнительные свойства и характеристики букетов из матрицы ERP-системы. Хранит расширенную информацию о букетах для маркетплейсов: описания, ссылки на изображения и видео, категории для Яндекс и Flowwow, габариты и вес товара. Используется для интеграции с внешними платформами продаж и управления каталогом. + +**Файл модели:** `erp24/records/MatrixErpProperty.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `matrix_erp_property` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | GUID букета из матрицы (связь с `matrix_erp`) | +| `description` | TEXT | Описание букета (nullable) | +| `image_id` | INTEGER | ID изображения из таблицы files (nullable) | +| `date` | VARCHAR(100) | Дата создания/обновления | +| `created_admin_id` | INTEGER | ID администратора, создавшего запись (nullable) | +| `created_at` | VARCHAR(100) | Дата создания записи (nullable) | +| `updated_admin_id` | INTEGER | ID администратора, обновившего запись (nullable) | +| `updated_at` | VARCHAR(100) | Дата обновления записи (nullable) | +| `display_name` | VARCHAR(100) | Человекочитаемое название букета в маркетплейсе (nullable) | +| `external_image_url` | VARCHAR(255) | Ссылка на изображение на внешнем ресурсе (nullable) | +| `product_url` | VARCHAR(255) | Ссылка на карточку товара (nullable) | +| `yandex_category` | VARCHAR(100) | Категория в Яндекс.Маркет (nullable) | +| `flowwow_category` | VARCHAR(100) | Категория в Flowwow (nullable) | +| `flowwow_subcategory` | VARCHAR(100) | Подкатегория в Flowwow (nullable) | +| `length` | FLOAT | Длина товара в см (nullable) | +| `width` | FLOAT | Ширина товара в см (nullable) | +| `height` | FLOAT | Высота товара в см (nullable) | +| `weight` | FLOAT | Вес товара в кг (nullable) | + +--- + +## Дополнительные свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$imageFile` | mixed | Временное свойство для загрузки файла изображения | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'matrix_erp_property'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = MatrixErpProperty::tableName(); +// 'matrix_erp_property' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает обязательные поля: `guid`, `date` +2. Проверяет типы данных: + - TEXT для `description` + - FLOAT для габаритов (`length`, `width`, `height`, `weight`) + - INTEGER для ID полей + - VARCHAR для строковых полей +3. Валидирует загрузку файлов (`imageFile`): только PNG и JPG +4. Ограничивает длину полей + +**Правила валидации:** +- **required**: `guid`, `date` +- **string (TEXT)**: `description` +- **number (float)**: `length`, `width`, `height`, `weight` +- **integer**: `image_id`, `created_admin_id`, `updated_admin_id` +- **file**: `imageFile` (png, jpg, skipOnEmpty) +- **safe**: `mediaFile`, `id`, `display_name`, `external_image_url`, `product_url`, `yandex_category`, `flowwow_category`, `flowwow_subcategory` +- **string (max 100)**: `guid`, `date`, `created_at`, `updated_at`, `display_name`, `yandex_category`, `flowwow_category`, `flowwow_subcategory` +- **string (max 255)**: `url_link_video`, `external_image_url`, `product_url` + +**Пример:** +```php +$property = new MatrixErpProperty(); +$property->guid = 'букет-guid-123'; +$property->description = 'Роскошный букет из 25 красных роз'; +$property->display_name = 'Букет "Любовь"'; +$property->date = date('Y-m-d'); +$property->yandex_category = 'Букеты/Розы'; +$property->flowwow_category = 'Розы'; +$property->length = 50; +$property->width = 30; +$property->height = 40; +$property->weight = 1.5; + +if ($property->validate()) { + $property->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет названия полей на русском языке для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `guid` → "Guid" +- `description` → "Описание" +- `image_id` → "Изображение" +- `url_link_video` → "Ссылка на видео" +- `date` → "Дата" +- `created_admin_id` → "Created Admin ID" +- `created_at` → "Created At" +- `updated_admin_id` → "Updated Admin ID" +- `updated_at` → "Updated At" +- `display_name` → "Название на маркетплейсе" +- `external_image_url` → "Ссылка на изображение на внешнем ресурсе" +- `product_url` → "Ссылка на продуктовую карточку" +- `yandex_category` → "Категория в Яндексе" +- `flowwow_category` → "Категория в flowwow" +- `flowwow_subcategory` → "Подкатегория в flowwow" +- `length` → "Длина" +- `width` → "Ширина" +- `height` → "Высота" +- `weight` → "Вес" + +--- + +### `getMediaFiles(): ActiveQuery` + +Возвращает связь с медиа-файлами букета. + +**Возвращает:** ActiveQuery для связанных моделей `MatrixErpMedia` + +**Тип связи:** hasMany (один ко многим) + +**Логика работы:** +1. Устанавливает связь через внешний ключ `guid` → `guid` +2. Сортирует по полю `foto_order` по возрастанию +3. Возвращает все медиа-файлы, отсортированные по порядку отображения + +**Пример:** +```php +$property = MatrixErpProperty::findOne(['guid' => $guid]); +$mediaFiles = $property->mediaFiles; + +foreach ($mediaFiles as $media) { + echo "{$media->name}: {$media->file->url}" . PHP_EOL; +} +``` + +**Связанные таблицы:** +- **Текущая таблица:** `matrix_erp_property` +- **Связанная таблица:** `matrix_erp_media` +- **Внешний ключ:** `guid` (FK) ↔ `guid` (FK) +- **Сортировка:** `foto_order ASC` + +--- + +## Связи (Relations) + +### `getMediaFiles()` + +Медиа-файлы букета. + +```php +$mediaFiles = $property->mediaFiles; // MatrixErpMedia[] +``` + +**Тип:** hasMany +**Связанная модель:** `MatrixErpMedia` +**FK:** `guid` ↔ `guid` +**Сортировка:** `foto_order ASC` +**Описание:** Возвращает все изображения и видео букета в порядке отображения + +--- + +## Примеры использования + +### Создание свойств букета для маркетплейса + +```php +$property = new MatrixErpProperty(); +$property->guid = 'букет-guid-123'; +$property->description = 'Элегантный букет из 25 красных роз Фридом с зеленью'; +$property->display_name = 'Букет "Страсть" 25 роз'; +$property->date = date('Y-m-d'); + +// Категории маркетплейсов +$property->yandex_category = 'Цветы и букеты/Букеты из роз'; +$property->flowwow_category = 'Розы'; +$property->flowwow_subcategory = 'Букеты'; + +// Ссылки +$property->external_image_url = 'https://storage.yandexcloud.net/buckets/bouquet-123.jpg'; +$property->product_url = 'https://site.com/catalog/bouquet-123'; + +// Габариты +$property->length = 50; // см +$property->width = 30; // см +$property->height = 40; // см +$property->weight = 1.5; // кг + +$property->save(); +``` + +### Получение свойств букета + +```php +$property = MatrixErpProperty::findOne(['guid' => $bucketGuid]); + +if ($property) { + echo "Название: {$property->display_name}" . PHP_EOL; + echo "Описание: {$property->description}" . PHP_EOL; + echo "Категория Яндекс: {$property->yandex_category}" . PHP_EOL; + echo "Габариты: {$property->length}x{$property->width}x{$property->height} см" . PHP_EOL; + echo "Вес: {$property->weight} кг" . PHP_EOL; +} +``` + +### Обновление данных для маркетплейса + +```php +$property = MatrixErpProperty::findOne(['guid' => $guid]); + +if ($property) { + $property->display_name = 'Букет "Любовь" Premium'; + $property->yandex_category = 'Цветы и букеты/Премиум букеты'; + $property->external_image_url = 'https://new-storage.com/image.jpg'; + $property->save(); +} +``` + +### Получение букета с медиа + +```php +$property = MatrixErpProperty::find() + ->where(['guid' => $guid]) + ->with('mediaFiles') + ->one(); + +echo "Букет: {$property->display_name}" . PHP_EOL; +echo "Фотографии:" . PHP_EOL; + +foreach ($property->mediaFiles as $media) { + if ($media->file) { + echo "- {$media->name}: {$media->file->url}" . PHP_EOL; + } +} +``` + +### Массовое обновление категорий + +```php +$properties = MatrixErpProperty::find() + ->where(['like', 'guid', 'rose-%']) + ->all(); + +foreach ($properties as $property) { + $property->flowwow_category = 'Розы'; + $property->flowwow_subcategory = 'Букеты'; + $property->save(); +} +``` + +### Экспорт данных для Яндекс.Маркет + +```php +$properties = MatrixErpProperty::find() + ->where(['not', ['yandex_category' => null]]) + ->all(); + +$xml = new SimpleXMLElement(''); + +foreach ($properties as $property) { + $offer = $xml->addChild('offer'); + $offer->addAttribute('id', $property->guid); + $offer->addChild('name', $property->display_name); + $offer->addChild('description', $property->description); + $offer->addChild('categoryId', $property->yandex_category); + $offer->addChild('picture', $property->external_image_url); + $offer->addChild('url', $property->product_url); + + // Габариты + $dimensions = $offer->addChild('dimensions'); + $dimensions->addAttribute('length', $property->length); + $dimensions->addAttribute('width', $property->width); + $dimensions->addAttribute('height', $property->height); + + $offer->addChild('weight', $property->weight); +} + +$xml->asXML('yandex_feed.xml'); +``` + +### Экспорт для Flowwow + +```php +$properties = MatrixErpProperty::find() + ->where(['not', ['flowwow_category' => null]]) + ->all(); + +$flowwowData = []; + +foreach ($properties as $property) { + $flowwowData[] = [ + 'id' => $property->guid, + 'name' => $property->display_name, + 'description' => $property->description, + 'category' => $property->flowwow_category, + 'subcategory' => $property->flowwow_subcategory, + 'image' => $property->external_image_url, + 'url' => $property->product_url, + 'dimensions' => [ + 'length' => $property->length, + 'width' => $property->width, + 'height' => $property->height, + ], + 'weight' => $property->weight, + ]; +} + +file_put_contents('flowwow_feed.json', json_encode($flowwowData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `guid` | Обязательное, макс. 100 символов | +| `date` | Обязательное, макс. 100 символов | +| `description` | TEXT, nullable | +| `image_id` | Целое число, nullable | +| `display_name` | Макс. 100 символов, safe, nullable | +| `external_image_url` | Макс. 255 символов, safe, nullable | +| `product_url` | Макс. 255 символов, safe, nullable | +| `yandex_category` | Макс. 100 символов, safe, nullable | +| `flowwow_category` | Макс. 100 символов, safe, nullable | +| `flowwow_subcategory` | Макс. 100 символов, safe, nullable | +| `length` | Число (float), nullable | +| `width` | Число (float), nullable | +| `height` | Число (float), nullable | +| `weight` | Число (float), nullable | +| `imageFile` | Файл (png, jpg), skipOnEmpty | + +--- + +## Связанные модели + +- **[MatrixErp](./MatrixErp.md)** — матрица букетов (связь по `guid`) +- **[MatrixErpMedia](./MatrixErpMedia.md)** — медиа-файлы букета (связь по `guid`) +- **Files** — таблица файлов (связь по `image_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + matrix_erp ||--o| matrix_erp_property : "has_properties" + matrix_erp_property ||--o{ matrix_erp_media : "has_media" + matrix_erp_property }o--|| files : "has_image" + + matrix_erp { + int id PK + string guid UK + string name + } + + matrix_erp_property { + int id PK + string guid FK + text description + string display_name + string yandex_category + string flowwow_category + float length + float width + float height + float weight + } + + matrix_erp_media { + int id PK + string guid FK + string name + int file_id + int foto_order + } + + files { + int id PK + string url + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[MatrixErp] -->|guid| B[MatrixErpProperty] + B --> C[Данные для маркетплейсов] + C --> D[display_name] + C --> E[description] + C --> F[yandex_category] + C --> G[flowwow_category] + C --> H[external_image_url] + B --> I[Габариты и вес] + I --> J[length/width/height] + I --> K[weight] + B -->|guid| L[MatrixErpMedia] + L --> M[Медиа-файлы] + C --> N[Экспорт в Яндекс] + C --> O[Экспорт в Flowwow] + H --> N + H --> O +``` + +--- + +## Особенности реализации + +### Интеграция с маркетплейсами + +Модель специально разработана для интеграции с внешними платформами: + +**Яндекс.Маркет:** +- `yandex_category` — категория товара +- `external_image_url` — ссылка на изображение +- `product_url` — ссылка на карточку + +**Flowwow:** +- `flowwow_category` — основная категория +- `flowwow_subcategory` — подкатегория +- Габариты и вес для логистики + +### Габариты для доставки + +Поля `length`, `width`, `height`, `weight` используются для: +- Расчета стоимости доставки +- Определения возможности доставки +- Подбора упаковки +- Интеграции с курьерскими службами + +### Внешние ссылки + +Поля `external_image_url` и `product_url`: +- Позволяют использовать CDN для изображений +- Ссылаются на публичные URL без авторизации +- Используются в фидах маркетплейсов + +### Связь с медиа через guid + +Модель связана с медиа-файлами не через первичный ключ, а через `guid`: +```php +public function getMediaFiles() { + return $this->hasMany(MatrixErpMedia::class, ['guid' => 'guid']) + ->orderBy(['foto_order' => SORT_ASC]); +} +``` + +Это позволяет: +- Легко получать все медиа букета +- Автоматически сортировать по порядку +- Использовать eager loading + +--- + +## Использование в MatrixErp + +Модель является расширением основной модели матрицы: + +```php +// В MatrixErp +public function getMatrixProperty() +{ + return $this->hasOne(MatrixErpProperty::class, ['guid' => 'guid']); +} + +// Использование +$matrix = MatrixErp::find() + ->where(['id' => $id]) + ->with(['matrixProperty', 'matrixProperty.mediaFiles']) + ->one(); + +echo "Букет: {$matrix->name}" . PHP_EOL; +echo "Для маркетплейсов: {$matrix->matrixProperty->display_name}" . PHP_EOL; +echo "Категория Яндекс: {$matrix->matrixProperty->yandex_category}" . PHP_EOL; +``` + +--- + +## Сценарии использования + +### 1. Создание полной карточки товара + +```php +// Создаем матрицу +$matrix = new MatrixErp(); +$matrix->guid = 'bouquet-love-25'; +$matrix->name = 'Букет 25 роз'; +$matrix->save(); + +// Добавляем свойства +$property = new MatrixErpProperty(); +$property->guid = $matrix->guid; +$property->display_name = 'Букет "Любовь" - 25 красных роз'; +$property->description = 'Роскошный букет из свежих красных роз Фридом...'; +$property->date = date('Y-m-d'); +$property->yandex_category = 'Цветы и букеты/Розы'; +$property->flowwow_category = 'Розы'; +$property->length = 50; +$property->width = 30; +$property->height = 40; +$property->weight = 1.5; +$property->external_image_url = 'https://cdn.site.com/bouquet-love-25.jpg'; +$property->product_url = 'https://site.com/catalog/bouquet-love-25'; +$property->save(); +``` + +### 2. Автоматическая генерация фидов + +```php +class MarketplaceFeedGenerator +{ + public function generateYandexFeed() + { + $properties = MatrixErpProperty::find() + ->where(['not', ['yandex_category' => null]]) + ->all(); + + // Генерация YML файла + return $this->buildYML($properties); + } + + public function generateFlowwowFeed() + { + $properties = MatrixErpProperty::find() + ->where(['not', ['flowwow_category' => null]]) + ->all(); + + // Генерация JSON файла + return $this->buildJSON($properties); + } +} +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MatrixErpPropertyDynamic.md b/erp24/docs/models/MatrixErpPropertyDynamic.md new file mode 100644 index 00000000..3e5bd8fc --- /dev/null +++ b/erp24/docs/models/MatrixErpPropertyDynamic.md @@ -0,0 +1,267 @@ +# Класс: MatrixErpPropertyDynamic + + +## Mindmap + +```mermaid +mindmap + root((MatrixErpPropertyDynamic)) + Таблица БД + matrix_erp_property_dynamic + Свойства + id + int + product_id + string + category + int + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель динамических свойств товаров матрицы в ERP24. Хранит версионные изменения свойств товаров (компоненты состава, артикулы) с поддержкой временных периодов действия для отслеживания истории изменений. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`matrix_erp_property_dynamic` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `product_id` | varchar(36) | GUID товара из products_1c | +| `value` | text / null | Значение свойства | +| `category` | int | Категория свойства: 1=components, 2=articule | +| `date_from` | datetime | Дата и время начала действия | +| `date_end` | datetime / null | Дата и время окончания действия | +| `active` | int | Признак активности записи | + +## Константы категорий + +| Значение | Описание | Поле | +|----------|----------|------| +| `1` | Состав/компоненты | components | +| `2` | Артикул | articule | + +## Методы + +### fieldByCategory() +**Описание:** Возвращает маппинг категорий на названия полей. + +**Возвращает:** `array` — ассоциативный массив [category_id => field_name] + +```php +public static function fieldByCategory(): array +// Returns: [1 => 'components', 2 => 'articule'] +``` + +## Диаграмма связей + +```mermaid +erDiagram + MatrixErpPropertyDynamic { + int id PK + varchar product_id FK + text value + int category + datetime date_from + datetime date_end + int active + } + + Products1c { + varchar guid PK + varchar name + varchar articule + text components + } + + Products1c ||--o{ MatrixErpPropertyDynamic : "product_id" +``` + +## Диаграмма версионирования + +```mermaid +flowchart TD + subgraph "История изменений артикула" + V1[Версия 1
    date_from: 01.01.2024
    date_end: 01.03.2024
    value: ART-001] + V2[Версия 2
    date_from: 01.03.2024
    date_end: 01.06.2024
    value: ART-001-A] + V3[Версия 3 ACTIVE
    date_from: 01.06.2024
    date_end: null
    value: ART-002] + end + + V1 --> V2 + V2 --> V3 +``` + +## Примеры использования + +### Создание записи изменения состава +```php +// Закрываем текущую активную версию +$currentVersion = MatrixErpPropertyDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'category' => 1, // components + 'active' => 1 + ]) + ->one(); + +if ($currentVersion) { + $currentVersion->date_end = date('Y-m-d H:i:s'); + $currentVersion->active = 0; + $currentVersion->save(); +} + +// Создаём новую версию +$newVersion = new MatrixErpPropertyDynamic(); +$newVersion->product_id = $productGuid; +$newVersion->category = 1; // components +$newVersion->value = 'Роза красная 7 шт, Гипсофила, Эвкалипт'; +$newVersion->date_from = date('Y-m-d H:i:s'); +$newVersion->active = 1; +$newVersion->save(); +``` + +### Получение актуального значения свойства +```php +function getCurrentPropertyValue($productGuid, $category) +{ + $property = MatrixErpPropertyDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'category' => $category, + 'active' => 1 + ]) + ->one(); + + return $property ? $property->value : null; +} + +$components = getCurrentPropertyValue($guid, 1); +$articule = getCurrentPropertyValue($guid, 2); +``` + +### Получение значения на определённую дату +```php +function getPropertyValueAtDate($productGuid, $category, $date) +{ + return MatrixErpPropertyDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'category' => $category + ]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere([ + 'or', + ['date_end' => null], + ['>=', 'date_end', $date] + ]) + ->one(); +} + +// Какой был артикул 1 февраля? +$historicArticule = getPropertyValueAtDate($guid, 2, '2024-02-01 12:00:00'); +``` + +### История изменений свойства +```php +$history = MatrixErpPropertyDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'category' => 1 // components + ]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $version) { + $period = $version->date_end + ? "{$version->date_from} - {$version->date_end}" + : "{$version->date_from} - настоящее время"; + + echo "Период: {$period}\n"; + echo "Состав: {$version->value}\n\n"; +} +``` + +### Массовое обновление артикулов +```php +$updates = [ + 'guid-1' => 'NEW-ART-001', + 'guid-2' => 'NEW-ART-002', +]; + +$now = date('Y-m-d H:i:s'); + +foreach ($updates as $guid => $newArticule) { + // Закрываем старую версию + MatrixErpPropertyDynamic::updateAll( + ['date_end' => $now, 'active' => 0], + ['product_id' => $guid, 'category' => 2, 'active' => 1] + ); + + // Создаём новую + $prop = new MatrixErpPropertyDynamic(); + $prop->product_id = $guid; + $prop->category = 2; + $prop->value = $newArticule; + $prop->date_from = $now; + $prop->active = 1; + $prop->save(); +} +``` + +### Анализ частоты изменений +```php +$changeStats = MatrixErpPropertyDynamic::find() + ->select([ + 'product_id', + 'category', + 'COUNT(*) as version_count' + ]) + ->groupBy(['product_id', 'category']) + ->having(['>', 'COUNT(*)', 1]) + ->orderBy(['version_count' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($changeStats as $stat) { + $field = MatrixErpPropertyDynamic::fieldByCategory()[$stat['category']]; + echo "Товар {$stat['product_id']}: {$field} менялся {$stat['version_count']} раз\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `product_id` | required, string (max 36) | +| `category` | required, integer | +| `date_from` | required, safe | +| `value` | string, default: null | +| `date_end` | safe, default: null | +| `active` | integer, default: null | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары из 1С (связь по product_id) +- [MatrixErp](./MatrixErp.md) — матричные букеты + +## Особенности реализации + +1. **Temporal versioning**: Каждое изменение создаёт новую запись с периодом действия +2. **Две категории свойств**: Поддержка компонентов состава (1) и артикулов (2) +3. **Активная версия**: Флаг active=1 указывает на текущую актуальную версию +4. **Бессрочные периоды**: date_end = null означает действие до настоящего момента +5. **Связь по GUID**: Привязка к товарам через GUID для синхронизации с 1С +6. **Статический маппинг**: Метод fieldByCategory() для преобразования category в имя поля diff --git a/erp24/docs/models/MatrixErpPropertySearch.md b/erp24/docs/models/MatrixErpPropertySearch.md new file mode 100644 index 00000000..c4b8452a --- /dev/null +++ b/erp24/docs/models/MatrixErpPropertySearch.md @@ -0,0 +1,206 @@ +# Класс: MatrixErpPropertySearch + + +## Mindmap + +```mermaid +mindmap + root((MatrixErpPropertySearch)) + Таблица БД + ActiveRecord + Наследование + extends MatrixErpProperty +``` + +## Назначение +Search-модель для поиска и фильтрации свойств товаров в матрице ERP24. Стандартная Gii-модель для поиска по GUID, описанию, дате и связанному изображению. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MatrixErpProperty` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `image_id`, `created_admin_id`, `created_at`, `updated_admin_id`, `updated_at` — integer +- `guid`, `description`, `date` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска свойств. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MatrixErpProperty::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, image_id, created_admin_id, created_at, updated_admin_id, updated_at + - like: guid, description, date + +**Примечание:** Использует `like` вместо `ilike` (регистрозависимый поиск). + +## Диаграмма связей + +```mermaid +erDiagram + MatrixErpProperty { + int id PK + varchar guid + varchar description + date date + int image_id FK + int created_admin_id FK + datetime created_at + int updated_admin_id FK + datetime updated_at + } + + Files { + int id PK + varchar path + } + + Admin { + int id PK + varchar name + } + + MatrixErpProperty }o--o| Files : "image_id" + MatrixErpProperty }o--o| Admin : "created_admin_id" + MatrixErpProperty }o--o| Admin : "updated_admin_id" +``` + +## Диаграмма структуры свойства + +```mermaid +flowchart TD + A[MatrixErpProperty] --> B[guid - уникальный идентификатор] + A --> C[description - описание свойства] + A --> D[date - дата привязки] + A --> E[image_id - изображение] + + F[Аудит] --> G[created_admin_id] + F --> H[created_at] + F --> I[updated_admin_id] + F --> J[updated_at] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MatrixErpPropertySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по GUID +```php +$searchModel = new MatrixErpPropertySearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpPropertySearch' => [ + 'guid' => 'abc-123-def', + ] +]); +``` + +### Поиск по описанию +```php +$searchModel = new MatrixErpPropertySearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpPropertySearch' => [ + 'description' => 'красный', + ] +]); +``` + +### Поиск по дате +```php +$searchModel = new MatrixErpPropertySearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpPropertySearch' => [ + 'date' => '2024-01-15', + ] +]); +``` + +### Поиск по ID изображения +```php +$searchModel = new MatrixErpPropertySearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpPropertySearch' => [ + 'image_id' => 12345, + ] +]); +``` + +### Поиск по создателю +```php +$searchModel = new MatrixErpPropertySearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpPropertySearch' => [ + 'created_admin_id' => 5, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'guid', + 'description', + 'date', + [ + 'attribute' => 'image_id', + 'format' => 'raw', + 'value' => function($model) { + return $model->image ? Html::img($model->image->getUrl(), ['width' => 50]) : '-'; + }, + ], + 'created_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [MatrixErpProperty](./MatrixErpProperty.md) — базовая модель свойств +- [MatrixErp](./MatrixErp.md) — матрица ERP товаров +- [Files](./Files.md) — файлы изображений +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Стандартная Gii-модель**: Без кастомного formName параметра +2. **like вместо ilike**: Регистрозависимый поиск (отличие от других моделей) +3. **Аудит**: created_admin_id, updated_admin_id, created_at, updated_at +4. **GUID**: Уникальный идентификатор для внешних систем +5. **Изображение**: image_id для связи с файлом +6. **Дата**: date для временной привязки свойства diff --git a/erp24/docs/models/MatrixErpSearch.md b/erp24/docs/models/MatrixErpSearch.md new file mode 100644 index 00000000..b25cc038 --- /dev/null +++ b/erp24/docs/models/MatrixErpSearch.md @@ -0,0 +1,225 @@ +# Класс: MatrixErpSearch + + +## Mindmap + +```mermaid +mindmap + root((MatrixErpSearch)) + Таблица БД + ActiveRecord + Наследование + extends MatrixErp +``` + +## Назначение +Search-модель для поиска и фильтрации товаров в матрице ERP24. Стандартная Gii-модель для поиска по основным атрибутам товара: артикул, наименование, код, группа, компоненты и периоды актуальности. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MatrixErp` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `category_id`, `active`, `created_admin_id`, `created_at`, `updated_admin_id`, `updated_at` — integer +- `guid`, `parent_id`, `group_name`, `code`, `name`, `components`, `articule`, `date_from`, `date_to` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска товаров. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос MatrixErp::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, category_id, active, created_admin_id, created_at, updated_admin_id, updated_at + - like: guid, parent_id, group_name, code, name, components, articule, date_from, date_to + +**Примечание:** Использует `like` вместо `ilike` (регистрозависимый поиск). + +## Диаграмма связей + +```mermaid +erDiagram + MatrixErp { + int id PK + varchar guid + varchar parent_id + varchar group_name + varchar code + varchar name + varchar components + varchar articule + int category_id FK + int active + date date_from + date date_to + int created_admin_id FK + datetime created_at + int updated_admin_id FK + datetime updated_at + } + + MatrixType { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + MatrixErp }o--o| MatrixType : "category_id" + MatrixErp }o--o| Admin : "created_admin_id" + MatrixErp }o--o| Admin : "updated_admin_id" +``` + +## Диаграмма структуры товара + +```mermaid +flowchart TD + A[MatrixErp] --> B[Идентификация] + B --> C[guid - внешний GUID] + B --> D[articule - артикул] + B --> E[code - код] + + A --> F[Классификация] + F --> G[category_id - категория] + F --> H[group_name - группа] + F --> I[parent_id - родитель] + + A --> J[Данные] + J --> K[name - наименование] + J --> L[components - компоненты] + + A --> M[Актуальность] + M --> N[active - активность] + M --> O[date_from - дата начала] + M --> P[date_to - дата окончания] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MatrixErpSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по артикулу +```php +$searchModel = new MatrixErpSearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpSearch' => [ + 'articule' => 'FLOWER-001', + ] +]); +``` + +### Поиск по наименованию +```php +$searchModel = new MatrixErpSearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpSearch' => [ + 'name' => 'Букет', + ] +]); +``` + +### Поиск активных товаров +```php +$searchModel = new MatrixErpSearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpSearch' => [ + 'active' => 1, + ] +]); +``` + +### Поиск по категории +```php +$searchModel = new MatrixErpSearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpSearch' => [ + 'category_id' => 5, + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new MatrixErpSearch(); +$dataProvider = $searchModel->search([ + 'MatrixErpSearch' => [ + 'group_name' => 'Розы', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'articule', + 'name', + 'group_name', + 'code', + [ + 'attribute' => 'active', + 'value' => function($model) { + return $model->active ? 'Да' : 'Нет'; + }, + ], + 'date_from', + 'date_to', + ], +]) ?> +``` + +## Связанные модели + +- [MatrixErp](./MatrixErp.md) — базовая модель матрицы ERP +- [MatrixType](./MatrixType.md) — типы/категории товаров +- [MatrixErpProperty](./MatrixErpProperty.md) — свойства товаров +- [MarketplacePrices](./MarketplacePrices.md) — цены на маркетплейсах +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Стандартная Gii-модель**: Без кастомного formName параметра +2. **like вместо ilike**: Регистрозависимый поиск +3. **Иерархия**: parent_id для древовидной структуры +4. **Компоненты**: components для состава букета +5. **Периоды актуальности**: date_from, date_to для сезонности +6. **GUID**: Уникальный идентификатор для внешних систем (1С) +7. **Аудит**: created_admin_id, updated_admin_id, created_at, updated_at diff --git a/erp24/docs/models/MatrixType.md b/erp24/docs/models/MatrixType.md new file mode 100644 index 00000000..f8cbcea9 --- /dev/null +++ b/erp24/docs/models/MatrixType.md @@ -0,0 +1,279 @@ +# Класс: MatrixType + + +## Mindmap + +```mermaid +mindmap + root((MatrixType)) + Таблица БД + {{%erp24.matrix_type}} + Свойства + id + int + active + int + name + string + created_by + int + created_at + string + deleted + int + Связи + Parent + 1:1 self + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов матриц ассортимента в ERP24. Иерархическая структура категорий для классификации матричных букетов с поддержкой вложенности, мягкого удаления и полного аудита изменений. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`erp24.matrix_type` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: `updated_at` | +| `BlameableBehavior` | createdByAttribute: `created_by`, updatedByAttribute: `updated_by` | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `parent_id` | int / null | ID родительской категории (null = корневая) | +| `name` | varchar(255) | Название типа матрицы | +| `active` | int | Активность записи (0/1, default: 1) | +| `deleted` | int | Признак удаления (0/1) | +| `created_by` | int | ID пользователя-создателя (автоматически) | +| `created_at` | datetime | Дата создания (автоматически) | +| `updated_by` | int / null | ID пользователя, обновившего запись | +| `updated_at` | datetime / null | Дата обновления | +| `deleted_by` | int / null | ID пользователя, удалившего запись | +| `deleted_at` | datetime / null | Дата удаления | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getParent()` | hasOne | MatrixType | Родительская категория | + +## Методы + +### collectAncestorIds(array $map) +**Описание:** Собирает ID всех предков текущей категории. + +**Параметры:** +- `$map` (array) — ассоциативный массив [id => MatrixType], предварительно загруженные записи + +**Возвращает:** `array` — массив ID предков от непосредственного родителя к корню + +```php +public function collectAncestorIds(array $map): array +``` + +## Диаграмма связей + +```mermaid +erDiagram + MatrixType { + int id PK + int parent_id FK + varchar name + int active + int deleted + int created_by FK + datetime created_at + int updated_by FK + datetime updated_at + int deleted_by FK + datetime deleted_at + } + + Admin { + int id PK + varchar username + } + + MatrixType ||--o{ MatrixType : "parent_id" + Admin ||--o{ MatrixType : "created_by" + Admin ||--o{ MatrixType : "updated_by" + Admin ||--o{ MatrixType : "deleted_by" +``` + +## Диаграмма иерархии + +```mermaid +flowchart TD + subgraph "Иерархия типов матрицы" + ROOT[Все букеты
    parent_id = null] + ROOT --> A[Сезонные] + ROOT --> B[Круглогодичные] + ROOT --> C[Праздничные] + + A --> A1[Весенние] + A --> A2[Летние] + A --> A3[Осенние] + A --> A4[Зимние] + + C --> C1[8 марта] + C --> C2[День матери] + C --> C3[Новый год] + end +``` + +## Примеры использования + +### Создание категории +```php +$type = new MatrixType(); +$type->name = 'Премиум букеты'; +$type->parent_id = $parentCategoryId; // или null для корневой +$type->active = 1; +$type->deleted = 0; +// created_by, created_at заполнятся автоматически +$type->save(); +``` + +### Получение дерева категорий +```php +$allTypes = MatrixType::find() + ->where(['deleted' => 0, 'active' => 1]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +$tree = []; +$map = ArrayHelper::index($allTypes, 'id'); + +foreach ($allTypes as $type) { + if ($type->parent_id === null) { + $tree[$type->id] = [ + 'model' => $type, + 'children' => [] + ]; + } else { + $tree[$type->parent_id]['children'][$type->id] = [ + 'model' => $type, + 'children' => [] + ]; + } +} +``` + +### Получение потомков категории +```php +function getDescendants($parentId, $allTypes) +{ + $descendants = []; + foreach ($allTypes as $type) { + if ($type->parent_id === $parentId) { + $descendants[] = $type; + $descendants = array_merge($descendants, getDescendants($type->id, $allTypes)); + } + } + return $descendants; +} +``` + +### Получение цепочки предков +```php +$type = MatrixType::findOne($categoryId); +$allTypes = MatrixType::find()->indexBy('id')->all(); + +$ancestorIds = $type->collectAncestorIds($allTypes); +// Результат: [parent_id, grandparent_id, ...] + +$breadcrumbs = []; +foreach (array_reverse($ancestorIds) as $id) { + $breadcrumbs[] = $allTypes[$id]->name; +} +$breadcrumbs[] = $type->name; + +echo implode(' > ', $breadcrumbs); +// "Все букеты > Сезонные > Весенние" +``` + +### Мягкое удаление категории +```php +$type = MatrixType::findOne($categoryId); +$type->deleted = 1; +$type->deleted_by = Yii::$app->user->id; +$type->deleted_at = date('Y-m-d H:i:s'); +$type->save(); +``` + +### Формирование списка для выбора +```php +function buildFlatList($parentId = null, $level = 0, $allTypes) +{ + $result = []; + foreach ($allTypes as $type) { + if ($type->parent_id === $parentId && !$type->deleted && $type->active) { + $result[$type->id] = str_repeat('— ', $level) . $type->name; + $result += buildFlatList($type->id, $level + 1, $allTypes); + } + } + return $result; +} + +$selectList = buildFlatList(null, 0, MatrixType::find()->all()); +// [1 => 'Сезонные', 2 => '— Весенние', 3 => '— Летние', ...] +``` + +### Проверка уникальности имени +```php +// Уникальность проверяется в рамках одного родителя +$exists = MatrixType::find() + ->where([ + 'name' => $newName, + 'parent_id' => $parentId, + 'deleted' => 0 + ]) + ->exists(); + +if ($exists) { + throw new \Exception('Категория с таким названием уже существует'); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255), trim, unique в рамках parent_id | +| `parent_id` | integer, default: null | +| `active` | integer | +| `deleted` | integer | +| `created_by`, `updated_by`, `deleted_by` | integer | +| `created_at`, `updated_at`, `deleted_at` | safe | + +### Составное уникальное ограничение + +| Поля | Описание | +|------|----------| +| `[name, parent_id]` | Уникальность имени в рамках одного родителя | + +## Связанные модели + +- [MatrixErp](./MatrixErp.md) — матричные букеты, привязанные к типу +- [Admin](./Admin.md) — пользователи (created_by, updated_by, deleted_by) + +## Особенности реализации + +1. **Иерархическая структура**: Древовидная организация через parent_id +2. **Мягкое удаление**: Флаг deleted с фиксацией deleted_by и deleted_at +3. **Полный аудит**: TimestampBehavior + BlameableBehavior для автоматического отслеживания +4. **Уникальность в контексте**: Имена уникальны только в рамках одного родителя +5. **Сбор предков**: Метод collectAncestorIds для построения breadcrumbs +6. **Схема erp24**: Таблица в отдельной схеме БД diff --git a/erp24/docs/models/MatrixTypeSearch.md b/erp24/docs/models/MatrixTypeSearch.md new file mode 100644 index 00000000..906de8e7 --- /dev/null +++ b/erp24/docs/models/MatrixTypeSearch.md @@ -0,0 +1,169 @@ +# Класс: MatrixTypeSearch + + +## Mindmap + +```mermaid +mindmap + root((MatrixTypeSearch)) + Таблица БД + ActiveRecord + Наследование + extends MatrixType +``` + +## Назначение +Search-модель для поиска и фильтрации типов товаров в матрице ERP24. Нестандартная модель, использующая ArrayDataProvider вместо ActiveDataProvider и реализующая сложную логику фильтрации с учётом иерархии родителей. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`MatrixType` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `active` — safe + +### search($params): ArrayDataProvider +**Описание:** Создаёт провайдер данных с фильтрацией по активности и логикой наследования родителей. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ArrayDataProvider` — провайдер данных на основе массива + +**Логика:** +1. Загружает параметры с пустым formName (load($params, '')) +2. Получает все типы с `deleted = 0`, сортировка по `parent_id ASC, id ASC` +3. Если `active` не указан — возвращает все типы +4. Если `active = 1` — возвращает только активные типы +5. Если `active = 0` — возвращает неактивные типы И всех их предков (ancestors) +6. Использует метод `collectAncestorIds()` базовой модели для построения иерархии +7. Возвращает ArrayDataProvider без пагинации и сортировки + +## Диаграмма логики фильтрации + +```mermaid +flowchart TD + A[Начало поиска] --> B{active = ?} + + B -->|null/пусто| C[Все типы с deleted=0] + B -->|1| D[Только active=1] + B -->|0| E[Неактивные + предки] + + E --> F[Собрать неактивные] + F --> G[Для каждого вызвать collectAncestorIds] + G --> H[Объединить ID в include map] + H --> I[Отфильтровать по include] + + C --> J[ArrayDataProvider] + D --> J + I --> J + + J --> K[pagination: false] + K --> L[sort: false] +``` + +## Диаграмма иерархии типов + +```mermaid +flowchart TD + A[Родительский тип] --> B[Дочерний тип 1] + A --> C[Дочерний тип 2] + B --> D[Внук 1] + B --> E[Внук 2] + + style D fill:#f96,stroke:#333 + style E fill:#f96,stroke:#333 + + F[При active=0] --> G[Включить D,E неактивные] + G --> H[Включить B - родитель] + H --> I[Включить A - прародитель] +``` + +## Примеры использования + +### Получение всех типов +```php +$searchModel = new MatrixTypeSearch(); +$dataProvider = $searchModel->search([]); +// Вернёт все типы с deleted=0 +``` + +### Только активные типы +```php +$searchModel = new MatrixTypeSearch(); +$dataProvider = $searchModel->search([ + 'active' => 1, +]); +``` + +### Неактивные с предками +```php +$searchModel = new MatrixTypeSearch(); +$dataProvider = $searchModel->search([ + 'active' => 0, +]); +// Вернёт неактивные типы и всех их родителей +``` + +### Использование в контроллере +```php +public function actionIndex() +{ + $searchModel = new MatrixTypeSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'parent_id', + [ + 'attribute' => 'active', + 'value' => function($model) { + return $model->active ? 'Да' : 'Нет'; + }, + 'filter' => [ + '' => 'Все', + '1' => 'Активные', + '0' => 'Неактивные + предки', + ], + ], + ], +]) ?> +``` + +## Связанные модели + +- [MatrixType](./MatrixType.md) — базовая модель типов +- [MatrixErp](./MatrixErp.md) — товары матрицы ERP + +## Особенности реализации + +1. **ArrayDataProvider**: Вместо ActiveDataProvider — все данные загружаются в память +2. **Пустой formName**: load($params, '') для прямого доступа к параметрам +3. **Без пагинации**: pagination: false для отображения всех записей +4. **Без сортировки**: sort: false — сортировка задана в запросе +5. **Иерархическая фильтрация**: При active=0 включаются все предки неактивных +6. **collectAncestorIds()**: Метод базовой модели для сбора ID родителей +7. **Soft delete**: Фильтр deleted=0 для исключения удалённых +8. **Сортировка**: ORDER BY parent_id ASC, id ASC для правильного порядка иерархии diff --git a/erp24/docs/models/Meeting.md b/erp24/docs/models/Meeting.md new file mode 100644 index 00000000..f5e093b4 --- /dev/null +++ b/erp24/docs/models/Meeting.md @@ -0,0 +1,317 @@ +# Класс: Meeting + + +## Mindmap + +```mermaid +mindmap + root((Meeting)) + Таблица БД + meeting + Свойства + id + int + title + string + location + int + start + string + end + string + created_by + int + Связи + Creator + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель встреч и совещаний в ERP24. Управление рабочими встречами с поддержкой различных локаций (офис, Telegram, Zoom), привязкой к задачам и системой участников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`meeting` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Публичные свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$mode` | mixed | Режим работы формы | +| `$cssClassArray` | array | CSS-классы для статусов участников | + +## Константы локаций + +```php +public static $location_array = [ + 1 => 'Офис', + 2 => 'Телеграм', + 3 => 'Зум' +]; +``` + +## CSS-классы статусов + +```php +public $cssClassArray = [ + -2 => 'success', // Подтверждено + -1 => 'primary', // Придёт + 0 => 'secondary', // Не ответил +]; +``` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `title` | varchar(255) | Тема встречи | +| `description` | text / null | Описание встречи | +| `location` | int | Место проведения (1=Офис, 2=Telegram, 3=Zoom) | +| `start` | datetime | Дата и время начала | +| `end` | datetime | Дата и время окончания | +| `created_by` | int | ID создателя (автоматически) | +| `created_at` | datetime | Дата создания (автоматически) | +| `updated_by` | int / null | ID пользователя, обновившего запись | +| `updated_at` | datetime / null | Дата обновления | +| `task_id` | int / null | ID связанной задачи | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getCreator()` | hasOne | Admin | Создатель встречи | + +## Методы + +### validateDate() +**Описание:** Валидация дат начала и окончания встречи. + +**Проверки:** +- Дата начала не может быть позже даты окончания +- Встреча должна проходить в течение одного дня + +```php +public function validateDate(): void +``` + +### addParticipants($participants) +**Описание:** Добавляет участников к встрече, игнорируя уже добавленных. + +**Параметры:** +- `$participants` (array) — массив ID пользователей + +**Возвращает:** `array` — массив ID новых участников + +```php +public function addParticipants(array $participants): array +``` + +## Диаграмма связей + +```mermaid +erDiagram + Meeting { + int id PK + varchar title + text description + int location + datetime start + datetime end + int created_by FK + datetime created_at + int updated_by FK + datetime updated_at + int task_id FK + } + + MeetingLinkAdmin { + int id PK + int meeting_id FK + int admin_id FK + int status + } + + Admin { + int id PK + varchar username + } + + Task { + int id PK + varchar title + } + + Meeting ||--o{ MeetingLinkAdmin : "meeting_id" + Admin ||--o{ MeetingLinkAdmin : "admin_id" + Admin ||--o{ Meeting : "created_by" + Task ||--o| Meeting : "task_id" +``` + +## Диаграмма workflow встречи + +```mermaid +flowchart TD + A[Создание встречи] --> B[Выбор локации] + B --> C[Установка времени] + C --> D[Добавление участников] + D --> E[Отправка приглашений] + E --> F{Статус участников} + F -->|Не ответил| G[Ожидание] + F -->|Придёт| H[Подтверждено] + F -->|Не придёт| I[Отклонено] + G --> J[Проведение встречи] + H --> J +``` + +## Примеры использования + +### Создание встречи +```php +$meeting = new Meeting(); +$meeting->scenario = 'create'; +$meeting->title = 'Еженедельный статус'; +$meeting->description = 'Обсуждение текущих задач команды'; +$meeting->location = 3; // Zoom +$meeting->start = '2024-03-15 10:00:00'; +$meeting->end = '2024-03-15 11:30:00'; +$meeting->task_id = $relatedTaskId; // опционально +// created_by и created_at заполнятся автоматически +$meeting->save(); +``` + +### Добавление участников +```php +$meeting = Meeting::findOne($meetingId); +$participantIds = [5, 12, 18, 25]; // ID сотрудников + +$newlyAdded = $meeting->addParticipants($participantIds); + +echo "Добавлено новых участников: " . count($newlyAdded); +``` + +### Получение встреч на день +```php +$date = '2024-03-15'; + +$meetings = Meeting::find() + ->where(['>=', 'start', $date . ' 00:00:00']) + ->andWhere(['<=', 'start', $date . ' 23:59:59']) + ->with(['creator']) + ->orderBy(['start' => SORT_ASC]) + ->all(); + +foreach ($meetings as $meeting) { + $location = Meeting::$location_array[$meeting->location]; + echo "{$meeting->start} - {$meeting->title} ({$location})\n"; +} +``` + +### Получение встреч пользователя +```php +$userId = Yii::$app->user->id; + +$myMeetings = Meeting::find() + ->innerJoin('meeting_admin_link mal', 'meeting.id = mal.meeting_id') + ->where(['mal.admin_id' => $userId]) + ->orWhere(['meeting.created_by' => $userId]) + ->orderBy(['start' => SORT_ASC]) + ->all(); +``` + +### Обновление встречи +```php +$meeting = Meeting::findOne($meetingId); +$meeting->scenario = 'update'; +$meeting->end = '2024-03-15 12:00:00'; // Продление +$meeting->description .= "\n\nОбновлено: добавлена повестка дня"; +// updated_by и updated_at заполнятся автоматически +$meeting->save(); +``` + +### Форматирование для календаря +```php +$meetings = Meeting::find() + ->where(['>=', 'start', $startDate]) + ->andWhere(['<=', 'end', $endDate]) + ->all(); + +$events = []; +foreach ($meetings as $meeting) { + $events[] = [ + 'id' => $meeting->id, + 'title' => $meeting->title, + 'start' => $meeting->start, + 'end' => $meeting->end, + 'className' => 'location-' . $meeting->location, + 'extendedProps' => [ + 'location' => Meeting::$location_array[$meeting->location], + 'description' => $meeting->description, + ] + ]; +} + +return json_encode($events); +``` + +### Проверка конфликтов расписания +```php +function hasConflict($userId, $start, $end, $excludeMeetingId = null) +{ + $query = Meeting::find() + ->innerJoin('meeting_admin_link mal', 'meeting.id = mal.meeting_id') + ->where(['mal.admin_id' => $userId]) + ->andWhere(['<', 'start', $end]) + ->andWhere(['>', 'end', $start]); + + if ($excludeMeetingId) { + $query->andWhere(['!=', 'meeting.id', $excludeMeetingId]); + } + + return $query->exists(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `title` | required, string (max 255) | +| `description` | string | +| `location` | required, integer | +| `start` | required, safe, validateDate | +| `end` | required, safe, validateDate | +| `created_by` | default: user.id (сценарий create) | +| `created_at` | default: NOW() (сценарий create) | +| `updated_by` | default: user.id (сценарий update) | +| `updated_at` | default: NOW() (сценарий update) | +| `task_id` | integer | + +### Кастомная валидация дат + +| Правило | Описание | +|---------|----------| +| start <= end | Дата начала не может быть позже окончания | +| Один день | Встреча должна проходить в течение одного дня | + +## Связанные модели + +- [MeetingLinkAdmin](./MeetingLinkAdmin.md) — связь встречи с участниками +- [Admin](./Admin.md) — пользователи системы +- [Task](./Task.md) — связанные задачи + +## Особенности реализации + +1. **Сценарии**: Используются сценарии create/update для автозаполнения полей +2. **Локации**: Три типа мест проведения (офис, Telegram, Zoom) +3. **Однодневность**: Валидация ограничивает встречу одним календарным днём +4. **Участники**: Связь через промежуточную таблицу MeetingLinkAdmin +5. **Привязка к задачам**: Опциональная связь с Task через task_id +6. **CSS-классы**: Готовые классы для визуализации статусов участников diff --git a/erp24/docs/models/MeetingLinkAdmin.md b/erp24/docs/models/MeetingLinkAdmin.md new file mode 100644 index 00000000..7b5fe665 --- /dev/null +++ b/erp24/docs/models/MeetingLinkAdmin.md @@ -0,0 +1,285 @@ +# Класс: MeetingLinkAdmin + + +## Mindmap + +```mermaid +mindmap + root((MeetingLinkAdmin)) + Таблица БД + meeting_admin_link + Свойства + id + int + meeting_id + int + admin_id + int + Наследование + extends ActiveRecord +``` + +## Назначение +Связующая модель между встречами и участниками в ERP24. Хранит информацию о приглашённых на встречу сотрудниках и их статусе участия (придёт, не придёт, не ответил). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`meeting_admin_link` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы статусов + +```php +public static $status_array = [ + 0 => 'Не ответил', + 1 => 'Придет', + -1 => 'Не придет' +]; +``` + +| Значение | Описание | +|----------|----------| +| `0` | Не ответил (по умолчанию) | +| `1` | Придёт | +| `-1` | Не придёт | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `meeting_id` | int | FK на встречу | +| `admin_id` | int | FK на участника | +| `status` | int | Статус участия (0, 1, -1) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getParticipant()` | hasOne | Admin | Участник встречи | + +## Диаграмма связей + +```mermaid +erDiagram + Meeting { + int id PK + varchar title + datetime start + datetime end + } + + MeetingLinkAdmin { + int id PK + int meeting_id FK + int admin_id FK + int status + } + + Admin { + int id PK + varchar username + varchar name + } + + Meeting ||--o{ MeetingLinkAdmin : "meeting_id" + Admin ||--o{ MeetingLinkAdmin : "admin_id" +``` + +## Диаграмма статусов участия + +```mermaid +stateDiagram-v2 + [*] --> НеОтветил: Приглашение отправлено + НеОтветил --> Придет: Подтверждение + НеОтветил --> НеПридет: Отклонение + Придет --> НеПридет: Изменение решения + НеПридет --> Придет: Изменение решения + + НеОтветил: status = 0 + Придет: status = 1 + НеПридет: status = -1 +``` + +## Примеры использования + +### Добавление участника к встрече +```php +$link = new MeetingLinkAdmin(); +$link->meeting_id = $meeting->id; +$link->admin_id = $userId; +// status по умолчанию = 0 (Не ответил) +$link->save(); +``` + +### Получение участников встречи +```php +$participants = MeetingLinkAdmin::find() + ->where(['meeting_id' => $meetingId]) + ->with(['participant']) + ->all(); + +foreach ($participants as $link) { + $statusText = MeetingLinkAdmin::$status_array[$link->status]; + echo "{$link->participant->name}: {$statusText}\n"; +} +``` + +### Обновление статуса участия +```php +$link = MeetingLinkAdmin::find() + ->where([ + 'meeting_id' => $meetingId, + 'admin_id' => Yii::$app->user->id + ]) + ->one(); + +if ($link) { + $link->status = 1; // Придёт + $link->save(); +} +``` + +### Получение встреч пользователя по статусу +```php +// Встречи, на которые пользователь согласился +$confirmedMeetings = MeetingLinkAdmin::find() + ->where([ + 'admin_id' => $userId, + 'status' => 1 + ]) + ->with(['meeting']) + ->all(); + +foreach ($confirmedMeetings as $link) { + echo "{$link->meeting->title} - {$link->meeting->start}\n"; +} +``` + +### Статистика по встрече +```php +$stats = MeetingLinkAdmin::find() + ->select(['status', 'COUNT(*) as count']) + ->where(['meeting_id' => $meetingId]) + ->groupBy('status') + ->asArray() + ->all(); + +$result = [ + 'total' => 0, + 'confirmed' => 0, + 'declined' => 0, + 'pending' => 0, +]; + +foreach ($stats as $stat) { + $result['total'] += $stat['count']; + switch ($stat['status']) { + case 1: $result['confirmed'] = $stat['count']; break; + case -1: $result['declined'] = $stat['count']; break; + case 0: $result['pending'] = $stat['count']; break; + } +} + +echo "Подтвердили: {$result['confirmed']}/{$result['total']}"; +``` + +### Массовое приглашение участников +```php +$meeting = Meeting::findOne($meetingId); +$teamMemberIds = [5, 12, 18, 25, 30]; + +$existingIds = MeetingLinkAdmin::find() + ->select('admin_id') + ->where(['meeting_id' => $meetingId]) + ->column(); + +foreach ($teamMemberIds as $userId) { + if (!in_array($userId, $existingIds)) { + $link = new MeetingLinkAdmin(); + $link->meeting_id = $meetingId; + $link->admin_id = $userId; + $link->save(); + } +} +``` + +### Удаление участника из встречи +```php +MeetingLinkAdmin::deleteAll([ + 'meeting_id' => $meetingId, + 'admin_id' => $userId +]); +``` + +### Проверка участия пользователя +```php +function isParticipant($meetingId, $userId) +{ + return MeetingLinkAdmin::find() + ->where([ + 'meeting_id' => $meetingId, + 'admin_id' => $userId + ]) + ->exists(); +} + +function willAttend($meetingId, $userId) +{ + return MeetingLinkAdmin::find() + ->where([ + 'meeting_id' => $meetingId, + 'admin_id' => $userId, + 'status' => 1 + ]) + ->exists(); +} +``` + +### Формирование списка для UI +```php +$links = MeetingLinkAdmin::find() + ->where(['meeting_id' => $meetingId]) + ->with(['participant']) + ->all(); + +$participantsList = []; +foreach ($links as $link) { + $participantsList[] = [ + 'id' => $link->admin_id, + 'name' => $link->participant->name, + 'status' => $link->status, + 'statusText' => MeetingLinkAdmin::$status_array[$link->status], + 'statusClass' => match($link->status) { + 1 => 'success', + -1 => 'danger', + default => 'secondary' + } + ]; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `meeting_id` | required, integer | +| `admin_id` | required, integer | +| `status` | integer (implicit) | + +## Связанные модели + +- [Meeting](./Meeting.md) — встречи +- [Admin](./Admin.md) — участники (сотрудники) + +## Особенности реализации + +1. **Many-to-Many**: Связующая таблица между Meeting и Admin +2. **Статусы участия**: Три статуса для отслеживания подтверждений +3. **Статический справочник**: $status_array для отображения статусов +4. **Простая структура**: Минимум полей для чистой связи +5. **Без дублирования**: Один участник может быть добавлен к встрече только один раз diff --git a/erp24/docs/models/MeetingSearch.md b/erp24/docs/models/MeetingSearch.md new file mode 100644 index 00000000..0f6dc1e9 --- /dev/null +++ b/erp24/docs/models/MeetingSearch.md @@ -0,0 +1,268 @@ +# Класс: MeetingSearch + + +## Mindmap + +```mermaid +mindmap + root((MeetingSearch)) + Таблица БД + ActiveRecord + Наследование + extends Meeting +``` + +## Назначение +Search-модель для поиска и фильтрации встреч (совещаний) в календаре ERP24. Комплексная модель с поддержкой трёх режимов поиска (контроль/участие/доступ), объединением запросов через UNION, навигацией по датам и обработкой дубликатов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Meeting` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$mode` | string | Режим поиска: -1 (контроль), 0 (участвую), 1 (доступ), null (все) | +| `$date_start` | string | Начало периода поиска | +| `$date_end` | string | Конец периода поиска | +| `$date_today` | string | Текущая дата для навигации | +| `$format` | string | Формат календаря: timeGridDay, dayGridWeek, month | +| `$repetition_action` | int | Обработка дубликатов: -1 (убрать), 0 (в подкатегорию), 1 (оставить) | +| `$permission` | string | Фильтр по имени администратора для режима доступа | + +## Справочники режимов + +### mode_array +```php +[ + -1 => 'Контроль', // Встречи, созданные пользователем + 0 => 'Участвую', // Встречи, где пользователь участник + 1 => 'Доступ', // Встречи с предоставленным доступом +] +``` + +### repetition_action_array +```php +[ + -1 => 'Убрать', // Удалить дубликаты + 0 => 'В подкатегорию', // Объединить mode в строку + 1 => 'Оставить', // Показать все записи +] +``` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска с дефолтными значениями. + +**Возвращает:** `array` — массив правил + +**Дефолтные значения:** +- `date_start` — первый день предыдущего месяца +- `date_end` — последний день следующего месяца +- `date_today` — текущая дата +- `repetition_action` — 0 (в подкатегорию) + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ArrayDataProvider +**Описание:** Главный метод поиска с объединением подзапросов через UNION. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ArrayDataProvider` — провайдер данных + +**Логика:** +1. Загружает и валидирует параметры +2. В зависимости от mode создаёт подзапрос: + - mode = -1: searchCreator() + - mode = 0: searchInvited() + - mode = 1: searchAccess() + - mode = null: UNION всех трёх +3. Выполняет запрос с ORDER BY mode ASC +4. Обрабатывает дубликаты согласно repetition_action +5. Возвращает ArrayDataProvider + +### searchCreator(): ActiveQuery +**Описание:** Поиск встреч, где текущий пользователь является создателем. + +**Возвращает:** `ActiveQuery` — запрос + +**Логика:** +- Фильтр: created_by = текущий пользователь +- mode = '-1' +- Фильтр по date_start/date_end + +### searchInvited(): ActiveQuery +**Описание:** Поиск встреч, где текущий пользователь является участником. + +**Возвращает:** `ActiveQuery` — запрос + +**Логика:** +- INNER JOIN с meeting_admin_link +- Фильтр: meeting_admin_link.admin_id = текущий пользователь +- mode = '0' +- Фильтр по date_start/date_end + +### searchAccess(): ActiveQuery +**Описание:** Поиск встреч, к которым дали доступ через calendar_admin_link. + +**Возвращает:** `ActiveQuery` — запрос + +**Логика:** +- INNER JOIN с meeting_admin_link и calendar_admin_link +- LEFT JOIN с admin для получения имени +- Фильтр: calendar_admin_link.admin_id = текущий пользователь +- mode = имя администратора (из admin.name) +- Фильтр по permission и date_start/date_end + +### getPreviousDate(): array +**Описание:** Вычисляет даты для навигации назад. + +**Возвращает:** `array` — [date_start, date_end, date_today] + +**Логика:** +- timeGridDay: вчера +- dayGridWeek: предыдущая неделя +- month: предыдущий месяц + +### getNextDate(): array +**Описание:** Вычисляет даты для навигации вперёд. + +**Возвращает:** `array` — [date_start, date_end, date_today] + +**Логика:** +- timeGridDay: завтра +- dayGridWeek: следующая неделя +- month: следующий месяц + +## Диаграмма режимов поиска + +```mermaid +flowchart TD + A[MeetingSearch] --> B{mode?} + + B -->|mode = -1| C[searchCreator] + B -->|mode = 0| D[searchInvited] + B -->|mode = 1| E[searchAccess] + B -->|mode = null| F[UNION всех] + + C --> G[created_by = user] + D --> H[meeting_admin_link.admin_id = user] + E --> I[calendar_admin_link.admin_id = user] + + F --> J[searchCreator UNION searchInvited UNION searchAccess] +``` + +## Диаграмма связей + +```mermaid +erDiagram + Meeting { + int id PK + int location + int created_by FK + varchar title + varchar description + datetime start + datetime end + } + + MeetingAdminLink { + int id PK + int meeting_id FK + int admin_id FK + } + + CalendarAdminLink { + int id PK + int calendar_admin_id FK + int admin_id FK + } + + Admin { + int id PK + varchar name + } + + Meeting ||--o{ MeetingAdminLink : "участники" + MeetingAdminLink }o--|| Admin : "admin_id" + Admin ||--o{ CalendarAdminLink : "доступ к календарю" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new MeetingSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск встреч где я создатель +```php +$searchModel = new MeetingSearch(); +$dataProvider = $searchModel->search([ + 'mode' => -1, +]); +``` + +### Поиск встреч где я участник +```php +$searchModel = new MeetingSearch(); +$dataProvider = $searchModel->search([ + 'mode' => 0, +]); +``` + +### Поиск за конкретный период +```php +$searchModel = new MeetingSearch(); +$dataProvider = $searchModel->search([ + 'date_start' => '2024-01-01 00:00:00', + 'date_end' => '2024-01-31 23:59:59', +]); +``` + +### Навигация по календарю +```php +$searchModel = new MeetingSearch(); +$searchModel->load($params); + +// Предыдущий период +$prevDates = $searchModel->getPreviousDate(); + +// Следующий период +$nextDates = $searchModel->getNextDate(); +``` + +## Связанные модели + +- [Meeting](./Meeting.md) — базовая модель встреч +- [MeetingAdminLink](./MeetingAdminLink.md) — связь встреч и участников +- [CalendarAdminLink](./CalendarAdminLink.md) — доступ к календарям +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **ArrayDataProvider**: Результат — массив моделей, не ActiveQuery +2. **UNION запросы**: Объединение трёх подзапросов для разных режимов +3. **Динамический mode**: Для searchAccess mode = имя админа +4. **Обработка дубликатов**: repetition_action управляет отображением повторов +5. **Навигация по датам**: getPreviousDate/getNextDate для пагинации по времени +6. **Форматы календаря**: timeGridDay, dayGridWeek, month +7. **Дефолтные значения**: Автоматические даты для 3-месячного периода diff --git a/erp24/docs/models/Messager.md b/erp24/docs/models/Messager.md new file mode 100644 index 00000000..ee94af4b --- /dev/null +++ b/erp24/docs/models/Messager.md @@ -0,0 +1,600 @@ +# Class: Messager + + +## Mindmap + +```mermaid +mindmap + root((Messager)) + Таблица БД + messager + Свойства + id + int + from_id + int + to_id + int + msg + string + created_at + string + read_status + int + Связи + Files + 1:N Files + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель системы внутреннего мессенджера и комментариев к задачам в ERP24. Используется для обмена сообщениями между сотрудниками и добавления комментариев к задачам. Поддерживает прикрепление файлов, отслеживание статуса прочтения и различные типы сообщений. + +Модель используется в двух контекстах: +1. Личные сообщения между сотрудниками (type_id = 0) +2. Комментарии к задачам (type_id = 1) + +--- + +## Файл модели + +`/erp24/records/Messager.php` + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`messager` + +--- + +## Поля таблицы + +| Имя | Тип | Ключ | Описание | +|-----|-----|------|----------| +| `id` | int | PK | Первичный ключ | +| `from_id` | int | FK | ID отправителя (Admin) | +| `to_id` | int | FK | ID получателя (Admin) | +| `msg` | text | | Текст сообщения | +| `created_at` | timestamp | | Время создания сообщения | +| `read_status` | int | | Статус прочтения (0 - не прочитано, 1 - прочитано) | +| `task_id` | int | FK | ID связанной задачи | +| `type_id` | int | | Тип: 0 - сообщение, 1 - комментарий | + +--- + +## Описание полей + +### id +Автоинкрементный первичный ключ таблицы. Уникальный идентификатор сообщения. + +### from_id +Внешний ключ на таблицу `admin`. ID сотрудника-отправителя сообщения. Обязательное поле. + +### to_id +Внешний ключ на таблицу `admin`. ID сотрудника-получателя сообщения. Обязательное поле. + +### msg +Текстовое содержание сообщения или комментария. Обязательное поле. Может содержать обычный текст или HTML-разметку. + +### created_at +Временная метка создания сообщения. Обязательное поле. Используется для сортировки сообщений в хронологическом порядке. + +### read_status +Статус прочтения сообщения получателем: +- `0` — сообщение не прочитано +- `1` — сообщение прочитано + +Используется для отображения непрочитанных сообщений и счетчика новых сообщений. + +### task_id +Внешний ключ на таблицу `task`. ID задачи, к которой относится сообщение. Обязательное поле. Используется для группировки комментариев и сообщений по задачам. + +### type_id +Тип сообщения: +- `0` — обычное сообщение в мессенджере +- `1` — комментарий к задаче + +Определяет контекст использования записи и влияет на отображение в интерфейсе. + +--- + +## Отношения (Relations) + +### getFiles() +**Тип:** `hasMany` +**Модель:** `Files` +**Ключ:** `['entity_id' => 'id']` +**Условие:** `['like', 'entity', 'messager']` +**Описание:** Связь с прикрепленными файлами сообщения + +**Пример:** +```php +$message = Messager::findOne($id); +$attachments = $message->files; +foreach ($attachments as $file) { + echo "Файл: {$file->url} ({$file->file_type})\n"; +} +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['from_id', 'to_id', 'msg', 'created_at', 'task_id'] +``` + +### Целочисленные поля +```php +['from_id', 'to_id', 'read_status', 'task_id', 'type_id'] // integer +``` + +### Текстовые поля +```php +['msg'] // text +``` + +### Безопасные поля +```php +['created_at'] // safe +``` + +--- + +## Методы модели + +### tableName() +**Тип:** `public static` +**Параметры:** нет +**Возвращает:** `string` +**Описание:** Возвращает имя таблицы базы данных + +**Логика работы:** +Статический метод, возвращающий строку `'messager'` — название таблицы в базе данных, связанной с данной моделью. + +**Пример:** +```php +$tableName = Messager::tableName(); +// Результат: 'messager' +``` + +--- + +### rules() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` +**Описание:** Определяет правила валидации для атрибутов модели + +**Логика работы:** +Возвращает массив правил валидации: +1. Обязательные поля: from_id, to_id, msg, created_at, task_id +2. Целочисленные поля: from_id, to_id, read_status, task_id, type_id +3. Текстовое поле: msg +4. Безопасное поле для массового присваивания: created_at + +**Пример:** +```php +$message = new Messager(); +$message->from_id = 1; +$message->to_id = 5; +$message->msg = 'Тестовое сообщение'; +// Валидация не пройдет, так как не заполнены обязательные поля created_at и task_id +if (!$message->validate()) { + print_r($message->errors); +} +``` + +--- + +### attributeLabels() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` +**Описание:** Возвращает метки (labels) для атрибутов модели + +**Логика работы:** +Определяет человекочитаемые названия атрибутов, используемые в формах и сообщениях об ошибках. + +**Возвращаемое значение:** +```php +[ + 'id' => 'ID', + 'from_id' => 'From ID', + 'to_id' => 'To ID', + 'msg' => 'Msg', + 'created_at' => 'Created At', + 'read_status' => 'Read Status', + 'task_id' => 'Task ID', + 'type_id' => 'Type ID', +] +``` + +--- + +### getFiles() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `\yii\db\ActiveQuery` +**Описание:** Определяет связь модели с файловыми вложениями + +**Логика работы:** +1. Создает связь типа `hasMany` с моделью `Files` +2. Связывает через поле `entity_id` в таблице files, которое равно `id` текущего сообщения +3. Добавляет условие `entity LIKE 'messager'` для фильтрации только файлов, относящихся к сообщениям + +**Вызовы сторонних методов:** +- `$this->hasMany(Files::class, ['entity_id' => 'id'])` — создание связи один-ко-многим с моделью Files +- `->andWhere(['like', 'entity', 'messager'])` — добавление условия фильтрации по типу сущности + +**Пример:** +```php +$message = Messager::findOne($messageId); +$files = $message->getFiles()->all(); + +// Или через магическое свойство +$files = $message->files; + +// Подсчет количества файлов +$fileCount = $message->getFiles()->count(); +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Messager }o--|| Admin : "from (sender)" + Messager }o--|| Admin : "to (recipient)" + Messager }o--|| Task : "relates to" + Messager ||--o{ Files : "has attachments" + + Messager { + int id PK + int from_id FK "Отправитель" + int to_id FK "Получатель" + text msg "Сообщение" + timestamp created_at "Создано" + int read_status "Прочитано" + int task_id FK "Задача" + int type_id "Тип" + } + + Admin { + int id PK + string name "Имя" + string email + int group_id + } + + Task { + int id PK + string title "Название" + text description "Описание" + int status_id "Статус" + int assigned_to FK + } + + Files { + int id PK + int entity_id FK + string entity "messager" + string url "Путь к файлу" + string file_type "Тип файла" + timestamp created_at + } +``` + +--- + +## Примеры использования + +### Отправка личного сообщения + +```php +use yii_app\records\Messager; + +$message = new Messager(); +$message->from_id = Yii::$app->user->id; // Текущий пользователь +$message->to_id = 5; // ID получателя +$message->msg = 'Привет! Как дела с проектом?'; +$message->created_at = date('Y-m-d H:i:s'); +$message->task_id = 0; // Нет привязки к задаче (или ID задачи, если есть контекст) +$message->type_id = 0; // Обычное сообщение +$message->read_status = 0; // Не прочитано + +if ($message->save()) { + echo "Сообщение отправлено"; +} else { + print_r($message->errors); +} +``` + +--- + +### Добавление комментария к задаче + +```php +use yii_app\records\Messager; + +$comment = new Messager(); +$comment->from_id = Yii::$app->user->id; +$comment->to_id = $taskAssignedTo; // Исполнитель задачи +$comment->msg = 'Задача выполнена. Проверьте, пожалуйста.'; +$comment->created_at = date('Y-m-d H:i:s'); +$comment->task_id = $taskId; +$comment->type_id = 1; // Комментарий к задаче +$comment->read_status = 0; + +if ($comment->save()) { + // Отправка уведомления исполнителю + Yii::$app->session->setFlash('success', 'Комментарий добавлен'); +} +``` + +--- + +### Получение диалога между двумя пользователями + +```php +use yii_app\records\Messager; + +$userId1 = 1; +$userId2 = 5; + +$dialog = Messager::find() + ->where([ + 'or', + ['and', ['from_id' => $userId1], ['to_id' => $userId2]], + ['and', ['from_id' => $userId2], ['to_id' => $userId1]] + ]) + ->andWhere(['type_id' => 0]) // Только сообщения, без комментариев + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +foreach ($dialog as $message) { + $sender = ($message->from_id == $userId1) ? 'Я' : 'Собеседник'; + echo "{$sender}: {$message->msg} ({$message->created_at})\n"; +} +``` + +--- + +### Получение комментариев к задаче + +```php +use yii_app\records\Messager; + +$taskId = 123; + +$comments = Messager::find() + ->where(['task_id' => $taskId]) + ->andWhere(['type_id' => 1]) // Только комментарии + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +echo "Комментарии к задаче #{$taskId}:\n"; +foreach ($comments as $comment) { + $author = Admin::findOne($comment->from_id); + echo "[{$comment->created_at}] {$author->name}: {$comment->msg}\n"; + + // Вывод вложений + if ($comment->files) { + foreach ($comment->files as $file) { + echo " Вложение: {$file->url}\n"; + } + } +} +``` + +--- + +### Пометка сообщений как прочитанных + +```php +use yii_app\records\Messager; + +// Пометить все непрочитанные сообщения для пользователя +Messager::updateAll( + ['read_status' => 1], + [ + 'to_id' => Yii::$app->user->id, + 'read_status' => 0 + ] +); + +// Пометить конкретное сообщение +$message = Messager::findOne($messageId); +if ($message && $message->to_id == Yii::$app->user->id) { + $message->read_status = 1; + $message->save(); +} +``` + +--- + +### Получение непрочитанных сообщений + +```php +use yii_app\records\Messager; + +$userId = Yii::$app->user->id; + +// Количество непрочитанных +$unreadCount = Messager::find() + ->where(['to_id' => $userId]) + ->andWhere(['read_status' => 0]) + ->andWhere(['type_id' => 0]) // Только сообщения + ->count(); + +echo "Непрочитанных сообщений: {$unreadCount}"; + +// Список непрочитанных +$unreadMessages = Messager::find() + ->where(['to_id' => $userId]) + ->andWhere(['read_status' => 0]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($unreadMessages as $msg) { + $sender = Admin::findOne($msg->from_id); + echo "От {$sender->name}: {$msg->msg}\n"; +} +``` + +--- + +### Сообщения с файлами + +```php +use yii_app\records\Messager; +use yii_app\records\Files; + +$message = new Messager(); +$message->from_id = Yii::$app->user->id; +$message->to_id = 10; +$message->msg = 'Отчет за месяц'; +$message->created_at = date('Y-m-d H:i:s'); +$message->task_id = 0; +$message->type_id = 0; +$message->read_status = 0; + +if ($message->save()) { + // Прикрепление файла + $file = new Files(); + $file->entity_id = $message->id; + $file->entity = 'messager'; + $file->url = 'uploads/reports/report_january.pdf'; + $file->file_type = 'pdf'; + $file->created_at = date('Y-m-d H:i:s'); + $file->save(); + + echo "Сообщение с вложением отправлено"; +} +``` + +--- + +### Статистика переписки + +```php +use yii_app\records\Messager; + +$userId = Yii::$app->user->id; + +// Всего отправлено сообщений +$sentCount = Messager::find() + ->where(['from_id' => $userId]) + ->count(); + +// Всего получено сообщений +$receivedCount = Messager::find() + ->where(['to_id' => $userId]) + ->count(); + +// Активные диалоги (уникальные собеседники) +$contacts = Messager::find() + ->select('DISTINCT(CASE WHEN from_id = :userId THEN to_id ELSE from_id END) as contact_id') + ->where(['or', ['from_id' => $userId], ['to_id' => $userId]]) + ->addParams([':userId' => $userId]) + ->column(); + +echo "Отправлено: {$sentCount}\n"; +echo "Получено: {$receivedCount}\n"; +echo "Собеседников: " . count($contacts) . "\n"; +``` + +--- + +## Связанные модели + +- **Admin** — сотрудники (отправители и получатели) +- **Task** — задачи (контекст сообщений и комментариев) +- **Files** — файловые вложения к сообщениям + +--- + +## Бизнес-логика + +### Типы сообщений + +**Обычные сообщения (type_id = 0)** +- Личная переписка между сотрудниками +- Могут быть связаны с задачей через task_id +- Отображаются в интерфейсе мессенджера + +**Комментарии к задачам (type_id = 1)** +- Привязаны к конкретной задаче +- Видны всем участникам задачи +- Отображаются в карточке задачи + +### Статус прочтения + +- При создании сообщения `read_status = 0` +- При открытии сообщения получателем статус меняется на `1` +- Используется для счетчика непрочитанных сообщений +- Влияет на уведомления получателя + +### Файловые вложения + +- Связываются через модель `Files` с `entity = 'messager'` +- Могут быть документы, изображения, архивы +- Хранятся на файловой системе, в БД только ссылки +- Доступны для скачивания через интерфейс + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +CREATE INDEX idx_messager_from_id ON messager(from_id); +CREATE INDEX idx_messager_to_id ON messager(to_id); +CREATE INDEX idx_messager_task_id ON messager(task_id); +CREATE INDEX idx_messager_type_id ON messager(type_id); +CREATE INDEX idx_messager_read_status ON messager(read_status); +CREATE INDEX idx_messager_created_at ON messager(created_at); +CREATE INDEX idx_messager_to_read ON messager(to_id, read_status); +``` + +--- + +## Замечания + +1. **Обязательность task_id** — даже для личных сообщений требуется task_id, обычно используется значение 0 +2. **Двусторонняя связь** — для диалога нужны две записи (туда и обратно) или один from_id/to_id меняется местами +3. **Удаление** — сообщения не удаляются физически, только архивируются (требует доработки) +4. **Вложения** — связываются через entity_id и условие `entity LIKE 'messager'` +5. **Безопасность** — проверка прав доступа должна быть на уровне контроллера + +--- + +## Связанные документы + +- [Task.md](./Task.md) — модель задач +- [Admin.md](./Admin.md) — модель сотрудников +- [Files.md](./Files.md) — модель файлов + +--- + +## Версия + +Документация актуальна для версии модели на 2025-12-11 diff --git a/erp24/docs/models/MessagerAccepted.md b/erp24/docs/models/MessagerAccepted.md new file mode 100644 index 00000000..0d48b5e8 --- /dev/null +++ b/erp24/docs/models/MessagerAccepted.md @@ -0,0 +1,227 @@ +# Модель MessagerAccepted + + +## Mindmap + +```mermaid +mindmap + root((MessagerAccepted)) + Таблица БД + messager_accepted + Свойства + msg_id + int + admin_id + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `MessagerAccepted` фиксирует факт принятия/прочтения сообщений сотрудниками. Создаёт связь многие-ко-многим между сообщениями и сотрудниками, которые их приняли. Используется для отслеживания ознакомления персонала с важными уведомлениями и рассылками. + +**Файл модели:** `erp24/records/MessagerAccepted.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `messager_accepted` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `msg_id` | INTEGER | ID сообщения (FK → messager.id, часть PK) | +| `admin_id` | INTEGER | ID сотрудника (FK → admin.id, часть PK) | +| `created_at` | TIMESTAMP | Дата и время принятия сообщения | + +--- + +## Особенности + +- **Составной уникальный ключ** по `msg_id` и `admin_id` +- Каждый сотрудник может принять конкретное сообщение только один раз +- Таблица связей для реализации many-to-many между Messager и Admin +- Все поля обязательные + +--- + +## Диаграмма связей + +```mermaid +erDiagram + messager_accepted }o--|| messager : "message" + messager_accepted }o--|| admin : "accepted_by" + + messager_accepted { + int msg_id PK,FK + int admin_id PK,FK + timestamp created_at + } + + messager { + int id PK + string title + text content + timestamp created_at + } + + admin { + int id PK + string name + string email + } +``` + +--- + +## Примеры использования + +### Отметка о принятии сообщения + +```php +$accepted = new MessagerAccepted(); +$accepted->msg_id = $messageId; +$accepted->admin_id = Yii::$app->user->id; +$accepted->created_at = date('Y-m-d H:i:s'); + +if ($accepted->save()) { + echo "Сообщение принято"; +} +``` + +### Проверка принятия сообщения + +```php +$isAccepted = MessagerAccepted::find() + ->where([ + 'msg_id' => $messageId, + 'admin_id' => $adminId + ]) + ->exists(); + +if ($isAccepted) { + echo "Сообщение уже прочитано"; +} else { + echo "Сообщение не прочитано"; +} +``` + +### Получение списка принявших сообщение + +```php +$acceptedBy = MessagerAccepted::find() + ->alias('ma') + ->innerJoin('admin a', 'a.id = ma.admin_id') + ->select(['a.id', 'a.name', 'ma.created_at']) + ->where(['ma.msg_id' => $messageId]) + ->orderBy(['ma.created_at' => SORT_ASC]) + ->asArray() + ->all(); + +echo "Сообщение приняли:\n"; +foreach ($acceptedBy as $admin) { + echo "- {$admin['name']} ({$admin['created_at']})\n"; +} +``` + +### Получение непрочитанных сообщений + +```php +$unreadMessages = Messager::find() + ->alias('m') + ->leftJoin( + 'messager_accepted ma', + 'ma.msg_id = m.id AND ma.admin_id = :adminId', + [':adminId' => $adminId] + ) + ->where(['ma.msg_id' => null]) + ->all(); + +echo "Непрочитанных сообщений: " . count($unreadMessages); +``` + +### Статистика прочтения + +```php +$stats = MessagerAccepted::find() + ->select([ + 'msg_id', + 'COUNT(*) as read_count', + 'MIN(created_at) as first_read', + 'MAX(created_at) as last_read' + ]) + ->groupBy('msg_id') + ->asArray() + ->all(); +``` + +### Массовая отметка о прочтении + +```php +$messageIds = [1, 2, 3, 4, 5]; +$adminId = Yii::$app->user->id; +$now = date('Y-m-d H:i:s'); + +foreach ($messageIds as $msgId) { + $exists = MessagerAccepted::find() + ->where(['msg_id' => $msgId, 'admin_id' => $adminId]) + ->exists(); + + if (!$exists) { + $accepted = new MessagerAccepted(); + $accepted->msg_id = $msgId; + $accepted->admin_id = $adminId; + $accepted->created_at = $now; + $accepted->save(); + } +} +``` + +### Отчёт по ознакомлению с рассылкой + +```php +$messageId = 100; + +// Получаем целевую аудиторию (например, все активные сотрудники) +$targetAdmins = Admin::find() + ->where(['status' => 1]) + ->count(); + +// Получаем количество прочитавших +$readCount = MessagerAccepted::find() + ->where(['msg_id' => $messageId]) + ->count(); + +$percentage = ($targetAdmins > 0) + ? round($readCount / $targetAdmins * 100, 1) + : 0; + +echo "Ознакомились: {$readCount} из {$targetAdmins} ({$percentage}%)"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `msg_id` | Обязательное, целое число | +| `admin_id` | Обязательное, целое число | +| `created_at` | Обязательное | +| `msg_id + admin_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[Messager](./Messager.md)** — сообщения +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MessagerUser.md b/erp24/docs/models/MessagerUser.md new file mode 100644 index 00000000..1a44ed4c --- /dev/null +++ b/erp24/docs/models/MessagerUser.md @@ -0,0 +1,263 @@ +# Класс: MessagerUser + + +## Mindmap + +```mermaid +mindmap + root((MessagerUser)) + Таблица БД + messager_user + Свойства + id + int + client_id + int + client_type + int + platform_id + int + phone + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель пользователей мессенджеров в ERP24. Хранит связь между клиентами в системе Salebot и их аккаунтами в мессенджерах (Telegram и др.) для отправки уведомлений и маркетинговых рассылок. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`messager_user` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `client_id` | int | ID клиента в Salebot | +| `client_type` | int | Тип мессенджера (0=Telegram) | +| `platform_id` | int | ID клиента в мессенджере | +| `phone` | varchar(255) | Телефон в формате 79876543210 | +| `is_subscribed` | int / null | Флаг подписки на рассылки | + +## Типы мессенджеров + +| Значение | Мессенджер | +|----------|------------| +| `0` | Telegram | + +## Диаграмма связей + +```mermaid +erDiagram + MessagerUser { + int id PK + int client_id + int client_type + int platform_id + varchar phone + int is_subscribed + } + + Users { + int id PK + varchar phone + } + + SalebotClient { + int id + varchar name + } + + Users ||--o{ MessagerUser : "phone" + SalebotClient ||--o{ MessagerUser : "client_id" +``` + +## Диаграмма интеграции + +```mermaid +flowchart LR + subgraph "ERP24" + A[Users] + B[MessagerUser] + end + + subgraph "Salebot" + C[Salebot Client] + end + + subgraph "Мессенджеры" + D[Telegram] + E[WhatsApp] + F[VK] + end + + A -->|phone| B + B -->|client_id| C + C -->|platform_id| D + C -.->|future| E + C -.->|future| F +``` + +## Примеры использования + +### Создание записи пользователя мессенджера +```php +$messagerUser = new MessagerUser(); +$messagerUser->client_id = $salebotClientId; +$messagerUser->client_type = 0; // Telegram +$messagerUser->platform_id = $telegramUserId; +$messagerUser->phone = '79876543210'; +$messagerUser->is_subscribed = 1; +$messagerUser->save(); +``` + +### Поиск пользователя по телефону +```php +$messagerUser = MessagerUser::find() + ->where(['phone' => $phone]) + ->one(); + +if ($messagerUser) { + echo "Telegram ID: {$messagerUser->platform_id}"; +} +``` + +### Получение подписчиков для рассылки +```php +$subscribers = MessagerUser::find() + ->where([ + 'client_type' => 0, // Telegram + 'is_subscribed' => 1 + ]) + ->all(); + +foreach ($subscribers as $user) { + // Отправка сообщения через Salebot API + $salebot->sendMessage($user->client_id, $messageText); +} +``` + +### Проверка наличия в мессенджере +```php +function hasMessenger($phone, $messengerType = 0) +{ + return MessagerUser::find() + ->where([ + 'phone' => $phone, + 'client_type' => $messengerType + ]) + ->exists(); +} + +if (hasMessenger('79876543210')) { + echo "Клиент есть в Telegram"; +} +``` + +### Отписка пользователя +```php +$user = MessagerUser::find() + ->where(['phone' => $phone]) + ->one(); + +if ($user) { + $user->is_subscribed = 0; + $user->save(); +} +``` + +### Получение пользователей по Salebot client_id +```php +$users = MessagerUser::find() + ->where(['client_id' => $salebotClientId]) + ->all(); + +// Один клиент Salebot может иметь несколько мессенджеров +foreach ($users as $user) { + $messengerName = $user->client_type === 0 ? 'Telegram' : 'Unknown'; + echo "{$messengerName}: {$user->platform_id}\n"; +} +``` + +### Статистика по мессенджерам +```php +$stats = MessagerUser::find() + ->select([ + 'client_type', + 'COUNT(*) as total', + 'SUM(CASE WHEN is_subscribed = 1 THEN 1 ELSE 0 END) as subscribed' + ]) + ->groupBy('client_type') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $messengerName = $stat['client_type'] === 0 ? 'Telegram' : 'Other'; + echo "{$messengerName}: {$stat['subscribed']}/{$stat['total']} подписчиков\n"; +} +``` + +### Синхронизация с базой клиентов +```php +// Поиск клиентов ERP24 с Telegram +$usersWithTelegram = Users::find() + ->innerJoin( + 'messager_user mu', + 'users.phone = mu.phone AND mu.client_type = 0' + ) + ->all(); +``` + +### Массовая отправка уведомлений +```php +function sendBulkNotification($message, $filterConditions = []) +{ + $query = MessagerUser::find() + ->where(['is_subscribed' => 1, 'client_type' => 0]); + + if ($filterConditions) { + $query->andWhere($filterConditions); + } + + $recipients = $query->all(); + $sent = 0; + + foreach ($recipients as $user) { + if (Salebot::send($user->client_id, $message)) { + $sent++; + } + } + + return $sent; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `client_id` | required, integer | +| `client_type` | required, integer | +| `platform_id` | required, integer | +| `phone` | string (max 255) | +| `is_subscribed` | integer | + +## Связанные модели + +- [Users](./Users.md) — клиенты ERP24 (связь по phone) + +## Особенности реализации + +1. **Интеграция с Salebot**: client_id связывает запись с клиентом в системе Salebot +2. **Мультимессенджер**: Поле client_type позволяет хранить разные мессенджеры +3. **Platform ID**: Уникальный идентификатор пользователя в конкретном мессенджере +4. **Связь по телефону**: Телефон служит связующим полем с таблицей Users +5. **Управление подпиской**: Флаг is_subscribed для контроля рассылок +6. **Формат телефона**: Хранится в формате 79876543210 (без + и пробелов) diff --git a/erp24/docs/models/Metrics.md b/erp24/docs/models/Metrics.md new file mode 100644 index 00000000..42245475 --- /dev/null +++ b/erp24/docs/models/Metrics.md @@ -0,0 +1,439 @@ +# Abstract Class: Metrics + +## Mindmap + +```mermaid +mindmap + root((Metrics)) + Таблица БД + Model абстрактный + Свойства + dateStart + string + dateEnd + string + cluster + mixed + store + mixed + alias + array + Наследники + SalesMetrics + 1:N продажи + FotMetrics + 1:N ФОТ + WriteOffsMetrics + 1:N списания + Наследование + extends Model +``` + +## Назначение + +Абстрактный класс Metrics представляет базовую функциональность для расчёта и сохранения метрик в системе ERP24. Предоставляет унифицированный механизм для сбора данных из различных источников (продажи, ФОТ, списания), их агрегации по сменам и дням, и сохранения в нормализованную структуру таблиц `rnp_index`, `rnp_data`, `rnp_alias`. Является родительским классом для специализированных метрик (SalesMetrics, FotMetrics, WriteOffsMetrics). + +## Пространство имён + +```php +namespace yii_app\records\metrics; +``` + +## Родительский класс + +```php +\yii\base\Model +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка +- `yii\base\Model` - базовая модель Yii2 +- `yii\db\Exception` - исключения базы данных +- `yii\db\Expression` - SQL выражения +- `yii\db\Query` - построитель запросов +- `yii\helpers\ArrayHelper` - работа с массивами +- `yii_app\records\RnpAlias` - псевдонимы метрик +- `yii_app\records\RnpData` - данные метрик +- `yii_app\records\RnpIndex` - индексы метрик +- `yii_app\records\StoreDynamic` - динамика магазинов (кластеризация) + +## Свойства (Properties) + +### Публичные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `dateStart` | `string` | Дата начала периода расчёта метрик (формат: Y-m-d) | +| `dateEnd` | `string` | Дата окончания периода расчёта метрик (формат: Y-m-d) | +| `cluster` | `mixed` | ID кластера для фильтрации (опционально) | +| `store` | `mixed` | ID магазина для фильтрации (опционально) | + +### Защищённые свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `alias` | `array` | Массив псевдонимов метрик (должен быть переопределён в наследниках) | +| `selectQuery` | `array` | Массив выражений для SELECT запроса | +| `listOfExceptionsByShiftType` | `array` | Список исключений метрик по типам смен (1 - день, 2 - ночь, 4 - сутки) | +| `calculateShifts` | `bool` | Флаг расчёта метрик по сменам (по умолчанию true) | +| `calculateDay` | `bool` | Флаг расчёта метрик по дням (по умолчанию true) | + +### Приватные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `calculateIndex` | `bool` | Флаг расчёта только индексов без данных (для оптимизации) | + +## Константы типов смен + +```php +const SHIFT_TYPE_DAY = 1; // Дневная смена (08:00-20:00) +const SHIFT_TYPE_NIGHT = 2; // Ночная смена (20:00-08:00) +const SHIFT_TYPE_FULL = 4; // Полные сутки (для дневных метрик) +``` + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['cluster', 'store'], 'safe'], + [['dateStart', 'dateEnd'], 'date', 'format' => 'php:Y-m-d'], + ['dateStart', 'compare', 'compareAttribute' => 'dateEnd', 'operator' => '<='], + ]; +} +``` + +### Описание правил: +1. **safe**: `cluster` и `store` безопасны для массового присвоения +2. **date**: Даты должны быть в формате Y-m-d +3. **compare**: Дата начала должна быть меньше или равна дате окончания + +## Абстрактные методы + +### getQueryDataDay() + +**Описание:** Абстрактный метод для получения запроса данных по дням. Должен быть реализован в классах-наследниках. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery|bool` - запрос для получения дневных данных или false, если не требуется + +**Должен быть реализован в:** +- SalesMetrics - данные продаж за день +- WriteOffsMetrics - данные списаний за день +- FotMetrics - не используется (возвращает false) + +--- + +### getQueryDataShifts() + +**Описание:** Абстрактный метод для получения запроса данных по сменам. Должен быть реализован в классах-наследниках. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery|bool` - запрос для получения данных по сменам или false, если не требуется + +**Должен быть реализован в:** +- SalesMetrics - данные продаж по сменам +- FotMetrics - данные ФОТ по сменам +- WriteOffsMetrics - не используется (возвращает false) + +## Методы + +### loadDefaultValues() + +**Описание:** Защищённый метод для загрузки значений по умолчанию. Может быть переопределён в наследниках для инициализации специфичных свойств. + +**Параметры:** Нет + +**Возвращает:** Void + +--- + +### loadSelectQuery() + +**Описание:** Защищённый метод для формирования массива SELECT выражений на основе псевдонимов и флагов расчёта. + +**Параметры:** Нет + +**Возвращает:** Void + +**Логика работы:** +1. Создаёт временный массив `$temp` +2. Перебирает массив `$this->alias` +3. Для каждого псевдонима формирует SQL выражение в зависимости от флагов: + - Если `calculateShifts` и `calculateDay`: использует IF для выбора между данными смены и дня + - Если только `calculateShifts`: берёт данные из `data_shifts` + - Если только `calculateDay`: берёт данные из `data_day` +4. Заполняет массив `$this->selectQuery` + +**Пример сформированного выражения:** +```sql +IF (shift_types.shift_type != 4, data_shifts.sales_sum, data_day.sales_sum) +``` + +--- + +### insertData() + +**Описание:** Финальный публичный метод для вставки/обновления метрик в БД. Выполняет полный цикл обработки: валидация → сбор данных → вычисление индексов → запись в БД. + +**Параметры:** Нет + +**Возвращает:** `string` - сообщение о результатах выполнения (время выполнения каждого этапа) + +**Логика работы:** + +1. **Валидация**: Проверяет корректность свойств объекта через `$this->validate()` +2. **Сбор данных**: + - Вызывает `getQueryDataCollection()` для получения запроса + - Использует `batch(1000)` для обработки данных порциями по 1000 записей +3. **Расчёт индексов**: + - Получает существующие индексы из `rnp_index` за указанный период + - Формирует новые индексы через запрос с `calculateIndex = true` + - Вычисляет разницу через `array_diff_key()` для определения новых записей +4. **Вставка индексов**: + - Формирует массив для `batchInsert` в таблицу `rnp_index` + - Использует транзакцию для атомарности операции +5. **Подготовка данных**: + - Получает маппинг псевдонимов из `rnp_alias` + - Получает ID индексов для связывания данных + - Формирует массив данных с учётом исключений по типам смен +6. **Вставка данных**: + - Удаляет старые данные через `RnpData::deleteAll()` + - Вставляет новые данные через `batchInsert` в таблицу `rnp_data` + - Использует транзакцию +7. **Возврат статистики**: Возвращает строку с временем выполнения каждого этапа + +**Вызовы сторонних методов:** +- `$this->validate()` - валидация модели +- `$this->getQueryDataCollection()` - получение запроса данных +- `->batch(1000)` - пакетная обработка +- `RnpIndex::find()` - поиск индексов +- `RnpAlias::find()` - получение псевдонимов +- `Yii::$app->db->beginTransaction()` - начало транзакции +- `batchInsert()` - массовая вставка +- `RnpData::deleteAll()` - удаление старых данных + +**Пример:** +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; +$metrics->cluster = 1; + +$result = $metrics->insertData(); +echo $result; +// Результат: +// |Время поиска: 2.450000 sec.|Время записи индексов: 0.350000 sec.|Время записи данных: 1.200000 sec.| +``` + +--- + +### getQueryDataCollection() + +**Описание:** Финальный публичный метод для построения основного запроса сбора данных. Объединяет данные из разных источников (смены, дни) с учётом фильтров и временного диапазона. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения агрегированных данных метрик + +**Логика работы:** + +1. **Инициализация**: Вызывает `loadDefaultValues()` и `loadSelectQuery()` +2. **Генерация дат**: Создаёт подзапрос для генерации всех дат в указанном диапазоне через CROSS JOIN с таблицами units, tens, hundreds, thousands +3. **Генерация типов смен**: Формирует подзапрос с типами смен (1, 2, 4) в зависимости от флагов +4. **Основной запрос**: + - Начинается с таблицы `StoreDynamic` (кластеризация магазинов) + - JOIN с `city_store` для получения данных магазинов + - LEFT JOIN с подзапросом дат + - LEFT JOIN с подзапросом типов смен +5. **JOIN с данными**: + - Если `calculateShifts = true`: LEFT JOIN с `data_shifts` (результат getQueryDataShifts()) + - Если `calculateDay = true`: LEFT JOIN с `data_day` (результат getQueryDataDay()) +6. **Фильтрация**: + - По диапазону дат (dateStart, dateEnd) + - По кластеру (опционально) + - По магазину (опционально) +7. **Возврат**: ActiveQuery для дальнейшей обработки + +**Вызовы сторонних методов:** +- `$this->loadDefaultValues()` - загрузка дефолтных значений +- `$this->loadSelectQuery()` - формирование SELECT выражений +- `(new Query())->from()` - создание подзапросов +- `StoreDynamic::find()` - основной запрос +- `$this->getQueryDataShifts()` - данные по сменам +- `$this->getQueryDataDay()` - данные по дням +- `new Expression()` - SQL выражения + +**Структура запроса (упрощённо):** +```sql +SELECT + dates_column.date, + shift_types.shift_type, + store_dynamic.value_int AS cluster_id, + city_store.id AS store_id, + IF(shift_types.shift_type != 4, data_shifts.metric1, data_day.metric1) AS metric1, + ... +FROM store_dynamic +INNER JOIN city_store ON ... +LEFT JOIN (SELECT date FROM ...) dates_column ON ... +LEFT JOIN (SELECT shift_type FROM ...) shift_types ON 1=1 +LEFT JOIN (subquery_shifts) data_shifts ON ... +LEFT JOIN (subquery_day) data_day ON ... +WHERE dates_column.date BETWEEN :dateStart AND :dateEnd +``` + +--- + +### getDataArray() + +**Описание:** Финальный защищённый метод для получения сохранённых данных метрик из БД в виде массива. Используется для чтения ранее рассчитанных метрик. + +**Параметры:** +- `$clusterId` (mixed) - ID кластера +- `$storeId` (mixed) - ID магазина +- `$shift` (mixed) - Тип смены (1, 2, 4) + +**Возвращает:** `array` - массив данных метрик [['date' => ..., 'shift' => ..., 'alias' => ..., 'value' => ...], ...] + +**Логика работы:** +1. Выполняет JOIN трёх таблиц: `rnp_data`, `rnp_alias`, `rnp_index` +2. Фильтрует по: + - Псевдонимам из массива `$this->alias` + - Диапазону дат (dateStart, dateEnd) + - ID кластера (если указан) + - ID магазина (если указан) + - Типу смены (если указан) +3. Сортирует по дате, типу смены и ID псевдонима +4. Возвращает массив с данными + +**Вызовы сторонних методов:** +- `RnpData::find()` - поиск данных +- `->innerJoin()` - внутренние соединения +- `->andWhere()` - условия фильтрации +- `->andFilterWhere()` - условная фильтрация +- `->orderBy()` - сортировка +- `->asArray()` - возврат массива +- `->all()` - выполнение запроса + +**Пример:** +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; +$metrics->alias = ['sales_sum', 'count_sales']; + +$data = $metrics->getDataArray(1, 5, 1); +print_r($data); +// Результат: +// [ +// ['date' => '2025-01-01', 'shift' => 1, 'alias' => 'sales_sum', 'value' => 150000.50], +// ['date' => '2025-01-01', 'shift' => 1, 'alias' => 'count_sales', 'value' => 320], +// ... +// ] +``` + +## Поток данных + +```mermaid +flowchart TD + A[Инициализация Metrics наследника] --> B[Установка dateStart, dateEnd] + B --> C[insertData()] + C --> D{Валидация} + D -->|Ошибка| E[Возврат сообщения об ошибке] + D -->|OK| F[getQueryDataCollection] + F --> G[loadSelectQuery] + F --> H[Генерация дат CROSS JOIN] + F --> I[Генерация типов смен] + F --> J[getQueryDataShifts - наследник] + F --> K[getQueryDataDay - наследник] + J --> L[LEFT JOIN data_shifts] + K --> M[LEFT JOIN data_day] + L --> N[Пакетная обработка batch 1000] + M --> N + N --> O[Расчёт новых индексов] + O --> P[Вставка в rnp_index] + P --> Q[Маппинг псевдонимов] + Q --> R[Формирование данных] + R --> S[Удаление старых RnpData] + S --> T[Вставка в rnp_data] + T --> U[Возврат статистики времени] +``` + +## Структура таблиц метрик + +```mermaid +erDiagram + RNP_INDEX ||--o{ RNP_DATA : "has many" + RNP_DATA }o--|| RNP_ALIAS : "belongs to" + STORE_DYNAMIC ||--o{ RNP_INDEX : "has many" + CITY_STORE ||--o{ RNP_INDEX : "has many" + + RNP_INDEX { + int id PK + int cluster_id FK + int store_id FK + int shift_type + date date + } + + RNP_DATA { + int id PK + int index_id FK + int alias_id FK + float value + } + + RNP_ALIAS { + int id PK + string alias + string description + } + + STORE_DYNAMIC { + int id PK + int store_id FK + int value_int + date date_from + date date_to + } + + CITY_STORE { + int id PK + string store_name + } +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [SalesMetrics](./SalesMetrics.md) | Model | Метрики продаж | +| [FotMetrics](./FotMetrics.md) | Model | Метрики ФОТ | +| [WriteOffsMetrics](./WriteOffsMetrics.md) | Model | Метрики списаний | +| `RnpIndex` | Model | Индексы метрик | +| `RnpData` | Model | Данные метрик | +| `RnpAlias` | Model | Псевдонимы метрик | +| `StoreDynamic` | Model | Динамика кластеров магазинов | +| `MetricsCalculatorJob` | Job | Фоновый расчёт метрик | + +## Примечания + +1. **Абстрактный класс**: Не может быть инстанцирован напрямую, только через наследников +2. **Пакетная обработка**: Данные обрабатываются порциями по 1000 записей для оптимизации памяти +3. **Транзакции**: Все операции записи выполняются в транзакциях для обеспечения целостности +4. **Производительность**: Используется batch insert вместо отдельных save() для ускорения +5. **Нормализация данных**: Метрики хранятся в нормализованной структуре (index + data + alias) + +--- + +**Связанная документация:** +- [SalesMetrics](./SalesMetrics.md) +- [FotMetrics](./FotMetrics.md) +- [WriteOffsMetrics](./WriteOffsMetrics.md) +- [Архитектура системы метрик](../architecture/metrics-system.md) +- [Руководство по расчёту метрик](../guides/metrics-calculation.md) diff --git a/erp24/docs/models/ModulesUniFields.md b/erp24/docs/models/ModulesUniFields.md new file mode 100644 index 00000000..96a7120e --- /dev/null +++ b/erp24/docs/models/ModulesUniFields.md @@ -0,0 +1,276 @@ +# Модель ModulesUniFields + + +## Mindmap + +```mermaid +mindmap + root((ModulesUniFields)) + Таблица БД + modules_uni_fields + Свойства + id + int + modul + string + name + string + name_eng + string + group_id + int + type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ModulesUniFields` представляет универсальные настраиваемые поля для различных модулей системы. Позволяет динамически добавлять кастомные поля к формам без изменения структуры базы данных. Используется для расширения функциональности модулей и настройки CRM-форм. + +**Файл модели:** `erp24/records/ModulesUniFields.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `modules_uni_fields` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `modul` | VARCHAR(25) | Код модуля (orders, users, tasks и т.д.) | +| `name` | VARCHAR(150) | Название поля на русском | +| `name_eng` | VARCHAR(55) | Системное название поля (латиница) | +| `group_id` | INTEGER | ID группы полей | +| `type` | VARCHAR(25) | Тип поля (text, select, checkbox и т.д.) | +| `tip` | VARCHAR(55) | Тип данных (число, массив, строка, текст, email) | +| `select_val` | TEXT | Список опций для select (JSON) | +| `default_val` | VARCHAR(155) | Значение по умолчанию | +| `sql_func` | TEXT | SQL-параметры для запросов | +| `dostup_values` | TEXT | Ограничение доступа к значениям | +| `posit` | INTEGER | Позиция/порядок отображения | +| `required` | INTEGER | Обязательное поле (1/0) | +| `required_status` | VARCHAR(250) | Статусы воронки, для которых поле обязательно | +| `placeholder` | VARCHAR(150) | Подсказка в поле | +| `leftclass` | VARCHAR(155) | CSS-класс для левой части | +| `class` | VARCHAR(200) | CSS-класс поля | +| `class_block` | VARCHAR(155) | CSS-класс блока | +| `attr` | TEXT | HTML-атрибуты поля | +| `style` | TEXT | Inline-стили | +| `html` | TEXT | Произвольный HTML | +| `image_config` | TEXT | Конфигурация для изображений | +| `api` | INTEGER | Доступность через API (1/0) | +| `user_dostup` | INTEGER | Ограничение доступа по пользователю | +| `date_add` | TIMESTAMP | Дата добавления | +| `dostup_arr` | TEXT | Массив доступов (JSON) | +| `admin_id` | INTEGER | ID создавшего сотрудника | + +--- + +## Типы полей (type) + +| Тип | Описание | +|-----|----------| +| `text` | Текстовое поле | +| `textarea` | Многострочное текстовое поле | +| `select` | Выпадающий список | +| `multiselect` | Множественный выбор | +| `checkbox` | Флажок | +| `radio` | Радио-кнопки | +| `date` | Выбор даты | +| `datetime` | Дата и время | +| `file` | Загрузка файла | +| `image` | Загрузка изображения | +| `number` | Числовое поле | +| `email` | Email | +| `phone` | Телефон | +| `hidden` | Скрытое поле | + +--- + +## Типы данных (tip) + +| Тип | Описание | +|-----|----------| +| `string` | Строка | +| `text` | Текст (длинный) | +| `number` | Число | +| `array` | Массив | +| `email` | Email | +| `eng` | Только латиница | +| `json` | JSON-данные | + +--- + +## Примеры использования + +### Получение полей для модуля + +```php +$fields = ModulesUniFields::find() + ->where(['modul' => 'orders']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($fields as $field) { + echo "{$field->name} ({$field->name_eng}): тип {$field->type}\n"; +} +``` + +### Создание нового поля + +```php +$field = new ModulesUniFields(); +$field->modul = 'orders'; +$field->name = 'Источник заказа'; +$field->name_eng = 'order_source'; +$field->group_id = 1; +$field->type = 'select'; +$field->tip = 'string'; +$field->select_val = json_encode([ + ['value' => 'site', 'label' => 'Сайт'], + ['value' => 'phone', 'label' => 'Телефон'], + ['value' => 'walk_in', 'label' => 'Самовизит'], +]); +$field->posit = 10; +$field->required = 1; +$field->placeholder = 'Выберите источник'; +$field->api = 1; +$field->admin_id = Yii::$app->user->id; +$field->date_add = date('Y-m-d H:i:s'); +$field->save(); +``` + +### Генерация формы на основе полей + +```php +$fields = ModulesUniFields::find() + ->where(['modul' => 'users']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($fields as $field) { + $required = $field->required ? 'required' : ''; + $placeholder = $field->placeholder ?: $field->name; + + switch ($field->type) { + case 'text': + echo ""; + break; + + case 'select': + $options = json_decode($field->select_val, true); + echo ""; + break; + + case 'textarea': + echo ""; + break; + } +} +``` + +### Получение обязательных полей для статуса + +```php +$status = 'completed'; + +$requiredFields = ModulesUniFields::find() + ->where(['modul' => 'orders']) + ->andWhere([ + 'or', + ['required' => 1], + ['like', 'required_status', $status] + ]) + ->all(); +``` + +### Фильтрация полей по доступу + +```php +$userRole = 'manager'; + +$accessibleFields = ModulesUniFields::find() + ->where(['modul' => 'crm']) + ->andWhere([ + 'or', + ['dostup_arr' => ''], + ['like', 'dostup_arr', $userRole] + ]) + ->all(); +``` + +### Получение полей для API + +```php +$apiFields = ModulesUniFields::find() + ->where([ + 'modul' => 'orders', + 'api' => 1 + ]) + ->select(['name_eng', 'name', 'type', 'required']) + ->asArray() + ->all(); +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + modules_uni_fields { + int id PK + string modul + string name + string name_eng + int group_id + string type + string tip + text select_val + string default_val + int posit + int required + int api + int admin_id FK + } + + admin ||--o{ modules_uni_fields : "created" +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name`, `name_eng`, `group_id`, `tip` | Обязательные | +| `modul`, `type` | Строка, макс. 25 символов | +| `name`, `placeholder` | Строка, макс. 150 символов | +| `name_eng`, `tip` | Строка, макс. 55 символов | +| `default_val`, `leftclass`, `class_block` | Строка, макс. 155 символов | +| `class` | Строка, макс. 200 символов | +| `required_status` | Строка, макс. 250 символов | +| `group_id`, `posit`, `required`, `api`, `user_dostup`, `admin_id` | Целое число | + +--- + +## Связанные модели + +- **[Admin](./Admin.md)** — сотрудники (создатель поля) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Motivation.md b/erp24/docs/models/Motivation.md new file mode 100644 index 00000000..1e30eba5 --- /dev/null +++ b/erp24/docs/models/Motivation.md @@ -0,0 +1,279 @@ +# Модель Motivation + + +## Mindmap + +```mermaid +mindmap + root((Motivation)) + Таблица БД + motivation + Свойства + id + int + store_id + int + year + int + month + int + updated_at + string + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Motivation` представляет записи мотивации сотрудников по магазинам. Хранит информацию о периодах мотивации (год/месяц) для каждого магазина. Используется для учёта и расчёта мотивационных показателей персонала. + +**Файл модели:** `erp24/records/Motivation.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `motivation` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `store_id` | INTEGER | ID магазина | +| `year` | INTEGER | Год периода мотивации | +| `month` | INTEGER | Месяц периода мотивации (1-12) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата последнего обновления | + +--- + +## Описание полей + +### `store_id` — Магазин + +Идентификатор магазина, для которого создана запись мотивации. + +**Связь:** `city_store.id` + +### `year` — Год + +Год периода мотивации. + +**Формат:** 4 цифры (например, 2025) + +### `month` — Месяц + +Месяц периода мотивации. + +**Допустимые значения:** 1-12 + +--- + +## Behaviors + +Модель использует `TimestampBehavior` для автоматического заполнения дат: + +```php +public function behaviors() +{ + return [ + [ + 'class' => TimestampBehavior::class, + 'createdAtAttribute' => 'created_at', + 'updatedAtAttribute' => 'updated_at', + 'value' => new Expression('NOW()'), + ] + ]; +} +``` + +--- + +## Методы модели + +### `getWeekRange($date, $weekNumber, $month, $year): array` (static) + +Возвращает диапазон дат для указанной недели месяца. + +**Параметры:** +| Параметр | Тип | Описание | +|----------|-----|----------| +| `$date` | mixed | Дата (timestamp или строка) или null | +| `$weekNumber` | int\|null | Номер недели в месяце (1-5) | +| `$month` | int\|null | Месяц (1-12) | +| `$year` | int\|null | Год | + +**Возвращает:** `array` — массив с ключами `start_time` и `end_time` + +**Логика работы:** +1. Если передана только дата — извлекает из неё день, месяц, год и вычисляет номер недели +2. Если переданы weekNumber, month, year — рассчитывает диапазон для указанной недели +3. Если параметры не переданы — использует текущую дату +4. Номер недели определяется как `intdiv(день - 1, 7) + 1` + +**Пример:** +```php +// Диапазон для 2-й недели декабря 2025 +$range = Motivation::getWeekRange(null, 2, 12, 2025); +// ['start_time' => '2025-12-08', 'end_time' => '2025-12-14'] + +// Диапазон для даты +$range = Motivation::getWeekRange('2025-12-11'); +// ['start_time' => '2025-12-08', 'end_time' => '2025-12-14'] +``` + +--- + +### `getWeek($date): int` (static) + +Возвращает номер недели в месяце для указанной даты. + +**Параметры:** +| Параметр | Тип | Описание | +|----------|-----|----------| +| `$date` | string\|null | Дата в формате 'd-m-Y' или null (текущая дата) | + +**Возвращает:** `int` — номер недели (1-5) + +**Формула:** `floor((день - 1) / 7) + 1` + +**Пример:** +```php +$week = Motivation::getWeek('11-12-2025'); // 2 (вторая неделя декабря) +$week = Motivation::getWeek(); // номер текущей недели +``` + +--- + +### `getAdjustmentEditors(): array` (static) + +Возвращает список ID групп сотрудников, имеющих право редактировать корректировки мотивации. + +**Возвращает:** `array` — массив ID групп AdminGroup + +**Список групп с правами редактирования:** +- `AdminGroup::GROUP_IT` — IT-отдел +- `AdminGroup::GROUP_OPERATIONAL_DIRECTOR` — Операционный директор +- `AdminGroup::DIRECTOR` — Директор +- `AdminGroup::GROUP_FINANCE_DIRECTOR` — Финансовый директор + +**Пример:** +```php +$editors = Motivation::getAdjustmentEditors(); +if (in_array($admin->admin_group_id, $editors)) { + // Разрешить редактирование корректировок +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + motivation }o--|| city_store : "belongs_to" + motivation ||--o{ motivation_items : "has_many" + + motivation { + int id PK + int store_id FK + int year + int month + timestamp created_at + timestamp updated_at + } + + city_store { + int id PK + string name + int cluster_id + } + + motivation_items { + int id PK + int motivation_id FK + int admin_id FK + float value + } +``` + +--- + +## Примеры использования + +### Создание записи мотивации + +```php +$motivation = new Motivation(); +$motivation->store_id = $storeId; +$motivation->year = 2025; +$motivation->month = 12; +$motivation->save(); +``` + +### Поиск мотивации за период + +```php +$motivation = Motivation::findOne([ + 'store_id' => $storeId, + 'year' => 2025, + 'month' => 12 +]); +``` + +### Мотивация всех магазинов за месяц + +```php +$motivations = Motivation::find() + ->where(['year' => 2025, 'month' => 12]) + ->all(); +``` + +### Получение диапазона текущей недели + +```php +$weekRange = Motivation::getWeekRange(); +// Используется для фильтрации данных по неделе +$sales = Sales::find() + ->where(['>=', 'date', $weekRange['start_time']]) + ->andWhere(['<=', 'date', $weekRange['end_time']]) + ->all(); +``` + +### Проверка прав на редактирование + +```php +$currentAdmin = Yii::$app->user->identity; +$canEdit = in_array( + $currentAdmin->admin_group_id, + Motivation::getAdjustmentEditors() +); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `store_id` | Обязательное, целое число | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `created_at` | Автозаполнение (TimestampBehavior) | +| `updated_at` | Автозаполнение (TimestampBehavior) | + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазин, для которого создана мотивация +- **MotivationItems** — детализация мотивации по сотрудникам +- **[AdminGroup](./AdminGroup.md)** — группы с правами редактирования +- **[Admin](./Admin.md)** — сотрудники, получающие мотивацию + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/MotivationBuh.md b/erp24/docs/models/MotivationBuh.md new file mode 100644 index 00000000..a42ead6f --- /dev/null +++ b/erp24/docs/models/MotivationBuh.md @@ -0,0 +1,159 @@ +# Класс: MotivationBuh + + +## Mindmap + +```mermaid +mindmap + root((MotivationBuh)) + Таблица БД + motivation_buh + Свойства + id + int + inn + int + year + int + month + int + updated_at + string + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель бухгалтерских данных для расчёта мотивации в ERP24. Хранит заголовки периодов (месяц/год) по ИНН фирмы для агрегации показателей мотивации сотрудников на основе бухгалтерской отчётности. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`motivation_buh` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: `updated_at` | +| `BlameableBehavior` | createdByAttribute: `created_by`, updatedByAttribute: `updated_by` | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `inn` | int | ИНН фирмы источника данных | +| `year` | int | Год отчётного периода | +| `month` | int | Месяц отчётного периода (1-12) | +| `created_at` | datetime | Дата создания записи (автоматически) | +| `updated_at` | datetime | Дата обновления записи (автоматически) | +| `created_by` | int | ID создателя (автоматически) | +| `updated_by` | int | ID редактора (автоматически) | + +## Диаграмма связей + +```mermaid +erDiagram + MotivationBuh { + int id PK + int inn + int year + int month + datetime created_at + datetime updated_at + int created_by FK + int updated_by FK + } + + MotivationBuhValue { + int id PK + int motivation_buh_id FK + int store_id + int value_int + float value_float + } + + Firms { + varchar inn PK + varchar name + } + + MotivationBuh ||--o{ MotivationBuhValue : "motivation_buh_id" + Firms ||--o{ MotivationBuh : "inn" +``` + +## Примеры использования + +### Создание записи периода +```php +$motivationBuh = new MotivationBuh(); +$motivationBuh->inn = 7707083893; +$motivationBuh->year = 2024; +$motivationBuh->month = 3; +// created_at, updated_at, created_by, updated_by заполнятся автоматически +$motivationBuh->save(); +``` + +### Получение данных за период +```php +$period = MotivationBuh::find() + ->where([ + 'inn' => $firmInn, + 'year' => 2024, + 'month' => 3 + ]) + ->one(); +``` + +### Получение всех периодов за год +```php +$yearPeriods = MotivationBuh::find() + ->where([ + 'inn' => $firmInn, + 'year' => 2024 + ]) + ->orderBy(['month' => SORT_ASC]) + ->all(); +``` + +### Проверка существования периода +```php +$exists = MotivationBuh::find() + ->where([ + 'inn' => $inn, + 'year' => $year, + 'month' => $month + ]) + ->exists(); + +if (!$exists) { + // Создать новый период +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `inn` | required, integer | +| `year` | required, integer | +| `month` | required, integer | + +## Связанные модели + +- [MotivationBuhValue](./MotivationBuhValue.md) — значения показателей мотивации +- [Firms](./Firms.md) — фирмы (связь по ИНН) + +## Особенности реализации + +1. **Периодическая структура**: Данные группируются по году и месяцу +2. **Привязка к фирме**: ИНН определяет источник бухгалтерских данных +3. **Автоматический аудит**: TimestampBehavior и BlameableBehavior +4. **Заголовок периода**: Служит родительской записью для MotivationBuhValue diff --git a/erp24/docs/models/MotivationBuhValue.md b/erp24/docs/models/MotivationBuhValue.md new file mode 100644 index 00000000..cf28c934 --- /dev/null +++ b/erp24/docs/models/MotivationBuhValue.md @@ -0,0 +1,226 @@ +# Класс: MotivationBuhValue + + +## Mindmap + +```mermaid +mindmap + root((MotivationBuhValue)) + Таблица БД + motivation_buh_value + Свойства + id + int + motivation_buh_id + int + store_id + int + motivation_group_id + int + value_id + int + value_type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель значений показателей мотивации из бухгалтерии в ERP24. Хранит конкретные значения показателей для расчёта мотивации по магазинам с поддержкой различных типов данных (int, float, string). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`motivation_buh_value` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: `updated_at` | +| `BlameableBehavior` | createdByAttribute: `created_by`, updatedByAttribute: `updated_by` | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `motivation_buh_id` | int | FK на период (MotivationBuh) | +| `store_id` | int | ID магазина | +| `motivation_group_id` | int | ID группы показателей | +| `value_id` | int | ID показателя | +| `value_type` | varchar(10) | Тип значения (int/float/string) | +| `value_int` | int / null | Целочисленное значение | +| `value_float` | float / null | Значение с плавающей точкой | +| `value_string` | varchar(255) / null | Строковое значение | + +## Диаграмма связей + +```mermaid +erDiagram + MotivationBuh { + int id PK + int inn + int year + int month + } + + MotivationBuhValue { + int id PK + int motivation_buh_id FK + int store_id FK + int motivation_group_id FK + int value_id FK + varchar value_type + int value_int + float value_float + varchar value_string + } + + Store { + int id PK + varchar name + } + + MotivationValueGroup { + int id PK + varchar name + } + + MotivationBuh ||--o{ MotivationBuhValue : "motivation_buh_id" + Store ||--o{ MotivationBuhValue : "store_id" + MotivationValueGroup ||--o{ MotivationBuhValue : "motivation_group_id" +``` + +## Диаграмма EAV-структуры + +```mermaid +flowchart TB + subgraph "Entity-Attribute-Value" + E[MotivationBuh
    Период+Фирма] + A1[value_id: 1
    Выручка] + A2[value_id: 2
    Себестоимость] + A3[value_id: 3
    Маржа] + + V1[value_float: 1500000.50] + V2[value_float: 950000.00] + V3[value_float: 550000.50] + end + + E --> A1 + E --> A2 + E --> A3 + A1 --> V1 + A2 --> V2 + A3 --> V3 +``` + +## Примеры использования + +### Создание значения показателя +```php +$value = new MotivationBuhValue(); +$value->motivation_buh_id = $periodId; +$value->store_id = $storeId; +$value->motivation_group_id = $groupId; +$value->value_id = $metricId; +$value->value_type = 'float'; +$value->value_float = 1500000.50; +$value->save(); +``` + +### Получение всех показателей магазина за период +```php +$values = MotivationBuhValue::find() + ->where([ + 'motivation_buh_id' => $periodId, + 'store_id' => $storeId + ]) + ->all(); + +foreach ($values as $val) { + $actualValue = match($val->value_type) { + 'int' => $val->value_int, + 'float' => $val->value_float, + 'string' => $val->value_string, + default => null + }; + echo "Показатель {$val->value_id}: {$actualValue}\n"; +} +``` + +### Получение значения конкретного показателя +```php +$metric = MotivationBuhValue::find() + ->where([ + 'motivation_buh_id' => $periodId, + 'store_id' => $storeId, + 'value_id' => $metricId + ]) + ->one(); + +$value = $metric->{'value_' . $metric->value_type}; +``` + +### Агрегация по группе показателей +```php +$totals = MotivationBuhValue::find() + ->select(['motivation_group_id', 'SUM(value_float) as total']) + ->where([ + 'motivation_buh_id' => $periodId, + 'value_type' => 'float' + ]) + ->groupBy('motivation_group_id') + ->asArray() + ->all(); +``` + +### Обновление или создание значения +```php +$value = MotivationBuhValue::find() + ->where([ + 'motivation_buh_id' => $periodId, + 'store_id' => $storeId, + 'value_id' => $metricId + ]) + ->one() ?? new MotivationBuhValue(); + +$value->motivation_buh_id = $periodId; +$value->store_id = $storeId; +$value->motivation_group_id = $groupId; +$value->value_id = $metricId; +$value->value_type = 'float'; +$value->value_float = $newValue; +$value->save(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `motivation_buh_id` | required, integer | +| `store_id` | required, integer | +| `motivation_group_id` | required, integer | +| `value_id` | required, integer | +| `value_type` | required, string (max 10) | +| `value_int` | integer, default: null | +| `value_float` | number, default: null | +| `value_string` | string (max 255), default: null | + +## Связанные модели + +- [MotivationBuh](./MotivationBuh.md) — периоды бухгалтерских данных +- [MotivationValueGroup](./MotivationValueGroup.md) — группы показателей +- [Store](./Store.md) — магазины + +## Особенности реализации + +1. **EAV-паттерн**: Entity-Attribute-Value для гибкого хранения показателей +2. **Полиморфные значения**: Три поля (value_int, value_float, value_string) для разных типов +3. **Детализация по магазинам**: Каждое значение привязано к конкретному магазину +4. **Группировка**: Показатели объединяются в группы через motivation_group_id +5. **Автоматический аудит**: TimestampBehavior и BlameableBehavior для отслеживания изменений diff --git a/erp24/docs/models/MotivationCostsItem.md b/erp24/docs/models/MotivationCostsItem.md new file mode 100644 index 00000000..e65e22b7 --- /dev/null +++ b/erp24/docs/models/MotivationCostsItem.md @@ -0,0 +1,256 @@ +# Класс: MotivationCostsItem + + +## Mindmap + +```mermaid +mindmap + root((MotivationCostsItem)) + Таблица БД + {{%motivation_costs_items}} + Свойства + id + int + name + string + code + string + data_type + string + order + int + is_active + bool + Наследование + extends ActiveRecord +``` + +## Назначение +Справочник статей затрат для системы мотивации в ERP24. Определяет типы затрат (списания, брак, пересорт и др.), которые учитываются при расчёте мотивации сотрудников магазинов. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`motivation_costs_items` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Конфигурация | +|-----------|--------------| +| `TimestampBehavior` | createdAtAttribute: `created_at`, updatedAtAttribute: `updated_at` | +| `BlameableBehavior` | createdByAttribute: `created_by`, updatedByAttribute: `updated_by` | + +## Константы типов данных + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `DATA_TYPE_INT` | `int` | Целое число | +| `DATA_TYPE_FLOAT` | `float` | Число с плавающей точкой | +| `DATA_TYPE_STRING` | `string` | Строка | + +## Константы кодов статей + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `CODE_EMPLOYEES_QUANTITY` | `34` | Количество сотрудников | + +## Константы названий статей списаний + +| Константа | Значение | +|-----------|----------| +| `ITEM_WRITE_OFF_OF_ILLIQUID_GOODS_SPOILAGE_EXPIRATION_OF_SHELF_LIFE` | Списание неликвидного товара: порча, истечение срока годности | +| `ITEM_DEFECTIVE_DELIVERY` | Брак с поставки | +| `ITEM_DEFECT_DUE_TO_EQUIPMENT_FAILURE` | Брак из-за поломки оборудования | +| `ITEM_REGRADING` | Пересорт | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Наименование статьи затрат | +| `code` | varchar(255) | Уникальный код статьи | +| `data_type` | varchar | Тип данных (int/float/string) | +| `order` | int | Порядок отображения | +| `is_active` | bool | Флаг активности | +| `created_at` | datetime | Дата создания (автоматически) | +| `updated_at` | datetime | Дата обновления (автоматически) | + +## Методы + +### writeOffsToMotivationItemMap($itemType) +**Описание:** Преобразует тип списания в название статьи мотивации. + +**Параметры:** +- `$itemType` (int) — тип списания из WriteOffsErp + +**Возвращает:** `string` — название статьи мотивации или пустую строку + +```php +public static function writeOffsToMotivationItemMap($itemType): string +``` + +### writeOffsToMotivationItemArray() +**Описание:** Возвращает массив маппинга типов списаний на статьи мотивации. + +**Возвращает:** `array` — ассоциативный массив [writeoff_type => item_name] + +```php +public static function writeOffsToMotivationItemArray(): array +``` + +### getWriteOffsItems() +**Описание:** Возвращает массив типов списаний, учитываемых в мотивации. + +**Возвращает:** `array` — массив констант типов списаний + +```php +public static function getWriteOffsItems(): array +``` + +### getDataTypeList() +**Описание:** Возвращает список доступных типов данных для UI. + +**Возвращает:** `array` — ассоциативный массив [type => label] + +```php +public static function getDataTypeList(): array +``` + +## Диаграмма связей + +```mermaid +erDiagram + MotivationCostsItem { + int id PK + varchar name UK + varchar code UK + varchar data_type + int order UK + bool is_active + datetime created_at + datetime updated_at + } + + WriteOffsErp { + int id PK + int type + float amount + } + + MotivationCostsItem ||--o{ WriteOffsErp : "маппинг по type" +``` + +## Диаграмма маппинга списаний + +```mermaid +flowchart LR + subgraph "WriteOffsErp types" + W1[WRITE_OFFS_TYPE_BRAK] + W2[WRITE_OFFS_TYPE_DELIVERY_BRAK] + W3[WRITE_OFFS_TYPE_DUE_TO_EQUIPMENT_FAILURE_BRAK] + W4[WRITE_OFFS_TYPE_RESORTING] + end + + subgraph "MotivationCostsItem" + M1[Списание неликвида] + M2[Брак с поставки] + M3[Брак из-за оборудования] + M4[Пересорт] + end + + W1 --> M1 + W2 --> M2 + W3 --> M3 + W4 --> M4 +``` + +## Примеры использования + +### Создание статьи затрат +```php +$item = new MotivationCostsItem(); +$item->name = 'Расходы на упаковку'; +$item->code = 'packaging_costs'; +$item->data_type = MotivationCostsItem::DATA_TYPE_FLOAT; +$item->order = 15; +$item->is_active = true; +$item->save(); +``` + +### Получение активных статей затрат +```php +$items = MotivationCostsItem::find() + ->where(['is_active' => true]) + ->orderBy(['order' => SORT_ASC]) + ->all(); +``` + +### Маппинг списания на статью мотивации +```php +$writeOffType = WriteOffsErp::WRITE_OFFS_TYPE_BRAK; +$motivationItemName = MotivationCostsItem::writeOffsToMotivationItemMap($writeOffType); +// Результат: 'Списание неликвидного товара: порча, истечение срока годности' + +$item = MotivationCostsItem::find() + ->where(['name' => $motivationItemName]) + ->one(); +``` + +### Формирование списка для выбора типа данных +```php +$dataTypes = MotivationCostsItem::getDataTypeList(); +// Результат: +// [ +// 'int' => 'Целое число', +// 'float' => 'Число с плавающей точкой', +// 'string' => 'Строка' +// ] + +echo Html::dropDownList('data_type', null, $dataTypes); +``` + +### Получение статей, связанных со списаниями +```php +$writeOffTypes = MotivationCostsItem::getWriteOffsItems(); +$writeOffItemNames = MotivationCostsItem::writeOffsToMotivationItemArray(); + +$writeOffItems = MotivationCostsItem::find() + ->where(['in', 'name', array_values($writeOffItemNames)]) + ->all(); +``` + +### Поиск статьи по коду +```php +$employeesItem = MotivationCostsItem::find() + ->where(['code' => MotivationCostsItem::CODE_EMPLOYEES_QUANTITY]) + ->one(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255), unique | +| `code` | required, string (max 255), unique | +| `data_type` | required, in [int, float, string] | +| `order` | required, integer, unique | +| `is_active` | boolean | + +## Связанные модели + +- [WriteOffsErp](./WriteOffsErp.md) — списания ERP (маппинг типов) +- [MotivationValue](./MotivationValue.md) — значения мотивации + +## Особенности реализации + +1. **Справочник статей**: Определяет типы затрат для системы мотивации +2. **Маппинг списаний**: Статические методы связывают типы списаний со статьями +3. **Полиморфные типы**: Поле data_type определяет тип хранимого значения +4. **Уникальные ограничения**: name, code и order должны быть уникальны +5. **Автоматический аудит**: TimestampBehavior и BlameableBehavior +6. **Флаг активности**: is_active для мягкого скрытия статей diff --git a/erp24/docs/models/MotivationValue.md b/erp24/docs/models/MotivationValue.md new file mode 100644 index 00000000..59d6eddc --- /dev/null +++ b/erp24/docs/models/MotivationValue.md @@ -0,0 +1,241 @@ +# Класс: MotivationValue + + +## Mindmap + +```mermaid +mindmap + root((MotivationValue)) + Таблица БД + motivation_value + Свойства + id + int + motivation_id + int + motivation_group_id + int + value_id + int + value_type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель значений показателей мотивации в ERP24. Хранит конкретные значения показателей для расчёта мотивации с поддержкой различных типов данных (int, float, string) в структуре EAV (Entity-Attribute-Value). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`motivation_value` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `motivation_id` | int | FK на запись мотивации | +| `motivation_group_id` | int | FK на группу показателей | +| `value_id` | int | ID показателя | +| `value_type` | varchar(10) | Тип значения (int/float/string) | +| `value_int` | int / null | Целочисленное значение | +| `value_float` | float / null | Значение с плавающей точкой | +| `value_string` | varchar(255) / null | Строковое значение | + +## Диаграмма связей + +```mermaid +erDiagram + Motivation { + int id PK + int store_id FK + int year + int month + } + + MotivationValue { + int id PK + int motivation_id FK + int motivation_group_id FK + int value_id + varchar value_type + int value_int + float value_float + varchar value_string + } + + MotivationValueGroup { + int id PK + varchar name + varchar alias + } + + Motivation ||--o{ MotivationValue : "motivation_id" + MotivationValueGroup ||--o{ MotivationValue : "motivation_group_id" +``` + +## Диаграмма EAV-структуры + +```mermaid +flowchart TB + subgraph "Entity (Motivation)" + E[Мотивация
    Магазин + Период] + end + + subgraph "Attributes (value_id)" + A1[Выручка] + A2[Конверсия] + A3[Средний чек] + A4[Комментарий] + end + + subgraph "Values" + V1[value_float: 1500000.00] + V2[value_float: 15.5] + V3[value_int: 2500] + V4[value_string: Отличная работа] + end + + E --> A1 + E --> A2 + E --> A3 + E --> A4 + A1 --> V1 + A2 --> V2 + A3 --> V3 + A4 --> V4 +``` + +## Примеры использования + +### Создание значения показателя +```php +$value = new MotivationValue(); +$value->motivation_id = $motivationId; +$value->motivation_group_id = $groupId; +$value->value_id = 1; // ID показателя "Выручка" +$value->value_type = 'float'; +$value->value_float = 1500000.00; +$value->save(); +``` + +### Получение всех показателей мотивации +```php +$values = MotivationValue::find() + ->where(['motivation_id' => $motivationId]) + ->all(); + +foreach ($values as $val) { + $actualValue = match($val->value_type) { + 'int' => $val->value_int, + 'float' => $val->value_float, + 'string' => $val->value_string, + default => null + }; + echo "Показатель {$val->value_id}: {$actualValue}\n"; +} +``` + +### Получение значения конкретного показателя +```php +$metric = MotivationValue::find() + ->where([ + 'motivation_id' => $motivationId, + 'value_id' => $metricId + ]) + ->one(); + +if ($metric) { + $value = $metric->{'value_' . $metric->value_type}; +} +``` + +### Получение показателей по группе +```php +$groupValues = MotivationValue::find() + ->where([ + 'motivation_id' => $motivationId, + 'motivation_group_id' => $groupId + ]) + ->all(); +``` + +### Обновление или создание значения +```php +$value = MotivationValue::find() + ->where([ + 'motivation_id' => $motivationId, + 'value_id' => $metricId + ]) + ->one() ?? new MotivationValue(); + +$value->motivation_id = $motivationId; +$value->motivation_group_id = $groupId; +$value->value_id = $metricId; +$value->value_type = 'float'; +$value->value_float = $newValue; +$value->save(); +``` + +### Агрегация по группам +```php +$totals = MotivationValue::find() + ->select(['motivation_group_id', 'SUM(value_float) as total']) + ->where([ + 'motivation_id' => $motivationId, + 'value_type' => 'float' + ]) + ->groupBy('motivation_group_id') + ->asArray() + ->all(); +``` + +### Массовое сохранение показателей +```php +$metrics = [ + ['value_id' => 1, 'type' => 'float', 'value' => 1500000.00], + ['value_id' => 2, 'type' => 'float', 'value' => 15.5], + ['value_id' => 3, 'type' => 'int', 'value' => 2500], +]; + +foreach ($metrics as $metric) { + $val = new MotivationValue(); + $val->motivation_id = $motivationId; + $val->motivation_group_id = $groupId; + $val->value_id = $metric['value_id']; + $val->value_type = $metric['type']; + $val->{'value_' . $metric['type']} = $metric['value']; + $val->save(); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `motivation_id` | required, integer | +| `motivation_group_id` | required, integer | +| `value_id` | required, integer | +| `value_type` | required, string (max 10) | +| `value_int` | integer, default: null | +| `value_float` | number, default: null | +| `value_string` | string (max 255), default: null | + +## Связанные модели + +- [Motivation](./Motivation.md) — записи мотивации (родитель) +- [MotivationValueGroup](./MotivationValueGroup.md) — группы показателей + +## Особенности реализации + +1. **EAV-паттерн**: Гибкое хранение произвольных показателей +2. **Полиморфные значения**: Три типизированных поля для разных типов данных +3. **Группировка**: Показатели объединяются в группы через motivation_group_id +4. **Идентификация показателя**: value_id определяет конкретный показатель +5. **Динамический доступ**: Значение читается из поля value_{value_type} diff --git a/erp24/docs/models/MotivationValueGroup.md b/erp24/docs/models/MotivationValueGroup.md new file mode 100644 index 00000000..03075cc4 --- /dev/null +++ b/erp24/docs/models/MotivationValueGroup.md @@ -0,0 +1,181 @@ +# Класс: MotivationValueGroup + + +## Mindmap + +```mermaid +mindmap + root((MotivationValueGroup)) + Таблица БД + motivation_value_group + Свойства + id + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник групп показателей мотивации в ERP24. Определяет категории показателей для логической группировки метрик в системе расчёта мотивации сотрудников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`motivation_value_group` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(80) | Название группы | +| `alias` | varchar(80) | Алиас (системное имя) | + +## Диаграмма связей + +```mermaid +erDiagram + MotivationValueGroup { + int id PK + varchar name + varchar alias + } + + MotivationValue { + int id PK + int motivation_group_id FK + int value_id + } + + MotivationBuhValue { + int id PK + int motivation_group_id FK + int value_id + } + + MotivationValueGroup ||--o{ MotivationValue : "motivation_group_id" + MotivationValueGroup ||--o{ MotivationBuhValue : "motivation_group_id" +``` + +## Типичные группы показателей + +| Название | Алиас | Описание | +|----------|-------|----------| +| Выручка | revenue | Показатели продаж и выручки | +| Затраты | costs | Статьи расходов | +| Эффективность | efficiency | KPI эффективности | +| Качество | quality | Показатели качества работы | +| Персонал | staff | Показатели по персоналу | + +## Примеры использования + +### Создание группы показателей +```php +$group = new MotivationValueGroup(); +$group->name = 'Показатели продаж'; +$group->alias = 'sales_metrics'; +$group->save(); +``` + +### Получение всех групп +```php +$groups = MotivationValueGroup::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Поиск группы по алиасу +```php +$revenueGroup = MotivationValueGroup::find() + ->where(['alias' => 'revenue']) + ->one(); +``` + +### Формирование списка для выбора +```php +$groupsList = ArrayHelper::map( + MotivationValueGroup::find()->all(), + 'id', + 'name' +); + +echo Html::dropDownList('motivation_group_id', null, $groupsList); +``` + +### Получение показателей группы +```php +$group = MotivationValueGroup::findOne($groupId); + +$values = MotivationValue::find() + ->where([ + 'motivation_id' => $motivationId, + 'motivation_group_id' => $group->id + ]) + ->all(); +``` + +### Группировка показателей по категориям +```php +$groups = MotivationValueGroup::find()->indexBy('id')->all(); + +$values = MotivationValue::find() + ->where(['motivation_id' => $motivationId]) + ->all(); + +$grouped = []; +foreach ($values as $val) { + $groupName = $groups[$val->motivation_group_id]->name ?? 'Без группы'; + $grouped[$groupName][] = $val; +} + +foreach ($grouped as $groupName => $groupValues) { + echo "

    {$groupName}

    "; + foreach ($groupValues as $val) { + echo "- Показатель {$val->value_id}\n"; + } +} +``` + +### Статистика по группам +```php +$stats = MotivationValue::find() + ->select(['motivation_group_id', 'COUNT(*) as count']) + ->where(['motivation_id' => $motivationId]) + ->groupBy('motivation_group_id') + ->asArray() + ->all(); + +$groups = ArrayHelper::index(MotivationValueGroup::find()->all(), 'id'); + +foreach ($stats as $stat) { + $groupName = $groups[$stat['motivation_group_id']]->name ?? 'Unknown'; + echo "{$groupName}: {$stat['count']} показателей\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 80) | +| `alias` | required, string (max 80) | + +## Связанные модели + +- [MotivationValue](./MotivationValue.md) — значения показателей +- [MotivationBuhValue](./MotivationBuhValue.md) — значения бухгалтерских показателей + +## Особенности реализации + +1. **Справочник категорий**: Простая структура для группировки показателей +2. **Алиас для кода**: Поле alias используется для программного доступа +3. **Используется в EAV**: Группирует записи в MotivationValue и MotivationBuhValue +4. **Компактная модель**: Минимум полей для базовой функциональности diff --git a/erp24/docs/models/MultipleModel.md b/erp24/docs/models/MultipleModel.md new file mode 100644 index 00000000..749549b5 --- /dev/null +++ b/erp24/docs/models/MultipleModel.md @@ -0,0 +1,276 @@ +# Класс: MultipleModel + + +## Mindmap + +```mermaid +mindmap + root((MultipleModel)) + Таблица БД + ActiveRecord + Наследование + extends yiibaseModel +``` + +## Назначение +Вспомогательный класс для работы с множественными моделями в формах Yii2 в ERP24. Обеспечивает создание, загрузку и валидацию набора однотипных моделей из POST-данных для табличных форм ввода. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`\yii\base\Model` + +## Статические методы + +### createMultiple($modelClass, $multipleModels = []) +**Описание:** Создаёт массив моделей из POST-данных формы. + +**Параметры:** +- `$modelClass` (string) — полное имя класса модели +- `$multipleModels` (array) — существующие модели для обновления + +**Возвращает:** `array` — массив моделей + +**Логика:** +1. Получает данные формы по имени класса (formName) +2. Для существующих моделей (с id) — использует из $multipleModels +3. Для новых записей — создаёт новые экземпляры + +**Пример:** +```php +$items = MultipleModel::createMultiple(OrderItem::class, $order->items); +``` + +### createMultipleModel($modelClass, $formNameCustom = '', $formFieldCustom = '', $multipleModels = []) +**Описание:** Расширенная версия createMultiple с поддержкой кастомных имён формы. + +**Параметры:** +- `$modelClass` (string) — класс модели +- `$formNameCustom` (string) — кастомное имя формы +- `$formFieldCustom` (string) — кастомное поле в форме +- `$multipleModels` (array) — существующие модели + +**Возвращает:** `array` — массив моделей + +**Пример:** +```php +$products = MultipleModel::createMultipleModel( + SaleProduct::class, + 'SaleForm', + 'products', + $existingProducts +); +``` + +### createMultipleModelFromArray($modelClass, $data) +**Описание:** Создаёт массив моделей из произвольного массива данных. + +**Параметры:** +- `$modelClass` (string) — класс модели +- `$data` (array) — массив данных + +**Возвращает:** `array` — массив пустых моделей + +**Пример:** +```php +$models = MultipleModel::createMultipleModelFromArray( + OrderItem::class, + $importedData +); +``` + +### loadMultipleFromArray($models, $data, $formName = null, $subLevel = []) +**Описание:** Загружает данные в массив моделей из произвольного массива. + +**Параметры:** +- `$models` (array) — массив моделей для загрузки +- `$data` (array) — данные для загрузки +- `$formName` (string|null) — имя формы +- `$subLevel` (array) — вложенные уровни для ArrayHelper::getValue + +**Возвращает:** `bool` — успешность загрузки + +**Пример:** +```php +$success = MultipleModel::loadMultipleFromArray($models, $jsonData); +``` + +## Диаграмма процесса создания моделей + +```mermaid +flowchart TD + A[POST-данные формы] --> B[createMultiple] + B --> C[Получение formName] + C --> D[Извлечение данных
    Yii::$app->request->post] + + D --> E{Есть существующие
    модели?} + E -->|Да| F[Индексация по id] + E -->|Нет| G[Пустой массив] + + F --> H[Цикл по POST-данным] + G --> H + + H --> I{Есть id
    в элементе?} + I -->|Да| J{Модель существует?} + I -->|Нет| K[Новая модель] + + J -->|Да| L[Использовать существующую] + J -->|Нет| K + + K --> M[Добавить в результат] + L --> M + + M --> N{Ещё элементы?} + N -->|Да| H + N -->|Нет| O[Вернуть массив моделей] +``` + +## Примеры использования + +### Базовое использование в контроллере +```php +public function actionUpdate($id) +{ + $order = Order::findOne($id); + $items = $order->items; + + if (Yii::$app->request->isPost) { + // Создаём модели из POST + $items = MultipleModel::createMultiple(OrderItem::class, $items); + + // Загружаем данные + Model::loadMultiple($items, Yii::$app->request->post()); + + // Валидация + $valid = Model::validateMultiple($items); + + if ($valid) { + // Сохранение + foreach ($items as $item) { + $item->order_id = $order->id; + $item->save(); + } + return $this->redirect(['view', 'id' => $id]); + } + } + + return $this->render('update', [ + 'order' => $order, + 'items' => $items, + ]); +} +``` + +### Форма с динамическими строками +```php +// В представлении (view) + $item): ?> +
    + + + + +
    + +``` + +### Создание новых моделей для пустой формы +```php +public function actionCreate() +{ + $order = new Order(); + $items = [new OrderItem()]; // Одна пустая строка + + if (Yii::$app->request->isPost) { + $items = MultipleModel::createMultiple(OrderItem::class); + // ... + } + + return $this->render('create', [ + 'order' => $order, + 'items' => $items, + ]); +} +``` + +### Использование с вложенной структурой +```php +// POST: ['SaleForm' => ['products' => [...]]] +$products = MultipleModel::createMultipleModel( + SaleProduct::class, + 'SaleForm', + 'products' +); +``` + +### Загрузка из JSON-данных +```php +$jsonData = json_decode($request->getRawBody(), true); + +$models = MultipleModel::createMultipleModelFromArray( + ImportItem::class, + $jsonData['items'] +); + +$success = MultipleModel::loadMultipleFromArray($models, $jsonData['items']); + +if ($success) { + foreach ($models as $model) { + $model->save(); + } +} +``` + +### Обновление с удалением +```php +$existingItems = $order->items; +$newItems = MultipleModel::createMultiple(OrderItem::class, $existingItems); + +Model::loadMultiple($newItems, Yii::$app->request->post()); + +// Найти удалённые +$newIds = array_filter(ArrayHelper::getColumn($newItems, 'id')); +$deletedIds = array_diff(ArrayHelper::getColumn($existingItems, 'id'), $newIds); + +// Удалить +OrderItem::deleteAll(['id' => $deletedIds]); + +// Сохранить оставшиеся +foreach ($newItems as $item) { + $item->save(); +} +``` + +### JavaScript для динамического добавления строк +```javascript +// Добавление новой строки +$('#add-item').click(function() { + var index = $('.item-row').length; + var template = $('#item-template').html(); + template = template.replace(/{index}/g, index); + $('#items-container').append(template); +}); + +// Удаление строки +$(document).on('click', '.remove-item', function() { + $(this).closest('.item-row').remove(); +}); +``` + +## Связанные модели + +Используется с любыми моделями, поддерживающими табличный ввод: +- OrderItem, SaleProduct — позиции заказов +- ProductComponent — компоненты товаров +- TaskChecklist — чек-листы задач + +## Особенности реализации + +1. **Не ActiveRecord**: Наследует yii\base\Model +2. **Статические методы**: Все методы статические, класс не инстанцируется +3. **Работа с POST**: Автоматическое извлечение данных из $_POST +4. **Сохранение существующих**: Модели с id переиспользуются +5. **Кастомные имена**: Поддержка formName и fieldName +6. **Вложенные данные**: loadMultipleFromArray с subLevel +7. **Индексация по id**: ArrayHelper::map для быстрого поиска diff --git a/erp24/docs/models/NewsLetterDeliveryStatus.md b/erp24/docs/models/NewsLetterDeliveryStatus.md new file mode 100644 index 00000000..881ab77d --- /dev/null +++ b/erp24/docs/models/NewsLetterDeliveryStatus.md @@ -0,0 +1,206 @@ +# Класс: NewsLetterDeliveryStatus + + +## Mindmap + +```mermaid +mindmap + root((NewsLetterDeliveryStatus)) + Таблица БД + news_letter_delivery_status + Свойства + id + int + check_guid + string + phone + string + sent_at + string + status + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель статусов доставки рассылок в ERP24. Отслеживает результаты отправки сообщений клиентам через различные каналы (Telegram, Viber, SMS) с привязкой к чекам продаж. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`news_letter_delivery_status` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `check_guid` | varchar(40) | GUID чека продажи | +| `phone` | varchar(20) | Номер телефона получателя | +| `sent_at` | datetime | Дата и время отправки | +| `status` | int | Статус доставки | + +## Константы статусов + +| Значение | Канал доставки | +|----------|----------------| +| `1` | Telegram | +| `2` | Viber | +| `3` | SMS | +| `-1` | Не отправилось (ошибка) | + +## Диаграмма связей + +```mermaid +erDiagram + NewsLetterDeliveryStatus { + int id PK + varchar check_guid FK + varchar phone + datetime sent_at + int status + } + + Sales { + varchar guid PK + varchar phone + datetime date + } + + Sales ||--o{ NewsLetterDeliveryStatus : "check_guid" +``` + +## Диаграмма каскадной отправки + +```mermaid +flowchart TD + A[Событие: чек закрыт] --> B[Получение телефона клиента] + B --> C{Есть в Telegram?} + C -->|Да| D[Отправка в TG] + C -->|Нет| E{Есть в Viber?} + D --> F{Успешно?} + F -->|Да| G[status = 1] + F -->|Нет| E + E -->|Да| H[Отправка в Viber] + E -->|Нет| I[Отправка SMS] + H --> J{Успешно?} + J -->|Да| K[status = 2] + J -->|Нет| I + I --> L{Успешно?} + L -->|Да| M[status = 3] + L -->|Нет| N[status = -1] +``` + +## Примеры использования + +### Запись статуса доставки +```php +$status = new NewsLetterDeliveryStatus(); +$status->check_guid = $sale->guid; +$status->phone = $customerPhone; +$status->sent_at = date('Y-m-d H:i:s'); +$status->status = 1; // Telegram +$status->save(); +``` + +### Получение статуса доставки для чека +```php +$delivery = NewsLetterDeliveryStatus::find() + ->where(['check_guid' => $checkGuid]) + ->one(); + +if ($delivery) { + $channel = match($delivery->status) { + 1 => 'Telegram', + 2 => 'Viber', + 3 => 'SMS', + -1 => 'Не доставлено', + default => 'Неизвестно' + }; + echo "Рассылка отправлена через: {$channel}"; +} +``` + +### Статистика по каналам доставки +```php +$stats = NewsLetterDeliveryStatus::find() + ->select(['status', 'COUNT(*) as count']) + ->where(['>=', 'sent_at', date('Y-m-01')]) + ->groupBy('status') + ->asArray() + ->all(); + +$channels = [1 => 'Telegram', 2 => 'Viber', 3 => 'SMS', -1 => 'Ошибка']; + +foreach ($stats as $stat) { + $channelName = $channels[$stat['status']] ?? 'Unknown'; + echo "{$channelName}: {$stat['count']}\n"; +} +``` + +### Получение неудачных отправок +```php +$failed = NewsLetterDeliveryStatus::find() + ->where(['status' => -1]) + ->andWhere(['>=', 'sent_at', date('Y-m-d', strtotime('-7 days'))]) + ->all(); + +foreach ($failed as $item) { + echo "Чек: {$item->check_guid}, Телефон: {$item->phone}\n"; +} +``` + +### Проверка отправки рассылки +```php +$wasSent = NewsLetterDeliveryStatus::find() + ->where([ + 'check_guid' => $checkGuid, + 'phone' => $phone + ]) + ->andWhere(['>', 'status', 0]) + ->exists(); + +if (!$wasSent) { + // Отправить рассылку +} +``` + +### Анализ эффективности каналов +```php +$dailyStats = NewsLetterDeliveryStatus::find() + ->select([ + 'DATE(sent_at) as date', + 'status', + 'COUNT(*) as count' + ]) + ->where(['>=', 'sent_at', date('Y-m-01')]) + ->groupBy(['DATE(sent_at)', 'status']) + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `check_guid` | required, string (max 40) | +| `phone` | required, string (max 20) | +| `sent_at` | required, safe | +| `status` | integer | + +## Связанные модели + +- [Sales](./Sales.md) — продажи (связь по check_guid) + +## Особенности реализации + +1. **Привязка к чеку**: Каждая рассылка связана с конкретным чеком продажи +2. **Мультиканальность**: Поддержка Telegram, Viber и SMS +3. **Каскадная отправка**: Попытка отправки через разные каналы при неудаче +4. **Отслеживание ошибок**: Статус -1 для неудачных попыток +5. **Временная метка**: sent_at фиксирует точное время отправки diff --git a/erp24/docs/models/NotifiableUser.md b/erp24/docs/models/NotifiableUser.md new file mode 100644 index 00000000..53bb9b51 --- /dev/null +++ b/erp24/docs/models/NotifiableUser.md @@ -0,0 +1,240 @@ +# Класс: NotifiableUser + + +## Mindmap + +```mermaid +mindmap + root((NotifiableUser)) + Таблица БД + notifiable_user + Свойства + id + int + phone + string + type + string + data + string + status + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель пользователей для уведомлений в ERP24. Хранит информацию о получателях уведомлений с типом сообщения и дополнительными данными для персонализированной отправки. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`notifiable_user` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `phone` | varchar(16) | Номер телефона получателя | +| `type` | varchar(255) | Тип сообщения/уведомления | +| `data` | text | Дополнительные данные в формате JSON | +| `status` | int | Вспомогательный статус для маркировки записей | + +## Диаграмма связей + +```mermaid +erDiagram + NotifiableUser { + int id PK + varchar phone + varchar type + text data + int status + } + + Users { + int id PK + varchar phone + } + + NotificationTemplate { + int id PK + varchar type + text template + } + + Users ||--o{ NotifiableUser : "phone" + NotificationTemplate ||--o{ NotifiableUser : "type" +``` + +## Диаграмма потока уведомлений + +```mermaid +flowchart TD + A[Событие в системе] --> B[Определение получателей] + B --> C[Создание NotifiableUser записей] + C --> D[Добавление в очередь] + D --> E{Тип уведомления} + E -->|SMS| F[SMS Gateway] + E -->|Push| G[Push Service] + E -->|Email| H[Mail Service] + F --> I[Отправка] + G --> I + H --> I + I --> J[Обновление status] +``` + +## Примеры использования + +### Создание записи для уведомления +```php +$user = new NotifiableUser(); +$user->phone = '79876543210'; +$user->type = 'order_ready'; +$user->data = json_encode([ + 'order_id' => 12345, + 'store_name' => 'Магазин на Арбате', + 'customer_name' => 'Иван' +]); +$user->status = 0; // Ожидает отправки +$user->save(); +``` + +### Получение пользователей для рассылки по типу +```php +$recipients = NotifiableUser::find() + ->where(['type' => 'promotion']) + ->andWhere(['status' => 0]) + ->all(); + +foreach ($recipients as $recipient) { + $data = json_decode($recipient->data, true); + // Отправка уведомления + $sms->send($recipient->phone, $data['message']); + + // Маркировка как отправленное + $recipient->status = 1; + $recipient->save(); +} +``` + +### Массовое добавление получателей +```php +$phones = ['79876543210', '79876543211', '79876543212']; +$notificationType = 'flash_sale'; +$notificationData = [ + 'sale_name' => 'Распродажа роз', + 'discount' => 30, + 'valid_until' => '2024-03-20' +]; + +foreach ($phones as $phone) { + $user = new NotifiableUser(); + $user->phone = $phone; + $user->type = $notificationType; + $user->data = json_encode($notificationData); + $user->status = 0; + $user->save(); +} +``` + +### Удаление обработанных записей +```php +// Удаление отправленных уведомлений +NotifiableUser::deleteAll(['status' => 1]); +``` + +### Поиск по данным уведомления +```php +// Поиск уведомлений для конкретного заказа +$notifications = NotifiableUser::find() + ->where(['like', 'data', '"order_id":12345']) + ->all(); +``` + +### Статистика по типам уведомлений +```php +$stats = NotifiableUser::find() + ->select(['type', 'status', 'COUNT(*) as count']) + ->groupBy(['type', 'status']) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $statusLabel = $stat['status'] === 0 ? 'Ожидает' : 'Отправлено'; + echo "{$stat['type']}: {$stat['count']} ({$statusLabel})\n"; +} +``` + +### Обработка очереди уведомлений +```php +function processNotificationQueue($batchSize = 100) +{ + $pending = NotifiableUser::find() + ->where(['status' => 0]) + ->limit($batchSize) + ->all(); + + foreach ($pending as $notification) { + $data = json_decode($notification->data, true); + + try { + sendNotification($notification->phone, $notification->type, $data); + $notification->status = 1; // Успешно + } catch (\Exception $e) { + $notification->status = -1; // Ошибка + Yii::error("Failed to send notification: " . $e->getMessage()); + } + + $notification->save(); + } + + return count($pending); +} +``` + +### Копирование записей с маркировкой +```php +// Маркировка перед копированием +NotifiableUser::updateAll(['status' => 99], ['type' => 'promotion']); + +// Копирование в архив +$toArchive = NotifiableUser::find() + ->where(['status' => 99]) + ->all(); + +foreach ($toArchive as $record) { + // Копирование в архивную таблицу... +} + +// Удаление маркированных +NotifiableUser::deleteAll(['status' => 99]); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `phone` | required, string (max 16) | +| `type` | required, string (max 255) | +| `data` | required, string | +| `status` | integer | + +## Связанные модели + +- [Users](./Users.md) — клиенты (связь по phone) +- [NotificationTemplate](./NotificationTemplate.md) — шаблоны уведомлений (связь по type) + +## Особенности реализации + +1. **Очередь уведомлений**: Записи служат очередью для отправки +2. **JSON данные**: Поле data хранит произвольные данные для персонализации +3. **Статус обработки**: Поле status для отслеживания состояния отправки +4. **Маркировка записей**: status используется для пакетной обработки +5. **Типизация**: Поле type определяет тип и шаблон уведомления +6. **Связь по телефону**: Телефон служит идентификатором получателя diff --git a/erp24/docs/models/Notification.md b/erp24/docs/models/Notification.md new file mode 100644 index 00000000..21bd0fd6 --- /dev/null +++ b/erp24/docs/models/Notification.md @@ -0,0 +1,475 @@ +# Class: Notification + + +## Mindmap + +```mermaid +mindmap + root((Notification)) + Таблица БД + notification + Свойства + id + int + name + string + content + string + type + string + created_by + int + created_at + string + Связи + Files + 1:N Files + Author + 1:1 Admin + Status + 1:1 NotificationStatus + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель уведомлений для сотрудников ERP24. Используется для создания и рассылки системных уведомлений внутри компании. Поддерживает Rich Text редактор с возможностью добавления изображений, планирование времени отправки и выбор получателей. + +Модель работает в связке с `NotificationService`, который отвечает за фактическую рассылку уведомлений выбранным сотрудникам. + +--- + +## Файл модели + +`/erp24/records/Notification.php` + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`notification` + +--- + +## Поля таблицы + +| Имя | Тип | Ключ | Описание | +|-----|-----|------|----------| +| `id` | int | PK | Первичный ключ | +| `name` | string(256) | | Название уведомления | +| `description` | string(256) | null | Краткое описание содержания | +| `content` | text | | Описание уведомления (текст + изображения) с полноценным редактором | +| `type` | text | | Тип уведомления (выпадающий список) | +| `created_by` | int | FK | ID сотрудника, благодаря которому уведомление создано | +| `created_at` | timestamp | | Время создания уведомления | +| `send_at` | timestamp | | Время отсылки уведомления | + +--- + +## Описание полей + +### id +Автоинкрементный первичный ключ таблицы. Уникальный идентификатор уведомления. + +### name +Заголовок уведомления. Обязательное поле, длина от 3 до 256 символов. Отображается в списке уведомлений и в заголовке сообщения. + +### description +Краткое описание содержания уведомления. Необязательное поле, длина от 3 до 256 символов. Используется для предпросмотра в списках и уведомлениях. + +### content +Полное содержание уведомления с поддержкой форматирования через Rich Text редактор. Обязательное поле, минимальная длина 14 символов. Может содержать HTML-разметку, ссылки на загруженные изображения. + +### type +Тип уведомления из выпадающего списка. Обязательное поле. Определяет категорию уведомления (например, "информационное", "срочное", "административное"). + +### created_by +Внешний ключ на таблицу `admin`. Автоматически заполняется ID текущего пользователя при создании уведомления. + +### created_at +Временная метка создания записи уведомления. Автоматически устанавливается при сохранении. + +### send_at +Запланированное время отправки уведомления. Может совпадать с `created_at` (немедленная отправка) или быть в будущем (отложенная отправка). + +--- + +## Константы + +### Сценарии +```php +const SCENARIO_ADD = 'add'; +``` + +Сценарий для добавления нового уведомления. Включает валидацию полей: name, description, content, type, send_at, recipients, isImmediate. + +--- + +## Дополнительные свойства (не из БД) + +### $recipients +**Тип:** `mixed` +**Описание:** Массив ID получателей уведомления (сотрудников). Используется только при создании уведомления, не хранится в таблице. + +### $isImmediate +**Тип:** `mixed` +**Описание:** Флаг немедленной отправки. Если установлен в `true`, то `send_at` автоматически приравнивается к `created_at`. + +### $imageFiles +**Тип:** `mixed` +**Описание:** Массив загружаемых файлов изображений. Валидируется на формат (png, jpg) и количество (максимум 4 файла). + +--- + +## Отношения (Relations) + +### getFiles() +**Тип:** `hasMany` +**Модель:** `Files` +**Ключ:** `['entity_id' => 'id']` +**Условие:** `['entity' => 'notification']` +**Описание:** Связь с прикрепленными файлами уведомления + +**Пример:** +```php +$notification = Notification::findOne($id); +$attachedFiles = $notification->files; +foreach ($attachedFiles as $file) { + echo "Файл: {$file->url}\n"; +} +``` + +--- + +### getAuthor() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'created_by']` +**Описание:** Связь с автором уведомления (сотрудником, создавшим уведомление) + +**Пример:** +```php +$notification = Notification::findOne($id); +$author = $notification->author; +echo "Создано: {$author->name}"; +``` + +--- + +### getStatus() +**Тип:** `hasOne` +**Модель:** `NotificationStatus` +**Ключ:** `['notification_id' => 'id']` +**Условие:** `['admin_id' => Yii::$app->user->id]` +**Описание:** Статус прочтения уведомления для текущего пользователя + +**Пример:** +```php +$notification = Notification::findOne($id); +$status = $notification->status; +if ($status && $status->is_read) { + echo "Прочитано"; +} +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['name', 'description', 'content', 'type', 'created_by', 'created_at', 'send_at', 'recipients'] +``` + +### Текстовые поля +```php +['content', 'type'] // text +``` + +### Целочисленные поля +```php +['created_by'] // integer +``` + +### Безопасные поля +```php +['description', 'created_at', 'send_at', 'isImmediate'] // safe +``` + +### Ограничения длины +```php +['name', 'description'] // max: 256, min: 3 +['content'] // min: 14 +``` + +### Файлы +```php +['imageFiles'] // skipOnEmpty, extensions: 'png, jpg', maxFiles: 4 +``` + +--- + +## Методы модели + +### scenarios() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` +**Описание:** Определяет доступные сценарии и активные атрибуты для каждого сценария + +**Логика работы:** +1. Получает сценарии родительского класса через `parent::scenarios()` +2. Добавляет кастомный сценарий `SCENARIO_ADD` с набором активных атрибутов +3. Возвращает объединенный массив сценариев + +**Сценарий ADD:** +- Активные атрибуты: name, description, content, type, send_at, recipients, isImmediate +- Используется при создании нового уведомления через форму + +**Пример:** +```php +$notification = new Notification(); +$notification->scenario = Notification::SCENARIO_ADD; +$notification->setAttributes($_POST['Notification']); +``` + +--- + +### upload() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `bool` +**Описание:** Загрузка и сохранение уведомления с последующей инициализацией рассылки + +**Логика работы:** +1. Валидирует данные модели через `$this->validate()` +2. Устанавливает `created_by` из текущего пользователя `Yii::$app->user->id` +3. Устанавливает `created_at` как текущую дату и время +4. Если флаг `isImmediate == true`, то `send_at` устанавливается равным `created_at` +5. Сохраняет запись в базу данных через `$this->save()` +6. Передает объект уведомления в `NotificationService::initNotification()` для рассылки +7. Возвращает `true` при успешной обработке + +**Вызовы сторонних методов:** +- `$this->validate()` — валидация данных модели согласно rules() +- `Yii::$app->user->id` — получение ID текущего авторизованного пользователя +- `$this->save()` — сохранение записи в таблицу notification +- `NotificationService::initNotification($this)` — инициализация рассылки уведомления получателям + +**Примечание:** В коде присутствует закомментированный блок загрузки изображений через модель Files. В текущей реализации загрузка файлов не активна. + +**Пример:** +```php +$notification = new Notification(); +$notification->scenario = Notification::SCENARIO_ADD; +$notification->name = 'Важное объявление'; +$notification->description = 'Изменения в графике работы'; +$notification->content = '

    С 1 января меняется график работы офиса...

    '; +$notification->type = 'administrative'; +$notification->recipients = [1, 5, 10, 15]; // ID сотрудников +$notification->isImmediate = true; + +if ($notification->upload()) { + echo "Уведомление успешно создано и отправлено"; +} else { + print_r($notification->errors); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Notification ||--o{ Files : "has attachments" + Notification }o--|| Admin : "created by" + Notification ||--o{ NotificationStatus : "has statuses" + + Notification { + int id PK + string name "Название" + string description "Краткое описание" + text content "Полный текст" + text type "Тип" + int created_by FK + timestamp created_at "Создано" + timestamp send_at "Отправлено" + } + + Files { + int id PK + int entity_id FK + string entity "notification" + string url "Путь к файлу" + string file_type "Тип файла" + timestamp created_at + } + + Admin { + int id PK + string name "Имя сотрудника" + string email + int group_id + } + + NotificationStatus { + int id PK + int notification_id FK + int admin_id FK + int is_read "Прочитано" + timestamp read_at + } +``` + +--- + +## Примеры использования + +### Создание срочного уведомления + +```php +use yii_app\records\Notification; + +$notification = new Notification(); +$notification->scenario = Notification::SCENARIO_ADD; +$notification->name = 'СРОЧНО: Технические работы'; +$notification->description = 'Плановое обслуживание системы'; +$notification->content = '

    Технические работы

    С 22:00 до 02:00 будут проводиться плановые технические работы. Система будет недоступна.

    '; +$notification->type = 'urgent'; +$notification->recipients = Admin::find()->select('id')->column(); // Все сотрудники +$notification->isImmediate = true; + +if ($notification->upload()) { + Yii::$app->session->setFlash('success', 'Уведомление отправлено всем сотрудникам'); +} else { + Yii::$app->session->setFlash('error', 'Ошибка при создании уведомления'); +} +``` + +--- + +### Планирование уведомления + +```php +use yii_app\records\Notification; + +$notification = new Notification(); +$notification->scenario = Notification::SCENARIO_ADD; +$notification->name = 'Корпоративное мероприятие'; +$notification->description = 'Приглашение на День компании'; +$notification->content = '

    Приглашаем вас на корпоративное мероприятие 15 января...

    '; +$notification->type = 'event'; +$notification->recipients = [1, 2, 3, 4, 5]; // Выбранные сотрудники +$notification->isImmediate = false; +$notification->send_at = '2025-01-10 09:00:00'; // Отложенная отправка + +if ($notification->upload()) { + echo "Уведомление запланировано на {$notification->send_at}"; +} +``` + +--- + +### Получение уведомлений с файлами + +```php +use yii_app\records\Notification; + +$notifications = Notification::find() + ->with(['files', 'author']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($notifications as $notification) { + echo "Уведомление: {$notification->name}\n"; + echo "Автор: {$notification->author->name}\n"; + + if ($notification->files) { + echo "Вложения:\n"; + foreach ($notification->files as $file) { + echo " - {$file->url} ({$file->file_type})\n"; + } + } +} +``` + +--- + +### Проверка статуса прочтения + +```php +use yii_app\records\Notification; + +$notification = Notification::findOne($id); +$status = $notification->getStatus()->one(); + +if ($status) { + if ($status->is_read) { + echo "Прочитано: {$status->read_at}"; + } else { + echo "Не прочитано"; + } +} else { + echo "Статус не найден"; +} +``` + +--- + +### Фильтрация по типу и дате + +```php +use yii_app\records\Notification; + +// Уведомления за последний месяц +$recentNotifications = Notification::find() + ->where(['>=', 'created_at', date('Y-m-d H:i:s', strtotime('-1 month'))]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +// Срочные уведомления +$urgentNotifications = Notification::find() + ->where(['type' => 'urgent']) + ->andWhere(['>=', 'send_at', date('Y-m-d')]) + ->all(); + +// Запланированные уведомления +$scheduledNotifications = Notification::find() + ->where(['>', 'send_at', date('Y-m-d H:i:s')]) + ->orderBy(['send_at' => SORT_ASC]) + ->all(); +``` + +--- + +## Связанные модели + +- **Admin** — сотрудники (создатели уведомлений) +- **Files** — файловые вложения +- **NotificationStatus** — статусы прочтения уведомлений сотрудниками + +--- + +## Связанные сервисы + +- **NotificationService** — сервис рассылки уведомлений получателям + +--- + +## Версия + +Документация актуальна для версии модели на 2025-12-11 diff --git a/erp24/docs/models/NotificationStatus.md b/erp24/docs/models/NotificationStatus.md new file mode 100644 index 00000000..063b73fc --- /dev/null +++ b/erp24/docs/models/NotificationStatus.md @@ -0,0 +1,206 @@ +# Класс: NotificationStatus + + +## Mindmap + +```mermaid +mindmap + root((NotificationStatus)) + Таблица БД + notification_status + Свойства + id + int + notification_id + int + admin_id + int + status + int + Связи + Notification + 1:1 Notification + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель статусов уведомлений пользователей в ERP24. Отслеживает состояние доставки и прочтения уведомлений для каждого получателя (администратора системы). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`notification_status` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `notification_id` | int | FK на уведомление | +| `admin_id` | int | FK на получателя (Admin) | +| `status` | int | Статус уведомления (0/1/2) | + +## Константы статусов + +| Значение | Описание | +|----------|----------| +| `0` | Уведомление создано | +| `1` | Уведомление отправлено | +| `2` | Уведомление прочитано | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getNotification()` | hasOne | Notification | Связанное уведомление | + +## Диаграмма связей + +```mermaid +erDiagram + Notification { + int id PK + varchar title + text message + datetime created_at + } + + NotificationStatus { + int id PK + int notification_id FK + int admin_id FK + int status + } + + Admin { + int id PK + varchar username + } + + Notification ||--o{ NotificationStatus : "notification_id" + Admin ||--o{ NotificationStatus : "admin_id" +``` + +## Диаграмма жизненного цикла + +```mermaid +stateDiagram-v2 + [*] --> Создано: Создание уведомления + Создано --> Отправлено: Отправка + Отправлено --> Прочитано: Открытие пользователем + + Создано: status = 0 + Отправлено: status = 1 + Прочитано: status = 2 +``` + +## Примеры использования + +### Создание статуса для получателя +```php +$status = new NotificationStatus(); +$status->notification_id = $notification->id; +$status->admin_id = $userId; +$status->status = 0; // Создано +$status->save(); +``` + +### Получение непрочитанных уведомлений пользователя +```php +$unread = NotificationStatus::find() + ->where([ + 'admin_id' => Yii::$app->user->id, + 'status' => 1 // Отправлено, но не прочитано + ]) + ->with(['notification']) + ->orderBy(['id' => SORT_DESC]) + ->all(); + +foreach ($unread as $item) { + echo "{$item->notification->title}\n"; +} +``` + +### Пометить уведомление как прочитанное +```php +$status = NotificationStatus::find() + ->where([ + 'notification_id' => $notificationId, + 'admin_id' => Yii::$app->user->id + ]) + ->one(); + +if ($status) { + $status->status = 2; // Прочитано + $status->save(); +} +``` + +### Подсчёт непрочитанных +```php +$unreadCount = NotificationStatus::find() + ->where([ + 'admin_id' => $userId, + 'status' => 1 + ]) + ->count(); + +echo "У вас {$unreadCount} непрочитанных уведомлений"; +``` + +### Массовая рассылка уведомления +```php +$notification = new Notification(); +$notification->title = 'Важное объявление'; +$notification->message = 'Текст уведомления'; +$notification->save(); + +$recipients = Admin::find()->where(['active' => 1])->all(); + +foreach ($recipients as $admin) { + $status = new NotificationStatus(); + $status->notification_id = $notification->id; + $status->admin_id = $admin->id; + $status->status = 0; + $status->save(); +} + +// После отправки обновляем статусы +NotificationStatus::updateAll( + ['status' => 1], + ['notification_id' => $notification->id] +); +``` + +### Пометить все как прочитанные +```php +NotificationStatus::updateAll( + ['status' => 2], + ['admin_id' => $userId, 'status' => 1] +); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `notification_id` | required, integer | +| `admin_id` | required, integer | +| `status` | required, integer | + +## Связанные модели + +- [Notification](./Notification.md) — уведомления +- [Admin](./Admin.md) — получатели (администраторы) + +## Особенности реализации + +1. **Персонализированные статусы**: Каждый получатель имеет свой статус +2. **Три состояния**: Создано → Отправлено → Прочитано +3. **Many-to-Many**: Связь уведомлений с получателями +4. **Отслеживание прочтения**: Возможность узнать, кто прочитал уведомление diff --git a/erp24/docs/models/OrderStoreSort.md b/erp24/docs/models/OrderStoreSort.md new file mode 100644 index 00000000..6b9b4250 --- /dev/null +++ b/erp24/docs/models/OrderStoreSort.md @@ -0,0 +1,212 @@ +# Класс: OrderStoreSort + + +## Mindmap + +```mermaid +mindmap + root((OrderStoreSort)) + Таблица БД + order_store_sort + Свойства + id + int + product_id + string + store_id + string + position + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель приоритетов магазинов для распределения заказов в ERP24. Определяет порядок выбора магазинов при делении заказа на несколько точек исполнения для конкретных товаров. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`order_store_sort` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `product_id` | varchar(40) | GUID товара из products_1c | +| `store_id` | varchar(40) | GUID магазина из products_1c | +| `position` | int | Приоритет (чем меньше, тем выше) | + +## Диаграмма связей + +```mermaid +erDiagram + OrderStoreSort { + int id PK + varchar product_id FK + varchar store_id FK + int position + } + + Products1c { + varchar guid PK + varchar name + } + + Store { + varchar id PK + varchar name + } + + Products1c ||--o{ OrderStoreSort : "product_id" + Store ||--o{ OrderStoreSort : "store_id" +``` + +## Диаграмма логики распределения + +```mermaid +flowchart TD + A[Новый заказ с товаром X] --> B[Загрузка приоритетов
    для товара X] + B --> C[Сортировка магазинов
    по position ASC] + C --> D[Магазин 1
    position=1] + D --> E{Есть остаток?} + E -->|Да| F[Назначить магазин 1] + E -->|Нет| G[Магазин 2
    position=2] + G --> H{Есть остаток?} + H -->|Да| I[Назначить магазин 2] + H -->|Нет| J[Магазин 3
    position=3] + J --> K[...] +``` + +## Примеры использования + +### Создание приоритета для товара +```php +$sort = new OrderStoreSort(); +$sort->product_id = $productGuid; +$sort->store_id = $storeGuid; +$sort->position = 1; // Наивысший приоритет +$sort->save(); +``` + +### Получение отсортированных магазинов для товара +```php +$stores = OrderStoreSort::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['position' => SORT_ASC]) + ->all(); + +foreach ($stores as $store) { + echo "Магазин {$store->store_id}: приоритет {$store->position}\n"; +} +``` + +### Выбор магазина для заказа +```php +function selectStoreForProduct($productGuid, $quantity) +{ + $stores = OrderStoreSort::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['position' => SORT_ASC]) + ->all(); + + foreach ($stores as $sortItem) { + $stock = Products1c::find() + ->where(['guid' => $productGuid, 'store_id' => $sortItem->store_id]) + ->one(); + + if ($stock && $stock->quantity >= $quantity) { + return $sortItem->store_id; + } + } + + return null; // Нет подходящего магазина +} +``` + +### Обновление приоритетов +```php +// Сделать магазин приоритетным для товара +$sort = OrderStoreSort::find() + ->where([ + 'product_id' => $productGuid, + 'store_id' => $storeGuid + ]) + ->one(); + +if ($sort) { + $sort->position = 1; + $sort->save(); + + // Сдвинуть остальные + OrderStoreSort::updateAll( + ['position' => new Expression('position + 1')], + [ + 'and', + ['product_id' => $productGuid], + ['!=', 'store_id', $storeGuid] + ] + ); +} +``` + +### Массовая настройка приоритетов +```php +$priorities = [ + 'store-guid-1' => 1, + 'store-guid-2' => 2, + 'store-guid-3' => 3, +]; + +foreach ($priorities as $storeGuid => $position) { + $sort = OrderStoreSort::find() + ->where([ + 'product_id' => $productGuid, + 'store_id' => $storeGuid + ]) + ->one() ?? new OrderStoreSort(); + + $sort->product_id = $productGuid; + $sort->store_id = $storeGuid; + $sort->position = $position; + $sort->save(); +} +``` + +### Получение товаров без настроенных приоритетов +```php +$productsWithPriorities = OrderStoreSort::find() + ->select('product_id') + ->distinct() + ->column(); + +$productsWithoutPriorities = Products1c::find() + ->where(['not in', 'guid', $productsWithPriorities]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `product_id` | required, string (max 40) | +| `store_id` | required, string (max 40) | +| `position` | integer | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары (связь по product_id) +- [Store](./Store.md) — магазины (связь по store_id) + +## Особенности реализации + +1. **Приоритизация**: Меньшее значение position = более высокий приоритет +2. **Per-product настройка**: Приоритеты задаются для каждого товара отдельно +3. **GUID связи**: Использование GUID для интеграции с 1С +4. **Распределение заказов**: Влияет на логику автоматического назначения магазина +5. **Каскадный выбор**: При отсутствии остатка переход к следующему по приоритету diff --git a/erp24/docs/models/OrdersAmo.md b/erp24/docs/models/OrdersAmo.md new file mode 100644 index 00000000..3a9abf79 --- /dev/null +++ b/erp24/docs/models/OrdersAmo.md @@ -0,0 +1,531 @@ +# Модель OrdersAmo + + +## Mindmap + +```mermaid +mindmap + root((OrdersAmo)) + Таблица БД + orders_amo + Свойства + id + int + nomer + string + created_at + string + created_id + int + amo_id + int + pipeline_id + int + Связи + Sales + 1:N Sales + StoreObj + 1:1 CityStore + FloristObj + 1:1 Admin + CourierObj + 1:1 Admin + Status + 1:1 OrdersStatus + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `OrdersAmo` представляет заказы из CRM-системы AmoCRM. Является основной моделью для хранения данных о заказах клиентов, включая информацию о доставке, флористах, курьерах, оплате, рекламациях и интеграции с 1С. + +**Файл модели:** `erp24/records/OrdersAmo.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `orders_amo` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Основные поля заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `nomer` | VARCHAR(25) | Номер заказа | +| `name` | VARCHAR(255) | Название заказа | +| `price` | INTEGER | Стоимость заказа | +| `bonus` | INTEGER | Начисленные бонусы | +| `key_code` | VARCHAR(36) | Уникальный код заказа | +| `products_json` | TEXT | JSON с товарами заказа | + +### Интеграция с AmoCRM + +| Поле | Тип | Описание | +|------|-----|----------| +| `amo_id` | INTEGER | ID сделки в AmoCRM | +| `pipeline_id` | INTEGER | ID воронки в AmoCRM | +| `status_id` | INTEGER | ID статуса в AmoCRM | +| `amo_sync` | INTEGER | Флаг синхронизации с AmoCRM | +| `amo_sync_time` | VARCHAR | Время последней синхронизации | +| `client_id` | INTEGER | ID клиента в AmoCRM | +| `contact_id` | INTEGER | ID контакта в AmoCRM | + +### Источник заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| `istochnik_id` | INTEGER | ID источника | +| `istochnik` | VARCHAR(100) | Название источника | +| `phone_source` | VARCHAR(36) | Телефон источника | +| `setka_id` | INTEGER | ID сети | +| `brend` | VARCHAR(120) | Бренд (название сетки) | +| `site_id` | INTEGER | ID сайта | +| `site_url` | VARCHAR(250) | URL сайта | + +### Геолокация + +| Поле | Тип | Описание | +|------|-----|----------| +| `city_id` | INTEGER | ID города | +| `city` | VARCHAR(100) | Название города | + +### Информация о клиенте + +| Поле | Тип | Описание | +|------|-----|----------| +| `phone` | VARCHAR(25) | Телефон заказа | +| `phone_client` | VARCHAR(25) | Телефон клиента | +| `email` | VARCHAR(40) | Email клиента | +| `client` | VARCHAR(125) | ФИО клиента | +| `client_anonim` | INTEGER | Флаг анонимности (0/1) | +| `client_ip` | VARCHAR(16) | IP-адрес клиента | + +### Получатель заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| `pol_name` | VARCHAR(120) | ФИО получателя | +| `pol_phone` | VARCHAR(35) | Телефон получателя | + +### Состав заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| `povod` | VARCHAR(55) | Повод заказа | +| `order_text` | TEXT | Текст заказа (состав) | +| `shariki` | VARCHAR(45) | Шарики | +| `toper` | VARCHAR(55) | Топер | +| `text_card` | TEXT | Текст открытки | + +### Доставка + +| Поле | Тип | Описание | +|------|-----|----------| +| `delivery` | VARCHAR(65) | Тип доставки | +| `delivery_date` | VARCHAR | Дата доставки | +| `delivery_time` | VARCHAR(25) | Время доставки | +| `delivery_time_extend` | VARCHAR(25) | Расширенное время доставки | +| `delivery_adress` | TEXT | Адрес доставки | +| `delivery_rayon` | VARCHAR(55) | Район доставки | +| `delivery_oblast` | VARCHAR(100) | Область доставки | +| `price_dostavka` | INTEGER | Стоимость доставки | +| `price_dostavka_zatrat` | INTEGER | Затраты на доставку | +| `dostavka_povtor_tip` | VARCHAR(40) | Тип повторной доставки | +| `price_dostavka_povtor` | INTEGER | Стоимость повторной доставки | +| `price_dostavka_povtor_zatrat` | INTEGER | Затраты на повторную доставку | +| `dostavka_comment` | TEXT | Комментарий к доставке | + +### Курьер + +| Поле | Тип | Описание | +|------|-----|----------| +| `courier_id` | INTEGER | ID курьера | +| `courier` | VARCHAR(35) | ФИО курьера | +| `courier_zabor` | VARCHAR | Время забора курьером | +| `courier_dostavil` | VARCHAR | Время доставки курьером | +| `courier_gps` | VARCHAR(100) | GPS-координаты курьера | +| `courier_opozdal` | INTEGER | Флаг опоздания курьера | + +### Флорист + +| Поле | Тип | Описание | +|------|-----|----------| +| `florist_id` | INTEGER | ID флориста | +| `florist` | VARCHAR(35) | ФИО флориста | +| `florist_peredano` | VARCHAR | Время передачи заказа флористу | +| `florist_zabor` | VARCHAR | Время забора заказа | +| `florist_foto` | VARCHAR | Время отправки фото | +| `florist_sobrano` | VARCHAR | Время сборки букета | +| `florist_sborka_night` | INTEGER | Ночная сборка (0/1) | +| `florist_soglas` | INTEGER | Согласование с флористом | +| `florist_pravki` | INTEGER | Количество правок флориста | +| `florist_comment` | TEXT | Комментарий флориста | + +### Магазин + +| Поле | Тип | Описание | +|------|-----|----------| +| `store` | VARCHAR(45) | Название магазина | +| `store_id` | VARCHAR(55) | ID магазина | + +### Менеджеры + +| Поле | Тип | Описание | +|------|-----|----------| +| `responsible_user_id` | INTEGER | ID ответственного | +| `responsible_user` | VARCHAR(66) | ФИО ответственного | +| `manager_id` | INTEGER | ID менеджера | +| `manager` | VARCHAR(200) | ФИО менеджера | +| `manager_courier_time` | VARCHAR | Время назначения курьера | +| `created_id` | INTEGER | ID создателя заказа | + +### Оплата + +| Поле | Тип | Описание | +|------|-----|----------| +| `pay` | VARCHAR(65) | Статус оплаты | +| `pay_text` | VARCHAR(255) | Текст оплаты | +| `payment_type_id` | VARCHAR(120) | ID типа оплаты | + +### Отказ + +| Поле | Тип | Описание | +|------|-----|----------| +| `otkaz_id` | VARCHAR(25) | ID причины отказа | +| `otkaz_text` | TEXT | Текст отказа | +| `otkaz_manager` | VARCHAR(55) | Менеджер, оформивший отказ | +| `otkaz_id_rop` | VARCHAR(125) | ID отказа РОП | + +### Рекламации (проблемы) + +| Поле | Тип | Описание | +|------|-----|----------| +| `problem_status` | INTEGER | Статус жалобы | +| `problem_id` | INTEGER | ID проблемы | +| `problem_date` | VARCHAR | Дата проблемы | +| `problem_text` | TEXT | Текст жалобы | +| `problem_price` | INTEGER | Стоимость решения рекламации | +| `problem_manager` | VARCHAR(120) | Менеджер по рекламации | +| `reshenie_id` | INTEGER | ID решения | +| `problem_comment` | TEXT | Комментарий к проблеме | +| `problem_comment_manager` | TEXT | Комментарий менеджера | +| `problem_reshenie_date` | VARCHAR | Дата решения проблемы | + +### Опоздание + +| Поле | Тип | Описание | +|------|-----|----------| +| `opozdanie_status` | INTEGER | Статус опоздания | +| `opozdanie_text` | TEXT | Текст об опоздании | +| `opozdanie_user_id` | INTEGER | ID пользователя (опоздание) | + +### Логистика + +| Поле | Тип | Описание | +|------|-----|----------| +| `status_logist` | INTEGER | Проверено логистом (0/1) | +| `status_logist_time` | VARCHAR | Время проверки логистом | + +### SMS-уведомления + +| Поле | Тип | Описание | +|------|-----|----------| +| `sms_foto` | INTEGER | SMS с фото отправлено | +| `sms_samovivoz` | INTEGER | SMS о самовывозе | +| `sms_predano` | INTEGER | SMS о передаче | +| `sms_dostavleno` | INTEGER | SMS о доставке | +| `sms_oplata` | INTEGER | SMS об оплате | +| `sms_status_order` | INTEGER | SMS о статусе заказа | + +### UTM-метки + +| Поле | Тип | Описание | +|------|-----|----------| +| `utm_source` | VARCHAR(255) | UTM Source | +| `utm_term` | VARCHAR(255) | UTM Term | +| `utm_medium` | VARCHAR(255) | UTM Medium | +| `utm_campaign` | VARCHAR(255) | UTM Campaign | +| `utm_content` | VARCHAR(250) | UTM Content | + +### Техническая информация + +| Поле | Тип | Описание | +|------|-----|----------| +| `is_mobile` | INTEGER | Заказ с мобильного (0/1) | +| `os` | VARCHAR(120) | Операционная система | +| `ip` | VARCHAR(35) | IP-адрес | +| `sid` | VARCHAR(36) | Session ID | +| `uber_time` | INTEGER | Время Uber | +| `manager_time` | INTEGER | Время менеджера | + +### Даты + +| Поле | Тип | Описание | +|------|-----|----------| +| `created_at` | VARCHAR | Дата создания | +| `update_at` | VARCHAR | Дата обновления | +| `updated_at` | VARCHAR | Дата обновления (альтернативное) | +| `closed_at` | VARCHAR | Дата закрытия | + +### Синхронизация с 1С + +| Поле | Тип | Описание | +|------|-----|----------| +| `1c_send` | INTEGER | Отправлено в 1С (0/1) | +| `check_id_arr` | TEXT | Массив ID чеков | +| `data_md5` | VARCHAR(50) | MD5-хеш данных | + +### Дополнительно + +| Поле | Тип | Описание | +|------|-----|----------| +| `tags_arr` | TEXT | Массив тегов (JSON) | +| `tags` | TEXT | Теги (строка) | +| `comment` | TEXT | Комментарий к заказу | +| `client_comment_foto` | VARCHAR(200) | Комментарий клиента к фото | +| `client_adress_zamena` | INTEGER | Замена адреса | +| `client_adress_zamena_cnt` | INTEGER | Количество замен адреса | +| `client_pravki_cnt` | INTEGER | Количество правок клиента | +| `client_foro_prosmotr` | INTEGER | Просмотр фото клиентом | + +--- + +## Константы статусов + +```php +public const STATUS_NAMES = [ + 142 => 'Успешно', + 143 => 'Отказ', +]; +``` + +--- + +## Связи (Relations) + +### `getSales()` + +Чеки продаж, связанные с заказом. + +```php +$sales = $order->sales; // Sales[] +``` + +**Тип:** hasMany +**Связанная модель:** `Sales` +**FK:** `order_id` → `id` +**Сортировка:** `date ASC` + +### `getStoreObj()` + +Магазин, обрабатывающий заказ. + +```php +$store = $order->storeObj; // CityStore +``` + +**Тип:** hasOne +**Связанная модель:** `CityStore` +**FK:** `id` → `store_id` + +### `getFloristObj()` + +Флорист, собирающий заказ. + +```php +$florist = $order->floristObj; // Admin +``` + +**Тип:** hasOne +**Связанная модель:** `Admin` +**FK:** `id` → `florist_id` + +### `getCourierObj()` + +Курьер, доставляющий заказ. + +```php +$courier = $order->courierObj; // Admin +``` + +**Тип:** hasOne +**Связанная модель:** `Admin` +**FK:** `id` → `courier_id` + +### `getStatus()` + +Статус заказа из справочника. + +```php +$status = $order->status; // OrdersStatus +``` + +**Тип:** hasOne +**Связанная модель:** `OrdersStatus` +**FK:** `status_id` → `status_id` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + orders_amo ||--o{ sales : "has_sales" + orders_amo }o--|| city_store : "store" + orders_amo }o--|| admin : "florist" + orders_amo }o--|| admin : "courier" + orders_amo }o--|| orders_status : "status" + orders_amo }o--|| city : "city" + + orders_amo { + int id PK + string nomer UK + int amo_id + int status_id FK + string name + int price + int store_id FK + int florist_id FK + int courier_id FK + string delivery_date + string delivery_time + int problem_status + int 1c_send + } + + sales { + string id PK + int order_id FK + string date + float summ + } + + city_store { + int id PK + string name + } + + admin { + int id PK + string name + int group_id + } +``` + +--- + +## Жизненный цикл заказа + +```mermaid +stateDiagram-v2 + [*] --> Создан: Новый заказ + Создан --> ПереданФлористу: Назначение флориста + ПереданФлористу --> Собран: Сборка букета + Собран --> ФотоОтправлено: Фото клиенту + ФотоОтправлено --> КурьерНазначен: Назначение курьера + КурьерНазначен --> Забран: Курьер забрал + Забран --> Доставлен: Доставка + Доставлен --> Успешно: Завершение + + Создан --> Отказ: Отмена + ПереданФлористу --> Отказ: Отмена + Собран --> Проблема: Рекламация + Доставлен --> Проблема: Рекламация + Проблема --> Решено: Урегулирование +``` + +--- + +## Примеры использования + +### Получение заказа с данными + +```php +$order = OrdersAmo::find() + ->where(['id' => $orderId]) + ->with(['storeObj', 'floristObj', 'courierObj', 'status']) + ->one(); +``` + +### Поиск заказов по дате доставки + +```php +$orders = OrdersAmo::find() + ->where(['delivery_date' => date('Y-m-d')]) + ->andWhere(['!=', 'status_id', 143]) // исключая отказы + ->orderBy(['delivery_time' => SORT_ASC]) + ->all(); +``` + +### Получение заказов флориста + +```php +$orders = OrdersAmo::find() + ->where(['florist_id' => $floristId]) + ->andWhere(['delivery_date' => date('Y-m-d')]) + ->andWhere(['florist_sobrano' => null]) + ->all(); +``` + +### Получение проблемных заказов + +```php +$problemOrders = OrdersAmo::find() + ->where(['>', 'problem_status', 0]) + ->andWhere(['problem_reshenie_date' => null]) + ->all(); +``` + +### Статистика по менеджеру + +```php +$stats = OrdersAmo::find() + ->select([ + 'manager_id', + 'COUNT(*) as total_orders', + 'SUM(price) as total_sum' + ]) + ->where(['>=', 'created_at', $startDate]) + ->groupBy('manager_id') + ->asArray() + ->all(); +``` + +### Проверка статуса заказа + +```php +$order = OrdersAmo::findOne($id); +$statusName = OrdersAmo::STATUS_NAMES[$order->status_id] ?? 'Неизвестно'; +``` + +--- + +## Валидация + +Модель имеет обширный набор правил валидации для 130+ полей. Ключевые правила: + +| Поле | Правило | +|------|---------| +| `nomer`, `created_at`, `amo_id`, `status_id`, `name`, `price` | Обязательные | +| `phone`, `phone_client`, `delivery_time` | Макс. 25 символов | +| `name`, `pay_text`, `utm_source` | Макс. 255 символов | +| `istochnik`, `city`, `courier_gps` | Макс. 100 символов | +| `price`, `bonus`, `status_id`, `florist_id`, `courier_id` | Целое число | + +--- + +## Связанные модели + +- **[Sales](./SalesProducts.md)** — чеки продаж +- **[CityStore](./CityStore.md)** — магазины +- **[Admin](./Admin.md)** — сотрудники (флористы, курьеры) +- **OrdersStatus** — статусы заказов +- **City** — города + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/OrdersAmoSearch.md b/erp24/docs/models/OrdersAmoSearch.md new file mode 100644 index 00000000..ab874e21 --- /dev/null +++ b/erp24/docs/models/OrdersAmoSearch.md @@ -0,0 +1,209 @@ +# Класс: OrdersAmoSearch + + +## Mindmap + +```mermaid +mindmap + root((OrdersAmoSearch)) + Таблица БД + ActiveRecord + Наследование + extends OrdersAmo +``` + +## Назначение +Search-модель для поиска заказов из amoCRM в ERP24. Специализированная модель с методом searchDelivery() для поиска заказов с доставкой до текущей даты, eager loading связей и сортировкой по дате доставки. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`OrdersAmo` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. Содержит ~100 атрибутов, соответствующих структуре заказа amoCRM. + +**Возвращает:** `array` — массив правил + +**Основные группы атрибутов:** + +**Integer поля (~50):** +- Идентификаторы: id, created_id, amo_id, pipeline_id, status_id, client_id, contact_id +- Связи: courier_id, florist_id, manager_id, employee_id +- Цены: price, bonus, price_dostavka, price_dostavka_zatrat +- Статусы: status_logist, problem_status, opozdanie_status +- Флаги: client_anonim, florist_sborka_night, florist_soglas, is_mobile, amo_sync, 1c_send + +**Safe поля (~50):** +- Даты: created_at, update_at, delivery_date, closed_at +- Текст: nomer, name, order_text, comment, description +- Клиент: phone, phone_client, email, client +- Доставка: delivery, delivery_time, delivery_adress +- UTM: utm_source, utm_term, utm_medium, utm_campaign, utm_content +- JSON: products_json, tags_arr, check_id_arr + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### searchDelivery($params): ActiveDataProvider +**Описание:** Поиск заказов с доставкой до текущей даты. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос OrdersAmo::find() +2. Подключает eager loading: sales, storeObj, courierObj, floristObj, status +3. Устанавливает сортировку по умолчанию: delivery_date DESC, update_at DESC +4. Загружает параметры +5. Применяет условие: delivery_date <= текущая дата +6. Возвращает ActiveDataProvider + +## Диаграмма структуры заказа + +```mermaid +erDiagram + OrdersAmo { + int id PK + varchar nomer + int amo_id + int pipeline_id + int status_id FK + int price + int bonus + int client_id FK + int contact_id FK + int courier_id FK + int florist_id FK + int manager_id FK + int store_id FK + datetime delivery_date + varchar delivery_adress + int status_logist + int problem_status + } + + Sales { + int id PK + int order_id FK + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + AmoStatus { + int id PK + varchar name + } + + OrdersAmo ||--o{ Sales : "sales" + OrdersAmo }o--|| CityStore : "storeObj" + OrdersAmo }o--o| Admin : "courierObj" + OrdersAmo }o--o| Admin : "floristObj" + OrdersAmo }o--|| AmoStatus : "status" +``` + +## Диаграмма потока доставки + +```mermaid +flowchart TD + A[searchDelivery] --> B[OrdersAmo::find] + B --> C[Eager loading] + C --> D[sales] + C --> E[storeObj] + C --> F[courierObj] + C --> G[floristObj] + C --> H[status] + + B --> I[Сортировка] + I --> J[delivery_date DESC] + I --> K[update_at DESC] + + B --> L[Фильтр] + L --> M[delivery_date <= NOW] +``` + +## Примеры использования + +### Поиск заказов на доставку +```php +public function actionDelivery() +{ + $searchModel = new OrdersAmoSearch(); + $dataProvider = $searchModel->searchDelivery(Yii::$app->request->queryParams); + + return $this->render('delivery', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Фильтрация по статусу +```php +$searchModel = new OrdersAmoSearch(); +$searchModel->status_id = 5; // Конкретный статус +$dataProvider = $searchModel->searchDelivery([]); +``` + +### GridView для доставки +```php + $dataProvider, + 'columns' => [ + 'nomer', + 'delivery_date:datetime', + 'delivery_adress', + [ + 'attribute' => 'courier_id', + 'value' => 'courierObj.name', + ], + [ + 'attribute' => 'florist_id', + 'value' => 'floristObj.name', + ], + [ + 'attribute' => 'store_id', + 'value' => 'storeObj.name', + ], + [ + 'attribute' => 'status_id', + 'value' => 'status.name', + ], + 'price', + ], +]) ?> +``` + +## Связанные модели + +- [OrdersAmo](./OrdersAmo.md) — базовая модель заказов amoCRM +- [Sales](./Sales.md) — продажи +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы (курьер, флорист, менеджер) +- [AmoStatus](./AmoStatus.md) — статусы amoCRM + +## Особенности реализации + +1. **Специализированный метод**: searchDelivery() вместо стандартного search() +2. **Eager loading**: 5 связей загружаются заранее для оптимизации +3. **Сортировка по умолчанию**: delivery_date DESC, update_at DESC +4. **Фильтр по дате**: Только заказы с delivery_date <= сейчас +5. **Интеграция amoCRM**: amo_id, pipeline_id, status_id для синхронизации +6. **UTM метки**: Полный набор utm_* для аналитики +7. **Логистика**: status_logist, problem_status для отслеживания доставки +8. **Массив атрибутов**: ~100 полей для полного описания заказа diff --git a/erp24/docs/models/OrdersStatus.md b/erp24/docs/models/OrdersStatus.md new file mode 100644 index 00000000..a277257f --- /dev/null +++ b/erp24/docs/models/OrdersStatus.md @@ -0,0 +1,471 @@ +# Модель OrdersStatus + + +## Mindmap + +```mermaid +mindmap + root((OrdersStatus)) + Таблица БД + orders_status + Свойства + id + int + client_id + int + name + string + name_public + string + status_id + int + pipeline_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `OrdersStatus` представляет справочник статусов заказов из AmoCRM. Хранит информацию о статусах сделок для разных воронок продаж, включая названия, цвета, типы и позиции в воронке. Используется для синхронизации статусов между ERP24 и AmoCRM. + +**Файл модели:** `erp24/records/OrdersStatus.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `orders_status` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (обязательное) | +| `status_id` | INTEGER | ID статуса в AmoCRM (обязательное) | +| `pipeline_id` | INTEGER | ID воронки продаж в AmoCRM (обязательное) | +| `account_id` | INTEGER | ID аккаунта AmoCRM (обязательное) | +| `client_id` | INTEGER | ID клиента в системе (обязательное) | + +### Названия статуса + +| Поле | Тип | Описание | +|------|-----|----------| +| `name` | VARCHAR(255) | Название статуса для внутреннего использования (обязательное) | +| `name_public` | VARCHAR(255) | Публичное название статуса для клиентов (обязательное) | + +### Настройки отображения + +| Поле | Тип | Описание | +|------|-----|----------| +| `color` | VARCHAR(25) | Цвет статуса в формате HEX (#99ccff) (обязательное) | +| `posit` | INTEGER | Позиция статуса в воронке (порядок) (обязательное) | + +### Классификация + +| Поле | Тип | Описание | +|------|-----|----------| +| `tip` | VARCHAR(20) | Тип статуса: рабочий, успешный, отказ (обязательное) | +| `type` | VARCHAR(20) | Категория статуса (обязательное) | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'client_id', 'name', 'name_public', 'status_id', 'pipeline_id', + 'account_id', 'tip', 'color', 'type', 'posit' +], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `client_id`, `status_id`, `pipeline_id`, `account_id`, `posit` | Целочисленные значения | +| `string`, max=255 | `name`, `name_public` | Длинные названия | +| `string`, max=20 | `tip`, `type` | Короткие классификаторы | +| `string`, max=25 | `color` | Код цвета | + +--- + +## Типы статусов (tip) + +Поле `tip` определяет категорию статуса в воронке продаж: + +| Значение | Описание | Примеры статусов | +|----------|----------|------------------| +| `work` | Рабочий статус | Новый, В работе, Ожидание оплаты, На сборке | +| `success` | Успешное завершение | Успешно реализовано, Доставлено, Оплачено | +| `fail` | Отказ от сделки | Отказ клиента, Недозвон, Некачественный лид | + +--- + +## Типы категорий (type) + +Поле `type` определяет более детальную категорию статуса: + +| Значение | Описание | +|----------|----------| +| `new` | Новая заявка | +| `processing` | Обрабатывается | +| `payment` | Ожидание оплаты | +| `production` | Производство/сборка | +| `delivery` | Доставка | +| `completed` | Завершено успешно | +| `cancelled` | Отменено | + +--- + +## Форматы цвета + +Поле `color` хранит цвет в формате HEX: + +```php +'#99ccff' // Голубой +'#ffcc66' // Оранжевый +'#66cc99' // Зеленый +'#ff6666' // Красный +'#cccccc' // Серый +``` + +--- + +## Примеры использования + +### Получение всех статусов для воронки + +```php +$statuses = OrdersStatus::find() + ->where(['pipeline_id' => $pipelineId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statuses as $status) { + echo "Статус: {$status->name} (позиция: {$status->posit})\n"; +} +``` + +### Получение информации о статусе по ID из AmoCRM + +```php +$status = OrdersStatus::findOne([ + 'status_id' => $amoStatusId, + 'pipeline_id' => $pipelineId +]); + +if ($status) { + echo "Название: {$status->name}\n"; + echo "Тип: {$status->tip}\n"; + echo "Цвет: {$status->color}\n"; +} +``` + +### Получение только рабочих статусов + +```php +$workStatuses = OrdersStatus::find() + ->where([ + 'pipeline_id' => $pipelineId, + 'tip' => 'work' + ]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +echo "Рабочие статусы:\n"; +foreach ($workStatuses as $status) { + echo "- {$status->name}\n"; +} +``` + +### Получение успешных и провальных статусов + +```php +// Успешные статусы +$successStatuses = OrdersStatus::find() + ->where([ + 'pipeline_id' => $pipelineId, + 'tip' => 'success' + ]) + ->all(); + +// Статусы отказа +$failStatuses = OrdersStatus::find() + ->where([ + 'pipeline_id' => $pipelineId, + 'tip' => 'fail' + ]) + ->all(); +``` + +### Создание нового статуса при синхронизации с AmoCRM + +```php +$status = new OrdersStatus(); +$status->client_id = 1; +$status->status_id = 142; // ID из AmoCRM +$status->pipeline_id = 123; // ID воронки из AmoCRM +$status->account_id = 456; // ID аккаунта из AmoCRM +$status->name = 'Успешно реализовано'; +$status->name_public = 'Заказ выполнен'; +$status->tip = 'success'; +$status->type = 'completed'; +$status->color = '#66cc99'; +$status->posit = 99; + +if ($status->save()) { + echo "Статус синхронизирован с AmoCRM"; +} +``` + +### Обновление статуса из AmoCRM + +```php +$status = OrdersStatus::findOne([ + 'status_id' => $amoStatusId, + 'pipeline_id' => $pipelineId +]); + +if ($status) { + $status->name = $amoStatusData['name']; + $status->color = $amoStatusData['color']; + $status->posit = $amoStatusData['sort']; + $status->save(); +} else { + // Создать новый статус + $status = new OrdersStatus(); + // ... заполнение полей + $status->save(); +} +``` + +### Получение списка статусов для выпадающего списка + +```php +$statusList = OrdersStatus::find() + ->select(['status_id', 'name']) + ->where(['pipeline_id' => $pipelineId]) + ->orderBy(['posit' => SORT_ASC]) + ->indexBy('status_id') + ->column(); + +// Результат: [142 => 'Успешно реализовано', 143 => 'Отказ', ...] +``` + +### Проверка типа статуса заказа + +```php +$order = OrdersAmo::findOne($orderId); + +$status = OrdersStatus::findOne([ + 'status_id' => $order->status_id, + 'pipeline_id' => $order->pipeline_id +]); + +if ($status) { + switch ($status->tip) { + case 'success': + echo "Заказ успешно завершен"; + break; + case 'fail': + echo "Заказ отменен"; + break; + case 'work': + echo "Заказ в работе"; + break; + } +} +``` + +### Получение статусов по позиции + +```php +// Первый статус в воронке (обычно "Новый") +$firstStatus = OrdersStatus::find() + ->where(['pipeline_id' => $pipelineId]) + ->orderBy(['posit' => SORT_ASC]) + ->one(); + +// Последний рабочий статус +$lastWorkStatus = OrdersStatus::find() + ->where([ + 'pipeline_id' => $pipelineId, + 'tip' => 'work' + ]) + ->orderBy(['posit' => SORT_DESC]) + ->one(); +``` + +### Группировка статусов по типу + +```php +$statusesByTip = OrdersStatus::find() + ->select(['tip', 'COUNT(*) as count']) + ->where(['pipeline_id' => $pipelineId]) + ->groupBy('tip') + ->asArray() + ->all(); + +foreach ($statusesByTip as $row) { + echo "Тип '{$row['tip']}': {$row['count']} статусов\n"; +} +``` + +### Статистика заказов по статусам + +```php +$orderStats = Yii::$app->db->createCommand(" + SELECT + os.name, + os.color, + os.tip, + COUNT(oa.id) as order_count, + SUM(oa.price) as total_amount + FROM orders_amo oa + JOIN orders_status os + ON oa.status_id = os.status_id + AND oa.pipeline_id = os.pipeline_id + WHERE oa.created_at >= :start_date + GROUP BY os.id, os.name, os.color, os.tip + ORDER BY os.posit +", [':start_date' => $startDate])->queryAll(); +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **OrdersAmo** - заказы из AmoCRM (через `status_id` и `pipeline_id`) +- **Client** - клиенты системы (через `client_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + orders_status ||--o{ orders_amo : "has_orders" + orders_status }o--|| client : "belongs_to" + + orders_status { + int id PK + int client_id FK + string name + string name_public + int status_id UK + int pipeline_id UK + int account_id + string tip + string color + string type + int posit + } + + orders_amo { + int id PK + int amo_id + int status_id FK + int pipeline_id FK + string name + int price + string delivery_date + } + + client { + int id PK + string name + } +``` + +--- + +## Визуализация воронки продаж + +```mermaid +stateDiagram-v2 + direction LR + + [*] --> Новый: tip=work, posit=1 + Новый --> ВРаботе: tip=work, posit=2 + ВРаботе --> ОжиданиеОплаты: tip=work, posit=3 + ОжиданиеОплаты --> НаСборке: tip=work, posit=4 + НаСборке --> Доставка: tip=work, posit=5 + Доставка --> Успешно: tip=success, posit=99 + + Новый --> Отказ: tip=fail + ВРаботе --> Отказ: tip=fail + ОжиданиеОплаты --> Отказ: tip=fail + + Успешно --> [*] + Отказ --> [*] +``` + +--- + +## Цветовая схема статусов + +```mermaid +pie title Распределение статусов по типам + "Рабочие (work)" : 60 + "Успешные (success)" : 30 + "Отказы (fail)" : 10 +``` + +--- + +## Процесс синхронизации статусов с AmoCRM + +```mermaid +flowchart TD + A[Начало синхронизации] --> B[Получить статусы из AmoCRM API] + B --> C[Для каждого статуса] + C --> D{Статус существует в БД?} + D -->|Да| E[Обновить данные статуса] + D -->|Нет| F[Создать новый статус] + E --> G[Сохранить изменения] + F --> G + G --> H{Есть еще статусы?} + H -->|Да| C + H -->|Нет| I[Удалить неактуальные статусы] + I --> J[Логировать результаты] + J --> K[Конец] +``` + +--- + +## Примеры типовых наборов статусов + +### Воронка продаж цветочного магазина + +```php +$flowershopStatuses = [ + ['name' => 'Новая заявка', 'tip' => 'work', 'posit' => 1, 'color' => '#99ccff'], + ['name' => 'Согласование букета', 'tip' => 'work', 'posit' => 2, 'color' => '#ffcc99'], + ['name' => 'Ожидание оплаты', 'tip' => 'work', 'posit' => 3, 'color' => '#ffff99'], + ['name' => 'Флорист собирает', 'tip' => 'work', 'posit' => 4, 'color' => '#ccffcc'], + ['name' => 'Отправка фото', 'tip' => 'work', 'posit' => 5, 'color' => '#ccffff'], + ['name' => 'Назначен курьер', 'tip' => 'work', 'posit' => 6, 'color' => '#ccccff'], + ['name' => 'В пути', 'tip' => 'work', 'posit' => 7, 'color' => '#ffccff'], + ['name' => 'Доставлено', 'tip' => 'success', 'posit' => 98, 'color' => '#66cc99'], + ['name' => 'Успешно', 'tip' => 'success', 'posit' => 99, 'color' => '#66cc66'], + ['name' => 'Отказ', 'tip' => 'fail', 'posit' => 100, 'color' => '#ff6666'], +]; +``` + +--- + +## Связанные модели + +- **[OrdersAmo](./OrdersAmo.md)** - заказы из AmoCRM +- **[OrdersUnion](./OrdersUnion.md)** - объединение заказов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/OrdersUnion.md b/erp24/docs/models/OrdersUnion.md new file mode 100644 index 00000000..72d2f539 --- /dev/null +++ b/erp24/docs/models/OrdersUnion.md @@ -0,0 +1,561 @@ +# Модель OrdersUnion + + +## Mindmap + +```mermaid +mindmap + root((OrdersUnion)) + Таблица БД + ActiveRecord + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `OrdersUnion` предоставляет единый интерфейс для работы с заказами из разных источников: AmoCRM, Яндекс.Маркет и Flowwow. Использует UNION-запросы для объединения данных из таблиц `orders_amo` и `marketplace_orders` в единую структуру. Применяется для отображения всех заказов в едином списке с общей фильтрацией и сортировкой. + +**Файл модели:** `erp24/records/OrdersUnion.php` +**Namespace:** `app\records` +**Таблица БД:** Виртуальная (UNION запрос) +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Особенности модели + +### Виртуальная структура +- Модель не связана напрямую с одной таблицей БД +- Использует UNION-запросы для объединения данных +- Данные только для чтения (read-only) +- Не поддерживает сохранение и изменение через ActiveRecord + +### Источники данных +1. **orders_amo** - заказы из AmoCRM +2. **marketplace_orders** - заказы с маркетплейсов (Яндекс.Маркет, Flowwow) + +--- + +## Константы + +### Типы источников заказов + +```php +const AMO = 'amo'; +const MARKETPLACE = 'marketplace'; +``` + +### Названия источников + +```php +const SOURCES = [ + 'amo' => 'AMO', + 'yandex' => 'ЯндексМаркет', + 'flowwow' => 'Flowwow', +]; +``` + +--- + +## Свойства модели + +### Основные поля + +| Поле | Тип | Описание | +|------|-----|----------| +| `source` | STRING | Источник заказа: 'amo', 'yandex', 'flowwow' | +| `id` | INTEGER | ID заказа в исходной таблице | +| `source_id` | STRING | Уникальный ID заказа в источнике (для AMO - это id, для MP - marketplace_order_id) | +| `delivery_date` | TIMESTAMP | Дата доставки | +| `status_id` | INTEGER | ID статуса заказа | +| `store_id` | INTEGER | ID магазина | +| `payment_type_id` | STRING | Тип оплаты (для AMO) или payment_method (для MP) | +| `price` | INTEGER | Стоимость заказа (price для AMO, total для MP) | +| `delivery_address` | TEXT | Адрес доставки | +| `products_json` | JSONB | JSON массив с информацией о товарах | + +### Дополнительные свойства + +```php +public $delivery_date; // Используется для фильтрации +public $source_id; // Внешний ID заказа +public $delivery_address; // Адрес доставки +``` + +--- + +## Методы + +### `primaryKey()` + +Определяет первичный ключ модели. + +```php +public static function primaryKey() +{ + return ['id']; +} +``` + +**Возвращает:** `array` - массив с полем `id` + +**Назначение:** Необходим для работы ActiveRecord, но модель не поддерживает обновление/удаление по ключу. + +--- + +### `getOrders($params = [])` + +Основной метод для получения объединенных заказов из всех источников. + +```php +public static function getOrders($params = []) +``` + +**Параметры:** +- `$params` (array) - массив фильтров: + - `source` (string) - фильтр по источнику ('amo', 'yandex', 'flowwow') + - `store_id` (int) - фильтр по магазину + - `delivery_date` (string) - фильтр по дате доставки (формат: Y-m-d) + +**Возвращает:** `yii\data\ArrayDataProvider` - провайдер данных с пагинацией и сортировкой + +**Описание работы:** + +1. **Формирование подзапроса для AMO:** + - Выбирает заказы из `orders_amo` + - Добавляет константу `source = 'amo'` + - Преобразует `store_id` из VARCHAR в INTEGER + - Формирует `products_json` с названиями букетов + - Использует `source_id` = преобразованный `id` в VARCHAR + +2. **Формирование подзапроса для маркетплейсов:** + - Выбирает заказы из `marketplace_orders` + - JOIN с `marketplace_order_delivery` для адреса и даты + - JOIN с `marketplace_store` для определения типа маркетплейса + - Определяет `source` на основе `warehouse_id`: + - `warehouse_id = 2` → 'yandex' + - `warehouse_id = 1` → 'flowwow' + - Формирует адрес из полей: country, city, street, house, apartment + - Формирует `products_json` с названиями товаров из `products_1c` + +3. **Объединение через UNION:** + - Объединяет результаты двух подзапросов + - Применяет фильтры из `$params` + - Добавляет сортировку по `id DESC` + +4. **Фильтрация по дате:** + - Если указан `delivery_date`, создает диапазон от 00:00:00 до 23:59:59 + - Использует BETWEEN для точного попадания в день + +5. **Возврат результата:** + - Оборачивает в `ArrayDataProvider` + - Настраивает пагинацию (20 записей на страницу) + - Настраивает сортировку по полям: id, delivery_date, store_id, price + +**Вызовы сторонних методов:** +- `yii\db\Query::select()` - формирование SELECT части запроса +- `yii\db\Query::from()` - указание таблицы источника +- `yii\db\Query::leftJoin()` - добавление LEFT JOIN +- `yii\db\Query::union()` - объединение запросов через UNION +- `yii\db\Query::andFilterWhere()` - добавление фильтров +- `yii\db\Query::orderBy()` - сортировка результатов +- `yii\db\Query::all()` - выполнение запроса и получение всех результатов +- `yii\data\ArrayDataProvider` - создание провайдера данных + +**Пример использования:** +```php +// Все заказы без фильтров +$dataProvider = OrdersUnion::getOrders(); + +// Заказы из AMO +$amoOrders = OrdersUnion::getOrders(['source' => 'amo']); + +// Заказы конкретного магазина на конкретную дату +$ordersFiltered = OrdersUnion::getOrders([ + 'store_id' => 5, + 'delivery_date' => '2025-12-11' +]); + +// Заказы с Яндекс.Маркета +$yandexOrders = OrdersUnion::getOrders(['source' => 'yandex']); +``` + +--- + +### `attributes()` + +Определяет дополнительные атрибуты модели. + +```php +public function attributes() +{ + return array_merge(parent::attributes(), ['delivery_date']); +} +``` + +**Возвращает:** `array` - массив названий атрибутов + +**Назначение:** Добавляет виртуальное поле `delivery_date` к атрибутам модели для использования в фильтрах и сортировке. + +**Вызовы сторонних методов:** +- `parent::attributes()` - получение базовых атрибутов от ActiveRecord +- `array_merge()` - объединение массивов атрибутов + +--- + +## Структура products_json + +### Для заказов AMO + +```json +[ + { + "name": "Букет 51 роза микс" + }, + { + "name": "Букет 101 красная роза" + } +] +``` + +Извлекается из поля `products_json` таблицы `orders_amo`. + +### Для заказов маркетплейсов + +```json +[ + { + "name": "Роза кустовая розовая" + }, + { + "name": "Хризантема белая" + } +] +``` + +Формируется через JOIN с таблицами: +- `marketplace_order_items` - позиции заказа +- `products_1c` - названия товаров по артикулу + +--- + +## Примеры использования + +### Получение всех заказов + +```php +$dataProvider = OrdersUnion::getOrders(); + +// В контроллере +return $this->render('index', [ + 'dataProvider' => $dataProvider, +]); + +// В представлении +echo GridView::widget([ + 'dataProvider' => $dataProvider, + 'columns' => [ + 'id', + 'source', + 'source_id', + 'delivery_date', + 'store_id', + 'price', + [ + 'attribute' => 'products_json', + 'format' => 'raw', + 'value' => function($model) { + $products = json_decode($model['products_json'], true); + return implode(', ', array_column($products, 'name')); + } + ], + ], +]); +``` + +### Фильтрация по источнику + +```php +// Только заказы из AMO +$amoProvider = OrdersUnion::getOrders([ + 'source' => OrdersUnion::AMO +]); + +// Только с Яндекс.Маркета +$yandexProvider = OrdersUnion::getOrders([ + 'source' => 'yandex' +]); + +// Только с Flowwow +$flowwowProvider = OrdersUnion::getOrders([ + 'source' => 'flowwow' +]); +``` + +### Фильтрация по магазину и дате + +```php +$todayOrders = OrdersUnion::getOrders([ + 'store_id' => 5, + 'delivery_date' => date('Y-m-d') +]); + +foreach ($todayOrders->getModels() as $order) { + echo "Заказ #{$order['id']} из {$order['source']}, "; + echo "доставка: {$order['delivery_address']}\n"; +} +``` + +### Получение статистики по источникам + +```php +$allOrders = OrdersUnion::getOrders([ + 'delivery_date' => date('Y-m-d') +]); + +$stats = []; +foreach ($allOrders->getModels() as $order) { + $source = $order['source']; + if (!isset($stats[$source])) { + $stats[$source] = [ + 'count' => 0, + 'total' => 0 + ]; + } + $stats[$source]['count']++; + $stats[$source]['total'] += $order['price']; +} + +foreach ($stats as $source => $data) { + $sourceName = OrdersUnion::SOURCES[$source] ?? $source; + echo "{$sourceName}: {$data['count']} заказов на сумму {$data['total']} руб.\n"; +} +``` + +### Работа с товарами из JSON + +```php +$orders = OrdersUnion::getOrders()->getModels(); + +foreach ($orders as $order) { + $products = json_decode($order['products_json'], true); + + echo "Заказ #{$order['id']} ({$order['source']}):\n"; + if (is_array($products) && !empty($products)) { + foreach ($products as $product) { + echo " - {$product['name']}\n"; + } + } else { + echo " Нет товаров\n"; + } +} +``` + +### Поиск заказа по внешнему ID + +```php +// Все заказы с определенным source_id +$allOrders = OrdersUnion::getOrders()->getModels(); +$targetSourceId = 'MP-12345'; + +$foundOrder = null; +foreach ($allOrders as $order) { + if ($order['source_id'] == $targetSourceId) { + $foundOrder = $order; + break; + } +} + +if ($foundOrder) { + echo "Найден заказ: ID={$foundOrder['id']}, "; + echo "Источник={$foundOrder['source']}\n"; +} +``` + +### Экспорт объединенных заказов в CSV + +```php +$orders = OrdersUnion::getOrders([ + 'delivery_date' => '2025-12-11' +])->getModels(); + +$csv = fopen('orders_union_export.csv', 'w'); +fputcsv($csv, ['ID', 'Источник', 'Внешний ID', 'Дата доставки', 'Магазин', 'Сумма', 'Адрес', 'Товары']); + +foreach ($orders as $order) { + $products = json_decode($order['products_json'], true); + $productNames = implode('; ', array_column($products, 'name')); + + fputcsv($csv, [ + $order['id'], + OrdersUnion::SOURCES[$order['source']] ?? $order['source'], + $order['source_id'], + $order['delivery_date'], + $order['store_id'], + $order['price'], + $order['delivery_address'], + $productNames + ]); +} + +fclose($csv); +``` + +--- + +## Структура SQL-запроса + +### Упрощенная схема UNION-запроса + +```sql +-- Подзапрос 1: Заказы AMO +SELECT + 'amo' AS source, + id, + CAST(id AS VARCHAR(64)) AS source_id, + delivery_date, + status_id, + NULLIF(store_id, '')::INTEGER AS store_id, + payment_type_id, + price, + delivery_adress as delivery_address, + products_json -- извлечение name из JSON +FROM orders_amo + +UNION ALL + +-- Подзапрос 2: Маркетплейсы +SELECT + CASE + WHEN ms.warehouse_id = 2 THEN 'yandex' + WHEN ms.warehouse_id = 1 THEN 'flowwow' + ELSE 'Marketplace' + END AS source, + mo.id, + mo.marketplace_order_id as source_id, + mod.delivery_end AS delivery_date, + mo.status_id, + mo.store_id, + payment_method, + mo.total, + CONCAT(country, ' ', city, ' ', street, ' ', house, ' ', apartment) AS delivery_address, + products_json -- агрегация из products_1c +FROM marketplace_orders mo +LEFT JOIN marketplace_order_delivery mod ON mo.id = mod.order_id +LEFT JOIN marketplace_store ms ON ms.warehouse_guid::TEXT = mo.warehouse_guid::TEXT + +ORDER BY id DESC +``` + +--- + +## Диаграмма источников данных + +```mermaid +erDiagram + OrdersUnion ||--o{ orders_amo : "UNION from" + OrdersUnion ||--o{ marketplace_orders : "UNION from" + marketplace_orders ||--|| marketplace_order_delivery : "delivery info" + marketplace_orders ||--|| marketplace_store : "store mapping" + marketplace_orders ||--o{ marketplace_order_items : "items" + marketplace_order_items }o--|| products_1c : "product info" + + OrdersUnion { + string source + int id + string source_id + timestamp delivery_date + int status_id + int store_id + string payment_type_id + int price + text delivery_address + jsonb products_json + } + + orders_amo { + int id PK + string nomer + int status_id + string delivery_date + int store_id + string payment_type_id + int price + text delivery_adress + jsonb products_json + } + + marketplace_orders { + int id PK + string marketplace_order_id + int status_id + int store_id + string warehouse_guid + string payment_method + float total + } +``` + +--- + +## Процесс формирования объединенного списка + +```mermaid +flowchart TD + A[Начало: getOrders] --> B[Создать подзапрос AMO] + B --> C[Создать подзапрос Маркетплейсы] + C --> D[Объединить через UNION] + D --> E{Есть фильтр source?} + E -->|Да| F[Применить фильтр по source] + E -->|Нет| G{Есть фильтр store_id?} + F --> G + G -->|Да| H[Применить фильтр по store_id] + G -->|Нет| I{Есть фильтр delivery_date?} + H --> I + I -->|Да| J[Применить фильтр BETWEEN по дате] + I -->|Нет| K[Добавить сортировку ORDER BY id DESC] + J --> K + K --> L[Выполнить запрос] + L --> M[Обернуть в ArrayDataProvider] + M --> N[Настроить пагинацию] + N --> O[Настроить сортировку] + O --> P[Вернуть DataProvider] + P --> Q[Конец] +``` + +--- + +## Преимущества использования OrdersUnion + +1. **Единый интерфейс** - работа со всеми заказами через один метод +2. **Общая фильтрация** - фильтры применяются ко всем источникам одинаково +3. **Единая пагинация** - все заказы отображаются в одном списке +4. **Упрощенная аналитика** - легко сравнивать заказы из разных источников +5. **Масштабируемость** - легко добавить новые источники заказов + +--- + +## Ограничения модели + +1. **Read-only** - модель не поддерживает save(), update(), delete() +2. **Виртуальная** - нет физической таблицы в БД +3. **Производительность** - UNION может быть медленным на больших объемах +4. **Отсутствие Relations** - нельзя использовать `with()` для eager loading +5. **Ограниченная фильтрация** - фильтры применяются только на уровне WHERE + +--- + +## Связанные модели + +- **[OrdersAmo](./OrdersAmo.md)** - заказы из AmoCRM +- **[MarketplaceOrders](./MarketplaceOrders.md)** - заказы маркетплейсов +- **[MarketplaceOrderDelivery](./MarketplaceOrderDelivery.md)** - доставка маркетплейсов +- **[MarketplaceStore](./MarketplaceStore.md)** - магазины маркетплейсов +- **[MarketplaceOrderItems](./MarketplaceOrderItems.md)** - товары маркетплейсов +- **[Products1c](./Products1c.md)** - товары 1С + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/OurCities.md b/erp24/docs/models/OurCities.md new file mode 100644 index 00000000..41f1b08e --- /dev/null +++ b/erp24/docs/models/OurCities.md @@ -0,0 +1,181 @@ +# Класс: OurCities + + +## Mindmap + +```mermaid +mindmap + root((OurCities)) + Таблица БД + our_cities + Свойства + id + int + city_name + string + citizens_quantity_type + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник городов присутствия компании в ERP24. Хранит информацию о городах, где работает сеть магазинов, с категоризацией по численности населения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`our_cities` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `city_name` | varchar(40) | Название города | +| `citizens_quantity_type` | int | Категория по численности населения | +| `guid` | varchar(36) / null | GUID города для синхронизации с 1С | + +## Категории городов по численности + +| Значение | Описание | Примерная численность | +|----------|----------|----------------------| +| `1` | Малый город | до 50 000 | +| `2` | Средний город | 50 000 - 250 000 | +| `3` | Большой город | 250 000 - 1 000 000 | +| `4` | Крупный город | более 1 000 000 | + +## Диаграмма связей + +```mermaid +erDiagram + OurCities { + int id PK + varchar city_name + int citizens_quantity_type + varchar guid + } + + Store { + int id PK + varchar city + } + + CityStore { + int id PK + int city_id FK + int store_id FK + } + + OurCities ||--o{ CityStore : "city_id" + Store ||--o{ CityStore : "store_id" +``` + +## Примеры использования + +### Создание города +```php +$city = new OurCities(); +$city->city_name = 'Казань'; +$city->citizens_quantity_type = 4; // Крупный город +$city->guid = '12345678-1234-1234-1234-123456789abc'; +$city->save(); +``` + +### Получение всех городов +```php +$cities = OurCities::find() + ->orderBy(['city_name' => SORT_ASC]) + ->all(); +``` + +### Получение городов по категории +```php +// Крупные города (миллионники) +$largeCities = OurCities::find() + ->where(['citizens_quantity_type' => 4]) + ->all(); +``` + +### Формирование списка для выбора +```php +$citiesList = ArrayHelper::map( + OurCities::find()->orderBy(['city_name' => SORT_ASC])->all(), + 'id', + 'city_name' +); + +echo Html::dropDownList('city_id', null, $citiesList); +``` + +### Поиск города по названию +```php +$city = OurCities::find() + ->where(['city_name' => 'Москва']) + ->one(); +``` + +### Группировка городов по категориям +```php +$categories = [ + 1 => 'Малые города', + 2 => 'Средние города', + 3 => 'Большие города', + 4 => 'Крупные города' +]; + +$cities = OurCities::find() + ->orderBy(['citizens_quantity_type' => SORT_ASC, 'city_name' => SORT_ASC]) + ->all(); + +$grouped = []; +foreach ($cities as $city) { + $categoryName = $categories[$city->citizens_quantity_type] ?? 'Другое'; + $grouped[$categoryName][] = $city->city_name; +} +``` + +### Статистика по категориям +```php +$stats = OurCities::find() + ->select(['citizens_quantity_type', 'COUNT(*) as count']) + ->groupBy('citizens_quantity_type') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $category = $categories[$stat['citizens_quantity_type']] ?? 'Unknown'; + echo "{$category}: {$stat['count']} городов\n"; +} +``` + +### Поиск по GUID +```php +$city = OurCities::find() + ->where(['guid' => $guid1c]) + ->one(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `city_name` | required, string (max 40) | +| `citizens_quantity_type` | required, integer | +| `guid` | string (max 36), default: null | + +## Связанные модели + +- [CityStore](./CityStore.md) — связь города с магазинами +- [Store](./Store.md) — магазины + +## Особенности реализации + +1. **Категоризация**: Города классифицируются по численности населения +2. **Синхронизация с 1С**: Поле guid для интеграции с внешними системами +3. **Простой справочник**: Минимальная структура для базовой функциональности +4. **Nullable GUID**: guid может быть null для городов без привязки к 1С diff --git a/erp24/docs/models/PageStatistics.md b/erp24/docs/models/PageStatistics.md new file mode 100644 index 00000000..11549315 --- /dev/null +++ b/erp24/docs/models/PageStatistics.md @@ -0,0 +1,208 @@ +# Класс: PageStatistics + + +## Mindmap + +```mermaid +mindmap + root((PageStatistics)) + Таблица БД + page_statistics + Свойства + id + int + admin_id + int + url + string + created_at + string + Связи + Admin + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель статистики посещений страниц в ERP24. Логирует все API-запросы пользователей с фиксацией URL, POST-параметров и времени для анализа активности и отладки. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`page_statistics` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `admin_id` | int | FK на пользователя (Admin) | +| `url` | varchar(255) | URL запроса | +| `post` | text / null | POST-параметры в JSON | +| `created_at` | datetime | Дата и время запроса | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getAdmin()` | hasOne | Admin | Пользователь | + +## Диаграмма связей + +```mermaid +erDiagram + PageStatistics { + int id PK + int admin_id FK + varchar url + text post + datetime created_at + } + + Admin { + int id PK + varchar username + varchar name + } + + Admin ||--o{ PageStatistics : "admin_id" +``` + +## Диаграмма потока логирования + +```mermaid +flowchart TD + A[HTTP запрос] --> B{Авторизован?} + B -->|Да| C[Получение admin_id] + B -->|Нет| D[Пропуск логирования] + C --> E[Создание PageStatistics] + E --> F[Сохранение URL] + F --> G{POST данные?} + G -->|Да| H[Сохранение POST как JSON] + G -->|Нет| I[post = null] + H --> J[Сохранение в БД] + I --> J + J --> K[Продолжение обработки запроса] +``` + +## Примеры использования + +### Логирование запроса +```php +$stat = new PageStatistics(); +$stat->admin_id = Yii::$app->user->id; +$stat->url = Yii::$app->request->url; +$stat->post = !empty($_POST) ? json_encode($_POST) : null; +$stat->created_at = date('Y-m-d H:i:s'); +$stat->save(); +``` + +### Получение активности пользователя +```php +$userActivity = PageStatistics::find() + ->where(['admin_id' => $userId]) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(100) + ->all(); + +foreach ($userActivity as $log) { + echo "{$log->created_at}: {$log->url}\n"; +} +``` + +### Статистика по страницам +```php +$pageStats = PageStatistics::find() + ->select(['url', 'COUNT(*) as visits']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('url') + ->orderBy(['visits' => SORT_DESC]) + ->limit(20) + ->asArray() + ->all(); + +foreach ($pageStats as $stat) { + echo "{$stat['url']}: {$stat['visits']} посещений\n"; +} +``` + +### Анализ активности за день +```php +$dailyActivity = PageStatistics::find() + ->where(['>=', 'created_at', date('Y-m-d 00:00:00')]) + ->andWhere(['<=', 'created_at', date('Y-m-d 23:59:59')]) + ->with(['admin']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Поиск по POST-параметрам +```php +$logsWithOrderId = PageStatistics::find() + ->where(['like', 'post', '"order_id"']) + ->all(); +``` + +### Статистика по пользователям +```php +$userStats = PageStatistics::find() + ->select(['admin_id', 'COUNT(*) as requests']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('admin_id') + ->orderBy(['requests' => SORT_DESC]) + ->asArray() + ->all(); + +$admins = ArrayHelper::index(Admin::find()->all(), 'id'); + +foreach ($userStats as $stat) { + $adminName = $admins[$stat['admin_id']]->name ?? 'Unknown'; + echo "{$adminName}: {$stat['requests']} запросов\n"; +} +``` + +### Очистка старых логов +```php +PageStatistics::deleteAll([ + '<', 'created_at', date('Y-m-d', strtotime('-30 days')) +]); +``` + +### Последний визит пользователя +```php +$lastVisit = PageStatistics::find() + ->where(['admin_id' => $userId]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastVisit) { + echo "Последний визит: {$lastVisit->created_at}"; + echo "Страница: {$lastVisit->url}"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `admin_id` | required, integer | +| `url` | required, string (max 255) | +| `created_at` | required, safe | +| `post` | string | + +## Связанные модели + +- [Admin](./Admin.md) — пользователи системы + +## Особенности реализации + +1. **Полное логирование**: Записываются все URL и POST-данные +2. **Привязка к пользователю**: Каждый запрос связан с admin_id +3. **JSON хранение POST**: Структурированное хранение POST-параметров +4. **Аналитика**: Возможность анализа популярности страниц и активности пользователей +5. **Отладка**: Помогает отслеживать действия пользователей при разборе инцидентов diff --git a/erp24/docs/models/PaymentTypes.md b/erp24/docs/models/PaymentTypes.md new file mode 100644 index 00000000..bea0d54d --- /dev/null +++ b/erp24/docs/models/PaymentTypes.md @@ -0,0 +1,183 @@ +# Класс: PaymentTypes + + +## Mindmap + +```mermaid +mindmap + root((PaymentTypes)) + Таблица БД + payment_types + Свойства + id + string + code + string + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов оплаты в ERP24. Определяет доступные способы оплаты (наличные, карта, QR-код и др.) для использования при оформлении чеков продаж. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`payment_types` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | varchar(36) | Первичный ключ (GUID) | +| `code` | varchar(36) | Код типа оплаты | +| `name` | varchar(255) | Название типа оплаты | + +## Типичные типы оплаты + +| Код | Название | +|-----|----------| +| `cash` | Наличные | +| `card` | Банковская карта | +| `qr` | QR-код (СБП) | +| `online` | Онлайн-оплата | +| `credit` | В кредит | +| `bonus` | Бонусами | + +## Диаграмма связей + +```mermaid +erDiagram + PaymentTypes { + varchar id PK + varchar code UK + varchar name + } + + Sales { + varchar guid PK + varchar payment_type_id FK + } + + SalesPayments { + int id PK + varchar payment_type_id FK + float amount + } + + PaymentTypes ||--o{ Sales : "payment_type_id" + PaymentTypes ||--o{ SalesPayments : "payment_type_id" +``` + +## Примеры использования + +### Создание типа оплаты +```php +$paymentType = new PaymentTypes(); +$paymentType->id = Yii::$app->security->generateRandomString(36); +$paymentType->code = 'qr_sbp'; +$paymentType->name = 'QR-код СБП'; +$paymentType->save(); +``` + +### Получение всех типов оплаты +```php +$paymentTypes = PaymentTypes::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Формирование списка для выбора +```php +$typesList = ArrayHelper::map( + PaymentTypes::find()->all(), + 'id', + 'name' +); + +echo Html::dropDownList('payment_type', null, $typesList); +``` + +### Поиск по коду +```php +$cashPayment = PaymentTypes::find() + ->where(['code' => 'cash']) + ->one(); +``` + +### Получение названия по ID +```php +$paymentType = PaymentTypes::findOne($paymentTypeId); +$name = $paymentType ? $paymentType->name : 'Неизвестный тип'; +``` + +### Статистика продаж по типам оплаты +```php +$stats = Sales::find() + ->select([ + 'payment_type_id', + 'COUNT(*) as count', + 'SUM(total) as total' + ]) + ->groupBy('payment_type_id') + ->asArray() + ->all(); + +$types = ArrayHelper::index(PaymentTypes::find()->all(), 'id'); + +foreach ($stats as $stat) { + $typeName = $types[$stat['payment_type_id']]->name ?? 'Unknown'; + echo "{$typeName}: {$stat['count']} чеков, сумма: {$stat['total']}\n"; +} +``` + +### Проверка существования типа +```php +$exists = PaymentTypes::find() + ->where(['code' => $paymentCode]) + ->exists(); + +if (!$exists) { + throw new \Exception("Тип оплаты '{$paymentCode}' не найден"); +} +``` + +### Получение ID по коду +```php +function getPaymentTypeIdByCode($code) +{ + $type = PaymentTypes::find() + ->where(['code' => $code]) + ->one(); + + return $type ? $type->id : null; +} + +$cashId = getPaymentTypeIdByCode('cash'); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `id` | required, string (max 36), unique | +| `code` | required, string (max 36) | +| `name` | required, string (max 255) | + +## Связанные модели + +- [Sales](./Sales.md) — продажи +- [SalesPayments](./SalesPayments.md) — платежи по чекам + +## Особенности реализации + +1. **GUID первичный ключ**: id типа varchar(36) для совместимости с 1С +2. **Уникальный код**: Поле code для программного обращения +3. **Простой справочник**: Минимальная структура (id, code, name) +4. **Интеграция с 1С**: GUID идентификаторы для синхронизации diff --git a/erp24/docs/models/PhoneChangeHistory.md b/erp24/docs/models/PhoneChangeHistory.md new file mode 100644 index 00000000..5df55756 --- /dev/null +++ b/erp24/docs/models/PhoneChangeHistory.md @@ -0,0 +1,237 @@ +# Класс: PhoneChangeHistory + + +## Mindmap + +```mermaid +mindmap + root((PhoneChangeHistory)) + Таблица БД + phone_change_history + Свойства + id + int + old_phone + string + new_phone + string + comment + string + client_id + int + changed_by + int + Связи + ChangedBy + 1:1 Admin + Client + 1:1 Users + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории изменений телефонных номеров клиентов в ERP24. Отслеживает все случаи смены номера телефона у клиента с фиксацией причины, исполнителя и времени изменения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`phone_change_history` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `old_phone` | varchar(255) | Старый номер телефона | +| `new_phone` | varchar(255) | Новый номер телефона | +| `comment` | varchar(255) | Причина смены номера | +| `client_id` | int | FK на клиента (Users) | +| `changed_by` | int | FK на администратора (Admin) | +| `changed_at` | datetime | Дата и время смены номера | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getChangedBy()` | hasOne | Admin | Администратор, выполнивший изменение | +| `getClient()` | hasOne | Users | Клиент, чей номер изменён | + +## Вспомогательные методы + +### getChangedByName() +**Описание:** Возвращает имя администратора, выполнившего изменение. + +**Возвращает:** `string|null` — имя администратора или null + +**Логика работы:** +1. Обращается к связи changedBy +2. Возвращает поле name связанной модели Admin +3. При отсутствии связи возвращает null + +### getClientName() +**Описание:** Возвращает имя клиента. + +**Возвращает:** `string|null` — имя клиента или null + +**Логика работы:** +1. Обращается к связи client +2. Возвращает поле name связанной модели Users +3. При отсутствии связи возвращает null + +## Диаграмма связей + +```mermaid +erDiagram + PhoneChangeHistory { + int id PK + varchar old_phone + varchar new_phone + varchar comment + int client_id FK + int changed_by FK + datetime changed_at + } + + Users { + int id PK + varchar phone + varchar name + } + + Admin { + int id PK + varchar name + } + + Users ||--o{ PhoneChangeHistory : "client_id" + Admin ||--o{ PhoneChangeHistory : "changed_by" +``` + +## Диаграмма процесса смены номера + +```mermaid +flowchart TD + A[Запрос на смену номера] --> B[Проверка прав администратора] + B --> C[Валидация нового номера] + C --> D{Номер корректный?} + D -->|Нет| E[Ошибка валидации] + D -->|Да| F[Создание записи PhoneChangeHistory] + F --> G[Сохранение old_phone] + G --> H[Сохранение new_phone] + H --> I[Сохранение comment] + I --> J[Фиксация changed_by и changed_at] + J --> K[Обновление телефона в Users] + K --> L[Успешная смена номера] +``` + +## Примеры использования + +### Запись истории смены номера +```php +$history = new PhoneChangeHistory(); +$history->client_id = $client->id; +$history->old_phone = $client->phone; +$history->new_phone = $newPhone; +$history->comment = 'Клиент попросил изменить номер'; +$history->changed_by = Yii::$app->user->id; +$history->changed_at = date('Y-m-d H:i:s'); +$history->save(); + +// Обновить номер клиента +$client->phone = $newPhone; +$client->save(); +``` + +### Получение истории изменений клиента +```php +$history = PhoneChangeHistory::find() + ->where(['client_id' => $clientId]) + ->orderBy(['changed_at' => SORT_DESC]) + ->all(); + +foreach ($history as $change) { + echo "{$change->changed_at}: {$change->old_phone} -> {$change->new_phone}\n"; + echo "Причина: {$change->comment}\n"; + echo "Изменил: {$change->changedByName}\n"; +} +``` + +### Поиск изменений за период +```php +$recentChanges = PhoneChangeHistory::find() + ->where(['>=', 'changed_at', date('Y-m-01')]) + ->with(['changedBy', 'client']) + ->orderBy(['changed_at' => SORT_DESC]) + ->all(); +``` + +### Поиск по номеру телефона +```php +// Найти клиента по старому номеру +$history = PhoneChangeHistory::find() + ->where(['old_phone' => $oldPhone]) + ->one(); + +if ($history) { + $currentPhone = $history->client->phone ?? 'Неизвестно'; + echo "Текущий номер клиента: {$currentPhone}"; +} +``` + +### Статистика изменений по администраторам +```php +$stats = PhoneChangeHistory::find() + ->select(['changed_by', 'COUNT(*) as count']) + ->where(['>=', 'changed_at', date('Y-m-01')]) + ->groupBy('changed_by') + ->asArray() + ->all(); + +$admins = ArrayHelper::index(Admin::find()->all(), 'id'); + +foreach ($stats as $stat) { + $adminName = $admins[$stat['changed_by']]->name ?? 'Unknown'; + echo "{$adminName}: {$stat['count']} изменений\n"; +} +``` + +### Проверка частоты смены номера +```php +$changeCount = PhoneChangeHistory::find() + ->where(['client_id' => $clientId]) + ->andWhere(['>=', 'changed_at', date('Y-m-d', strtotime('-30 days'))]) + ->count(); + +if ($changeCount > 3) { + Yii::warning("Клиент {$clientId} часто меняет номер телефона"); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `old_phone` | required, string (max 255) | +| `new_phone` | required, string (max 255) | +| `comment` | required, string (max 255) | +| `client_id` | required, integer | +| `changed_by` | required, integer | +| `changed_at` | required, safe | + +## Связанные модели + +- [Users](./Users.md) — клиенты +- [Admin](./Admin.md) — администраторы системы + +## Особенности реализации + +1. **Полный аудит**: Сохраняется как старый, так и новый номер +2. **Причина изменения**: Обязательное поле comment для объяснения причины +3. **Ответственность**: Фиксируется администратор, выполнивший изменение +4. **Временная метка**: Точное время изменения для хронологии +5. **Поиск по старым номерам**: Возможность найти клиента по его прежнему номеру diff --git a/erp24/docs/models/PlanStore.md b/erp24/docs/models/PlanStore.md new file mode 100644 index 00000000..bdbb824c --- /dev/null +++ b/erp24/docs/models/PlanStore.md @@ -0,0 +1,328 @@ +# Класс: PlanStore + + +## Mindmap + +```mermaid +mindmap + root((PlanStore)) + Таблица БД + plan_store + Свойства + store_id + int + year + int + month + int + day + int + shift_type + int + plan_sales + float + Связи + StoreName + 1:1 CityStore + Logs + 1:N PlanStoreLog + CreatedBy + 1:1 Admin + Наследование + extends ActiveRecord +``` + +## Назначение +Модель плановых показателей магазина в ERP24. Хранит планы продаж, среднего чека, ФОТ и количества чеков для каждого магазина с детализацией по дням и типам смен (дневная/ночная). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`plan_store` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `store_id` | int | FK на магазин (CityStore) | +| `year` | int | Год плана | +| `month` | int | Месяц плана (1-12) | +| `day` | int | День месяца (1-31) | +| `shift_type` | int | Тип смены (1=день, 2=ночь) | +| `plan_sales` | float | План продаж в рублях | +| `plan_avg_sales_value` | float | План среднего чека в рублях | +| `plan_fot` | float | План ФОТ в процентах (0-100) | +| `plan_count_sales` | int | План количества чеков | +| `created_by` | int | FK на создателя (Admin) | +| `created_at` | datetime | Дата создания | +| `updated_by` | int / null | FK на редактора (Admin) | +| `updated_at` | datetime / null | Дата обновления | + +## Константы и справочники + +### $shift_type_array +```php +public $shift_type_array = [ + 1 => 'День', + 2 => 'Ночь', +]; +``` + +### $day_name +```php +public $day_name = [ + 0 => 'Воскресение', + 1 => 'Понедельник', + 2 => 'Вторник', + 3 => 'Среда', + 4 => 'Четверг', + 5 => 'Пятница', + 6 => 'Суббота' +]; +``` + +### $month_name +Использует константы из `DateHelper::MONTH_NUMBER_NAMES` для локализованных названий месяцев. + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getStoreName()` | hasOne | CityStore | Магазин (выборка name, id) | +| `getLogs()` | hasMany | PlanStoreLog | История изменений плана | +| `getCreatedBy()` | hasOne | Admin | Создатель записи | + +## Методы + +### getId() +**Описание:** Генерирует составной идентификатор записи. + +**Возвращает:** `string` — уникальный идентификатор вида `store_year_month_day_shift` + +**Пример возвращаемого значения:** `"15_2024_12_25_1"` + +**Логика работы:** +1. Конкатенирует store_id, year, month, day, shift_type +2. Разделяет подчёркиванием +3. Используется для идентификации строки в интерфейсе + +## Диаграмма связей + +```mermaid +erDiagram + PlanStore { + int store_id PK,FK + int year PK + int month PK + int day PK + int shift_type PK + float plan_sales + float plan_avg_sales_value + float plan_fot + int plan_count_sales + int created_by FK + datetime created_at + int updated_by FK + datetime updated_at + } + + CityStore { + int id PK + varchar name + } + + PlanStoreLog { + int id PK + int store_id FK + int year + int month + int day + int shift_type + } + + Admin { + int id PK + varchar name + } + + CityStore ||--o{ PlanStore : "store_id" + PlanStore ||--o{ PlanStoreLog : "store+date+shift" + Admin ||--o{ PlanStore : "created_by" +``` + +## Диаграмма планирования + +```mermaid +flowchart TD + A[Установка плана на месяц] --> B[Выбор магазина] + B --> C[Выбор периода: год, месяц] + C --> D[Генерация дней месяца] + D --> E{Для каждого дня} + E --> F[Дневная смена] + E --> G[Ночная смена] + F --> H[Ввод plan_sales] + G --> H + H --> I[Ввод plan_avg_sales_value] + I --> J[Ввод plan_fot %] + J --> K[Ввод plan_count_sales] + K --> L[Сохранение с unique constraint] + L --> M[Запись в PlanStoreLog при изменении] +``` + +## Примеры использования + +### Создание плана на день +```php +$plan = new PlanStore(); +$plan->store_id = $storeId; +$plan->year = 2024; +$plan->month = 12; +$plan->day = 25; +$plan->shift_type = 1; // День +$plan->plan_sales = 150000; +$plan->plan_avg_sales_value = 2500; +$plan->plan_fot = 15; // 15% +$plan->plan_count_sales = 60; +$plan->created_by = Yii::$app->user->id; +$plan->created_at = date('Y-m-d H:i:s'); +$plan->save(); +``` + +### Получение плана магазина на месяц +```php +$plans = PlanStore::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->orderBy(['day' => SORT_ASC, 'shift_type' => SORT_ASC]) + ->all(); + +foreach ($plans as $plan) { + $shiftName = $plan->shift_type_array[$plan->shift_type]; + echo "День {$plan->day} ({$shiftName}): план {$plan->plan_sales} руб.\n"; +} +``` + +### Суммарный план на месяц +```php +$monthlyPlan = PlanStore::find() + ->select([ + 'SUM(plan_sales) as total_sales', + 'SUM(plan_count_sales) as total_count', + 'AVG(plan_avg_sales_value) as avg_check' + ]) + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->asArray() + ->one(); + +echo "План продаж: {$monthlyPlan['total_sales']} руб.\n"; +echo "План чеков: {$monthlyPlan['total_count']} шт.\n"; +``` + +### Сравнение плана по сменам +```php +$shiftComparison = PlanStore::find() + ->select([ + 'shift_type', + 'SUM(plan_sales) as total_sales' + ]) + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->groupBy('shift_type') + ->asArray() + ->all(); + +foreach ($shiftComparison as $shift) { + $shiftName = (new PlanStore())->shift_type_array[$shift['shift_type']]; + echo "{$shiftName}: {$shift['total_sales']} руб.\n"; +} +``` + +### Копирование плана на следующий месяц +```php +$currentPlans = PlanStore::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 11 + ]) + ->all(); + +foreach ($currentPlans as $plan) { + $newPlan = new PlanStore(); + $newPlan->attributes = $plan->attributes; + $newPlan->month = 12; + $newPlan->created_by = Yii::$app->user->id; + $newPlan->created_at = date('Y-m-d H:i:s'); + $newPlan->updated_by = null; + $newPlan->updated_at = null; + $newPlan->save(); +} +``` + +### Получение плана с историей изменений +```php +$plan = PlanStore::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12, + 'day' => 25, + 'shift_type' => 1 + ]) + ->with(['logs']) + ->one(); + +if ($plan && $plan->logs) { + echo "История изменений:\n"; + foreach ($plan->logs as $log) { + echo "{$log->created_at}: {$log->old_value} -> {$log->new_value}\n"; + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, integer | +| `year` | required, integer | +| `month` | required, integer | +| `day` | required, integer | +| `shift_type` | required, integer | +| `plan_sales` | required, number (min 0) | +| `plan_avg_sales_value` | required, number (min 0) | +| `plan_fot` | required, number (min 0, max 100) | +| `plan_count_sales` | required, integer | +| `created_by` | required, integer | +| `created_at` | required, safe | + +**Уникальное ограничение:** `[store_id, year, month, day, shift_type]` + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [PlanStoreLog](./PlanStoreLog.md) — история изменений плана +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Составной первичный ключ**: store_id + year + month + day + shift_type +2. **Разделение по сменам**: Отдельные планы для дневной и ночной смены +3. **ФОТ в процентах**: plan_fot ограничен диапазоном 0-100 +4. **Аудит изменений**: Связь с PlanStoreLog для истории +5. **Генерируемый ID**: Метод getId() для идентификации в UI +6. **Локализация**: Использование DateHelper для названий месяцев diff --git a/erp24/docs/models/PlanStoreGroupSearch.md b/erp24/docs/models/PlanStoreGroupSearch.md new file mode 100644 index 00000000..55047f0e --- /dev/null +++ b/erp24/docs/models/PlanStoreGroupSearch.md @@ -0,0 +1,217 @@ +# Класс: PlanStoreGroupSearch + + +## Mindmap + +```mermaid +mindmap + root((PlanStoreGroupSearch)) + Таблица БД + ActiveRecord + Наследование + extends PlanStore +``` + +## Назначение +Search-модель для группового поиска планов магазинов в ERP24. Специализированная модель с агрегацией по кластерам и магазинам, множественными JOIN и фильтрацией по правам доступа текущего пользователя. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`PlanStore` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$date_start_str` | string | Дата начала периода (default: первый день месяца) | +| `$date_end_str` | string | Дата окончания периода (default: последний день месяца) | +| `$mode_group` | int | Режим группировки | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска с дефолтными значениями. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `date_start_str`, `date_end_str` — safe +- `mode_group` — integer + +**Дефолтные значения:** +- `date_start_str` — первый день текущего месяца +- `date_end_str` — последний день текущего месяца + +### search($params): ActiveQuery +**Описание:** Создаёт запрос с агрегацией по кластерам и магазинам. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveQuery` — запрос (не DataProvider!) + +**Логика:** +1. Загружает и валидирует параметры (возвращает пустой запрос при ошибке) +2. INNER JOIN store_dynamic: связь с кластером по дате +3. INNER JOIN admin: фильтр по store_arr текущего пользователя +4. INNER JOIN city_store: получение названия магазина +5. SELECT с агрегацией: + - cluster_id из store_dynamic.value_int + - store_id, store_name + - plan_sales, plan_avg_sales, plan_count_sales, plan_fot, shift_type + - date из year.month.day +6. GROUP BY по всем выбранным полям + дате +7. Фильтр по date_start_str/date_end_str +8. ORDER BY date ASC + +## Диаграмма связей + +```mermaid +erDiagram + PlanStore { + int store_id FK + int year + int month + int day + int shift_type + decimal plan_sales + decimal plan_avg_sales_value + int plan_count_sales + decimal plan_fot + } + + StoreDynamic { + int store_id FK + int value_int + date date_from + date date_to + } + + Admin { + int id PK + varchar store_arr + } + + CityStore { + int id PK + varchar name + } + + PlanStore }o--|| CityStore : "store_id" + PlanStore }o--|| StoreDynamic : "динамика кластера" + Admin ||--o{ CityStore : "доступ к магазинам" +``` + +## Диаграмма потока запроса + +```mermaid +flowchart TD + A[search] --> B[Загрузка params] + B --> C{validate?} + C -->|Нет| D[WHERE 0=1] + C -->|Да| E[INNER JOIN store_dynamic] + + E --> F[INNER JOIN admin] + F --> G[Фильтр по store_arr] + + G --> H[INNER JOIN city_store] + H --> I[SELECT с агрегацией] + + I --> J[cluster_id] + I --> K[store_id, store_name] + I --> L[plan_sales, plan_fot...] + I --> M[date] + + J --> N[GROUP BY] + K --> N + L --> N + M --> N + + N --> O[WHERE date BETWEEN] + O --> P[ORDER BY date ASC] +``` + +## Формат SELECT + +```sql +SELECT + store_dynamic.value_int AS cluster_id, + plan_store.store_id AS store_id, + city_store.name AS store_name, + plan_store.plan_sales AS plan_sales, + plan_store.plan_avg_sales_value AS plan_avg_sales, + plan_store.plan_count_sales AS plan_count_sales, + plan_store.plan_fot AS plan_fot, + plan_store.shift_type AS shift_type, + DATE_FORMAT(CONCAT_WS('.', year, month, day), '%Y-%m-%d') AS date +``` + +## Примеры использования + +### Получение данных за период +```php +$searchModel = new PlanStoreGroupSearch(); +$query = $searchModel->search([ + 'date_start_str' => '2024-01-01', + 'date_end_str' => '2024-01-31', +]); +$plans = $query->all(); +``` + +### Стандартный поиск (текущий месяц) +```php +$searchModel = new PlanStoreGroupSearch(); +$query = $searchModel->search([]); +$plans = $query->all(); +``` + +### Получение данных для графика +```php +$searchModel = new PlanStoreGroupSearch(); +$query = $searchModel->search([ + 'date_start_str' => '2024-01-01', + 'date_end_str' => '2024-12-31', +]); + +$data = []; +foreach ($query->all() as $plan) { + $data[$plan->cluster_id][$plan->date] = [ + 'sales' => $plan->plan_sales, + 'fot' => $plan->plan_fot, + ]; +} +``` + +### Использование в контроллере +```php +public function actionGroup() +{ + $searchModel = new PlanStoreGroupSearch(); + $query = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('group', [ + 'searchModel' => $searchModel, + 'plans' => $query->all(), + ]); +} +``` + +## Связанные модели + +- [PlanStore](./PlanStore.md) — базовая модель планов +- [StoreDynamic](./StoreDynamic.md) — динамические параметры магазинов +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы +- [Cluster](./Cluster.md) — кластеры магазинов + +## Особенности реализации + +1. **Возвращает ActiveQuery**: Не DataProvider, а запрос для дальнейшей обработки +2. **Права доступа**: Фильтр по store_arr текущего пользователя +3. **Динамические кластеры**: JOIN store_dynamic с проверкой дат date_from/date_to +4. **Формирование даты**: DATE_FORMAT(CONCAT_WS('.', year, month, day), '%Y-%m-%d') +5. **GROUP BY**: Агрегация по всем полям + дате +6. **Дефолтный период**: Текущий месяц +7. **Пустой результат при ошибке**: WHERE 0=1 при невалидных параметрах diff --git a/erp24/docs/models/PlanStoreLog.md b/erp24/docs/models/PlanStoreLog.md new file mode 100644 index 00000000..6fda16a4 --- /dev/null +++ b/erp24/docs/models/PlanStoreLog.md @@ -0,0 +1,314 @@ +# Класс: PlanStoreLog + + +## Mindmap + +```mermaid +mindmap + root((PlanStoreLog)) + Таблица БД + plan_store_log + Свойства + id + int + value_type + int + old_value + float + new_value + float + created_by + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории изменений плановых показателей магазина в ERP24. Логирует все изменения в планах продаж, среднего чека, ФОТ и количества чеков с фиксацией старых и новых значений. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`plan_store_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_id` | int / null | FK на магазин (CityStore) | +| `year` | int / null | Год плана | +| `month` | int / null | Месяц плана | +| `day` | int / null | День плана | +| `shift_type` | int / null | Тип смены (1=день, 2=ночь) | +| `value_type` | int | Тип изменённого поля | +| `old_value` | float | Старое значение | +| `new_value` | float | Новое значение | +| `created_by` | int | FK на администратора (Admin) | +| `created_at` | datetime | Дата и время изменения | + +## Константы и справочники + +### $value_type_array +```php +public $value_type_array = [ + 1 => 'План продаж', + 2 => 'План фот', + 3 => 'План средний чек', + 4 => 'План кол-во продаж' +]; +``` + +### $shift_type_array +```php +public $shift_type_array = [ + 1 => 'День', + 2 => 'Ночь', +]; +``` + +### $day_name +```php +public $day_name = [ + 0 => 'Воскресение', + 1 => 'Понедельник', + 2 => 'Вторник', + 3 => 'Среда', + 4 => 'Четверг', + 5 => 'Пятница', + 6 => 'Суббота' +]; +``` + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getStoreName()` | hasOne | CityStore | Магазин (выборка name, id) | +| `getCreatedBy()` | hasOne | Admin | Автор изменения | + +## Диаграмма связей + +```mermaid +erDiagram + PlanStoreLog { + int id PK + int store_id FK + int year + int month + int day + int shift_type + int value_type + float old_value + float new_value + int created_by FK + datetime created_at + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + PlanStore { + int store_id PK + int year PK + int month PK + int day PK + int shift_type PK + } + + CityStore ||--o{ PlanStoreLog : "store_id" + Admin ||--o{ PlanStoreLog : "created_by" + PlanStore ||--o{ PlanStoreLog : "store+date+shift" +``` + +## Диаграмма логирования изменений + +```mermaid +flowchart TD + A[Редактирование PlanStore] --> B{Что изменилось?} + B -->|plan_sales| C[value_type = 1] + B -->|plan_fot| D[value_type = 2] + B -->|plan_avg_sales_value| E[value_type = 3] + B -->|plan_count_sales| F[value_type = 4] + C --> G[Создание PlanStoreLog] + D --> G + E --> G + F --> G + G --> H[Сохранение old_value] + H --> I[Сохранение new_value] + I --> J[Фиксация created_by, created_at] + J --> K[Сохранение в БД] +``` + +## Примеры использования + +### Запись изменения плана продаж +```php +$log = new PlanStoreLog(); +$log->store_id = $plan->store_id; +$log->year = $plan->year; +$log->month = $plan->month; +$log->day = $plan->day; +$log->shift_type = $plan->shift_type; +$log->value_type = 1; // План продаж +$log->old_value = $oldPlanSales; +$log->new_value = $newPlanSales; +$log->created_by = Yii::$app->user->id; +$log->created_at = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Получение истории изменений для плана +```php +$logs = PlanStoreLog::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12, + 'day' => 25, + 'shift_type' => 1 + ]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + $typeName = $log->value_type_array[$log->value_type]; + echo "{$log->created_at}: {$typeName} изменён с {$log->old_value} на {$log->new_value}\n"; +} +``` + +### Статистика изменений по типам +```php +$stats = PlanStoreLog::find() + ->select(['value_type', 'COUNT(*) as count']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('value_type') + ->asArray() + ->all(); + +$types = (new PlanStoreLog())->value_type_array; + +foreach ($stats as $stat) { + $typeName = $types[$stat['value_type']] ?? 'Unknown'; + echo "{$typeName}: {$stat['count']} изменений\n"; +} +``` + +### История изменений по магазину +```php +$storeLogs = PlanStoreLog::find() + ->where(['store_id' => $storeId]) + ->with(['createdBy', 'storeName']) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(50) + ->all(); + +foreach ($storeLogs as $log) { + $adminName = $log->createdBy->name ?? 'Unknown'; + $shiftName = $log->shift_type_array[$log->shift_type] ?? ''; + echo "{$log->day}.{$log->month}.{$log->year} ({$shiftName}): "; + echo "изменил {$adminName}\n"; +} +``` + +### Поиск крупных изменений +```php +$significantChanges = PlanStoreLog::find() + ->where(['>=', 'created_at', date('Y-m-01')]) + ->andWhere('ABS(new_value - old_value) / NULLIF(old_value, 0) > 0.2') // > 20% + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Анализ активности редактирования +```php +$adminActivity = PlanStoreLog::find() + ->select(['created_by', 'COUNT(*) as changes']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('created_by') + ->orderBy(['changes' => SORT_DESC]) + ->asArray() + ->all(); + +$admins = ArrayHelper::index(Admin::find()->all(), 'id'); + +foreach ($adminActivity as $stat) { + $adminName = $admins[$stat['created_by']]->name ?? 'Unknown'; + echo "{$adminName}: {$stat['changes']} изменений\n"; +} +``` + +### Восстановление предыдущего значения +```php +$lastLog = PlanStoreLog::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12, + 'day' => 25, + 'shift_type' => 1, + 'value_type' => 1 // План продаж + ]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastLog) { + $plan = PlanStore::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12, + 'day' => 25, + 'shift_type' => 1 + ]) + ->one(); + + if ($plan) { + $plan->plan_sales = $lastLog->old_value; + $plan->save(); + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, integer | +| `year` | required, integer | +| `month` | required, integer | +| `day` | required, integer | +| `shift_type` | required, integer | +| `value_type` | required, integer | +| `old_value` | required, number | +| `new_value` | required, number | +| `created_by` | required, integer | +| `created_at` | required, safe | + +## Связанные модели + +- [PlanStore](./PlanStore.md) — плановые показатели магазина +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Детализация изменений**: Фиксируется конкретное изменённое поле через value_type +2. **Полный аудит**: Сохраняются как старое, так и новое значение +3. **Связь с планом**: Привязка к конкретному дню и смене магазина +4. **Типизация изменений**: 4 типа изменяемых показателей (продажи, ФОТ, средний чек, количество) +5. **Ответственность**: Фиксируется автор каждого изменения +6. **Хронология**: Возможность отследить всю историю изменений плана diff --git a/erp24/docs/models/PlanStoreSearch.md b/erp24/docs/models/PlanStoreSearch.md new file mode 100644 index 00000000..7b215ea5 --- /dev/null +++ b/erp24/docs/models/PlanStoreSearch.md @@ -0,0 +1,255 @@ +# Класс: PlanStoreSearch + + +## Mindmap + +```mermaid +mindmap + root((PlanStoreSearch)) + Таблица БД + ActiveRecord + Наследование + extends PlanStore +``` + +## Назначение +Search-модель для поиска и фильтрации планов магазинов в ERP24. Модель с двумя методами поиска: основной search() с агрегацией по месяцам и searchItem() для детализации по конкретному магазину и периоду. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`PlanStore` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `store_id`, `year`, `month`, `day`, `shift_type`, `created_by`, `updated_by` — integer +- `plan_sales`, `plan_avg_sales_value`, `plan_fot` — number +- `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Основной метод поиска с агрегацией по месяцам. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с eager loading: storeName, createdBy +2. Применяет фильтры по всем полям (точное совпадение) +3. GROUP BY: store_id, year, month, created_by +4. SELECT с агрегацией: + - store_id, year, month, created_by + - SUM(plan_sales) + - AVG(plan_fot) + - SUM(plan_avg_sales_value) + - SUM(plan_count_sales) +5. ORDER BY: year DESC, month DESC, store_id ASC + +### searchItem($params): ActiveDataProvider +**Описание:** Детализированный поиск по конкретному магазину и периоду. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных без пагинации + +**Логика:** +1. Создаёт запрос с eager loading: storeName +2. Фильтрует по store_id, year, month (обязательные) +3. pagination: false — все записи без разбивки + +## Диаграмма связей + +```mermaid +erDiagram + PlanStore { + int store_id FK + int year + int month + int day + int shift_type + decimal plan_sales + decimal plan_avg_sales_value + int plan_count_sales + decimal plan_fot + int created_by FK + datetime created_at + int updated_by FK + datetime updated_at + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + PlanStore }o--|| CityStore : "storeName" + PlanStore }o--o| Admin : "createdBy" + PlanStore }o--o| Admin : "updatedBy" +``` + +## Диаграмма методов поиска + +```mermaid +flowchart TD + A[PlanStoreSearch] --> B{Метод} + + B -->|search| C[Агрегация по месяцам] + B -->|searchItem| D[Детализация по дням] + + C --> E[GROUP BY store_id, year, month] + C --> F[SUM plan_sales] + C --> G[AVG plan_fot] + C --> H[ORDER BY year DESC, month DESC] + + D --> I[WHERE store_id, year, month] + D --> J[pagination: false] + D --> K[Все дни месяца] +``` + +## Формат агрегации search() + +```sql +SELECT + store_id, + year, + month, + SUM(plan_sales) AS plan_sales, + AVG(plan_fot) AS plan_fot, + SUM(plan_avg_sales_value) AS plan_avg_sales_value, + SUM(plan_count_sales) AS plan_count_sales, + created_by +FROM plan_store +GROUP BY store_id, year, month, created_by +ORDER BY year DESC, month DESC, store_id ASC +``` + +## Примеры использования + +### Список планов по месяцам +```php +public function actionIndex() +{ + $searchModel = new PlanStoreSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Фильтрация по магазину +```php +$searchModel = new PlanStoreSearch(); +$dataProvider = $searchModel->search([ + 'PlanStoreSearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Фильтрация по году и месяцу +```php +$searchModel = new PlanStoreSearch(); +$dataProvider = $searchModel->search([ + 'PlanStoreSearch' => [ + 'year' => 2024, + 'month' => 3, + ] +]); +``` + +### Детализация плана по дням +```php +public function actionView($store_id, $year, $month) +{ + $searchModel = new PlanStoreSearch(); + $dataProvider = $searchModel->searchItem([ + 'PlanStoreSearch' => [ + 'store_id' => $store_id, + 'year' => $year, + 'month' => $month, + ] + ]); + + return $this->render('view', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### GridView с агрегацией +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + [ + 'attribute' => 'store_id', + 'value' => 'storeName.name', + ], + 'year', + 'month', + 'plan_sales:decimal', + 'plan_fot:decimal', + 'plan_avg_sales_value:decimal', + 'plan_count_sales', + [ + 'attribute' => 'created_by', + 'value' => 'createdBy.name', + ], + ], +]) ?> +``` + +### GridView детализации +```php + $dataProvider, + 'columns' => [ + 'day', + 'shift_type', + 'plan_sales:decimal', + 'plan_fot:decimal', + 'plan_avg_sales_value:decimal', + 'plan_count_sales', + ], +]) ?> +``` + +## Связанные модели + +- [PlanStore](./PlanStore.md) — базовая модель планов +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы +- [PlanStoreGroupSearch](./PlanStoreGroupSearch.md) — групповой поиск по кластерам + +## Особенности реализации + +1. **Два метода поиска**: search() для списка, searchItem() для детализации +2. **Агрегация по месяцам**: SUM/AVG для суммирования дневных значений +3. **Eager loading**: storeName, createdBy для оптимизации +4. **Сортировка**: Новые периоды сверху (year DESC, month DESC) +5. **searchItem без пагинации**: Все дни месяца в одном запросе +6. **shift_type**: Тип смены для разных планов в день diff --git a/erp24/docs/models/Prices.md b/erp24/docs/models/Prices.md index e870cea8..eadd83d4 100644 --- a/erp24/docs/models/Prices.md +++ b/erp24/docs/models/Prices.md @@ -1,5 +1,22 @@ # Class: Prices + +## Mindmap + +```mermaid +mindmap + root((Prices)) + Таблица БД + prices + Свойства + product_id + string + price + float + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель для управления ценами товаров. Хранит розничные цены на товары с привязкой к идентификатору товара. Используется для получения актуальной цены товара при формировании чеков продаж. diff --git a/erp24/docs/models/PricesDynamic.md b/erp24/docs/models/PricesDynamic.md new file mode 100644 index 00000000..4acd8300 --- /dev/null +++ b/erp24/docs/models/PricesDynamic.md @@ -0,0 +1,521 @@ +# Модель PricesDynamic + + +## Mindmap + +```mermaid +mindmap + root((PricesDynamic)) + Таблица БД + prices_dynamic + Свойства + id + int + product_id + string + price + float + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `PricesDynamic` представляет динамическую систему ценообразования товаров с поддержкой региональных цен и временных периодов действия. Позволяет отслеживать изменения цен во времени, управлять ценовой политикой по регионам и активировать/деактивировать ценовые периоды. Используется для гибкого управления ценами в разных регионах и временных рамках. + +**Файл модели:** `erp24/records/PricesDynamic.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `prices_dynamic` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `product_id` | VARCHAR(36) | GUID товара из таблицы товаров | +| `price` | FLOAT | Цена товара | +| `date_from` | DATE | Дата начала действия цены | +| `date_to` | DATE | Дата окончания действия цены (nullable) | +| `active` | INTEGER | Статус активности цены (0 или 1) | +| `region_id` | INTEGER | ID региона (nullable) | + +--- + +## Константы + +Модель определяет константы для управления статусом активности: + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `ACTIVE` | 1 | Цена активна | +| `NOT_ACTIVE` | 0 | Цена неактивна | + +**Использование:** +```php +$price->active = PricesDynamic::ACTIVE; +// или +$price->active = PricesDynamic::NOT_ACTIVE; +``` + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'prices_dynamic'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = PricesDynamic::tableName(); +// 'prices_dynamic' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает обязательные поля: `product_id`, `price` +2. Проверяет типы данных: + - FLOAT для `price` и `region_id` + - DATE для `date_from` и `date_to` + - INTEGER для `active` +3. Ограничивает длину `product_id` до 36 символов (GUID) +4. Разрешает безопасное присвоение для дат через правило `safe` + +**Правила валидации:** +- **required**: `product_id`, `price` +- **number (float)**: `price`, `region_id` +- **safe (date)**: `date_from`, `date_to` +- **integer**: `active` +- **string (max 36)**: `product_id` + +**Пример:** +```php +$price = new PricesDynamic(); +$price->product_id = 'abc-123-456-guid'; +$price->price = 1299.99; +$price->date_from = date('Y-m-d'); +$price->date_to = date('Y-m-d', strtotime('+30 days')); +$price->active = PricesDynamic::ACTIVE; +$price->region_id = 77; // Москва + +if ($price->validate()) { + $price->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет названия полей на русском и английском языках для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `product_id` → "Product ID" +- `price` → "Price" +- `date_from` → "Date From" +- `date_to` → "Date To" +- `active` → "Active" +- `region_id` → "Регион" + +**Пример:** +```php +$label = $price->getAttributeLabel('region_id'); +// "Регион" +``` + +--- + +## Примеры использования + +### Создание новой динамической цены + +```php +$price = new PricesDynamic(); +$price->product_id = 'abc-123-456-guid'; +$price->price = 1500.00; +$price->date_from = '2025-01-01'; +$price->date_to = '2025-12-31'; +$price->active = PricesDynamic::ACTIVE; +$price->region_id = 77; // Москва + +if ($price->save()) { + echo "Динамическая цена создана"; +} +``` + +### Получение активной цены товара + +```php +$activePrice = PricesDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'active' => PricesDynamic::ACTIVE + ]) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['or', + ['>=', 'date_to', date('Y-m-d')], + ['date_to' => null] + ]) + ->one(); + +if ($activePrice) { + echo "Текущая цена: {$activePrice->price} руб."; +} +``` + +### Получение цены для конкретного региона + +```php +$regionalPrice = PricesDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'region_id' => 52, // Нижний Новгород + 'active' => PricesDynamic::ACTIVE + ]) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['>=', 'date_to', date('Y-m-d')]) + ->one(); + +echo "Цена в НН: {$regionalPrice->price} руб."; +``` + +### Деактивация старой цены при создании новой + +```php +// Деактивируем текущую активную цену +$oldPrice = PricesDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'region_id' => $regionId, + 'active' => PricesDynamic::ACTIVE + ]) + ->one(); + +if ($oldPrice) { + $oldPrice->active = PricesDynamic::NOT_ACTIVE; + $oldPrice->date_to = date('Y-m-d H:i:s'); + $oldPrice->save(); +} + +// Создаем новую активную цену +$newPrice = new PricesDynamic(); +$newPrice->product_id = $productGuid; +$newPrice->region_id = $regionId; +$newPrice->price = 1800.00; +$newPrice->date_from = date('Y-m-d H:i:s'); +$newPrice->date_to = '2100-01-01 00:00:00'; // Далекая дата +$newPrice->active = PricesDynamic::ACTIVE; +$newPrice->save(); +``` + +### История изменения цен товара + +```php +$priceHistory = PricesDynamic::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['date_from' => SORT_DESC]) + ->all(); + +foreach ($priceHistory as $priceRecord) { + $status = $priceRecord->active ? 'Активна' : 'Неактивна'; + echo "С {$priceRecord->date_from} по {$priceRecord->date_to}: {$priceRecord->price} руб. ({$status})" . PHP_EOL; +} +``` + +### Цены для всех регионов + +```php +$regionalPrices = PricesDynamic::find() + ->where([ + 'product_id' => $productGuid, + 'active' => PricesDynamic::ACTIVE + ]) + ->andWhere(['not', ['region_id' => null]]) + ->indexBy('region_id') + ->all(); + +foreach ($regionalPrices as $regionId => $priceRecord) { + echo "Регион {$regionId}: {$priceRecord->price} руб." . PHP_EOL; +} +``` + +### Запланированное изменение цены + +```php +// Текущая цена (действует до 31.12.2025) +$currentPrice = new PricesDynamic(); +$currentPrice->product_id = $guid; +$currentPrice->price = 1000.00; +$currentPrice->date_from = '2025-01-01'; +$currentPrice->date_to = '2025-12-31'; +$currentPrice->active = PricesDynamic::ACTIVE; +$currentPrice->save(); + +// Будущая цена (начнет действовать с 01.01.2026) +$futurePrice = new PricesDynamic(); +$futurePrice->product_id = $guid; +$futurePrice->price = 1200.00; +$futurePrice->date_from = '2026-01-01'; +$futurePrice->date_to = '2026-12-31'; +$futurePrice->active = PricesDynamic::ACTIVE; +$futurePrice->save(); +``` + +### Поиск товаров по диапазону цен + +```php +$products = PricesDynamic::find() + ->where(['active' => PricesDynamic::ACTIVE]) + ->andWhere(['between', 'price', 500, 2000]) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['>=', 'date_to', date('Y-m-d')]) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `product_id` | Обязательное, макс. 36 символов (GUID) | +| `price` | Обязательное, число с плавающей точкой | +| `date_from` | Безопасное присвоение (safe), дата | +| `date_to` | Безопасное присвоение (safe), дата, nullable | +| `active` | Целое число (0 или 1) | +| `region_id` | Число, nullable | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары из 1С (связь по `product_id`) +- **[PricesRegion](./PricesRegion.md)** — региональные цены (статические) +- **[BouquetComposition](./BouquetComposition.md)** — использует динамические цены для расчета стоимости букетов +- **Regions** — справочник регионов (связь по `region_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o{ prices_dynamic : "has_prices" + prices_dynamic }o--|| regions : "belongs_to_region" + + products_1c { + string id PK + string name + string tip + } + + prices_dynamic { + int id PK + string product_id FK + float price + date date_from + date date_to + int active + int region_id FK + } + + regions { + int id PK + string name + int code + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Товар Products1c] -->|product_id| B[PricesDynamic] + B --> C[Проверка периода] + C -->|date_from <= NOW| D{Активна?} + D -->|active = 1| E[Проверка date_to] + E -->|date_to >= NOW or NULL| F[Актуальная цена] + D -->|active = 0| G[Неактивная цена] + B --> H[Проверка региона] + H -->|region_id| I[Региональная цена] + H -->|region_id = NULL| J[Базовая цена] + F --> K[Используется в продажах] + I --> K + J --> K +``` + +--- + +## Особенности реализации + +### Система версионирования цен + +Модель реализует систему версионирования цен через поля `date_from`, `date_to` и `active`: + +1. **Временные периоды**: каждая цена действует в определенный период +2. **Активность**: только активные цены участвуют в продажах +3. **Перекрытие**: новая цена деактивирует предыдущую + +**Пример логики смены цен:** +```php +// 1. Деактивация старой цены +$oldPrice->active = PricesDynamic::NOT_ACTIVE; +$oldPrice->date_to = date('Y-m-d H:i:s'); +$oldPrice->save(); + +// 2. Создание новой активной цены +$newPrice = new PricesDynamic(); +$newPrice->active = PricesDynamic::ACTIVE; +$newPrice->date_from = date('Y-m-d H:i:s'); +$newPrice->date_to = '2100-01-01 00:00:00'; +$newPrice->save(); +``` + +### Региональное ценообразование + +Поле `region_id` позволяет устанавливать разные цены для разных регионов: + +- **NULL** — базовая цена (для всех регионов) +- **52** — цена для Нижнего Новгорода +- **77** — цена для Москвы + +**Приоритет:** +1. Региональная цена (если указан `region_id`) +2. Базовая цена (если `region_id` = NULL) + +### Дата окончания действия + +Поле `date_to` может быть: +- **NULL** — цена действует бессрочно +- **Дата** — цена действует до указанной даты + +Для "вечных" цен часто используется дата `2100-01-01 00:00:00`. + +### Поиск актуальной цены + +Типичный запрос для получения актуальной цены: + +```php +PricesDynamic::find() + ->where([ + 'product_id' => $guid, + 'active' => PricesDynamic::ACTIVE + ]) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['or', + ['>=', 'date_to', date('Y-m-d')], + ['date_to' => null] + ]) + ->one(); +``` + +**Логика:** +1. Товар с нужным GUID +2. Цена активна +3. Дата начала <= текущая дата +4. Дата окончания >= текущая дата ИЛИ не указана + +--- + +## Использование в BouquetComposition + +Модель активно используется для установки цен на букеты: + +```php +// Из BouquetComposition::setCost() +$priceModel = PricesDynamic::find() + ->where(['product_id' => $this->guid, 'region_id' => $region_id]) + ->andWhere(['=', 'active', PricesDynamic::ACTIVE]) + ->one(); + +if ($priceModel) { + $priceModel->date_to = date('Y-m-d H:i:s'); + $priceModel->active = PricesDynamic::NOT_ACTIVE; + $priceModel->save(); +} + +$newPriceDynamic = new PricesDynamic(); +$newPriceDynamic->product_id = $this->guid; +$newPriceDynamic->active = PricesDynamic::ACTIVE; +$newPriceDynamic->date_from = date('Y-m-d H:i:s'); +$newPriceDynamic->date_to = date('2100-01-01 00:00:00'); +$newPriceDynamic->price = round($cost); +$newPriceDynamic->region_id = $region_id; +$newPriceDynamic->save(); +``` + +--- + +## Сценарии использования + +### 1. Сезонное ценообразование + +```php +// Летние цены (июнь-август) +$summerPrice = new PricesDynamic(); +$summerPrice->product_id = $guid; +$summerPrice->price = 800.00; +$summerPrice->date_from = '2025-06-01'; +$summerPrice->date_to = '2025-08-31'; +$summerPrice->active = PricesDynamic::ACTIVE; +$summerPrice->save(); + +// Зимние цены (декабрь-февраль) +$winterPrice = new PricesDynamic(); +$winterPrice->product_id = $guid; +$winterPrice->price = 1200.00; +$winterPrice->date_from = '2025-12-01'; +$winterPrice->date_to = '2026-02-28'; +$winterPrice->active = PricesDynamic::ACTIVE; +$winterPrice->save(); +``` + +### 2. Акционное ценообразование + +```php +// Обычная цена +$regularPrice = new PricesDynamic(); +$regularPrice->price = 1000.00; +$regularPrice->active = PricesDynamic::ACTIVE; +$regularPrice->save(); + +// Акция на неделю +$promoPrice = new PricesDynamic(); +$promoPrice->price = 700.00; +$promoPrice->date_from = date('Y-m-d'); +$promoPrice->date_to = date('Y-m-d', strtotime('+7 days')); +$promoPrice->active = PricesDynamic::ACTIVE; +$promoPrice->save(); +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/PricesRegion.md b/erp24/docs/models/PricesRegion.md new file mode 100644 index 00000000..fcdb3b63 --- /dev/null +++ b/erp24/docs/models/PricesRegion.md @@ -0,0 +1,512 @@ +# Модель PricesRegion + + +## Mindmap + +```mermaid +mindmap + root((PricesRegion)) + Таблица БД + prices_region + Свойства + id + int + product_id + string + region_id + int + price + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `PricesRegion` представляет справочник региональных цен на товары. Хранит розничные цены товаров для каждого региона отдельно, позволяя гибко управлять ценообразованием в зависимости от географического положения магазинов. Используется для установки разных цен на один и тот же товар в разных регионах. + +**Файл модели:** `erp24/records/PricesRegion.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `prices_region` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `product_id` | VARCHAR(36) | GUID товара из таблицы `products_1c` | +| `region_id` | INTEGER | ID региона | +| `price` | FLOAT | Розничная цена товара в регионе | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'prices_region'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = PricesRegion::tableName(); +// 'prices_region' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает все три поля (`product_id`, `region_id`, `price`) как обязательные +2. Устанавливает значение по умолчанию `null` для `region_id` +3. Проверяет тип данных: + - INTEGER для `region_id` + - FLOAT для `price` + - VARCHAR(36) для `product_id` +4. Ограничивает длину `product_id` до 36 символов (стандарт GUID) + +**Правила валидации:** +- **required**: `product_id`, `region_id`, `price` +- **default (null)**: `region_id` +- **integer**: `region_id` +- **number (float)**: `price` +- **string (max 36)**: `product_id` + +**Пример:** +```php +$regionPrice = new PricesRegion(); +$regionPrice->product_id = 'abc-123-456-guid'; +$regionPrice->region_id = 52; // Нижний Новгород +$regionPrice->price = 1299.99; + +if ($regionPrice->validate()) { + $regionPrice->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет английские названия полей для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `product_id` → "Product ID" +- `region_id` → "Region ID" +- `price` → "Price" + +**Пример:** +```php +$label = $regionPrice->getAttributeLabel('region_id'); +// "Region ID" +``` + +--- + +## Примеры использования + +### Создание региональной цены + +```php +$regionPrice = new PricesRegion(); +$regionPrice->product_id = 'abc-123-456-guid'; +$regionPrice->region_id = 77; // Москва +$regionPrice->price = 1500.00; + +if ($regionPrice->save()) { + echo "Региональная цена установлена"; +} +``` + +### Получение цены для конкретного региона + +```php +$price = PricesRegion::find() + ->where([ + 'product_id' => $productGuid, + 'region_id' => 52 // Нижний Новгород + ]) + ->one(); + +if ($price) { + echo "Цена в НН: {$price->price} руб."; +} else { + echo "Цена для региона не установлена"; +} +``` + +### Массовая установка цен для региона + +```php +$products = [ + 'guid-1' => 1000.00, + 'guid-2' => 1500.00, + 'guid-3' => 2000.00, +]; + +$regionId = 77; // Москва + +foreach ($products as $guid => $price) { + $regionPrice = PricesRegion::find() + ->where(['product_id' => $guid, 'region_id' => $regionId]) + ->one(); + + if (!$regionPrice) { + $regionPrice = new PricesRegion(); + $regionPrice->product_id = $guid; + $regionPrice->region_id = $regionId; + } + + $regionPrice->price = $price; + $regionPrice->save(); +} +``` + +### Сравнение цен между регионами + +```php +$productGuid = 'abc-123-456-guid'; + +$regionalPrices = PricesRegion::find() + ->where(['product_id' => $productGuid]) + ->indexBy('region_id') + ->all(); + +foreach ($regionalPrices as $regionId => $priceRecord) { + echo "Регион {$regionId}: {$priceRecord->price} руб." . PHP_EOL; +} +``` + +### Обновление цены в регионе + +```php +$regionPrice = PricesRegion::find() + ->where([ + 'product_id' => $productGuid, + 'region_id' => 52 + ]) + ->one(); + +if ($regionPrice) { + $regionPrice->price = 1800.00; + $regionPrice->save(); +} +``` + +### Поиск самых дорогих товаров в регионе + +```php +$expensiveProducts = PricesRegion::find() + ->where(['region_id' => 77]) // Москва + ->orderBy(['price' => SORT_DESC]) + ->limit(10) + ->all(); + +foreach ($expensiveProducts as $priceRecord) { + echo "Товар: {$priceRecord->product_id}, Цена: {$priceRecord->price} руб." . PHP_EOL; +} +``` + +### Расчет средней цены в регионе + +```php +$avgPrice = PricesRegion::find() + ->where(['region_id' => 52]) + ->average('price'); + +echo "Средняя цена в НН: {$avgPrice} руб."; +``` + +### Копирование цен из одного региона в другой + +```php +$sourceRegion = 77; // Москва +$targetRegion = 52; // Нижний Новгород +$priceMultiplier = 0.9; // Скидка 10% для НН + +$sourcePrices = PricesRegion::find() + ->where(['region_id' => $sourceRegion]) + ->all(); + +foreach ($sourcePrices as $sourcePrice) { + $targetPrice = PricesRegion::find() + ->where([ + 'product_id' => $sourcePrice->product_id, + 'region_id' => $targetRegion + ]) + ->one(); + + if (!$targetPrice) { + $targetPrice = new PricesRegion(); + $targetPrice->product_id = $sourcePrice->product_id; + $targetPrice->region_id = $targetRegion; + } + + $targetPrice->price = $sourcePrice->price * $priceMultiplier; + $targetPrice->save(); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `product_id` | Обязательное, макс. 36 символов (GUID) | +| `region_id` | Обязательное, целое число, default = null | +| `price` | Обязательное, число с плавающей точкой | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары из 1С (связь по `product_id`) +- **[PricesDynamic](./PricesDynamic.md)** — динамические цены с временными периодами +- **[PricesZakup](./PricesZakup.md)** — закупочные цены +- **Regions** — справочник регионов (связь по `region_id`) +- **[BouquetComposition](./BouquetComposition.md)** — использует региональные цены + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o{ prices_region : "has_regional_prices" + prices_region }o--|| regions : "belongs_to_region" + + products_1c { + string id PK + string name + string tip + } + + prices_region { + int id PK + string product_id FK + int region_id FK + float price + } + + regions { + int id PK + string name + int code + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Products1c] -->|product_id| B[PricesRegion] + C[Regions] -->|region_id| B + B --> D[Региональная цена] + D --> E[Расчет стоимости заказа] + D --> F[Отображение в каталоге] + D --> G[Формирование чека] + E --> H[Region 77 Москва] + E --> I[Region 52 НН] +``` + +--- + +## Особенности реализации + +### Составной ключ логический + +Хотя первичный ключ — `id`, логически уникальным является сочетание `(product_id, region_id)`. Один товар может иметь разные цены в разных регионах, но только одну цену в каждом регионе. + +**Рекомендация:** При работе с моделью всегда используйте оба поля для поиска: + +```php +$price = PricesRegion::find() + ->where(['product_id' => $guid, 'region_id' => $regionId]) + ->one(); +``` + +### Статичность цен + +В отличие от `PricesDynamic`, модель не поддерживает: +- Временные периоды действия цен +- Историю изменений +- Активацию/деактивацию + +При обновлении цены старое значение просто перезаписывается. + +### Интеграция с BouquetComposition + +Модель активно используется в системе букетов для установки региональных цен: + +```php +// Из BouquetComposition::setCost() +$pricesRegion = PricesRegion::find() + ->where(['product_id' => $this->guid, 'region_id' => $region_id]) + ->one(); + +if (!$pricesRegion) { + $pricesRegion = new PricesRegion(); + $pricesRegion->product_id = $this->guid; + $pricesRegion->region_id = $region_id; +} + +$pricesRegion->price = $newPrice; +$pricesRegion->save(); +``` + +--- + +## Регионы системы + +Основные регионы, используемые в ERP24: + +| ID | Регион | Описание | +|----|--------|----------| +| 52 | Нижний Новгород | Нижегородская область | +| 77 | Москва | Город Москва | + +**Пример из BouquetComposition:** +```php +const REGION_NN = 52; +const REGION_MSK = 77; + +public static function getRegions() { + return [ + self::REGION_NN, + self::REGION_MSK + ]; +} +``` + +--- + +## Использование в расчетах + +### Получение цены с учетом региона + +```php +function getProductPrice($productGuid, $regionId) { + // Сначала ищем региональную цену + $regionPrice = PricesRegion::findOne([ + 'product_id' => $productGuid, + 'region_id' => $regionId + ]); + + if ($regionPrice) { + return $regionPrice->price; + } + + // Если региональной нет, используем базовую из PricesDynamic + $dynamicPrice = PricesDynamic::find() + ->where(['product_id' => $productGuid, 'active' => 1]) + ->one(); + + return $dynamicPrice ? $dynamicPrice->price : 0; +} +``` + +### Синхронизация с PricesDynamic + +```php +// При создании динамической цены обновляем региональную +$dynamicPrice = PricesDynamic::findOne([ + 'product_id' => $guid, + 'region_id' => $regionId, + 'active' => 1 +]); + +if ($dynamicPrice) { + $regionPrice = PricesRegion::find() + ->where(['product_id' => $guid, 'region_id' => $regionId]) + ->one(); + + if (!$regionPrice) { + $regionPrice = new PricesRegion(); + $regionPrice->product_id = $guid; + $regionPrice->region_id = $regionId; + } + + $regionPrice->price = $dynamicPrice->price; + $regionPrice->save(); +} +``` + +--- + +## Сценарии использования + +### 1. Установка разных цен по регионам + +```php +$productGuid = 'abc-123-456-guid'; + +// Цена в Москве +$mskPrice = new PricesRegion(); +$mskPrice->product_id = $productGuid; +$mskPrice->region_id = 77; +$mskPrice->price = 2000.00; +$mskPrice->save(); + +// Цена в НН (ниже на 20%) +$nnPrice = new PricesRegion(); +$nnPrice->product_id = $productGuid; +$nnPrice->region_id = 52; +$nnPrice->price = 1600.00; +$nnPrice->save(); +``` + +### 2. Анализ ценовых различий + +```php +$products = Products1c::find()->where(['tip' => 'products'])->all(); + +foreach ($products as $product) { + $mskPrice = PricesRegion::findOne(['product_id' => $product->id, 'region_id' => 77]); + $nnPrice = PricesRegion::findOne(['product_id' => $product->id, 'region_id' => 52]); + + if ($mskPrice && $nnPrice) { + $diff = $mskPrice->price - $nnPrice->price; + $diffPercent = ($diff / $mskPrice->price) * 100; + + echo "{$product->name}: разница {$diff} руб. ({$diffPercent}%)" . PHP_EOL; + } +} +``` + +--- + +## Отличия от других ценовых моделей + +| Модель | Назначение | Регионы | Периоды | История | +|--------|-----------|---------|---------|---------| +| **PricesRegion** | Региональные розничные | Да | Нет | Нет | +| **PricesDynamic** | Динамические розничные | Да | Да | Да | +| **PricesZakup** | Закупочные | Нет | Нет | Нет | + +**PricesRegion** — это упрощенная статичная версия региональных цен, используется для быстрого доступа к актуальной цене в регионе без проверки временных периодов. + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/PricesZakup.md b/erp24/docs/models/PricesZakup.md new file mode 100644 index 00000000..39c43611 --- /dev/null +++ b/erp24/docs/models/PricesZakup.md @@ -0,0 +1,461 @@ +# Модель PricesZakup + + +## Mindmap + +```mermaid +mindmap + root((PricesZakup)) + Таблица БД + prices_zakup + Свойства + product_id + string + price + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `PricesZakup` представляет справочник закупочных цен товаров. Хранит актуальные закупочные цены для товаров из 1С. Используется для расчета себестоимости, планирования закупок и анализа рентабельности товаров. Простая модель с двумя полями: идентификатор товара и цена закупки. + +**Файл модели:** `erp24/records/PricesZakup.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `prices_zakup` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `product_id` | VARCHAR(36) | GUID товара (PRIMARY KEY, UNIQUE) | +| `price` | FLOAT | Закупочная цена товара | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'prices_zakup'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = PricesZakup::tableName(); +// 'prices_zakup' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает оба поля как обязательные +2. Проверяет тип данных для `price` (float) +3. Ограничивает длину `product_id` до 36 символов (стандарт GUID) +4. Проверяет уникальность `product_id` в таблице + +**Правила валидации:** +- **required**: `product_id`, `price` +- **number (float)**: `price` +- **string (max 36)**: `product_id` +- **unique**: `product_id` + +**Пример:** +```php +$zakup = new PricesZakup(); +$zakup->product_id = 'abc-123-456-guid'; +$zakup->price = 125.50; + +if ($zakup->validate()) { + $zakup->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет английские названия полей для использования в формах и сообщениях об ошибках. + +**Метки:** +- `product_id` → "Product ID" +- `price` → "Price" + +**Пример:** +```php +$label = $zakup->getAttributeLabel('price'); +// "Price" +``` + +--- + +## Примеры использования + +### Создание закупочной цены для товара + +```php +$zakup = new PricesZakup(); +$zakup->product_id = 'abc-123-456-guid'; +$zakup->price = 99.99; + +if ($zakup->save()) { + echo "Закупочная цена установлена"; +} +``` + +### Получение закупочной цены товара + +```php +$zakup = PricesZakup::findOne(['product_id' => $productGuid]); + +if ($zakup) { + echo "Закупочная цена: {$zakup->price} руб."; +} else { + echo "Закупочная цена не установлена"; +} +``` + +### Обновление закупочной цены + +```php +$zakup = PricesZakup::findOne(['product_id' => $productGuid]); + +if ($zakup) { + $zakup->price = 150.00; + $zakup->save(); +} else { + // Создаем новую запись + $zakup = new PricesZakup(); + $zakup->product_id = $productGuid; + $zakup->price = 150.00; + $zakup->save(); +} +``` + +### Массовое обновление цен по поставщику + +```php +use yii_app\records\Products1cOptions; + +// Получаем товары поставщика +$products = Products1cOptions::find() + ->where(['provider_id' => $providerId]) + ->all(); + +foreach ($products as $product) { + $zakup = PricesZakup::findOne(['product_id' => $product->id]); + + if (!$zakup) { + $zakup = new PricesZakup(); + $zakup->product_id = $product->id; + } + + // Устанавливаем цену из опций + $zakup->price = $product->price_zakup; + $zakup->save(); +} +``` + +### Поиск товаров по диапазону закупочных цен + +```php +$cheapProducts = PricesZakup::find() + ->where(['between', 'price', 50, 200]) + ->orderBy(['price' => SORT_ASC]) + ->all(); + +foreach ($cheapProducts as $zakup) { + echo "Товар: {$zakup->product_id}, Цена: {$zakup->price} руб." . PHP_EOL; +} +``` + +### Расчет средней закупочной цены + +```php +$averagePrice = PricesZakup::find() + ->average('price'); + +echo "Средняя закупочная цена: {$averagePrice} руб."; +``` + +### Поиск самых дорогих товаров + +```php +$expensiveProducts = PricesZakup::find() + ->orderBy(['price' => SORT_DESC]) + ->limit(10) + ->all(); + +foreach ($expensiveProducts as $zakup) { + echo "Товар: {$zakup->product_id}, Цена: {$zakup->price} руб." . PHP_EOL; +} +``` + +### Групповой импорт цен + +```php +$pricesData = [ + 'guid-1' => 100.00, + 'guid-2' => 150.00, + 'guid-3' => 200.00, +]; + +foreach ($pricesData as $guid => $price) { + $zakup = PricesZakup::findOne(['product_id' => $guid]); + + if (!$zakup) { + $zakup = new PricesZakup(); + $zakup->product_id = $guid; + } + + $zakup->price = $price; + $zakup->save(); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `product_id` | Обязательное, уникальное, макс. 36 символов (GUID) | +| `price` | Обязательное, число с плавающей точкой | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары из 1С (связь по `product_id`) +- **[Products1cOptions](./Products1cOptions.md)** — опции товаров (содержит дублирующее поле `price_zakup`) +- **[PricesDynamic](./PricesDynamic.md)** — динамические розничные цены +- **[PricesRegion](./PricesRegion.md)** — региональные цены + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o| prices_zakup : "has_purchase_price" + products_1c ||--o| products_1c_options : "has_options" + + products_1c { + string id PK + string name + string tip + } + + prices_zakup { + string product_id PK,FK + float price + } + + products_1c_options { + string id PK,FK + float price_zakup + int provider_id + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[1С Система] -->|Синхронизация| B[Products1c] + B -->|product_id| C[PricesZakup] + C --> D[Закупочная цена] + B -->|id| E[Products1cOptions] + E --> F[price_zakup дублирование] + D --> G[Расчет себестоимости] + D --> H[Планирование закупок] + D --> I[Анализ рентабельности] + F --> G +``` + +--- + +## Особенности реализации + +### Простая структура + +Модель имеет минималистичную структуру — всего два поля: +- `product_id` — идентификатор товара (первичный ключ) +- `price` — закупочная цена + +Это обеспечивает быстрый доступ к ценам закупки без сложных запросов. + +### Дублирование данных + +Закупочная цена дублируется в двух местах: +1. **PricesZakup** — основной справочник закупочных цен +2. **Products1cOptions** — поле `price_zakup` + +**Причины дублирования:** +- Разные источники данных (1С и ERP) +- Разная частота обновления +- Разные контексты использования + +### Уникальность product_id + +Поле `product_id` является одновременно: +- Первичным ключом +- Уникальным индексом +- Внешним ключом к `products_1c` + +Это гарантирует, что для каждого товара существует только одна закупочная цена. + +### Отсутствие истории изменений + +В отличие от `PricesDynamic`, модель не хранит историю изменения цен. При обновлении старое значение перезаписывается новым. Для отслеживания истории используются другие механизмы (логи, аудит). + +--- + +## Использование в расчетах + +### Расчет себестоимости букета + +```php +// Пример из BouquetComposition +$compositionProducts = $this->bouquetCompositionProducts; + +$selfCost = 0; +foreach ($compositionProducts as $item) { + $zakup = PricesZakup::findOne(['product_id' => $item->product_guid]); + + if ($zakup) { + $selfCost += $zakup->price * $item->count; + } +} + +echo "Себестоимость букета: {$selfCost} руб."; +``` + +### Расчет наценки + +```php +$zakup = PricesZakup::findOne(['product_id' => $productGuid]); +$retail = PricesDynamic::find() + ->where(['product_id' => $productGuid, 'active' => 1]) + ->one(); + +if ($zakup && $retail) { + $markup = (($retail->price / $zakup->price) - 1) * 100; + echo "Наценка: {$markup}%"; +} +``` + +### Планирование закупки + +```php +$products = PricesZakup::find() + ->innerJoin('products_1c_options', 'products_1c_options.id = prices_zakup.product_id') + ->where(['products_1c_options.main' => 1]) + ->all(); + +$totalCost = 0; +foreach ($products as $zakup) { + $options = Products1cOptions::findOne(['id' => $zakup->product_id]); + $orderQuantity = $options->min_order; + $totalCost += $zakup->price * $orderQuantity; +} + +echo "Стоимость минимального заказа: {$totalCost} руб."; +``` + +--- + +## Синхронизация с 1С + +Модель обновляется при синхронизации с 1С: + +```php +// Пример синхронизации +$products1c = Products1c::find()->where(['tip' => 'products'])->all(); + +foreach ($products1c as $product) { + // Получаем закупочную цену из 1С + $zakupPrice = $product->getZakupPriceFrom1C(); + + $zakup = PricesZakup::findOne(['product_id' => $product->id]); + + if (!$zakup) { + $zakup = new PricesZakup(); + $zakup->product_id = $product->id; + } + + $zakup->price = $zakupPrice; + $zakup->save(); +} +``` + +--- + +## Отличия от других ценовых моделей + +| Модель | Назначение | Временные периоды | Регионы | История | +|--------|-----------|------------------|---------|---------| +| **PricesZakup** | Закупочные цены | Нет | Нет | Нет | +| **PricesDynamic** | Розничные цены | Да | Да | Да | +| **PricesRegion** | Региональные цены | Нет | Да | Нет | +| **Products1cOptions.price_zakup** | Закупочные цены | Нет | Нет | Нет | + +--- + +## Сценарии использования + +### 1. Анализ рентабельности + +```php +$products = PricesZakup::find() + ->alias('z') + ->innerJoin('prices_dynamic d', 'd.product_id = z.product_id AND d.active = 1') + ->select(['z.product_id', 'z.price as zakup', 'd.price as retail']) + ->asArray() + ->all(); + +foreach ($products as $product) { + $profit = $product['retail'] - $product['zakup']; + $margin = ($profit / $product['retail']) * 100; + + echo "Товар: {$product['product_id']}, Маржа: {$margin}%" . PHP_EOL; +} +``` + +### 2. Отчет по закупкам + +```php +$totalZakup = PricesZakup::find()->sum('price'); +$avgZakup = PricesZakup::find()->average('price'); +$count = PricesZakup::find()->count(); + +echo "Всего товаров: {$count}" . PHP_EOL; +echo "Средняя закупочная цена: {$avgZakup} руб." . PHP_EOL; +echo "Общая стоимость: {$totalZakup} руб." . PHP_EOL; +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Product1cReplacement.md b/erp24/docs/models/Product1cReplacement.md new file mode 100644 index 00000000..12d35b13 --- /dev/null +++ b/erp24/docs/models/Product1cReplacement.md @@ -0,0 +1,318 @@ +# Класс: Product1cReplacement + + +## Mindmap + +```mermaid +mindmap + root((Product1cReplacement)) + Таблица БД + product_1c_replacement + Свойства + id + int + guid + string + guid_replacement + string + Связи + Product1CReplacementLogs + 1:N Product1cReplacementLog + Product + 1:1 Products1c + ReplacementProduct + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель замен товаров в ERP24. Определяет связи между товарами и их заменами для автоматической подстановки альтернативного товара при отсутствии основного на складе. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`product_1c_replacement` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Описание | +|-----------|----------| +| `TimestampBehavior` | Автоматическое заполнение created_at/updated_at | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(255) | GUID основного товара | +| `guid_replacement` | varchar(255) | GUID товара-замены | +| `created_at` | datetime / null | Дата создания (автозаполнение) | +| `updated_at` | datetime / null | Дата обновления (автозаполнение) | +| `deleted_at` | datetime / null | Дата мягкого удаления | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getProduct()` | hasOne | Products1c | Основной товар (tip='products') | +| `getReplacementProduct()` | hasOne | Products1c | Товар-замена | +| `getProduct1CReplacementLogs()` | hasMany | Product1cReplacementLog | История изменений | + +## Методы + +### softDelete() +**Описание:** Мягкое удаление записи. + +**Возвращает:** `bool` — результат сохранения + +**Логика работы:** +1. Устанавливает deleted_at в текущую дату/время +2. Сохраняет только поле deleted_at +3. Запись остаётся в БД, но исключается из выборок + +### restore() +**Описание:** Восстановление мягко удалённой записи. + +**Возвращает:** `bool` — результат сохранения + +**Логика работы:** +1. Устанавливает deleted_at в null +2. Сохраняет только поле deleted_at +3. Запись снова появляется в выборках + +### find() (переопределённый) +**Описание:** Возвращает ActiveQuery с автофильтром по мягкому удалению. + +**Возвращает:** `ActiveQuery` — запрос с условием `deleted_at = null` + +**Логика работы:** +1. Вызывает родительский find() +2. Добавляет условие `['deleted_at' => null]` +3. Все выборки автоматически исключают удалённые записи + +### afterSave() +**Описание:** Автоматическое логирование изменений после сохранения. + +**Параметры:** +- `$insert` (bool) — true если это вставка новой записи +- `$changedAttributes` (array) — изменённые атрибуты + +**Логика работы:** +1. Вызывает родительский afterSave +2. Если это обновление (не insert): + - Создаёт запись Product1cReplacementLog + - Сохраняет state_before и state_after + - Фиксирует admin_id текущего пользователя + +## Диаграмма связей + +```mermaid +erDiagram + Product1cReplacement { + int id PK + varchar guid FK + varchar guid_replacement FK + datetime created_at + datetime updated_at + datetime deleted_at + } + + Products1c { + varchar id PK + varchar name + varchar tip + } + + Product1cReplacementLog { + int id PK + int replacement_id FK + varchar state_before + varchar state_after + int admin_id FK + } + + Products1c ||--o{ Product1cReplacement : "guid" + Products1c ||--o{ Product1cReplacement : "guid_replacement" + Product1cReplacement ||--o{ Product1cReplacementLog : "replacement_id" +``` + +## Диаграмма логики замены товара + +```mermaid +flowchart TD + A[Запрос товара] --> B{Товар в наличии?} + B -->|Да| C[Вернуть основной товар] + B -->|Нет| D[Поиск в Product1cReplacement] + D --> E{Есть замена?} + E -->|Нет| F[Товар недоступен] + E -->|Да| G[Получить guid_replacement] + G --> H{Замена в наличии?} + H -->|Да| I[Вернуть товар-замену] + H -->|Нет| J[Рекурсивный поиск замены замены] + J --> K{Найдена замена?} + K -->|Да| I + K -->|Нет| F +``` + +## Примеры использования + +### Создание замены товара +```php +$replacement = new Product1cReplacement(); +$replacement->guid = $mainProductGuid; +$replacement->guid_replacement = $alternativeProductGuid; +$replacement->save(); +``` + +### Получение замены для товара +```php +$replacement = Product1cReplacement::find() + ->where(['guid' => $productGuid]) + ->one(); + +if ($replacement) { + $alternativeProduct = $replacement->replacementProduct; + echo "Замена: {$alternativeProduct->name}"; +} +``` + +### Поиск товара с каскадной заменой +```php +function findAvailableProduct($guid, $storeId, $visited = []) +{ + // Защита от циклических ссылок + if (in_array($guid, $visited)) { + return null; + } + $visited[] = $guid; + + // Проверяем наличие основного товара + $stock = Products1c::find() + ->where(['id' => $guid, 'store_id' => $storeId]) + ->andWhere(['>', 'quantity', 0]) + ->one(); + + if ($stock) { + return $stock; + } + + // Ищем замену + $replacement = Product1cReplacement::find() + ->where(['guid' => $guid]) + ->one(); + + if ($replacement) { + return findAvailableProduct($replacement->guid_replacement, $storeId, $visited); + } + + return null; +} +``` + +### Получение всех замен +```php +$replacements = Product1cReplacement::find() + ->with(['product', 'replacementProduct']) + ->all(); + +foreach ($replacements as $r) { + $mainName = $r->product->name ?? 'Unknown'; + $altName = $r->replacementProduct->name ?? 'Unknown'; + echo "{$mainName} -> {$altName}\n"; +} +``` + +### Мягкое удаление замены +```php +$replacement = Product1cReplacement::findOne($id); +if ($replacement) { + $replacement->softDelete(); + echo "Замена деактивирована"; +} +``` + +### Восстановление удалённой замены +```php +// Для поиска удалённых нужно обойти переопределённый find() +$deleted = Product1cReplacement::find() + ->where(['id' => $id]) + ->andWhere(['not', ['deleted_at' => null]]) + ->one(); + +// Альтернативный способ через прямой запрос +$deleted = (new \yii\db\Query()) + ->from('product_1c_replacement') + ->where(['id' => $id]) + ->one(); + +if ($deleted) { + $model = new Product1cReplacement(); + $model->setAttributes($deleted); + $model->setIsNewRecord(false); + $model->restore(); +} +``` + +### История изменений замены +```php +$replacement = Product1cReplacement::findOne($id); + +if ($replacement) { + $logs = $replacement->product1CReplacementLogs; + + foreach ($logs as $log) { + echo "{$log->date}: {$log->state_before} -> {$log->state_after}\n"; + } +} +``` + +### Массовое создание замен +```php +$replacementData = [ + ['guid1', 'replacement_guid1'], + ['guid2', 'replacement_guid2'], + ['guid3', 'replacement_guid3'], +]; + +foreach ($replacementData as [$guid, $replGuid]) { + $exists = Product1cReplacement::find() + ->where(['guid' => $guid]) + ->exists(); + + if (!$exists) { + $r = new Product1cReplacement(); + $r->guid = $guid; + $r->guid_replacement = $replGuid; + $r->save(); + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | required, string (max 255) | +| `guid_replacement` | required, string (max 255) | +| `created_at` | safe | +| `updated_at` | safe | +| `deleted_at` | safe | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары +- [Product1cReplacementLog](./Product1cReplacementLog.md) — история изменений + +## Особенности реализации + +1. **Soft Delete**: Мягкое удаление через deleted_at с автофильтром в find() +2. **Автологирование**: afterSave автоматически создаёт записи в логе +3. **TimestampBehavior**: Автоматические created_at/updated_at +4. **Интеграция с 1С**: Использование GUID для связи с товарами +5. **Фильтр по типу**: getProduct() фильтрует только товары (tip='products') +6. **Каскадные замены**: Возможность построения цепочки замен diff --git a/erp24/docs/models/Product1cReplacementLog.md b/erp24/docs/models/Product1cReplacementLog.md new file mode 100644 index 00000000..b588940a --- /dev/null +++ b/erp24/docs/models/Product1cReplacementLog.md @@ -0,0 +1,237 @@ +# Класс: Product1cReplacementLog + + +## Mindmap + +```mermaid +mindmap + root((Product1cReplacementLog)) + Таблица БД + product_1c_replacement_log + Свойства + id + int + replacement_id + int + state_before + string + state_after + string + admin_id + int + replacement + Product1cReplacementLog + Связи + Replacement + 1:1 Product1cReplacement + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории изменений замен товаров в ERP24. Логирует все изменения в настройках замен товаров с фиксацией состояния до и после изменения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`product_1c_replacement_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `replacement_id` | int | FK на запись замены (Product1cReplacement) | +| `state_before` | varchar(255) | GUID замены до изменения | +| `state_after` | varchar(255) | GUID замены после изменения | +| `date` | datetime / null | Дата и время изменения | +| `admin_id` | int | FK на администратора (Admin) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getReplacement()` | hasOne | Product1cReplacement | Связанная запись замены | + +## Диаграмма связей + +```mermaid +erDiagram + Product1cReplacementLog { + int id PK + int replacement_id FK + varchar state_before + varchar state_after + datetime date + int admin_id FK + } + + Product1cReplacement { + int id PK + varchar guid + varchar guid_replacement + } + + Admin { + int id PK + varchar name + } + + Products1c { + varchar id PK + varchar name + } + + Product1cReplacement ||--o{ Product1cReplacementLog : "replacement_id" + Admin ||--o{ Product1cReplacementLog : "admin_id" + Products1c ||--|| Product1cReplacementLog : "state_before/after = guid" +``` + +## Диаграмма процесса логирования + +```mermaid +flowchart TD + A[Изменение Product1cReplacement] --> B[afterSave triggered] + B --> C{Это update?} + C -->|Нет insert| D[Пропуск логирования] + C -->|Да| E[Создание Product1cReplacementLog] + E --> F[state_before = changedAttributes guid_replacement] + F --> G[state_after = текущий guid_replacement] + G --> H[admin_id = текущий пользователь] + H --> I[Сохранение лога] +``` + +## Примеры использования + +### Создание записи лога вручную +```php +$log = new Product1cReplacementLog(); +$log->replacement_id = $replacement->id; +$log->state_before = $oldReplacementGuid; +$log->state_after = $newReplacementGuid; +$log->admin_id = Yii::$app->user->id; +$log->date = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Получение истории изменений замены +```php +$logs = Product1cReplacementLog::find() + ->where(['replacement_id' => $replacementId]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + $beforeProduct = Products1c::findOne($log->state_before); + $afterProduct = Products1c::findOne($log->state_after); + + $beforeName = $beforeProduct->name ?? $log->state_before; + $afterName = $afterProduct->name ?? $log->state_after; + + echo "{$log->date}: {$beforeName} -> {$afterName}\n"; +} +``` + +### Получение логов с информацией о замене +```php +$logs = Product1cReplacementLog::find() + ->with(['replacement', 'replacement.product']) + ->orderBy(['date' => SORT_DESC]) + ->limit(50) + ->all(); + +foreach ($logs as $log) { + $mainProduct = $log->replacement->product->name ?? 'Unknown'; + echo "Товар: {$mainProduct}\n"; + echo "Изменение: {$log->state_before} -> {$log->state_after}\n"; +} +``` + +### Статистика изменений по администраторам +```php +$stats = Product1cReplacementLog::find() + ->select(['admin_id', 'COUNT(*) as count']) + ->where(['>=', 'date', date('Y-m-01')]) + ->groupBy('admin_id') + ->asArray() + ->all(); + +$admins = ArrayHelper::index(Admin::find()->all(), 'id'); + +foreach ($stats as $stat) { + $adminName = $admins[$stat['admin_id']]->name ?? 'Unknown'; + echo "{$adminName}: {$stat['count']} изменений\n"; +} +``` + +### Поиск изменений за период +```php +$recentLogs = Product1cReplacementLog::find() + ->where(['>=', 'date', date('Y-m-d', strtotime('-7 days'))]) + ->with(['replacement']) + ->orderBy(['date' => SORT_DESC]) + ->all(); +``` + +### Анализ частоты изменений по товару +```php +$frequentChanges = Product1cReplacementLog::find() + ->select(['replacement_id', 'COUNT(*) as changes']) + ->where(['>=', 'date', date('Y-m-01')]) + ->groupBy('replacement_id') + ->having(['>', 'COUNT(*)', 3]) + ->orderBy(['changes' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($frequentChanges as $item) { + $replacement = Product1cReplacement::findOne($item['replacement_id']); + $productName = $replacement->product->name ?? 'Unknown'; + echo "{$productName}: {$item['changes']} изменений замены\n"; +} +``` + +### Восстановление предыдущей замены +```php +$lastLog = Product1cReplacementLog::find() + ->where(['replacement_id' => $replacementId]) + ->orderBy(['date' => SORT_DESC]) + ->one(); + +if ($lastLog) { + $replacement = $lastLog->replacement; + $replacement->guid_replacement = $lastLog->state_before; + $replacement->save(); + + echo "Замена восстановлена до предыдущего состояния"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `replacement_id` | required, integer, exists в Product1cReplacement | +| `state_before` | required, string (max 255) | +| `state_after` | required, string (max 255) | +| `admin_id` | required, integer | +| `date` | safe | + +## Связанные модели + +- [Product1cReplacement](./Product1cReplacement.md) — замены товаров +- [Admin](./Admin.md) — администраторы +- [Products1c](./Products1c.md) — товары (для расшифровки GUID) + +## Особенности реализации + +1. **Автоматическое создание**: Записи создаются автоматически в afterSave родительской модели +2. **GUID хранение**: state_before/after хранят GUID товаров-замен +3. **Аудит**: Фиксируется администратор, выполнивший изменение +4. **Временная метка**: date для хронологии изменений +5. **Связь с заменой**: replacement_id для группировки истории по записям замен +6. **Возможность отката**: Хранение state_before позволяет восстановить предыдущее состояние diff --git a/erp24/docs/models/Product1cReplacementSearch.md b/erp24/docs/models/Product1cReplacementSearch.md new file mode 100644 index 00000000..49a21812 --- /dev/null +++ b/erp24/docs/models/Product1cReplacementSearch.md @@ -0,0 +1,181 @@ +# Класс: Product1cReplacementSearch + + +## Mindmap + +```mermaid +mindmap + root((Product1cReplacementSearch)) + Таблица БД + ActiveRecord + Наследование + extends Product1cReplacement +``` + +## Назначение +Search-модель для поиска и фильтрации замен товаров из 1С в ERP24. Модель с JOIN к products_1c для поиска по GUID, а также по артикулу и названию связанного товара. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Product1cReplacement` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$productName` | string | Для поиска по имени товара (не используется в текущей реализации) | +| `$productCode` | string | Для поиска по коду товара (не используется в текущей реализации) | +| `$productArticule` | string | Для поиска по артикулу товара (не используется в текущей реализации) | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `guid`, `guid_replacement`, `created_at`, `updated_at`, `deleted_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к таблице товаров. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с joinWith для связи product +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, created_at, updated_at + - ilike: guid, guid_replacement + - **orFilterWhere** ilike: products_1c.articule, products_1c.name по значению guid + +**Особенность:** Поиск по полю guid также ищет по артикулу и названию товара через OR. + +## Диаграмма связей + +```mermaid +erDiagram + Product1cReplacement { + int id PK + varchar guid FK + varchar guid_replacement + datetime created_at + datetime updated_at + datetime deleted_at + } + + Products1c { + varchar id PK + varchar articule + varchar name + } + + Product1cReplacement }o--|| Products1c : "guid -> id" +``` + +## Диаграмма логики поиска + +```mermaid +flowchart TD + A[Поиск по guid] --> B{Условия} + B --> C[ilike guid] + B --> D[OR ilike products_1c.articule] + B --> E[OR ilike products_1c.name] + + C --> F[Результат] + D --> F + E --> F +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new Product1cReplacementSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по GUID товара +```php +$searchModel = new Product1cReplacementSearch(); +$dataProvider = $searchModel->search([ + 'Product1cReplacementSearch' => [ + 'guid' => 'abc-123-def', + ] +]); +``` + +### Поиск по артикулу или названию (через guid) +```php +// Поле guid также ищет по артикулу и названию товара +$searchModel = new Product1cReplacementSearch(); +$dataProvider = $searchModel->search([ + 'Product1cReplacementSearch' => [ + 'guid' => 'FLOWER-001', // Найдёт по артикулу + ] +]); +``` + +### Поиск по GUID замены +```php +$searchModel = new Product1cReplacementSearch(); +$dataProvider = $searchModel->search([ + 'Product1cReplacementSearch' => [ + 'guid_replacement' => 'xyz-789', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'guid', + [ + 'attribute' => 'product', + 'value' => function($model) { + return $model->product ? $model->product->name : '-'; + }, + ], + 'guid_replacement', + 'created_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [Product1cReplacement](./Product1cReplacement.md) — базовая модель замен +- [Products1c](./Products1c.md) — товары из 1С + +## Особенности реализации + +1. **Мульти-поиск по guid**: Ищет по guid, articule и name через OR +2. **joinWith product**: Связь с таблицей products_1c +3. **Soft delete**: deleted_at для мягкого удаления +4. **ilike поиск**: Регистронезависимый для PostgreSQL +5. **Неиспользуемые свойства**: productName, productCode, productArticule объявлены, но не применяются в фильтрах diff --git a/erp24/docs/models/ProductionCalendar.md b/erp24/docs/models/ProductionCalendar.md new file mode 100644 index 00000000..11279f95 --- /dev/null +++ b/erp24/docs/models/ProductionCalendar.md @@ -0,0 +1,292 @@ +# Класс: ProductionCalendar + + +## Mindmap + +```mermaid +mindmap + root((ProductionCalendar)) + Таблица БД + production_calendar + Свойства + id + int + date + string + work + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель производственного календаря в ERP24. Хранит информацию о рабочих и выходных днях для корректного расчёта сроков, планирования и учёта рабочего времени с учётом государственных праздников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`production_calendar` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `date` | varchar(10) | Дата в формате YYYY-MM-DD | +| `work` | int | Признак рабочего дня (1=рабочий, 0=выходной/праздник) | + +## Значения поля work + +| Значение | Описание | +|----------|----------| +| `1` | Рабочий день (будни) | +| `0` | Выходной день или праздник | + +## Диаграмма связей + +```mermaid +erDiagram + ProductionCalendar { + int id PK + varchar date UK + int work + } + + Timetable { + int id PK + date work_date + } + + PlanStore { + int store_id PK + int year PK + int month PK + int day PK + } + + ProductionCalendar ||--o| Timetable : "используется для расчёта" + ProductionCalendar ||--o| PlanStore : "используется для планирования" +``` + +## Диаграмма использования календаря + +```mermaid +flowchart TD + A[Запрос: сколько рабочих дней?] --> B[Получение периода дат] + B --> C[Выборка из ProductionCalendar] + C --> D[Фильтр work = 1] + D --> E[Подсчёт количества] + E --> F[Результат: N рабочих дней] + + G[Запрос: следующий рабочий день] --> H[Текущая дата] + H --> I[Поиск в ProductionCalendar] + I --> J{work = 1?} + J -->|Да| K[Вернуть дату] + J -->|Нет| L[Дата + 1 день] + L --> I +``` + +## Примеры использования + +### Проверка рабочего дня +```php +$isWorkday = ProductionCalendar::find() + ->where([ + 'date' => date('Y-m-d'), + 'work' => 1 + ]) + ->exists(); + +if ($isWorkday) { + echo "Сегодня рабочий день"; +} else { + echo "Сегодня выходной или праздник"; +} +``` + +### Получение следующего рабочего дня +```php +function getNextWorkday($fromDate = null) +{ + $date = $fromDate ? new DateTime($fromDate) : new DateTime(); + $date->modify('+1 day'); + + while (true) { + $dateStr = $date->format('Y-m-d'); + $isWorkday = ProductionCalendar::find() + ->where(['date' => $dateStr, 'work' => 1]) + ->exists(); + + if ($isWorkday) { + return $dateStr; + } + + $date->modify('+1 day'); + + // Защита от бесконечного цикла + if ($date > (new DateTime())->modify('+1 year')) { + return null; + } + } +} + +$nextWorkday = getNextWorkday(); +echo "Следующий рабочий день: {$nextWorkday}"; +``` + +### Подсчёт рабочих дней в периоде +```php +$workdaysCount = ProductionCalendar::find() + ->where(['work' => 1]) + ->andWhere(['>=', 'date', '2024-12-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->count(); + +echo "Рабочих дней в декабре: {$workdaysCount}"; +``` + +### Получение списка выходных в месяце +```php +$holidays = ProductionCalendar::find() + ->where(['work' => 0]) + ->andWhere(['like', 'date', '2024-12%', false]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +foreach ($holidays as $day) { + echo "Выходной: {$day->date}\n"; +} +``` + +### Расчёт даты выполнения через N рабочих дней +```php +function addWorkdays($startDate, $workdays) +{ + $date = new DateTime($startDate); + $added = 0; + + while ($added < $workdays) { + $date->modify('+1 day'); + $dateStr = $date->format('Y-m-d'); + + $isWorkday = ProductionCalendar::find() + ->where(['date' => $dateStr, 'work' => 1]) + ->exists(); + + if ($isWorkday) { + $added++; + } + } + + return $date->format('Y-m-d'); +} + +$deadline = addWorkdays('2024-12-20', 5); +echo "Срок через 5 рабочих дней: {$deadline}"; +``` + +### Импорт производственного календаря +```php +$calendarData = [ + '2025-01-01' => 0, // Новый год + '2025-01-02' => 0, + '2025-01-03' => 0, + '2025-01-06' => 1, // Рабочий день + '2025-01-07' => 0, // Рождество + // ... +]; + +foreach ($calendarData as $date => $work) { + $existing = ProductionCalendar::find() + ->where(['date' => $date]) + ->one(); + + if ($existing) { + $existing->work = $work; + $existing->save(); + } else { + $day = new ProductionCalendar(); + $day->date = $date; + $day->work = $work; + $day->save(); + } +} +``` + +### Генерация календаря на год +```php +function generateYearCalendar($year) +{ + $startDate = new DateTime("{$year}-01-01"); + $endDate = new DateTime("{$year}-12-31"); + + $interval = new DateInterval('P1D'); + $period = new DatePeriod($startDate, $interval, $endDate->modify('+1 day')); + + foreach ($period as $date) { + $dateStr = $date->format('Y-m-d'); + $dayOfWeek = $date->format('N'); // 1=Пн, 7=Вс + + // По умолчанию: Сб и Вс - выходные + $work = ($dayOfWeek < 6) ? 1 : 0; + + $exists = ProductionCalendar::find() + ->where(['date' => $dateStr]) + ->exists(); + + if (!$exists) { + $day = new ProductionCalendar(); + $day->date = $dateStr; + $day->work = $work; + $day->save(); + } + } +} + +generateYearCalendar(2025); +``` + +### Статистика по месяцам +```php +$monthlyStats = ProductionCalendar::find() + ->select([ + "SUBSTRING(date, 1, 7) as month", + "SUM(work) as workdays", + "COUNT(*) - SUM(work) as holidays" + ]) + ->where(['like', 'date', '2024%', false]) + ->groupBy("SUBSTRING(date, 1, 7)") + ->orderBy(['month' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($monthlyStats as $stat) { + echo "{$stat['month']}: {$stat['workdays']} рабочих, {$stat['holidays']} выходных\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `date` | required, string (max 10) | +| `work` | required, integer | + +## Связанные модели + +- [Timetable](./Timetable.md) — расписание работы +- [PlanStore](./PlanStore.md) — планы магазинов +- [Holiday](./Holiday.md) — праздники (альтернативный справочник) + +## Особенности реализации + +1. **Простая структура**: Минимальный набор полей для эффективных запросов +2. **Формат даты**: varchar(10) для YYYY-MM-DD обеспечивает простое сравнение +3. **Бинарный признак**: work принимает только 0 или 1 +4. **Ежегодное обновление**: Требуется импорт данных на каждый год +5. **Учёт праздников**: Государственные праздники отмечаются как work=0 +6. **Переносы выходных**: Учитываются переносы рабочих дней с выходных diff --git a/erp24/docs/models/Products1c.md b/erp24/docs/models/Products1c.md index 7592b5b7..ce1c0bf9 100644 --- a/erp24/docs/models/Products1c.md +++ b/erp24/docs/models/Products1c.md @@ -1,5 +1,41 @@ # Модель: Products1c + +## Mindmap + +```mermaid +mindmap + root((Products1c)) + Таблица БД + products_1c + Свойства + id + string + parent_id + string + tip + string + code + string + name + string + articule + string + Связи + ProductClass + 1:1 ProductsClass + ExportImportStore + 1:1 ExportImportTable + Store + 1:1 CityStore + Options + 1:1 Products1cOptions + StoreAdmins + 1:N AdminStores + Наследование + extends yiidbActiveRecord +``` + ## Назначение `Products1c` — базовая модель ActiveRecord для работы с таблицей `products_1c`, представляющей товары и группы товаров, импортированные из 1С. Эта модель является основой для хранения каталога продукции ERP24 и используется для управления иерархией товаров, групп товаров, магазинов и связанных справочников. diff --git a/erp24/docs/models/Products1cAdditionalCharacteristics.md b/erp24/docs/models/Products1cAdditionalCharacteristics.md new file mode 100644 index 00000000..789f7b68 --- /dev/null +++ b/erp24/docs/models/Products1cAdditionalCharacteristics.md @@ -0,0 +1,262 @@ +# Класс: Products1cAdditionalCharacteristics + + +## Mindmap + +```mermaid +mindmap + root((Products1cAdditionalCharacteristics)) + Таблица БД + products_1c_additional_characteristics + Свойства + product_id + string + property_id + string + value + string + property + Products1cPropType + Связи + Property + 1:1 Products1cPropType + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель дополнительных характеристик товаров в ERP24. Реализует EAV-паттерн (Entity-Attribute-Value) для хранения произвольных свойств товаров из 1С с возможностью динамического расширения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`products_1c_additional_characteristics` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `product_id` | varchar(255) | GUID товара (PK, FK на Products1c) | +| `property_id` | varchar(255) | ID типа свойства (PK, FK на Products1cPropType) | +| `value` | varchar(255) | Значение свойства | + +## Составной первичный ключ + +```php +public static function primaryKey() +{ + return ['product_id', 'property_id']; +} +``` + +Первичный ключ состоит из двух полей: `product_id` + `property_id`, что обеспечивает уникальность пары товар-свойство. + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getProperty()` | hasOne | Products1cPropType | Тип свойства | + +## Диаграмма связей + +```mermaid +erDiagram + Products1cAdditionalCharacteristics { + varchar product_id PK,FK + varchar property_id PK,FK + varchar value + } + + Products1c { + varchar id PK + varchar name + } + + Products1cPropType { + varchar id PK + varchar name + } + + Products1c ||--o{ Products1cAdditionalCharacteristics : "product_id" + Products1cPropType ||--o{ Products1cAdditionalCharacteristics : "property_id" +``` + +## Диаграмма EAV-паттерна + +```mermaid +flowchart TD + subgraph Entity + A[Products1c
    Товар] + end + + subgraph Attribute + B[Products1cPropType
    Тип свойства] + end + + subgraph Value + C[Products1cAdditionalCharacteristics
    Значение] + end + + A --> C + B --> C + C -->|product_id| A + C -->|property_id| B + C -->|value| D[Строковое значение] +``` + +## Примеры использования + +### Добавление характеристики товару +```php +$characteristic = new Products1cAdditionalCharacteristics(); +$characteristic->product_id = $productGuid; +$characteristic->property_id = $propertyTypeId; +$characteristic->value = 'Красный'; +$characteristic->save(); +``` + +### Получение всех характеристик товара +```php +$characteristics = Products1cAdditionalCharacteristics::find() + ->where(['product_id' => $productGuid]) + ->with(['property']) + ->all(); + +foreach ($characteristics as $char) { + $propertyName = $char->property->name ?? 'Unknown'; + echo "{$propertyName}: {$char->value}\n"; +} +``` + +### Получение характеристик в виде массива +```php +$characteristics = Products1cAdditionalCharacteristics::find() + ->where(['product_id' => $productGuid]) + ->with(['property']) + ->all(); + +$result = []; +foreach ($characteristics as $char) { + $result[$char->property->name] = $char->value; +} + +// ['Цвет' => 'Красный', 'Размер' => 'M', 'Материал' => 'Хлопок'] +``` + +### Поиск товаров по характеристике +```php +// Найти товары красного цвета +$colorPropertyId = 'color-property-guid'; + +$productIds = Products1cAdditionalCharacteristics::find() + ->select('product_id') + ->where([ + 'property_id' => $colorPropertyId, + 'value' => 'Красный' + ]) + ->column(); + +$redProducts = Products1c::find() + ->where(['id' => $productIds]) + ->all(); +``` + +### Обновление или создание характеристики +```php +$char = Products1cAdditionalCharacteristics::find() + ->where([ + 'product_id' => $productGuid, + 'property_id' => $propertyId + ]) + ->one(); + +if (!$char) { + $char = new Products1cAdditionalCharacteristics(); + $char->product_id = $productGuid; + $char->property_id = $propertyId; +} + +$char->value = $newValue; +$char->save(); +``` + +### Массовый импорт характеристик +```php +$characteristics = [ + ['product_id' => 'guid1', 'property_id' => 'color', 'value' => 'Красный'], + ['product_id' => 'guid1', 'property_id' => 'size', 'value' => 'M'], + ['product_id' => 'guid2', 'property_id' => 'color', 'value' => 'Синий'], +]; + +foreach ($characteristics as $data) { + $char = new Products1cAdditionalCharacteristics(); + $char->setAttributes($data); + $char->save(); +} +``` + +### Удаление характеристик товара +```php +Products1cAdditionalCharacteristics::deleteAll([ + 'product_id' => $productGuid +]); +``` + +### Фильтрация товаров по нескольким характеристикам +```php +// Товары красного цвета размера M +$redMProducts = Products1c::find() + ->alias('p') + ->innerJoin( + 'products_1c_additional_characteristics c1', + 'p.id = c1.product_id AND c1.property_id = :colorProp AND c1.value = :colorVal', + [':colorProp' => $colorPropertyId, ':colorVal' => 'Красный'] + ) + ->innerJoin( + 'products_1c_additional_characteristics c2', + 'p.id = c2.product_id AND c2.property_id = :sizeProp AND c2.value = :sizeVal', + [':sizeProp' => $sizePropertyId, ':sizeVal' => 'M'] + ) + ->all(); +``` + +### Статистика по значениям свойства +```php +$colorStats = Products1cAdditionalCharacteristics::find() + ->select(['value', 'COUNT(*) as count']) + ->where(['property_id' => $colorPropertyId]) + ->groupBy('value') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($colorStats as $stat) { + echo "{$stat['value']}: {$stat['count']} товаров\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `product_id` | required, string (max 255) | +| `property_id` | required, string (max 255), exists в Products1cPropType | +| `value` | required, string (max 255) | + +## Связанные модели + +- [Products1c](./Products1c.md) — товары +- [Products1cPropType](./Products1cPropType.md) — типы свойств + +## Особенности реализации + +1. **EAV-паттерн**: Гибкое хранение произвольных свойств без изменения схемы БД +2. **Составной PK**: product_id + property_id обеспечивает уникальность +3. **GUID ключи**: Совместимость с 1С через GUID идентификаторы +4. **Внешний ключ**: property_id валидируется на существование в Products1cPropType +5. **Строковые значения**: Все значения хранятся как varchar(255) +6. **Синхронизация с 1С**: Данные импортируются из 1С при обмене diff --git a/erp24/docs/models/Products1cNomenclature.md b/erp24/docs/models/Products1cNomenclature.md new file mode 100644 index 00000000..e9c4c554 --- /dev/null +++ b/erp24/docs/models/Products1cNomenclature.md @@ -0,0 +1,455 @@ +# Модель Products1cNomenclature + + +## Mindmap + +```mermaid +mindmap + root((Products1cNomenclature)) + Таблица БД + products_1c_nomenclature + Свойства + id + string + location + string + name + string + type_num + string + category + string + Связи + Actualities + 1:N Products1cNomenclatureActuality + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Products1cNomenclature` представляет номенклатуру товаров из системы 1С. Это справочная таблица, содержащая детальную структурированную информацию о товарах: категории, подкатегории, виды, сорта, размеры, цвета и другие характеристики. Используется для категоризации и классификации товаров в системе ERP24. + +**Файл модели:** `erp24/records/Products1cNomenclature.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_1c_nomenclature` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(255) | GUID - уникальный номер номенклатуры в 1С (PRIMARY KEY) | +| `location` | VARCHAR(255) | Расположение номенклатуры в системе 1С (путь до папки) | +| `name` | VARCHAR(255) | Имя (название) номенклатуры | +| `type_num` | VARCHAR(255) | Название вида номенклатуры из логического условия | +| `category` | VARCHAR(255) | Категория товара | +| `subcategory` | VARCHAR(255) | Подкатегория товара (nullable) | +| `species` | VARCHAR(255) | Вид товара (nullable) | +| `sort` | VARCHAR(255) | Сорт товара (nullable) | +| `type` | VARCHAR(255) | Тип товара (nullable) | +| `size` | INTEGER | Размер товара (nullable) | +| `measure` | VARCHAR(255) | Единица измерения (nullable) | +| `color` | VARCHAR(255) | Цвет товара (nullable) | + +--- + +## Дополнительные атрибуты + +Модель расширяет стандартный набор атрибутов дополнительными виртуальными полями: + +| Атрибут | Тип | Описание | +|---------|-----|----------| +| `date_from` | mixed | Дата начала актуальности (виртуальное поле) | +| `date_to` | mixed | Дата окончания актуальности (виртуальное поле) | + +**Реализация:** +```php +public function attributes(): array +{ + return array_merge(parent::attributes(), [ + 'date_from', + 'date_to', + ]); +} +``` + +Эти поля используются для фильтрации актуальных товаров по датам без необходимости модификации структуры таблицы. + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'products_1c_nomenclature'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = Products1cNomenclature::tableName(); +// 'products_1c_nomenclature' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает обязательные поля: `id`, `location`, `name`, `type_num`, `category` +2. Устанавливает значения по умолчанию (null) для необязательных полей +3. Проверяет типы данных (integer для size, string для остальных) +4. Ограничивает длину строковых полей до 255 символов +5. Проверяет уникальность поля `id` +6. Разрешает безопасное присвоение для `date_from` и `date_to` + +**Правила валидации:** +- **required**: `id`, `location`, `name`, `type_num`, `category` +- **default (null)**: `size`, `species`, `subcategory`, `sort`, `measure`, `color`, `type` +- **integer**: `size` +- **string (max 255)**: все текстовые поля +- **unique**: `id` +- **safe**: `date_from`, `date_to` + +**Пример:** +```php +$model = new Products1cNomenclature(); +$model->id = 'abc-123-guid'; +$model->name = 'Роза красная 50см'; +$model->category = 'Цветы'; +// Валидация пройдет +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет названия полей на русском языке для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "GUID" +- `location` → "Расположение номенклатуры в системе 1С" +- `name` → "Имя" +- `type_num` → "Название вида номенклатуры" +- `category` → "Категория" +- `subcategory` → "Подкатегория" +- `species` → "Вид" +- `sort` → "Сорт" +- `type` → "Тип" +- `size` → "Размер" +- `measure` → "Единица измерения" +- `color` → "Цвет" + +**Пример:** +```php +$label = $model->getAttributeLabel('category'); +// "Категория" +``` + +--- + +### `getActualities(): ActiveQuery` + +Возвращает связь с записями актуальности номенклатуры. + +**Возвращает:** ActiveQuery для связанных записей `Products1cNomenclatureActuality` + +**Тип связи:** hasMany (один ко многим) + +**Логика работы:** +1. Устанавливает связь через внешний ключ `guid` → `id` +2. Возвращает все записи актуальности для данной номенклатуры +3. Связанная модель: `Products1cNomenclatureActuality` + +**Пример:** +```php +$nomenclature = Products1cNomenclature::findOne($guid); +$actualities = $nomenclature->actualities; +// Массив объектов Products1cNomenclatureActuality +``` + +**Связанные таблицы:** +- **Текущая таблица:** `products_1c_nomenclature` +- **Связанная таблица:** `products_1c_nomenclature_actuality` +- **Внешний ключ:** `guid` (FK) → `id` (PK) + +--- + +### `hasActuality(): bool` + +Проверяет, существуют ли записи актуальности для данной номенклатуры. + +**Возвращает:** `true` если есть записи актуальности, `false` если нет + +**Логика работы:** +1. Использует метод `getActualities()` для получения связи +2. Вызывает метод `exists()` для проверки наличия записей без их загрузки +3. Возвращает булевое значение + +**Оптимизация:** +Метод использует `exists()` вместо `count()`, что эффективнее для проверки наличия данных, так как не загружает записи из БД. + +**Пример:** +```php +$nomenclature = Products1cNomenclature::findOne($guid); +if ($nomenclature->hasActuality()) { + echo "Номенклатура актуальна"; +} else { + echo "Нет данных об актуальности"; +} +``` + +**Вызываемые методы:** +- `getActualities()` — получение связи с актуальностями +- `ActiveQuery::exists()` — проверка существования записей (встроенный метод Yii2) + +--- + +## Связи (Relations) + +### `getActualities()` + +Записи актуальности номенклатуры. + +```php +$actualities = $nomenclature->actualities; // Products1cNomenclatureActuality[] +``` + +**Тип:** hasMany +**Связанная модель:** `Products1cNomenclatureActuality` +**FK:** `guid` → `id` +**Описание:** Возвращает все записи актуальности для данной номенклатуры (периоды, когда товар был доступен) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c_nomenclature ||--o{ products_1c_nomenclature_actuality : "has_actualities" + + products_1c_nomenclature { + string id PK + string location + string name + string type_num + string category + string subcategory + string species + string sort + string type + int size + string measure + string color + } + + products_1c_nomenclature_actuality { + int id PK + string guid FK + date date_from + date date_to + } +``` + +--- + +## Примеры использования + +### Создание новой номенклатуры + +```php +$nomenclature = new Products1cNomenclature(); +$nomenclature->id = 'abc-123-456-guid'; +$nomenclature->location = 'Номенклатура/Цветы/Розы'; +$nomenclature->name = 'Роза красная Фридом 50см'; +$nomenclature->type_num = 'Цветы'; +$nomenclature->category = 'Розы'; +$nomenclature->subcategory = 'Красные розы'; +$nomenclature->species = 'Фридом'; +$nomenclature->size = 50; +$nomenclature->measure = 'шт'; +$nomenclature->color = 'Красный'; + +if ($nomenclature->save()) { + echo "Номенклатура создана успешно"; +} +``` + +### Поиск по категории + +```php +$roses = Products1cNomenclature::find() + ->where(['category' => 'Розы']) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($roses as $rose) { + echo $rose->name . ' - ' . $rose->size . 'см' . PHP_EOL; +} +``` + +### Поиск по цвету и размеру + +```php +$redRoses = Products1cNomenclature::find() + ->where([ + 'category' => 'Розы', + 'color' => 'Красный', + 'size' => 50 + ]) + ->all(); +``` + +### Фильтрация с использованием виртуальных полей date_from и date_to + +```php +$actualProducts = Products1cNomenclature::find() + ->where(['category' => 'Розы']) + ->andWhere(['<=', 'date_from', date('Y-m-d')]) + ->andWhere(['>=', 'date_to', date('Y-m-d')]) + ->all(); +``` + +### Получение номенклатуры с актуальностями + +```php +$nomenclature = Products1cNomenclature::find() + ->where(['id' => $guid]) + ->with('actualities') + ->one(); + +if ($nomenclature->hasActuality()) { + foreach ($nomenclature->actualities as $actuality) { + echo "Актуально с {$actuality->date_from} по {$actuality->date_to}" . PHP_EOL; + } +} +``` + +### Поиск по подстроке в названии + +```php +$products = Products1cNomenclature::find() + ->where(['like', 'name', 'Роза']) + ->andWhere(['category' => 'Цветы']) + ->limit(10) + ->all(); +``` + +### Группировка по категориям + +```php +$categories = Products1cNomenclature::find() + ->select(['category', 'COUNT(*) as count']) + ->groupBy('category') + ->asArray() + ->all(); + +foreach ($categories as $cat) { + echo "{$cat['category']}: {$cat['count']} товаров" . PHP_EOL; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `id` | Обязательное, уникальное, макс. 255 символов | +| `location` | Обязательное, макс. 255 символов | +| `name` | Обязательное, макс. 255 символов | +| `type_num` | Обязательное, макс. 255 символов | +| `category` | Обязательное, макс. 255 символов | +| `subcategory` | Необязательное, макс. 255 символов, default = null | +| `species` | Необязательное, макс. 255 символов, default = null | +| `sort` | Необязательное, макс. 255 символов, default = null | +| `type` | Необязательное, макс. 255 символов, default = null | +| `size` | Необязательное, целое число, default = null | +| `measure` | Необязательное, макс. 255 символов, default = null | +| `color` | Необязательное, макс. 255 символов, default = null | +| `date_from` | Безопасное присвоение (safe) | +| `date_to` | Безопасное присвоение (safe) | + +--- + +## Связанные модели + +- **Products1cNomenclatureActuality** — актуальность номенклатуры (периоды доступности) +- **[Products1c](./Products1c.md)** — товары из 1С (использует данные номенклатуры) +- **[Products1cOptions](./Products1cOptions.md)** — опции товаров (дополнительные настройки) + +--- + +## Особенности реализации + +### Виртуальные атрибуты + +Модель использует механизм расширения атрибутов через переопределение метода `attributes()`: + +```php +public function attributes(): array +{ + return array_merge(parent::attributes(), [ + 'date_from', + 'date_to', + ]); +} +``` + +Это позволяет использовать поля `date_from` и `date_to` в запросах и формах без изменения структуры таблицы, что удобно для фильтрации и временных данных. + +### GUID как первичный ключ + +В отличие от стандартного подхода с автоинкрементным `id`, модель использует GUID из 1С в качестве первичного ключа. Это обеспечивает: +- Уникальность между разными системами +- Синхронизацию с 1С без конфликтов +- Независимость от последовательности создания записей + +### Структурированная классификация + +Модель поддерживает многоуровневую классификацию товаров: +1. **category** — основная категория (Цветы, Упаковка) +2. **subcategory** — подкатегория (Розы, Тюльпаны) +3. **species** — вид (сорт/название) +4. **type** — тип товара +5. **sort** — сорт + +Эта структура позволяет гибко организовывать каталог товаров. + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[1С Номенклатура] -->|Синхронизация| B[Products1cNomenclature] + B -->|Содержит| C[Атрибуты товара] + C --> D[category] + C --> E[subcategory] + C --> F[species] + C --> G[size/color] + B -->|Связь| H[Products1cNomenclatureActuality] + H --> I[Периоды актуальности] + B -->|Используется в| J[Products1c] + B -->|Расширяется в| K[Products1cOptions] +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Products1cNomenclatureActuality.md b/erp24/docs/models/Products1cNomenclatureActuality.md new file mode 100644 index 00000000..2831091b --- /dev/null +++ b/erp24/docs/models/Products1cNomenclatureActuality.md @@ -0,0 +1,295 @@ +# Класс: Products1cNomenclatureActuality + + +## Mindmap + +```mermaid +mindmap + root((Products1cNomenclatureActuality)) + Таблица БД + products_1c_nomenclature_actuality + Свойства + id + int + guid + string + date_from + string + created_at + string + created_by + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель актуальности номенклатуры товаров в ERP24. Определяет периоды действия (активности) товаров в каталоге с возможностью временного отключения и повторного включения. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`products_1c_nomenclature_actuality` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `guid` | varchar(255) | GUID товара из номенклатуры | +| `date_from` | datetime | Дата и время начала активности | +| `date_to` | datetime / null | Дата и время окончания активности | +| `created_at` | datetime | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | +| `created_by` | int | FK на создателя (Admin) | +| `updated_by` | int / null | FK на редактора (Admin) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getProduct()` | hasOne | Products1cNomenclature | Номенклатурная позиция | + +## Диаграмма связей + +```mermaid +erDiagram + Products1cNomenclatureActuality { + int id PK + varchar guid FK + datetime date_from + datetime date_to + datetime created_at + datetime updated_at + int created_by FK + int updated_by FK + } + + Products1cNomenclature { + varchar id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + Products1cNomenclature ||--o{ Products1cNomenclatureActuality : "guid" + Admin ||--o{ Products1cNomenclatureActuality : "created_by" + Admin ||--o{ Products1cNomenclatureActuality : "updated_by" +``` + +## Диаграмма жизненного цикла актуальности + +```mermaid +flowchart TD + A[Товар создан] --> B[Установка date_from = NOW] + B --> C[Товар активен] + C --> D{Нужно деактивировать?} + D -->|Да| E[Установка date_to = NOW] + E --> F[Товар неактивен] + F --> G{Нужно реактивировать?} + G -->|Да| H[Новая запись с date_from = NOW] + H --> C + D -->|Нет| C + G -->|Нет| F +``` + +## Диаграмма временной шкалы + +```mermaid +gantt + title Пример периодов актуальности товара + dateFormat YYYY-MM-DD + section Товар A + Активен :active, 2024-01-01, 2024-03-15 + Неактивен :done, 2024-03-15, 2024-04-01 + Активен :active, 2024-04-01, 2024-12-31 +``` + +## Примеры использования + +### Создание записи актуальности +```php +$actuality = new Products1cNomenclatureActuality(); +$actuality->guid = $productGuid; +$actuality->date_from = date('Y-m-d H:i:s'); +$actuality->date_to = null; // Бессрочно активен +$actuality->created_at = date('Y-m-d H:i:s'); +$actuality->created_by = Yii::$app->user->id; +$actuality->save(); +``` + +### Проверка актуальности товара на дату +```php +function isProductActual($guid, $date = null) +{ + $date = $date ?? date('Y-m-d H:i:s'); + + return Products1cNomenclatureActuality::find() + ->where(['guid' => $guid]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $date] + ]) + ->exists(); +} + +if (isProductActual($productGuid)) { + echo "Товар актуален"; +} +``` + +### Получение актуальных товаров +```php +$now = date('Y-m-d H:i:s'); + +$actualProducts = Products1cNomenclatureActuality::find() + ->select('guid') + ->where(['<=', 'date_from', $now]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $now] + ]) + ->column(); + +$products = Products1cNomenclature::find() + ->where(['id' => $actualProducts]) + ->all(); +``` + +### Деактивация товара +```php +$actuality = Products1cNomenclatureActuality::find() + ->where(['guid' => $productGuid]) + ->andWhere(['date_to' => null]) + ->orderBy(['date_from' => SORT_DESC]) + ->one(); + +if ($actuality) { + $actuality->date_to = date('Y-m-d H:i:s'); + $actuality->updated_at = date('Y-m-d H:i:s'); + $actuality->updated_by = Yii::$app->user->id; + $actuality->save(); +} +``` + +### Реактивация товара +```php +// Создаём новую запись актуальности +$newActuality = new Products1cNomenclatureActuality(); +$newActuality->guid = $productGuid; +$newActuality->date_from = date('Y-m-d H:i:s'); +$newActuality->date_to = null; +$newActuality->created_at = date('Y-m-d H:i:s'); +$newActuality->created_by = Yii::$app->user->id; +$newActuality->save(); +``` + +### История актуальности товара +```php +$history = Products1cNomenclatureActuality::find() + ->where(['guid' => $productGuid]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $period) { + $from = $period->date_from; + $to = $period->date_to ?? 'по настоящее время'; + echo "Активен с {$from} по {$to}\n"; +} +``` + +### Установка актуальности на период +```php +$actuality = new Products1cNomenclatureActuality(); +$actuality->guid = $productGuid; +$actuality->date_from = '2024-12-01 00:00:00'; +$actuality->date_to = '2024-12-31 23:59:59'; +$actuality->created_at = date('Y-m-d H:i:s'); +$actuality->created_by = Yii::$app->user->id; +$actuality->save(); + +echo "Товар будет активен только в декабре 2024"; +``` + +### Массовая деактивация товаров +```php +$productGuids = ['guid1', 'guid2', 'guid3']; +$now = date('Y-m-d H:i:s'); + +Products1cNomenclatureActuality::updateAll( + [ + 'date_to' => $now, + 'updated_at' => $now, + 'updated_by' => Yii::$app->user->id + ], + [ + 'and', + ['in', 'guid', $productGuids], + ['date_to' => null] + ] +); +``` + +### Статистика актуальности +```php +$now = date('Y-m-d H:i:s'); + +$stats = [ + 'active' => Products1cNomenclatureActuality::find() + ->where(['<=', 'date_from', $now]) + ->andWhere([ + 'or', + ['date_to' => null], + ['>=', 'date_to', $now] + ]) + ->count(), + + 'inactive' => Products1cNomenclatureActuality::find() + ->where(['<', 'date_to', $now]) + ->count(), + + 'scheduled' => Products1cNomenclatureActuality::find() + ->where(['>', 'date_from', $now]) + ->count() +]; + +echo "Активных: {$stats['active']}\n"; +echo "Неактивных: {$stats['inactive']}\n"; +echo "Запланированных: {$stats['scheduled']}\n"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `guid` | required, string (max 255) | +| `date_from` | required, safe | +| `date_to` | safe, default: null | +| `created_at` | required, safe | +| `updated_at` | safe, default: null | +| `created_by` | required, integer | +| `updated_by` | integer, default: null | + +## Связанные модели + +- [Products1cNomenclature](./Products1cNomenclature.md) — номенклатура товаров +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Временное версионирование**: Поддержка периодов активности через date_from/date_to +2. **Бессрочная активность**: date_to = null означает активность до отмены +3. **История изменений**: Несколько записей для одного товара образуют историю +4. **Аудит**: Фиксация created_by/updated_by для отслеживания ответственных +5. **Планирование**: Возможность установить активность на будущий период +6. **Интеграция с 1С**: GUID для синхронизации с внешней системой diff --git a/erp24/docs/models/Products1cNomenclatureActualitySearch.md b/erp24/docs/models/Products1cNomenclatureActualitySearch.md new file mode 100644 index 00000000..0ee9e0c6 --- /dev/null +++ b/erp24/docs/models/Products1cNomenclatureActualitySearch.md @@ -0,0 +1,248 @@ +# Класс: Products1cNomenclatureActualitySearch + + +## Mindmap + +```mermaid +mindmap + root((Products1cNomenclatureActualitySearch)) + Таблица БД + ActiveRecord + Наследование + extends Products1cNomenclatureActuality +``` + +## Назначение +Search-модель для поиска и фильтрации актуальности номенклатуры 1С в ERP24. Расширенная модель со статическими методами для получения списков категорий, типов, цветов, подкатегорий, сортов, видов и размеров из связанных таблиц. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Products1cNomenclatureActuality` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$category` | string | Категория товара | +| `$type` | string | Тип товара | +| `$color` | string | Цвет товара | +| `$subcategory` | string | Подкатегория товара | +| `$sort` | string | Сорт товара | +| `$species` | string | Вид товара | +| `$size` | string | Размер товара | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `active`, `created_by`, `updated_by` — integer +- `guid`, `date_from`, `date_to`, `created_at`, `updated_at` — safe +- `category`, `type`, `color`, `subcategory`, `sort`, `species`, `size` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного имени формы. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Products1cNomenclatureActuality::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры с кастомным formName +4. Применяет фильтры: + - Точное совпадение: id, date_from, date_to, active, created_at, updated_at, created_by, updated_by + - ilike: guid + +### getCategoryList(): array (static) +**Описание:** Получает список уникальных категорий из Products1cNomenclature. + +**Возвращает:** `array` — ['category' => 'category', ...] + +### getTypeList(): array (static) +**Описание:** Получает список типов из дополнительных характеристик. + +**Возвращает:** `array` — ['value' => 'value', ...] + +**Логика:** Ищет property_id с name IN ['type', 'тип'] + +### getColorList(): array (static) +**Описание:** Получает список цветов из дополнительных характеристик. + +**Возвращает:** `array` — ['value' => 'value', ...] + +**Логика:** Ищет property_id с name IN ['цвет', 'color'] + +### getSubcategoryList(): array (static) +**Описание:** Получает список уникальных подкатегорий. + +**Возвращает:** `array` — ['subcategory' => 'subcategory', ...] + +### getSortList(): array (static) +**Описание:** Получает список сортов из дополнительных характеристик. + +**Возвращает:** `array` — ['value' => 'value', ...] + +**Логика:** Ищет property_id с name IN ['sort', 'сорт'] + +### getSpeciesList(): array (static) +**Описание:** Получает список уникальных видов (species). + +**Возвращает:** `array` — ['species' => 'species', ...] + +### getSizeList(): array (static) +**Описание:** Получает список размеров из дополнительных характеристик. + +**Возвращает:** `array` — ['value' => 'value', ...] + +**Логика:** Ищет property_id с name IN ['size', 'размер'] + +## Диаграмма связей + +```mermaid +erDiagram + Products1cNomenclatureActuality { + int id PK + varchar guid FK + date date_from + date date_to + int active + int created_by FK + int updated_by FK + datetime created_at + datetime updated_at + } + + Products1cNomenclature { + varchar id PK + varchar category + varchar subcategory + varchar species + } + + Products1cPropType { + int id PK + varchar name + } + + Products1cAdditionalCharacteristics { + int id PK + int property_id FK + varchar value + } + + Products1cNomenclatureActuality }o--|| Products1cNomenclature : "guid" + Products1cAdditionalCharacteristics }o--|| Products1cPropType : "property_id" +``` + +## Диаграмма источников данных для фильтров + +```mermaid +flowchart TD + A[Списки для фильтров] --> B{Источник} + + B -->|Номенклатура| C[Products1cNomenclature] + C --> D[getCategoryList] + C --> E[getSubcategoryList] + C --> F[getSpeciesList] + + B -->|Характеристики| G[Products1cAdditionalCharacteristics] + G --> H[getTypeList - type/тип] + G --> I[getColorList - цвет/color] + G --> J[getSortList - sort/сорт] + G --> K[getSizeList - size/размер] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new Products1cNomenclatureActualitySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск активных записей +```php +$searchModel = new Products1cNomenclatureActualitySearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureActualitySearch' => [ + 'active' => 1, + ] +]); +``` + +### Получение списков для фильтров +```php +// В контроллере или view +$categories = Products1cNomenclatureActualitySearch::getCategoryList(); +$types = Products1cNomenclatureActualitySearch::getTypeList(); +$colors = Products1cNomenclatureActualitySearch::getColorList(); +$subcategories = Products1cNomenclatureActualitySearch::getSubcategoryList(); +$sorts = Products1cNomenclatureActualitySearch::getSortList(); +$species = Products1cNomenclatureActualitySearch::getSpeciesList(); +$sizes = Products1cNomenclatureActualitySearch::getSizeList(); +``` + +### GridView с выпадающими фильтрами +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'guid', + 'date_from', + 'date_to', + [ + 'attribute' => 'active', + 'filter' => [0 => 'Нет', 1 => 'Да'], + ], + 'created_at:datetime', + ], +]) ?> +``` + +### Выпадающий список категорий +```php +field($model, 'category')->dropDownList( + Products1cNomenclatureActualitySearch::getCategoryList(), + ['prompt' => 'Выберите категорию'] +) ?> +``` + +## Связанные модели + +- [Products1cNomenclatureActuality](./Products1cNomenclatureActuality.md) — базовая модель актуальности +- [Products1cNomenclature](./Products1cNomenclature.md) — номенклатура 1С +- [Products1cPropType](./Products1cPropType.md) — типы свойств +- [Products1cAdditionalCharacteristics](./Products1cAdditionalCharacteristics.md) — дополнительные характеристики + +## Особенности реализации + +1. **Статические методы**: 7 методов для получения списков фильтров +2. **Двуязычные имена свойств**: type/тип, цвет/color, sort/сорт, size/размер +3. **ArrayHelper::map**: Преобразование результатов в формат для dropDownList +4. **Кастомный formName**: Поддержка различных форм +5. **Периоды актуальности**: date_from, date_to для сезонности +6. **Дополнительные свойства**: category, type, color и др. объявлены, но не используются в search() diff --git a/erp24/docs/models/Products1cNomenclatureSearch.md b/erp24/docs/models/Products1cNomenclatureSearch.md new file mode 100644 index 00000000..95ba3394 --- /dev/null +++ b/erp24/docs/models/Products1cNomenclatureSearch.md @@ -0,0 +1,199 @@ +# Класс: Products1cNomenclatureSearch + + +## Mindmap + +```mermaid +mindmap + root((Products1cNomenclatureSearch)) + Таблица БД + ActiveRecord + Наследование + extends Products1cNomenclature +``` + +## Назначение +Search-модель для поиска и фильтрации номенклатуры товаров из 1С в ERP24. Стандартная модель с ilike-поиском по всем текстовым атрибутам товара. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Products1cNomenclature` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `location`, `name`, `type_num`, `category`, `subcategory`, `species`, `sort`, `measure`, `color`, `type` — safe +- `size` — integer + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска номенклатуры. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Products1cNomenclature::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: size + - ilike: id, location, name, type_num, category, subcategory, species, sort, measure, color, type + +## Диаграмма структуры номенклатуры + +```mermaid +erDiagram + Products1cNomenclature { + varchar id PK + varchar location + varchar name + varchar type_num + varchar category + varchar subcategory + varchar species + varchar sort + varchar measure + varchar color + varchar type + int size + } +``` + +## Диаграмма классификации товара + +```mermaid +flowchart TD + A[Products1cNomenclature] --> B[Идентификация] + B --> C[id - GUID из 1С] + B --> D[location - расположение] + + A --> E[Классификация] + E --> F[category - категория] + E --> G[subcategory - подкатегория] + E --> H[type - тип] + E --> I[type_num - номер типа] + + A --> J[Характеристики] + J --> K[name - наименование] + J --> L[species - вид] + J --> M[sort - сорт] + J --> N[color - цвет] + J --> O[size - размер] + J --> P[measure - единица измерения] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new Products1cNomenclatureSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по наименованию +```php +$searchModel = new Products1cNomenclatureSearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureSearch' => [ + 'name' => 'Роза', + ] +]); +``` + +### Поиск по категории +```php +$searchModel = new Products1cNomenclatureSearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureSearch' => [ + 'category' => 'Цветы', + ] +]); +``` + +### Поиск по цвету +```php +$searchModel = new Products1cNomenclatureSearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureSearch' => [ + 'color' => 'красный', + ] +]); +``` + +### Поиск по размеру +```php +$searchModel = new Products1cNomenclatureSearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureSearch' => [ + 'size' => 50, // Точное совпадение + ] +]); +``` + +### Комбинированный поиск +```php +$searchModel = new Products1cNomenclatureSearch(); +$dataProvider = $searchModel->search([ + 'Products1cNomenclatureSearch' => [ + 'category' => 'Цветы', + 'subcategory' => 'Розы', + 'color' => 'красный', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'category', + 'subcategory', + 'species', + 'sort', + 'color', + 'size', + 'measure', + ], +]) ?> +``` + +## Связанные модели + +- [Products1cNomenclature](./Products1cNomenclature.md) — базовая модель номенклатуры +- [Products1c](./Products1c.md) — товары 1С +- [Products1cNomenclatureActuality](./Products1cNomenclatureActuality.md) — актуальность номенклатуры + +## Особенности реализации + +1. **GUID как ID**: Поле id является строковым (GUID из 1С) +2. **ilike для ID**: Поиск по id через ilike (не точное совпадение) +3. **Полная классификация**: category, subcategory, type, species, sort +4. **Характеристики товара**: color, size, measure +5. **size как integer**: Единственное числовое поле с точным совпадением +6. **Стандартная Gii-модель**: Без кастомного formName diff --git a/erp24/docs/models/Products1cOptions.md b/erp24/docs/models/Products1cOptions.md new file mode 100644 index 00000000..5f61bb99 --- /dev/null +++ b/erp24/docs/models/Products1cOptions.md @@ -0,0 +1,503 @@ +# Модель Products1cOptions + + +## Mindmap + +```mermaid +mindmap + root((Products1cOptions)) + Таблица БД + products_1c_options + Свойства + id + string + options + string + provider_id + int + expiration_days + int + min_lot + int + min_order + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `Products1cOptions` хранит дополнительные опции и параметры товаров из 1С. Содержит информацию о поставщиках, сроках годности, минимальных партиях заказа, закупочных ценах, доступных цветах и группах товаров. Используется для управления закупками и настройками товаров. + +**Файл модели:** `erp24/records/Products1cOptions.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_1c_options` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(36) | GUID товара из таблицы `products_1c` (PRIMARY KEY) | +| `options` | TEXT | Дополнительные опции товара в формате JSON/TEXT | +| `provider_id` | INTEGER | ID производителя/поставщика из таблицы `providers` | +| `expiration_days` | INTEGER | Срок годности товара в днях | +| `min_lot` | INTEGER | Минимальный шаг деления (партия закупки) | +| `min_order` | INTEGER | Минимальный заказ товара при заказе у поставщика | +| `price_zakup` | FLOAT | Цена закупки товара | +| `colors` | TEXT | Список возможных цветов товара (разделитель `;`) | +| `group_id` | INTEGER | Группа товаров (Кения, Эквадор, Россия) | +| `main` | INTEGER | Основной товар (1) или нет (0) - включение в закупку | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'products_1c_options'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = Products1cOptions::tableName(); +// 'products_1c_options' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает все поля как обязательные для заполнения +2. Проверяет типы данных: + - TEXT для `options` и `colors` + - INTEGER для счетчиков и ID + - FLOAT для `price_zakup` +3. Ограничивает длину `id` до 36 символов (стандарт GUID) +4. Проверяет уникальность `id` в таблице + +**Правила валидации:** +- **required**: все поля обязательны +- **string (TEXT)**: `options`, `colors` +- **integer**: `provider_id`, `expiration_days`, `min_lot`, `min_order`, `group_id`, `main` +- **number (float)**: `price_zakup` +- **string (max 36)**: `id` +- **unique**: `id` + +**Пример:** +```php +$options = new Products1cOptions(); +$options->id = 'abc-123-456-guid'; +$options->options = '{"special": true}'; +$options->provider_id = 1; +$options->expiration_days = 7; +$options->min_lot = 10; +$options->min_order = 50; +$options->price_zakup = 125.50; +$options->colors = 'Красный;Белый;Розовый'; +$options->group_id = 1; +$options->main = 1; + +if ($options->validate()) { + $options->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет английские названия полей для использования в формах и сообщениях об ошибках. В данной модели используются английские метки. + +**Метки:** +- `id` → "ID" +- `options` → "Options" +- `provider_id` → "Provider ID" +- `expiration_days` → "Expiration Days" +- `min_lot` → "Min Lot" +- `min_order` → "Min Order" +- `price_zakup` → "Price Zakup" +- `colors` → "Colors" +- `group_id` → "Group ID" +- `main` → "Main" + +**Пример:** +```php +$label = $options->getAttributeLabel('expiration_days'); +// "Expiration Days" +``` + +--- + +## Примеры использования + +### Создание опций для нового товара + +```php +$options = new Products1cOptions(); +$options->id = 'abc-123-456-guid'; // GUID товара +$options->options = json_encode([ + 'organic' => true, + 'imported' => true, + 'premium' => false +]); +$options->provider_id = 5; // ID поставщика +$options->expiration_days = 14; // Срок годности 14 дней +$options->min_lot = 5; // Минимальная партия 5 шт +$options->min_order = 25; // Минимальный заказ 25 шт +$options->price_zakup = 99.99; // Закупочная цена +$options->colors = 'Красный;Розовый;Белый'; // Доступные цвета +$options->group_id = 1; // Группа: Кения +$options->main = 1; // Основной товар в закупке + +if ($options->save()) { + echo "Опции товара созданы"; +} +``` + +### Поиск товаров по поставщику + +```php +$providerProducts = Products1cOptions::find() + ->where(['provider_id' => 5]) + ->all(); + +foreach ($providerProducts as $product) { + echo "Товар: {$product->id}, Цена закупки: {$product->price_zakup}" . PHP_EOL; +} +``` + +### Получение основных товаров для закупки + +```php +$mainProducts = Products1cOptions::find() + ->where(['main' => 1]) + ->orderBy(['price_zakup' => SORT_ASC]) + ->all(); + +foreach ($mainProducts as $product) { + echo "Товар: {$product->id}, Мин. заказ: {$product->min_order} шт" . PHP_EOL; +} +``` + +### Фильтрация по группе товаров + +```php +// Группа 1 - Кения +$kenyaProducts = Products1cOptions::find() + ->where(['group_id' => 1]) + ->all(); + +// Группа 2 - Эквадор +$ecuadorProducts = Products1cOptions::find() + ->where(['group_id' => 2]) + ->all(); +``` + +### Обновление закупочной цены + +```php +$options = Products1cOptions::findOne($productGuid); +if ($options) { + $options->price_zakup = 150.00; + $options->save(); +} +``` + +### Поиск товаров с коротким сроком годности + +```php +$perishable = Products1cOptions::find() + ->where(['<=', 'expiration_days', 7]) + ->orderBy(['expiration_days' => SORT_ASC]) + ->all(); + +foreach ($perishable as $product) { + echo "Товар: {$product->id}, Срок годности: {$product->expiration_days} дней" . PHP_EOL; +} +``` + +### Работа с цветами товара + +```php +$options = Products1cOptions::findOne($guid); + +// Разбор цветов +$colorsArray = explode(';', $options->colors); +// ['Красный', 'Белый', 'Розовый'] + +// Добавление нового цвета +$colorsArray[] = 'Желтый'; +$options->colors = implode(';', $colorsArray); +$options->save(); +``` + +### Расчет минимальной суммы заказа + +```php +$options = Products1cOptions::findOne($guid); +$minOrderSum = $options->min_order * $options->price_zakup; + +echo "Минимальная сумма заказа: {$minOrderSum} руб." . PHP_EOL; +``` + +### Проверка наличия опций + +```php +$options = Products1cOptions::findOne($guid); + +if ($options->options) { + $optionsData = json_decode($options->options, true); + + if (isset($optionsData['organic']) && $optionsData['organic']) { + echo "Товар органический"; + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `id` | Обязательное, уникальное, макс. 36 символов (GUID) | +| `options` | Обязательное, TEXT | +| `provider_id` | Обязательное, целое число | +| `expiration_days` | Обязательное, целое число | +| `min_lot` | Обязательное, целое число | +| `min_order` | Обязательное, целое число | +| `price_zakup` | Обязательное, число с плавающей точкой | +| `colors` | Обязательное, TEXT | +| `group_id` | Обязательное, целое число | +| `main` | Обязательное, целое число (0 или 1) | + +--- + +## Структура поля `options` + +Поле `options` содержит JSON-строку с дополнительными параметрами товара: + +```json +{ + "organic": true, + "imported": true, + "premium": false, + "seasonal": true, + "special_care": "Хранить в холодильнике" +} +``` + +**Формат:** JSON-объект с произвольными ключами + +**Пример использования:** +```php +$optionsData = json_decode($options->options, true); +if ($optionsData['organic']) { + echo "Органический товар"; +} +``` + +--- + +## Структура поля `colors` + +Поле `colors` содержит список доступных цветов, разделенных точкой с запятой: + +**Формат:** `"Цвет1;Цвет2;Цвет3"` + +**Пример:** `"Красный;Белый;Розовый;Желтый"` + +**Парсинг:** +```php +$colorsArray = explode(';', $options->colors); +// ['Красный', 'Белый', 'Розовый', 'Желтый'] +``` + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — основная таблица товаров (связь по `id`) +- **Providers** — таблица поставщиков (связь по `provider_id`) +- **[Products1cNomenclature](./Products1cNomenclature.md)** — номенклатура товаров + +--- + +## Группы товаров (group_id) + +Поле `group_id` определяет происхождение товара: + +| ID | Группа | Описание | +|----|--------|----------| +| 1 | Кения | Товары из Кении | +| 2 | Эквадор | Товары из Эквадора | +| 3 | Россия | Российские товары | + +**Пример:** +```php +$kenyaFlowers = Products1cOptions::find() + ->where(['group_id' => 1]) + ->all(); +``` + +--- + +## Поле `main` (основной товар) + +Поле `main` определяет, включается ли товар в автоматическую закупку: + +| Значение | Описание | +|----------|----------| +| 1 | Основной товар — включается в закупку | +| 0 | Дополнительный товар — не включается в автозакупку | + +**Пример:** +```php +// Только основные товары для закупки +$mainProducts = Products1cOptions::find() + ->where(['main' => 1]) + ->all(); +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o| products_1c_options : "has_options" + products_1c_options }o--|| providers : "belongs_to_provider" + + products_1c { + string id PK + string name + string tip + } + + products_1c_options { + string id PK,FK + text options + int provider_id FK + int expiration_days + int min_lot + int min_order + float price_zakup + text colors + int group_id + int main + } + + providers { + int id PK + string name + string contact + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Products1c] -->|id| B[Products1cOptions] + B --> C[Параметры закупки] + C --> D[provider_id] + C --> E[min_lot / min_order] + C --> F[price_zakup] + B --> G[Характеристики товара] + G --> H[expiration_days] + G --> I[colors] + G --> J[group_id] + B --> K[Управление закупками] + K --> L[main флаг] + K --> M[Расчет заказа] + D --> N[Providers] + N --> M +``` + +--- + +## Особенности реализации + +### Обязательность всех полей + +В отличие от большинства моделей, все поля данной модели обязательны для заполнения: + +```php +[['id', 'options', 'provider_id', 'expiration_days', 'min_lot', + 'colors', 'price_zakup', 'min_order', 'group_id', 'main'], 'required'] +``` + +Это гарантирует полноту данных для управления закупками. + +### Связь с Products1c + +Поле `id` является одновременно первичным и внешним ключом, связывающим опции с основной таблицей товаров: + +```php +// В Products1c +public function getOptions() +{ + return $this->hasOne(Products1cOptions::class, ['id' => 'id']); +} +``` + +### Формат хранения цветов + +Цвета хранятся в текстовом формате с разделителем `;`, что позволяет: +- Хранить произвольное количество цветов +- Легко добавлять/удалять цвета +- Парсить список через `explode()` + +--- + +## Использование в закупках + +Модель активно используется в системе закупок: + +```php +// Формирование заказа у поставщика +$products = Products1cOptions::find() + ->where([ + 'provider_id' => $providerId, + 'main' => 1 // Только основные товары + ]) + ->all(); + +foreach ($products as $product) { + // Округляем до min_lot + $quantity = ceil($neededQuantity / $product->min_lot) * $product->min_lot; + + // Проверяем min_order + if ($quantity < $product->min_order) { + $quantity = $product->min_order; + } + + $orderSum = $quantity * $product->price_zakup; + + // Создание заказа... +} +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Products1cPropType.md b/erp24/docs/models/Products1cPropType.md new file mode 100644 index 00000000..1f1317f7 --- /dev/null +++ b/erp24/docs/models/Products1cPropType.md @@ -0,0 +1,250 @@ +# Класс: Products1cPropType + + +## Mindmap + +```mermaid +mindmap + root((Products1cPropType)) + Таблица БД + products_1c_prop_type + Свойства + id + string + name + string + Связи + Products1cAdditionalCharacteristics + 1:N Products1cAdditionalCharacteristics + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов свойств товаров в ERP24. Определяет доступные типы дополнительных характеристик для товаров из 1С (цвет, размер, материал и др.). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`products_1c_prop_type` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | varchar(255) | GUID свойства (PK) | +| `name` | varchar(255) | Название типа свойства | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getProducts1cAdditionalCharacteristics()` | hasMany | Products1cAdditionalCharacteristics | Значения свойств | + +## Диаграмма связей + +```mermaid +erDiagram + Products1cPropType { + varchar id PK + varchar name + } + + Products1cAdditionalCharacteristics { + varchar product_id PK,FK + varchar property_id PK,FK + varchar value + } + + Products1c { + varchar id PK + varchar name + } + + Products1cPropType ||--o{ Products1cAdditionalCharacteristics : "property_id" + Products1c ||--o{ Products1cAdditionalCharacteristics : "product_id" +``` + +## Диаграмма EAV-структуры + +```mermaid +flowchart LR + subgraph Справочник типов + A[Products1cPropType] + A1[Цвет] + A2[Размер] + A3[Материал] + A4[Страна] + A --> A1 + A --> A2 + A --> A3 + A --> A4 + end + + subgraph Значения характеристик + B[Products1cAdditionalCharacteristics] + B1[Товар 1: Цвет = Красный] + B2[Товар 1: Размер = M] + B3[Товар 2: Цвет = Синий] + B --> B1 + B --> B2 + B --> B3 + end + + A1 -.-> B1 + A2 -.-> B2 + A1 -.-> B3 +``` + +## Примеры использования + +### Создание типа свойства +```php +$propType = new Products1cPropType(); +$propType->id = 'color-guid-from-1c'; +$propType->name = 'Цвет'; +$propType->save(); +``` + +### Получение всех типов свойств +```php +$propTypes = Products1cPropType::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($propTypes as $type) { + echo "{$type->name} (ID: {$type->id})\n"; +} +``` + +### Формирование списка для выбора +```php +$typesList = ArrayHelper::map( + Products1cPropType::find()->orderBy(['name' => SORT_ASC])->all(), + 'id', + 'name' +); + +echo Html::dropDownList('property_type', null, $typesList); +``` + +### Поиск типа по названию +```php +$colorType = Products1cPropType::find() + ->where(['name' => 'Цвет']) + ->one(); + +if ($colorType) { + echo "GUID цвета: {$colorType->id}"; +} +``` + +### Получение всех значений типа свойства +```php +$colorType = Products1cPropType::findOne($colorGuid); + +if ($colorType) { + $values = $colorType->products1cAdditionalCharacteristics; + + $uniqueValues = array_unique(array_map(function($char) { + return $char->value; + }, $values)); + + echo "Доступные цвета: " . implode(', ', $uniqueValues); +} +``` + +### Статистика использования свойств +```php +$stats = Products1cAdditionalCharacteristics::find() + ->select(['property_id', 'COUNT(*) as usage_count']) + ->groupBy('property_id') + ->asArray() + ->all(); + +$types = ArrayHelper::index(Products1cPropType::find()->all(), 'id'); + +foreach ($stats as $stat) { + $typeName = $types[$stat['property_id']]->name ?? 'Unknown'; + echo "{$typeName}: {$stat['usage_count']} использований\n"; +} +``` + +### Получение уникальных значений свойства +```php +function getPropertyValues($propertyId) +{ + return Products1cAdditionalCharacteristics::find() + ->select('value') + ->where(['property_id' => $propertyId]) + ->distinct() + ->orderBy(['value' => SORT_ASC]) + ->column(); +} + +$sizes = getPropertyValues($sizePropertyId); +// ['S', 'M', 'L', 'XL', 'XXL'] +``` + +### Импорт типов свойств из 1С +```php +$propsFrom1c = [ + ['id' => 'guid1', 'name' => 'Цвет'], + ['id' => 'guid2', 'name' => 'Размер'], + ['id' => 'guid3', 'name' => 'Материал'], +]; + +foreach ($propsFrom1c as $data) { + $existing = Products1cPropType::findOne($data['id']); + + if ($existing) { + $existing->name = $data['name']; + $existing->save(); + } else { + $propType = new Products1cPropType(); + $propType->id = $data['id']; + $propType->name = $data['name']; + $propType->save(); + } +} +``` + +### Поиск неиспользуемых типов свойств +```php +$usedPropertyIds = Products1cAdditionalCharacteristics::find() + ->select('property_id') + ->distinct() + ->column(); + +$unusedTypes = Products1cPropType::find() + ->where(['not in', 'id', $usedPropertyIds]) + ->all(); + +foreach ($unusedTypes as $type) { + echo "Неиспользуемый тип: {$type->name}\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `id` | required, string (max 255), unique | +| `name` | required, string (max 255) | + +## Связанные модели + +- [Products1cAdditionalCharacteristics](./Products1cAdditionalCharacteristics.md) — значения характеристик +- [Products1c](./Products1c.md) — товары + +## Особенности реализации + +1. **GUID первичный ключ**: id типа varchar для совместимости с 1С +2. **Простой справочник**: Минимальная структура (id, name) +3. **EAV-паттерн**: Является частью системы Entity-Attribute-Value +4. **Синхронизация с 1С**: GUID идентификаторы для обмена данными +5. **Расширяемость**: Новые типы свойств добавляются без изменения схемы БД diff --git a/erp24/docs/models/ProductsCatProperty.md b/erp24/docs/models/ProductsCatProperty.md new file mode 100644 index 00000000..ee30d642 --- /dev/null +++ b/erp24/docs/models/ProductsCatProperty.md @@ -0,0 +1,200 @@ +# Модель ProductsCatProperty + + +## Mindmap + +```mermaid +mindmap + root((ProductsCatProperty)) + Таблица БД + products_cat_property + Свойства + cat_id + int + id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ProductsCatProperty` связывает категории товаров со свойствами (характеристиками). Определяет, какие свойства доступны для товаров в конкретной категории. Реализует связь many-to-many между категориями и свойствами товаров. + +**Файл модели:** `erp24/records/ProductsCatProperty.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_cat_property` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `cat_id` | INTEGER | ID категории (FK, часть PK) | +| `id` | INTEGER | ID свойства (FK → cat_property.id, часть PK) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `cat_id` и `id` +- Таблица связей для many-to-many между категориями и свойствами +- Позволяет настраивать набор характеристик для каждой категории +- Все поля обязательные + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_cat_property }o--|| category : "category" + products_cat_property }o--|| cat_property : "property" + + products_cat_property { + int cat_id PK,FK + int id PK,FK + } + + category { + int id PK + string name + int parent_id + } + + cat_property { + int id PK + string name + string type + } +``` + +--- + +## Примеры использования + +### Привязка свойства к категории + +```php +$link = new ProductsCatProperty(); +$link->cat_id = $categoryId; +$link->id = $propertyId; +$link->save(); +``` + +### Получение свойств категории + +```php +$propertyIds = ProductsCatProperty::find() + ->select('id') + ->where(['cat_id' => $categoryId]) + ->column(); + +$properties = CatProperty::find() + ->where(['id' => $propertyIds]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($properties as $prop) { + echo "{$prop->name}\n"; +} +``` + +### Проверка наличия свойства в категории + +```php +$hasProperty = ProductsCatProperty::find() + ->where([ + 'cat_id' => $categoryId, + 'id' => $propertyId + ]) + ->exists(); +``` + +### Массовая привязка свойств + +```php +$categoryId = 5; +$propertyIds = [1, 2, 3, 4, 5]; + +foreach ($propertyIds as $propId) { + $exists = ProductsCatProperty::find() + ->where(['cat_id' => $categoryId, 'id' => $propId]) + ->exists(); + + if (!$exists) { + $link = new ProductsCatProperty(); + $link->cat_id = $categoryId; + $link->id = $propId; + $link->save(); + } +} +``` + +### Удаление связи + +```php +ProductsCatProperty::deleteAll([ + 'cat_id' => $categoryId, + 'id' => $propertyId +]); +``` + +### Получение категорий для свойства + +```php +$categoryIds = ProductsCatProperty::find() + ->select('cat_id') + ->where(['id' => $propertyId]) + ->column(); + +echo "Свойство используется в категориях: " . implode(', ', $categoryIds); +``` + +### Копирование свойств между категориями + +```php +$sourceCategoryId = 1; +$targetCategoryId = 2; + +$sourceProperties = ProductsCatProperty::find() + ->where(['cat_id' => $sourceCategoryId]) + ->all(); + +foreach ($sourceProperties as $prop) { + $exists = ProductsCatProperty::find() + ->where(['cat_id' => $targetCategoryId, 'id' => $prop->id]) + ->exists(); + + if (!$exists) { + $newLink = new ProductsCatProperty(); + $newLink->cat_id = $targetCategoryId; + $newLink->id = $prop->id; + $newLink->save(); + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `cat_id` | Обязательное, целое число | +| `id` | Обязательное, целое число | +| `cat_id + id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[CatProperty](./CatProperty.md)** — свойства товаров +- **[ProductsPropertyValue](./ProductsPropertyValue.md)** — значения свойств + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ProductsClass.md b/erp24/docs/models/ProductsClass.md new file mode 100644 index 00000000..e781b1bc --- /dev/null +++ b/erp24/docs/models/ProductsClass.md @@ -0,0 +1,257 @@ +# Модель ProductsClass + + +## Mindmap + +```mermaid +mindmap + root((ProductsClass)) + Таблица БД + products_class + Свойства + category_id + string + tip + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ProductsClass` представляет справочник классификации товаров. Определяет категории товаров (упаковка, горшечка, услуги, матрица и др.) для группировки номенклатуры из 1С. Используется для фильтрации товаров в различных модулях системы. + +**Файл модели:** `erp24/records/ProductsClass.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_class` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `category_id` | VARCHAR(36) | GUID категории товаров из 1С (часть составного PK) | +| `tip` | VARCHAR(25) | Тип класса — код группы товаров (часть составного PK) | + +--- + +## Первичный ключ + +Модель использует **составной первичный ключ** из двух полей: + +```php +public static function primaryKey() +{ + return ['category_id', 'tip']; +} +``` + +--- + +## Константы типов товаров + +Модель определяет константы для всех типов классификации: + +```php +public const HINT_WRAP = 'wrap'; // Упаковка +public const HINT_POTTED = 'potted'; // Горшечка +public const HINT_SERVICES = 'services'; // Услуги +public const HINT_SERVICES_DELIVERY = 'services_delivery'; // Услуги по доставке +public const HINT_SALUT = 'salut'; // Пиротехника +public const HINT_MATRIX = 'matrix'; // Матрица букетов +public const HINT_AUTHOR = 'author'; // Авторский букет +public const HINT_MARKETPLACE = 'marketplace'; // Товары для маркетплейсов +public const HINT_MARKETPLACE_ADDITIONAL = 'marketplace_additional'; // Доп. товары для маркетплейсов +public const HINT_RELATED = 'related'; // Сопутка +public const HINT_OTHER_ITEMS = 'other_items'; // Номенклатура 1% +``` + +### Устаревшие константы (TODO: заменить) + +```php +public const MARKETPLACE = 'marketplace'; // → HINT_MARKETPLACE +public const MARKETPLACE_ADDITIONAL = 'marketplace_additional'; // → HINT_MARKETPLACE_ADDITIONAL +``` + +--- + +## Описание типов товаров + +| Константа | Код | Описание | +|-----------|-----|----------| +| `HINT_WRAP` | `wrap` | Упаковочные материалы (бумага, плёнка, ленты) | +| `HINT_POTTED` | `potted` | Горшечные растения | +| `HINT_SERVICES` | `services` | Дополнительные услуги | +| `HINT_SERVICES_DELIVERY` | `services_delivery` | Услуги доставки | +| `HINT_SALUT` | `salut` | Пиротехнические изделия | +| `HINT_MATRIX` | `matrix` | Стандартные букеты из матрицы | +| `HINT_AUTHOR` | `author` | Авторские букеты (индивидуальная сборка) | +| `HINT_MARKETPLACE` | `marketplace` | Товары для продажи на маркетплейсах | +| `HINT_MARKETPLACE_ADDITIONAL` | `marketplace_additional` | Дополнительные товары для маркетплейсов | +| `HINT_RELATED` | `related` | Сопутствующие товары (открытки, игрушки) | +| `HINT_OTHER_ITEMS` | `other_items` | Прочая номенклатура (1% от продаж) | + +--- + +## Методы модели + +### `getHints(): array` (static) + +Возвращает словарь типов товаров с человекочитаемыми названиями. + +**Возвращает:** `array` — ассоциативный массив `['код' => 'Название']` + +```php +$hints = ProductsClass::getHints(); +// [ +// 'wrap' => 'Упаковка', +// 'potted' => 'Горшечка', +// 'services' => 'Услуги', +// 'services_delivery' => 'Услуги по доставке', +// 'salut' => 'Пиротехника', +// 'matrix' => 'Матрица', +// 'marketplace' => 'Товары для маркетплейсов', +// 'marketplace_additional' => 'Доп. товары для маркетплейсов', +// 'related' => 'Сопутка', +// 'other_items' => 'Номенклатура 1%', +// 'author' => 'Авторский букет', +// ] +``` + +**Использование:** +- Заполнение выпадающих списков в формах +- Отображение названий типов в интерфейсе +- Фильтрация товаров по типу + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_class ||--o{ products_1c : "classifies" + + products_class { + string category_id PK + string tip PK + } + + products_1c { + string id PK + string parent_id FK + string name + string tip + } +``` + +--- + +## Примеры использования + +### Получение всех классов товаров + +```php +$classes = ProductsClass::find()->all(); +``` + +### Проверка типа категории + +```php +$productClass = ProductsClass::findOne([ + 'category_id' => $categoryGuid +]); + +if ($productClass && $productClass->tip === ProductsClass::HINT_MATRIX) { + // Товар из матрицы букетов +} +``` + +### Получение товаров определённого класса + +```php +// Все GUID категорий упаковки +$wrapCategoryIds = ProductsClass::find() + ->select('category_id') + ->where(['tip' => ProductsClass::HINT_WRAP]) + ->column(); + +// Товары упаковки +$wrapProducts = Products1c::find() + ->where(['parent_id' => $wrapCategoryIds]) + ->all(); +``` + +### Использование в выпадающем списке + +```php +use yii\helpers\Html; + +echo Html::dropDownList( + 'product_type', + $selectedType, + ProductsClass::getHints(), + ['prompt' => 'Выберите тип товара'] +); +``` + +### Фильтрация по нескольким типам + +```php +// Товары для маркетплейсов (основные + дополнительные) +$marketplaceTypes = [ + ProductsClass::HINT_MARKETPLACE, + ProductsClass::HINT_MARKETPLACE_ADDITIONAL +]; + +$marketplaceCategories = ProductsClass::find() + ->select('category_id') + ->where(['tip' => $marketplaceTypes]) + ->column(); +``` + +### Проверка принадлежности товара к классу + +```php +function isProductInClass($productGuid, $classType): bool +{ + $product = Products1c::findOne($productGuid); + if (!$product) { + return false; + } + + return ProductsClass::find() + ->where([ + 'category_id' => $product->parent_id, + 'tip' => $classType + ]) + ->exists(); +} + +// Проверка: является ли товар услугой +$isService = isProductInClass($productGuid, ProductsClass::HINT_SERVICES); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `category_id` | Обязательное, макс. 36 символов, уникальное | +| `tip` | Обязательное, макс. 25 символов | +| `[category_id, tip]` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — номенклатура товаров из 1С +- **[MatrixErp](./MatrixErp.md)** — матрица букетов (для типа `matrix`) +- **[MarketplaceOrders](./MarketplaceOrders.md)** — заказы маркетплейсов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ProductsPropertyValue.md b/erp24/docs/models/ProductsPropertyValue.md new file mode 100644 index 00000000..69fedeb1 --- /dev/null +++ b/erp24/docs/models/ProductsPropertyValue.md @@ -0,0 +1,247 @@ +# Модель ProductsPropertyValue + + +## Mindmap + +```mermaid +mindmap + root((ProductsPropertyValue)) + Таблица БД + products_property_value + Свойства + property_id + int + id + int + val + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ProductsPropertyValue` хранит значения свойств (характеристик) для конкретных товаров. Связывает товар с его характеристиками, указывая конкретное значение каждого свойства. Используется для фильтрации товаров по характеристикам и отображения детальной информации о товаре. + +**Файл модели:** `erp24/records/ProductsPropertyValue.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_property_value` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `property_id` | INTEGER | ID свойства (FK → cat_property.id, часть PK) | +| `id` | INTEGER | ID товара (FK → products.id, часть PK) | +| `val` | INTEGER | Значение свойства (ID из справочника значений) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `property_id` и `id` +- Каждый товар может иметь только одно значение для каждого свойства +- Значение `val` ссылается на справочник допустимых значений свойства +- Все поля обязательные + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_property_value }o--|| cat_property : "property" + products_property_value }o--|| products : "product" + + products_property_value { + int property_id PK,FK + int id PK,FK + int val + } + + cat_property { + int id PK + string name + string type + } + + products { + int id PK + string name + float price + } +``` + +--- + +## Примеры использования + +### Установка значения свойства для товара + +```php +$propValue = new ProductsPropertyValue(); +$propValue->property_id = $propertyId; // ID свойства (например, "Цвет") +$propValue->id = $productId; // ID товара +$propValue->val = $valueId; // ID значения (например, "Красный") +$propValue->save(); +``` + +### Получение всех свойств товара + +```php +$productId = 123; + +$properties = ProductsPropertyValue::find() + ->alias('ppv') + ->innerJoin('cat_property cp', 'cp.id = ppv.property_id') + ->select(['cp.name as property_name', 'ppv.val']) + ->where(['ppv.id' => $productId]) + ->asArray() + ->all(); + +foreach ($properties as $prop) { + echo "{$prop['property_name']}: {$prop['val']}\n"; +} +``` + +### Фильтрация товаров по свойству + +```php +// Найти товары красного цвета +$colorPropertyId = 1; // ID свойства "Цвет" +$redValueId = 5; // ID значения "Красный" + +$productIds = ProductsPropertyValue::find() + ->select('id') + ->where([ + 'property_id' => $colorPropertyId, + 'val' => $redValueId + ]) + ->column(); + +$products = Products::find() + ->where(['id' => $productIds]) + ->all(); +``` + +### Обновление значения свойства + +```php +$propValue = ProductsPropertyValue::findOne([ + 'property_id' => $propertyId, + 'id' => $productId +]); + +if ($propValue) { + $propValue->val = $newValueId; + $propValue->save(); +} else { + // Создаём новую запись + $propValue = new ProductsPropertyValue(); + $propValue->property_id = $propertyId; + $propValue->id = $productId; + $propValue->val = $newValueId; + $propValue->save(); +} +``` + +### Удаление свойства товара + +```php +ProductsPropertyValue::deleteAll([ + 'property_id' => $propertyId, + 'id' => $productId +]); +``` + +### Массовая установка свойств товара + +```php +$productId = 123; +$properties = [ + 1 => 5, // Цвет: Красный + 2 => 3, // Размер: M + 3 => 1, // Материал: Хлопок +]; + +foreach ($properties as $propId => $valId) { + ProductsPropertyValue::deleteAll([ + 'property_id' => $propId, + 'id' => $productId + ]); + + $pv = new ProductsPropertyValue(); + $pv->property_id = $propId; + $pv->id = $productId; + $pv->val = $valId; + $pv->save(); +} +``` + +### Статистика по значениям свойства + +```php +$propertyId = 1; // Цвет + +$stats = ProductsPropertyValue::find() + ->select(['val', 'COUNT(*) as count']) + ->where(['property_id' => $propertyId]) + ->groupBy('val') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); + +echo "Распределение товаров по цветам:\n"; +foreach ($stats as $stat) { + echo "Значение {$stat['val']}: {$stat['count']} товаров\n"; +} +``` + +### Фильтрация по нескольким свойствам + +```php +// Найти товары: Цвет=Красный И Размер=M +$filters = [ + 1 => 5, // Цвет: Красный + 2 => 3, // Размер: M +]; + +$query = Products::find()->alias('p'); + +foreach ($filters as $propId => $valId) { + $alias = "ppv_{$propId}"; + $query->innerJoin( + ProductsPropertyValue::tableName() . " {$alias}", + "{$alias}.id = p.id AND {$alias}.property_id = :prop_{$propId} AND {$alias}.val = :val_{$propId}", + [":prop_{$propId}" => $propId, ":val_{$propId}" => $valId] + ); +} + +$products = $query->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `property_id` | Обязательное, целое число | +| `id` | Обязательное, целое число | +| `val` | Обязательное, целое число | +| `property_id + id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[CatProperty](./CatProperty.md)** — свойства товаров +- **[ProductsCatProperty](./ProductsCatProperty.md)** — связь категорий со свойствами + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ProductsVarieties.md b/erp24/docs/models/ProductsVarieties.md new file mode 100644 index 00000000..94f0a51d --- /dev/null +++ b/erp24/docs/models/ProductsVarieties.md @@ -0,0 +1,506 @@ +# Модель ProductsVarieties + + +## Mindmap + +```mermaid +mindmap + root((ProductsVarieties)) + Таблица БД + products_varieties + Свойства + id + int + product_id + string + color + string + name + string + description + string + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ProductsVarieties` представляет справочник разновидностей (сортов) товаров. Хранит информацию о различных вариантах одного товара: цвет, название сорта, описание и приоритет отображения. Используется для детального описания характеристик товаров, особенно цветов (например, сорта роз). + +**Файл модели:** `erp24/records/ProductsVarieties.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `products_varieties` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `product_id` | VARCHAR(36) | GUID товара из таблицы товаров | +| `color` | VARCHAR(120) | Цвет разновидности | +| `name` | VARCHAR(200) | Название разновидности (сорта) | +| `description` | TEXT | Детальное описание разновидности | +| `posit` | INTEGER | Приоритет/позиция сорта при отображении | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'products_varieties'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = ProductsVarieties::tableName(); +// 'products_varieties' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает все поля как обязательные для заполнения +2. Проверяет типы данных: + - TEXT для `description` + - INTEGER для `posit` + - VARCHAR для строковых полей +3. Ограничивает длину полей: + - `product_id`: 36 символов (GUID) + - `color`: 120 символов + - `name`: 200 символов + +**Правила валидации:** +- **required**: все поля обязательны +- **string (TEXT)**: `description` +- **integer**: `posit` +- **string (max 36)**: `product_id` +- **string (max 120)**: `color` +- **string (max 200)**: `name` + +**Пример:** +```php +$variety = new ProductsVarieties(); +$variety->product_id = 'abc-123-456-guid'; +$variety->color = 'Красный'; +$variety->name = 'Роза Фридом'; +$variety->description = 'Красная роза сорта Фридом, длина стебля 50см'; +$variety->posit = 1; + +if ($variety->validate()) { + $variety->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет английские названия полей для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `product_id` → "Product ID" +- `color` → "Color" +- `name` → "Name" +- `description` → "Description" +- `posit` → "Posit" + +**Пример:** +```php +$label = $variety->getAttributeLabel('posit'); +// "Posit" +``` + +--- + +## Примеры использования + +### Создание разновидности товара + +```php +$variety = new ProductsVarieties(); +$variety->product_id = 'abc-123-456-guid'; +$variety->color = 'Красный'; +$variety->name = 'Роза Фридом'; +$variety->description = 'Премиум красная роза сорта Фридом. Длина стебля 50см. Крупный бутон.'; +$variety->posit = 1; + +if ($variety->save()) { + echo "Разновидность создана"; +} +``` + +### Получение всех разновидностей товара + +```php +$varieties = ProductsVarieties::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($varieties as $variety) { + echo "{$variety->name} ({$variety->color})" . PHP_EOL; +} +``` + +### Поиск по цвету + +```php +$redVarieties = ProductsVarieties::find() + ->where(['color' => 'Красный']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($redVarieties as $variety) { + echo "{$variety->name}: {$variety->description}" . PHP_EOL; +} +``` + +### Получение разновидности с наивысшим приоритетом + +```php +$topVariety = ProductsVarieties::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['posit' => SORT_ASC]) + ->one(); + +if ($topVariety) { + echo "Рекомендуемый сорт: {$topVariety->name}"; +} +``` + +### Обновление приоритета + +```php +$variety = ProductsVarieties::findOne($id); + +if ($variety) { + $variety->posit = 5; // Понижаем приоритет + $variety->save(); +} +``` + +### Группировка по цветам + +```php +$colorGroups = ProductsVarieties::find() + ->select(['color', 'COUNT(*) as count']) + ->groupBy('color') + ->asArray() + ->all(); + +foreach ($colorGroups as $group) { + echo "Цвет: {$group['color']}, Сортов: {$group['count']}" . PHP_EOL; +} +``` + +### Поиск по подстроке в названии + +```php +$varieties = ProductsVarieties::find() + ->where(['like', 'name', 'Роза']) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Массовое добавление разновидностей + +```php +$varieties = [ + ['color' => 'Красный', 'name' => 'Роза Фридом', 'posit' => 1], + ['color' => 'Белый', 'name' => 'Роза Аваланж', 'posit' => 2], + ['color' => 'Розовый', 'name' => 'Роза Пинк Флойд', 'posit' => 3], +]; + +foreach ($varieties as $data) { + $variety = new ProductsVarieties(); + $variety->product_id = $productGuid; + $variety->color = $data['color']; + $variety->name = $data['name']; + $variety->description = "Описание сорта {$data['name']}"; + $variety->posit = $data['posit']; + $variety->save(); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `product_id` | Обязательное, макс. 36 символов (GUID) | +| `color` | Обязательное, макс. 120 символов | +| `name` | Обязательное, макс. 200 символов | +| `description` | Обязательное, TEXT | +| `posit` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары из 1С (связь по `product_id`) +- **[Products1cNomenclature](./Products1cNomenclature.md)** — номенклатура товаров (содержит базовые характеристики) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o{ products_varieties : "has_varieties" + + products_1c { + string id PK + string name + string tip + } + + products_varieties { + int id PK + string product_id FK + string color + string name + text description + int posit + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Products1c] -->|product_id| B[ProductsVarieties] + B --> C[Детальные характеристики] + C --> D[color] + C --> E[name сорт] + C --> F[description] + B --> G[Приоритет отображения] + G --> H[posit] + H --> I[Сортировка в каталоге] + C --> J[Отображение в карточке товара] +``` + +--- + +## Особенности реализации + +### Поле posit (приоритет) + +Поле `posit` определяет порядок отображения разновидностей: +- **Меньшее значение** = **выше приоритет** +- Используется для сортировки при выводе списка сортов +- Позволяет выделить "главный" или рекомендуемый сорт + +**Пример:** +```php +// Получаем разновидности с сортировкой по приоритету +$varieties = ProductsVarieties::find() + ->where(['product_id' => $guid]) + ->orderBy(['posit' => SORT_ASC]) // От меньшего к большему + ->all(); + +// Первая в списке — самая приоритетная +$mainVariety = $varieties[0]; +``` + +### Детальное описание + +Поле `description` (TEXT) позволяет хранить подробное описание сорта: +- Характеристики (длина стебля, размер бутона) +- Особенности ухода +- Происхождение (страна производства) +- Рекомендации по использованию + +### Цветовая классификация + +Поле `color` используется для: +- Фильтрации товаров по цвету в каталоге +- Группировки в отчетах +- Визуального отображения + +**Стандартные значения:** +- "Красный" +- "Белый" +- "Розовый" +- "Желтый" +- "Оранжевый" +- "Микс" (смешанные цвета) + +--- + +## Использование в каталоге + +### Отображение карточки товара + +```php +$product = Products1c::findOne($guid); +$varieties = ProductsVarieties::find() + ->where(['product_id' => $guid]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +echo "

    {$product->name}

    "; +echo "

    Доступные сорта:

    "; + +foreach ($varieties as $variety) { + echo "
    "; + echo "

    {$variety->name}

    "; + echo "

    Цвет: {$variety->color}

    "; + echo "

    {$variety->description}

    "; + echo "
    "; +} +``` + +### Фильтрация по цвету в каталоге + +```php +$selectedColor = 'Красный'; + +$products = Products1c::find() + ->innerJoin('products_varieties v', 'v.product_id = products_1c.id') + ->where(['v.color' => $selectedColor]) + ->groupBy('products_1c.id') + ->all(); +``` + +--- + +## Сценарии использования + +### 1. Каталог роз с сортами + +```php +// Создаем разные сорта красных роз +$freedomRose = new ProductsVarieties(); +$freedomRose->product_id = 'rose-red-guid'; +$freedomRose->color = 'Красный'; +$freedomRose->name = 'Фридом'; +$freedomRose->description = 'Классическая красная роза. Длина стебля 50см.'; +$freedomRose->posit = 1; // Самый популярный +$freedomRose->save(); + +$grandPrixRose = new ProductsVarieties(); +$grandPrixRose->product_id = 'rose-red-guid'; +$grandPrixRose->color = 'Красный'; +$grandPrixRose->name = 'Гранд При'; +$grandPrixRose->description = 'Премиум роза с крупным бутоном. Длина 60см.'; +$grandPrixRose->posit = 2; +$grandPrixRose->save(); +``` + +### 2. Рекомендация сорта + +```php +function getRecommendedVariety($productGuid) { + return ProductsVarieties::find() + ->where(['product_id' => $productGuid]) + ->orderBy(['posit' => SORT_ASC]) + ->one(); +} + +$recommended = getRecommendedVariety($guid); +echo "Рекомендуем: {$recommended->name}"; +``` + +### 3. Фильтр по цвету + +```php +class ProductFilter { + public function filterByColor($color) { + return ProductsVarieties::find() + ->where(['color' => $color]) + ->orderBy(['posit' => SORT_ASC, 'name' => SORT_ASC]) + ->all(); + } +} + +$filter = new ProductFilter(); +$redProducts = $filter->filterByColor('Красный'); +``` + +--- + +## Интеграция с Products1cNomenclature + +Модель дополняет информацию из номенклатуры: + +| Источник | Что хранит | +|----------|-----------| +| **Products1cNomenclature** | Базовые характеристики (категория, размер, тип) | +| **ProductsVarieties** | Детальное описание сортов с приоритетом | + +**Пример объединенного использования:** +```php +$nomenclature = Products1cNomenclature::findOne($guid); +$varieties = ProductsVarieties::find() + ->where(['product_id' => $guid]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +echo "Товар: {$nomenclature->name}" . PHP_EOL; +echo "Категория: {$nomenclature->category}" . PHP_EOL; +echo "Размер: {$nomenclature->size}см" . PHP_EOL; +echo "Доступные сорта:" . PHP_EOL; + +foreach ($varieties as $variety) { + echo "- {$variety->name} ({$variety->color})" . PHP_EOL; +} +``` + +--- + +## Отчеты и аналитика + +### Популярность сортов + +```php +// Подсчет количества сортов по цветам +$colorStats = ProductsVarieties::find() + ->select(['color', 'COUNT(*) as count']) + ->groupBy('color') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($colorStats as $stat) { + echo "Цвет {$stat['color']}: {$stat['count']} сортов" . PHP_EOL; +} +``` + +### Топ приоритетных сортов + +```php +$topVarieties = ProductsVarieties::find() + ->where(['<=', 'posit', 3]) // Приоритет 1-3 + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Promocode.md b/erp24/docs/models/Promocode.md new file mode 100644 index 00000000..f75a3408 --- /dev/null +++ b/erp24/docs/models/Promocode.md @@ -0,0 +1,577 @@ +# Model: Promocode + + +## Mindmap + +```mermaid +mindmap + root((Promocode)) + Таблица БД + promocode + Свойства + id + int + code + string + bonus + int + duration + int + active + int + used + int + Связи + CreatedBy + 1:1 Admin + UpdatedBy + 1:1 Admin + Parent + 1:1 Promocode + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель системы промокодов для начисления бонусов клиентам. Поддерживает три типа промокодов: многоразовые (общие), базовые (для генерации одноразовых) и одноразовые (персональные). Используется для маркетинговых акций, бонусных программ и персональных предложений клиентам. + +Система промокодов позволяет: +- Создавать массовые рассылки с уникальными кодами +- Отслеживать использование промокодов +- Управлять периодом действия бонусов +- Контролировать активность промокодов + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`promocode` + +--- + +## Константы + +### Статусы активности + +```php +const ACTIVE_ON = 1; // Активный промокод +const ACTIVE_OFF = 0; // Неактивный промокод +``` + +### Статусы использования + +```php +const USED_YES = 1; // Промокод использован +const USED_NO = 0; // Промокод не использован +``` + +### Типы промокодов + +```php +const BASE_SHARED = 0; // Многоразовый промокод (общий) +const BASE_BASE = 1; // База для генерации одноразовых +const BASE_SINGLE_USE = 2; // Одноразовый промокод (персональный) +``` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `code` | string(13) | **Промокод** (уникальный код для активации) | +| `bonus` | int | **Количество бонусов** получаемых по промокоду | +| `duration` | int | **Продолжительность действия** бонуса (в днях) | +| `active` | int | **Активность**: 0 - не активный, 1 - активный | +| `used` | int | **Использование**: 0 - не использован, 1 - использован | +| `base` | int | **Тип**: 0 - многоразовый, 1 - база, 2 - одноразовый | +| `parent_id` | int | **ID промокода базы** (FK к parent, если это одноразовый) | +| `date_start` | datetime | **Дата начала** действия промокода | +| `date_end` | datetime | **Дата окончания** действия промокода | +| `created_by` | int | **ID создателя** записи (FK к таблице admin) | +| `updated_by` | int | **ID редактора** записи (FK к таблице admin) | +| `created_at` | datetime | **Дата создания** записи | +| `updated_at` | datetime | **Дата изменения** записи | + +### Виртуальные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `generatePromocodeCount` | int | Количество промокодов для генерации (используется при создании базы) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'code', 'bonus', 'duration', 'active', + 'date_start', 'date_end', 'created_by', 'created_at' +] +``` + +### Строковые поля +```php +['code'] // max:13 +``` + +### Целочисленные поля +```php +[ + 'bonus', 'duration', 'active', 'used', + 'base', 'parent_id', 'created_by', 'updated_by' +] // integer +``` + +### Даты +```php +['date_start', 'date_end', 'created_at', 'updated_at'] // datetime format: Y-m-d H:i:s +``` + +### Безопасные поля +```php +['updated_by', 'updated_at', 'generatePromocodeCount'] // safe +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'code' => 'Code', + 'bonus' => 'Bonus', + 'duration' => 'Duration', + 'active' => 'Active', + 'used' => 'Used', + 'base' => 'Base', + 'parent_id' => 'Parent ID', + 'date_start' => 'Date Start', + 'date_end' => 'Date End', + 'created_by' => 'Created By', + 'updated_by' => 'Updated By', + 'created_at' => 'Created At', + 'updated_at' => 'Updated At', +] +``` + +--- + +## Связи (Relations) + +### getCreatedBy() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'created_by']` +**Описание:** Администратор, создавший промокод + +**Пример:** +```php +$promocode = Promocode::findOne($id); +$creator = $promocode->createdBy; +echo "Создал: {$creator->name}"; +``` + +### getUpdatedBy() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'updated_by']` +**Описание:** Администратор, обновивший промокод + +**Пример:** +```php +$promocode = Promocode::findOne($id); +if ($promocode->updated_by) { + $editor = $promocode->updatedBy; + echo "Изменил: {$editor->name}"; +} +``` + +### getParent() +**Тип:** `hasOne` +**Модель:** `Promocode` (self) +**Ключ:** `['id' => 'parent_id']` +**Описание:** Родительский промокод (база) для одноразового + +**Пример:** +```php +$singleUseCode = Promocode::findOne($id); +if ($singleUseCode->parent_id) { + $parent = $singleUseCode->parent; + echo "Создан из базы: {$parent->code}"; +} +``` + +--- + +## Примеры использования + +### Создание многоразового промокода + +```php +use yii_app\records\Promocode; +use Yii; + +$promocode = new Promocode(); +$promocode->code = 'SUMMER2025'; +$promocode->bonus = 500; // 500 рублей +$promocode->duration = 30; // Бонусы действуют 30 дней +$promocode->active = Promocode::ACTIVE_ON; +$promocode->used = Promocode::USED_NO; +$promocode->base = Promocode::BASE_SHARED; // Многоразовый +$promocode->date_start = '2025-06-01 00:00:00'; +$promocode->date_end = '2025-08-31 23:59:59'; +$promocode->created_by = Yii::$app->user->id; +$promocode->created_at = date('Y-m-d H:i:s'); + +if ($promocode->save()) { + echo "Промокод создан"; +} +``` + +### Создание базы для одноразовых промокодов + +```php +$base = new Promocode(); +$base->code = 'BASE_NEWUSER_2025'; +$base->bonus = 1000; +$base->duration = 60; +$base->active = Promocode::ACTIVE_ON; +$base->used = Promocode::USED_NO; +$base->base = Promocode::BASE_BASE; // База +$base->date_start = '2025-01-01 00:00:00'; +$base->date_end = '2025-12-31 23:59:59'; +$base->created_by = Yii::$app->user->id; +$base->created_at = date('Y-m-d H:i:s'); +$base->save(); + +// Генерация одноразовых промокодов из базы +$count = 100; // Сгенерировать 100 промокодов +for ($i = 0; $i < $count; $i++) { + $singleUse = new Promocode(); + $singleUse->code = strtoupper(substr(md5(uniqid(rand(), true)), 0, 10)); + $singleUse->bonus = $base->bonus; + $singleUse->duration = $base->duration; + $singleUse->active = Promocode::ACTIVE_ON; + $singleUse->used = Promocode::USED_NO; + $singleUse->base = Promocode::BASE_SINGLE_USE; // Одноразовый + $singleUse->parent_id = $base->id; + $singleUse->date_start = $base->date_start; + $singleUse->date_end = $base->date_end; + $singleUse->created_by = Yii::$app->user->id; + $singleUse->created_at = date('Y-m-d H:i:s'); + $singleUse->save(); +} +``` + +### Активация промокода клиентом + +```php +$code = 'SUMMER2025'; +$phone = '79991234567'; + +$promocode = Promocode::find() + ->where(['code' => $code]) + ->andWhere(['active' => Promocode::ACTIVE_ON]) + ->andWhere(['<=', 'date_start', date('Y-m-d H:i:s')]) + ->andWhere(['>=', 'date_end', date('Y-m-d H:i:s')]) + ->one(); + +if (!$promocode) { + throw new \Exception('Промокод не найден или неактивен'); +} + +// Проверка типа промокода +if ($promocode->base == Promocode::BASE_SINGLE_USE && $promocode->used == Promocode::USED_YES) { + throw new \Exception('Промокод уже использован'); +} + +// Начисление бонусов +$user = Users::find()->where(['phone' => $phone])->one(); +$user->balans += $promocode->bonus; +$user->save(); + +// Создание записи в истории бонусов +$bonusRecord = new UsersBonus(); +$bonusRecord->phone = $phone; +$bonusRecord->name = "Активация промокода {$promocode->code}"; +$bonusRecord->date = date('Y-m-d H:i:s'); +$bonusRecord->tip = 'credit'; +$bonusRecord->bonus = $promocode->bonus; +$bonusRecord->date_start = date('Y-m-d'); +$bonusRecord->date_end = date('Y-m-d', strtotime("+{$promocode->duration} days")); +$bonusRecord->save(); + +// Пометка промокода как использованного (для одноразовых) +if ($promocode->base == Promocode::BASE_SINGLE_USE) { + $promocode->used = Promocode::USED_YES; + $promocode->updated_at = date('Y-m-d H:i:s'); + $promocode->updated_by = Yii::$app->user->id ?? null; + $promocode->save(); +} + +echo "Промокод активирован, начислено {$promocode->bonus} бонусов"; +``` + +### Получение активных промокодов + +```php +$activePromocodes = Promocode::find() + ->where(['active' => Promocode::ACTIVE_ON]) + ->andWhere(['<=', 'date_start', date('Y-m-d H:i:s')]) + ->andWhere(['>=', 'date_end', date('Y-m-d H:i:s')]) + ->all(); + +foreach ($activePromocodes as $promo) { + $type = match($promo->base) { + Promocode::BASE_SHARED => 'Многоразовый', + Promocode::BASE_BASE => 'База', + Promocode::BASE_SINGLE_USE => 'Одноразовый', + }; + echo "{$promo->code} - {$type}, {$promo->bonus} бонусов\n"; +} +``` + +### Статистика использования промокодов + +```php +$stats = Promocode::find() + ->select([ + 'base', + 'COUNT(*) as total', + 'SUM(CASE WHEN used = 1 THEN 1 ELSE 0 END) as used_count', + 'SUM(bonus) as total_bonus' + ]) + ->groupBy('base') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $type = match((int)$stat['base']) { + 0 => 'Многоразовые', + 1 => 'Базы', + 2 => 'Одноразовые', + }; + echo "{$type}: {$stat['total']} шт., использовано: {$stat['used_count']}\n"; +} +``` + +### Деактивация промокода + +```php +$code = 'SUMMER2025'; + +$promocode = Promocode::find() + ->where(['code' => $code]) + ->one(); + +if ($promocode) { + $promocode->active = Promocode::ACTIVE_OFF; + $promocode->updated_at = date('Y-m-d H:i:s'); + $promocode->updated_by = Yii::$app->user->id; + $promocode->save(); + echo "Промокод деактивирован"; +} +``` + +--- + +## Бизнес-логика + +### Типы промокодов + +1. **Многоразовый** (`BASE_SHARED = 0`) + - Может быть использован неограниченное количество раз + - Общий для всех клиентов + - Не помечается как использованный + - Пример: `SUMMER2025`, `NEWUSER` + +2. **База** (`BASE_BASE = 1`) + - Шаблон для генерации одноразовых промокодов + - Не используется напрямую клиентами + - Хранит общие параметры (бонус, duration, даты) + - Из одной базы генерируется множество одноразовых + +3. **Одноразовый** (`BASE_SINGLE_USE = 2`) + - Может быть использован только один раз + - Персональный для каждого клиента + - Помечается как `used = 1` после активации + - Имеет `parent_id` - ссылка на базу + +### Жизненный цикл промокода + +```mermaid +stateDiagram-v2 + [*] --> Created: Создание + Created --> Active: Активация + Active --> Used: Использование (одноразовые) + Active --> Expired: Истек срок + Active --> Deactivated: Деактивация + Used --> [*] + Expired --> [*] + Deactivated --> Active: Реактивация + Deactivated --> [*] + + note right of Active + active=1 + used=0 + date_start <= NOW() + date_end >= NOW() + end note + + note right of Used + used=1 (для одноразовых) + end note +``` + +### Валидация промокода + +При активации проверяется: +1. Существование промокода +2. Активность (`active = 1`) +3. Период действия (`date_start` <= NOW() <= `date_end`) +4. Использование (для одноразовых: `used = 0`) + +### Начисление бонусов + +- **bonus** - сумма бонусов в рублях +- **duration** - срок действия бонусов в днях +- Бонусы начисляются на счет клиента +- Создается запись в `UsersBonus` с `date_start` и `date_end` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + Promocode { + int id PK + string code UK + int bonus + int duration + int active + int used + int base + int parent_id FK + datetime date_start + datetime date_end + int created_by FK + int updated_by FK + datetime created_at + datetime updated_at + } + + Promocode ||--o{ Promocode : "parent of" + Admin ||--o{ Promocode : "created" + Admin ||--o{ Promocode : "updated" + UsersBonus }o--|| Promocode : "activated" + + Admin { + int id PK + string name + } + + UsersBonus { + int id PK + string phone + float bonus + string name + } +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE promocode ADD PRIMARY KEY (id); + +-- Уникальный индекс на код +CREATE UNIQUE INDEX idx_promocode_code ON promocode(code); + +-- Индекс для поиска активных +CREATE INDEX idx_promocode_active ON promocode(active); + +-- Индекс для поиска по типу +CREATE INDEX idx_promocode_base ON promocode(base); + +-- Индекс для поиска по родителю +CREATE INDEX idx_promocode_parent_id ON promocode(parent_id); + +-- Составной индекс для валидации +CREATE INDEX idx_promocode_active_dates +ON promocode(active, date_start, date_end); + +-- Индекс для поиска неиспользованных +CREATE INDEX idx_promocode_used ON promocode(used); +``` + +--- + +## Связанные модели + +- [Admin](Admin.md) - Администраторы +- [Users](Users.md) - Клиенты +- [UsersBonus](UsersBonus.md) - Движения бонусов + +--- + +## Связанные сервисы + +- **PromocodeService** - Управление промокодами +- **BonusService** - Начисление бонусов +- **MarketingService** - Маркетинговые акции + +--- + +## API Endpoints + +- `POST /api2/promocode/activate` - Активация промокода +- `POST /admin/promocode/create` - Создание промокода +- `POST /admin/promocode/generate` - Генерация из базы +- `GET /admin/promocode/list` - Список промокодов +- `PUT /admin/promocode/deactivate` - Деактивация + +--- + +## Замечания + +1. **Код уникален** - max 13 символов, рекомендуется UPPERCASE. + +2. **Виртуальное свойство** - `generatePromocodeCount` используется только в формах. + +3. **Parent-child** - одноразовые промокоды имеют `parent_id`, указывающий на базу. + +4. **Duration в днях** - продолжительность действия начисленных бонусов. + +5. **Даты** - `date_start` и `date_end` определяют период действия промокода. + +6. **Активность** - можно деактивировать промокод до истечения срока. + +7. **Использование** - только одноразовые помечаются как `used = 1`. + +8. **Аудит** - `created_by`, `updated_by`, `created_at`, `updated_at` для отслеживания изменений. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/QUICK_REFERENCE.md b/erp24/docs/models/QUICK_REFERENCE.md index 78f3e93b..3946d5c8 100644 --- a/erp24/docs/models/QUICK_REFERENCE.md +++ b/erp24/docs/models/QUICK_REFERENCE.md @@ -1,5 +1,31 @@ # Quick Reference — Модели ERP24 +## Mindmap + +```mermaid +mindmap + root((Quick Reference)) + Топ-15 моделей + Admin + Сотрудник + Sales + Чек продажи + Users + Клиент + CityStore + Магазин + Products1c + Товар + Паттерны + hasOne + hasMany + via + Типы ключей + GUID из 1С + Автоинкремент + Композитные +``` + Быстрый справочник по основным моделям и их использованию. --- diff --git a/erp24/docs/models/QualityRating.md b/erp24/docs/models/QualityRating.md new file mode 100644 index 00000000..4b65f4df --- /dev/null +++ b/erp24/docs/models/QualityRating.md @@ -0,0 +1,272 @@ +# Класс: QualityRating + + +## Mindmap + +```mermaid +mindmap + root((QualityRating)) + Таблица БД + quality_rating + Свойства + id + int + year + int + month + int + admin_id + int + rating + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель рейтинга качества работы сотрудников в ERP24. Хранит помесячные оценки качества для расчёта мотивации и премирования персонала. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`quality_rating` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `year` | int | Год оценки | +| `month` | int | Месяц оценки (1-12) | +| `admin_id` | int | FK на сотрудника (Admin) | +| `rating` | float | Числовое значение рейтинга | + +## Статические методы + +### getQualityRating($adminId, $dateTo) +**Описание:** Получает рейтинг качества сотрудника на указанную дату. + +**Параметры:** +- `$adminId` (int) — ID сотрудника +- `$dateTo` (string) — Дата для определения периода (Y-m-d) + +**Возвращает:** `float|false` — значение рейтинга или false + +**Логика работы:** +1. Извлекает год и месяц из переданной даты +2. Ищет запись рейтинга для сотрудника на этот период +3. Возвращает последнее значение рейтинга (при наличии нескольких) +4. Использует scalar() для получения только значения rating + +## Диаграмма связей + +```mermaid +erDiagram + QualityRating { + int id PK + int year + int month + int admin_id FK + float rating + } + + QualityRatingLog { + int id PK + int rating_id FK + float old_rating + float new_rating + int created_by FK + } + + Admin { + int id PK + varchar name + } + + Admin ||--o{ QualityRating : "admin_id" + QualityRating ||--o{ QualityRatingLog : "rating_id" +``` + +## Диаграмма расчёта мотивации + +```mermaid +flowchart TD + A[Расчёт мотивации сотрудника] --> B[Получение базовых KPI] + B --> C[Вызов getQualityRating] + C --> D{Рейтинг найден?} + D -->|Да| E[Применение коэффициента рейтинга] + D -->|Нет| F[Использование дефолтного значения] + E --> G[Итоговый расчёт премии] + F --> G + G --> H[Результат мотивации] +``` + +## Примеры использования + +### Создание рейтинга сотрудника +```php +$rating = new QualityRating(); +$rating->year = 2024; +$rating->month = 12; +$rating->admin_id = $adminId; +$rating->rating = 4.5; +$rating->save(); +``` + +### Получение рейтинга за период +```php +$rating = QualityRating::getQualityRating($adminId, '2024-12-15'); + +if ($rating !== false) { + echo "Рейтинг качества: {$rating}"; +} else { + echo "Рейтинг не установлен"; +} +``` + +### Получение всех рейтингов сотрудника +```php +$ratings = QualityRating::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['year' => SORT_DESC, 'month' => SORT_DESC]) + ->all(); + +foreach ($ratings as $r) { + echo "{$r->month}/{$r->year}: {$r->rating}\n"; +} +``` + +### Средний рейтинг за год +```php +$avgRating = QualityRating::find() + ->where([ + 'admin_id' => $adminId, + 'year' => 2024 + ]) + ->average('rating'); + +echo "Средний рейтинг за 2024: {$avgRating}"; +``` + +### Рейтинг отдела за месяц +```php +$departmentAdmins = Admin::find() + ->where(['department_id' => $departmentId]) + ->select('id') + ->column(); + +$departmentRatings = QualityRating::find() + ->where([ + 'admin_id' => $departmentAdmins, + 'year' => 2024, + 'month' => 12 + ]) + ->with(['admin']) + ->orderBy(['rating' => SORT_DESC]) + ->all(); + +foreach ($departmentRatings as $r) { + $adminName = $r->admin->name ?? 'Unknown'; + echo "{$adminName}: {$r->rating}\n"; +} +``` + +### Топ сотрудников по рейтингу +```php +$topRatings = QualityRating::find() + ->where([ + 'year' => 2024, + 'month' => 12 + ]) + ->orderBy(['rating' => SORT_DESC]) + ->limit(10) + ->all(); +``` + +### Обновление рейтинга +```php +$rating = QualityRating::find() + ->where([ + 'admin_id' => $adminId, + 'year' => 2024, + 'month' => 12 + ]) + ->one(); + +if ($rating) { + $oldRating = $rating->rating; + $rating->rating = 4.8; + $rating->save(); + + // Логирование изменения + $log = new QualityRatingLog(); + $log->rating_id = $rating->id; + $log->old_rating = $oldRating; + $log->new_rating = $rating->rating; + $log->created_by = Yii::$app->user->id; + $log->created_at = date('Y-m-d H:i:s'); + $log->save(); +} +``` + +### Динамика рейтинга сотрудника +```php +$dynamics = QualityRating::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['year' => 2024]) + ->orderBy(['month' => SORT_ASC]) + ->asArray() + ->all(); + +// Данные для графика +$chartData = array_map(function($r) { + return [ + 'month' => $r['month'], + 'rating' => $r['rating'] + ]; +}, $dynamics); +``` + +### Сотрудники без рейтинга +```php +$ratedAdminIds = QualityRating::find() + ->select('admin_id') + ->where(['year' => 2024, 'month' => 12]) + ->distinct() + ->column(); + +$unratedAdmins = Admin::find() + ->where(['not in', 'id', $ratedAdminIds]) + ->andWhere(['active' => 1]) + ->all(); + +foreach ($unratedAdmins as $admin) { + echo "Без рейтинга: {$admin->name}\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `year` | required, integer | +| `month` | required, integer | +| `admin_id` | required, integer | +| `rating` | number | + +## Связанные модели + +- [Admin](./Admin.md) — сотрудники +- [QualityRatingLog](./QualityRatingLog.md) — история изменений рейтинга + +## Особенности реализации + +1. **Помесячный учёт**: Рейтинг выставляется на каждый месяц отдельно +2. **Числовой рейтинг**: float для гибкой системы оценки (например, 1-5 или 1-100) +3. **Статический метод**: getQualityRating для быстрого получения рейтинга +4. **Связь с мотивацией**: Рейтинг влияет на расчёт премий +5. **История изменений**: Связь с QualityRatingLog для аудита diff --git a/erp24/docs/models/QualityRatingLog.md b/erp24/docs/models/QualityRatingLog.md new file mode 100644 index 00000000..322f4e53 --- /dev/null +++ b/erp24/docs/models/QualityRatingLog.md @@ -0,0 +1,253 @@ +# Класс: QualityRatingLog + + +## Mindmap + +```mermaid +mindmap + root((QualityRatingLog)) + Таблица БД + quality_rating_log + Свойства + id + int + created_by + int + created_at + string + rating_id + int + new_rating + float + Связи + Rating + 1:1 QualityRating + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории изменений рейтинга качества сотрудников в ERP24. Логирует все изменения оценок качества с фиксацией старых и новых значений. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`quality_rating_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `created_by` | int | FK на создателя записи (Admin) | +| `created_at` | datetime | Время создания записи | +| `rating_id` | int | FK на рейтинг (QualityRating) | +| `old_rating` | float / null | Предыдущее значение рейтинга | +| `new_rating` | float | Новое значение рейтинга | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getRating()` | hasOne | QualityRating | Связанный рейтинг | + +## Диаграмма связей + +```mermaid +erDiagram + QualityRatingLog { + int id PK + int created_by FK + datetime created_at + int rating_id FK + float old_rating + float new_rating + } + + QualityRating { + int id PK + int year + int month + int admin_id FK + float rating + } + + Admin { + int id PK + varchar name + } + + QualityRating ||--o{ QualityRatingLog : "rating_id" + Admin ||--o{ QualityRatingLog : "created_by" +``` + +## Диаграмма процесса логирования + +```mermaid +flowchart TD + A[Изменение QualityRating] --> B{Тип операции?} + B -->|Создание| C[old_rating = null] + B -->|Обновление| D[old_rating = предыдущее значение] + C --> E[new_rating = текущее значение] + D --> E + E --> F[created_by = текущий пользователь] + F --> G[created_at = NOW] + G --> H[Сохранение QualityRatingLog] +``` + +## Примеры использования + +### Запись изменения рейтинга +```php +$log = new QualityRatingLog(); +$log->rating_id = $rating->id; +$log->old_rating = $previousRating; +$log->new_rating = $newRating; +$log->created_by = Yii::$app->user->id; +$log->created_at = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Получение истории изменений рейтинга +```php +$logs = QualityRatingLog::find() + ->where(['rating_id' => $ratingId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + $oldVal = $log->old_rating ?? 'N/A'; + echo "{$log->created_at}: {$oldVal} -> {$log->new_rating}\n"; +} +``` + +### История изменений рейтинга сотрудника +```php +$adminRatings = QualityRating::find() + ->where(['admin_id' => $adminId]) + ->select('id') + ->column(); + +$logs = QualityRatingLog::find() + ->where(['rating_id' => $adminRatings]) + ->with(['rating']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + $period = "{$log->rating->month}/{$log->rating->year}"; + echo "{$period}: {$log->old_rating} -> {$log->new_rating}\n"; +} +``` + +### Статистика изменений по администраторам +```php +$stats = QualityRatingLog::find() + ->select(['created_by', 'COUNT(*) as changes']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('created_by') + ->asArray() + ->all(); + +$admins = ArrayHelper::index(Admin::find()->all(), 'id'); + +foreach ($stats as $stat) { + $adminName = $admins[$stat['created_by']]->name ?? 'Unknown'; + echo "{$adminName}: {$stat['changes']} изменений\n"; +} +``` + +### Анализ значительных изменений +```php +$significantChanges = QualityRatingLog::find() + ->where(['not', ['old_rating' => null]]) + ->andWhere('ABS(new_rating - old_rating) > 1') + ->with(['rating', 'rating.admin']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($significantChanges as $log) { + $diff = $log->new_rating - $log->old_rating; + $direction = $diff > 0 ? 'повышение' : 'понижение'; + $adminName = $log->rating->admin->name ?? 'Unknown'; + echo "{$adminName}: {$direction} на {$diff}\n"; +} +``` + +### Получение последнего изменения рейтинга +```php +$lastChange = QualityRatingLog::find() + ->where(['rating_id' => $ratingId]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastChange) { + $changedBy = Admin::findOne($lastChange->created_by); + echo "Последнее изменение: {$changedBy->name} в {$lastChange->created_at}"; +} +``` + +### Откат к предыдущему значению рейтинга +```php +$lastLog = QualityRatingLog::find() + ->where(['rating_id' => $ratingId]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastLog && $lastLog->old_rating !== null) { + $rating = QualityRating::findOne($ratingId); + $rating->rating = $lastLog->old_rating; + $rating->save(); + + // Логируем откат + $rollbackLog = new QualityRatingLog(); + $rollbackLog->rating_id = $ratingId; + $rollbackLog->old_rating = $lastLog->new_rating; + $rollbackLog->new_rating = $lastLog->old_rating; + $rollbackLog->created_by = Yii::$app->user->id; + $rollbackLog->created_at = date('Y-m-d H:i:s'); + $rollbackLog->save(); +} +``` + +### Активность изменений за период +```php +$dailyActivity = QualityRatingLog::find() + ->select([ + 'DATE(created_at) as date', + 'COUNT(*) as changes' + ]) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('DATE(created_at)') + ->orderBy(['date' => SORT_ASC]) + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `created_by` | required, integer | +| `created_at` | required, safe | +| `rating_id` | required, integer | +| `old_rating` | number | +| `new_rating` | required, number | + +## Связанные модели + +- [QualityRating](./QualityRating.md) — рейтинги качества +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Полный аудит**: Сохраняются и старое, и новое значения +2. **Nullable old_rating**: null при создании нового рейтинга +3. **Ответственность**: Фиксируется автор изменения через created_by +4. **Временная метка**: Точное время изменения в created_at +5. **Связь с рейтингом**: rating_id для группировки истории +6. **Возможность отката**: Хранение old_rating позволяет восстановить предыдущее значение diff --git a/erp24/docs/models/RateCategoryAdminGroup.md b/erp24/docs/models/RateCategoryAdminGroup.md new file mode 100644 index 00000000..e965299d --- /dev/null +++ b/erp24/docs/models/RateCategoryAdminGroup.md @@ -0,0 +1,235 @@ +# Модель RateCategoryAdminGroup + + +## Mindmap + +```mermaid +mindmap + root((RateCategoryAdminGroup)) + Таблица БД + rate_category_admin_group + Свойства + id + int + category_id + int + admin_group_id + int + rate_1_condition + int + rate_1_id + int + rate_2_condition + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `RateCategoryAdminGroup` определяет условия выполнения норм смены для различных комбинаций категорий магазинов и должностей сотрудников. Хранит пороговые значения и соответствующие ставки для трёх уровней выполнения плана (норма, +15%, +30%). Используется для расчёта зарплаты с учётом категории магазина и должности. + +**Файл модели:** `erp24/records/RateCategoryAdminGroup.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `rate_category_admin_group` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `category_id` | INTEGER | ID категории магазина | +| `admin_group_id` | INTEGER | ID должности (FK → admin_group.id) | +| `rate_1_condition` | INTEGER | Условие 1: норма смены (количество) | +| `rate_1_id` | INTEGER | ID ставки для нормы (FK → rate_dict.id) | +| `rate_2_condition` | INTEGER | Условие 2: норма +15% | +| `rate_2_id` | INTEGER | ID ставки для +15% | +| `rate_3_condition` | INTEGER | Условие 3: норма +30% | +| `rate_3_id` | INTEGER | ID ставки для +30% | + +--- + +## Описание полей + +### Условия и ставки + +Система поддерживает три уровня выполнения плана: + +| Уровень | Условие | Описание | +|---------|---------|----------| +| 1 | `rate_1_condition` | Базовая норма смены | +| 2 | `rate_2_condition` | Перевыполнение на 15% | +| 3 | `rate_3_condition` | Перевыполнение на 30% | + +**Пример:** Для флориста в магазине категории A: +- Норма: 10 букетов → базовая ставка +- +15%: 12 букетов → повышенная ставка +- +30%: 13 букетов → максимальная ставка + +--- + +## Методы модели + +### `getNormaSmena($categoryId, $adminGroupId): ?array` + +Возвращает настройки норм смены для указанной категории магазина и должности. + +**Параметры:** +- `$categoryId` — ID категории магазина +- `$adminGroupId` — ID группы/должности сотрудника + +**Возвращает:** массив с настройками или `null` + +```php +$norma = RateCategoryAdminGroup::getNormaSmena(1, 5); + +if ($norma) { + echo "Норма: {$norma['rate_1_condition']}\n"; + echo "+15%: {$norma['rate_2_condition']}\n"; + echo "+30%: {$norma['rate_3_condition']}\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + rate_category_admin_group }o--|| admin_group : "position" + rate_category_admin_group }o--|| rate_dict : "rate_1" + rate_category_admin_group }o--|| rate_dict : "rate_2" + rate_category_admin_group }o--|| rate_dict : "rate_3" + + rate_category_admin_group { + int id PK + int category_id + int admin_group_id FK + int rate_1_condition + int rate_1_id FK + int rate_2_condition + int rate_2_id FK + int rate_3_condition + int rate_3_id FK + } + + admin_group { + int id PK + string name + } + + rate_dict { + int id PK + string name + float value + } +``` + +--- + +## Примеры использования + +### Создание настроек для должности + +```php +$config = new RateCategoryAdminGroup(); +$config->category_id = 1; // Категория A +$config->admin_group_id = 5; // Флорист +$config->rate_1_condition = 10; // Норма: 10 букетов +$config->rate_1_id = 1; // Базовая ставка +$config->rate_2_condition = 12; // +15%: 12 букетов +$config->rate_2_id = 2; // Повышенная ставка +$config->rate_3_condition = 13; // +30%: 13 букетов +$config->rate_3_id = 3; // Максимальная ставка +$config->save(); +``` + +### Расчёт ставки по результату смены + +```php +function calculateRate(int $categoryId, int $adminGroupId, int $result): ?int +{ + $norma = RateCategoryAdminGroup::getNormaSmena($categoryId, $adminGroupId); + + if (!$norma) { + return null; + } + + // Определяем уровень выполнения + if ($result >= $norma['rate_3_condition']) { + return $norma['rate_3_id']; // +30% + } elseif ($result >= $norma['rate_2_condition']) { + return $norma['rate_2_id']; // +15% + } elseif ($result >= $norma['rate_1_condition']) { + return $norma['rate_1_id']; // Норма + } + + return null; // Норма не выполнена +} + +$rateId = calculateRate(1, 5, 14); +if ($rateId) { + $rate = RateDict::findOne($rateId); + echo "Применяется ставка: {$rate->name}"; +} +``` + +### Получение всех настроек для должности + +```php +$configs = RateCategoryAdminGroup::find() + ->where(['admin_group_id' => $adminGroupId]) + ->all(); + +foreach ($configs as $config) { + echo "Категория {$config->category_id}: "; + echo "норма {$config->rate_1_condition}, "; + echo "+15% от {$config->rate_2_condition}, "; + echo "+30% от {$config->rate_3_condition}\n"; +} +``` + +### Получение настроек для категории магазина + +```php +$configs = RateCategoryAdminGroup::find() + ->where(['category_id' => $categoryId]) + ->all(); + +echo "Нормы для категории магазина {$categoryId}:\n"; +foreach ($configs as $config) { + $group = AdminGroup::findOne($config->admin_group_id); + echo "- {$group->name}: {$config->rate_1_condition}\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `category_id` | Обязательное, целое число | +| `admin_group_id` | Обязательное, целое число | +| `rate_1_condition` | Обязательное, целое число | +| `rate_1_id` | Обязательное, целое число | +| `rate_2_condition` | Обязательное, целое число | +| `rate_2_id` | Обязательное, целое число | +| `rate_3_condition` | Обязательное, целое число | +| `rate_3_id` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности сотрудников +- **[RateDict](./RateDict.md)** — справочник ставок +- **[RateStoreCategory](./RateStoreCategory.md)** — привязка магазинов к категориям + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/RateDict.md b/erp24/docs/models/RateDict.md new file mode 100644 index 00000000..c6cdfd37 --- /dev/null +++ b/erp24/docs/models/RateDict.md @@ -0,0 +1,188 @@ +# Модель RateDict + + +## Mindmap + +```mermaid +mindmap + root((RateDict)) + Таблица БД + rate_dict + Свойства + id + int + name + string + game_value + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `RateDict` представляет справочник ставок и коэффициентов для расчёта заработной платы и геймификации. Хранит названия ставок, их значения для расчёта оклада и баллы для системы мотивации. Используется в системе расчёта зарплаты и KPI сотрудников. + +**Файл модели:** `erp24/records/RateDict.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `rate_dict` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(100) | Название ставки/коэффициента | +| `value_type_add` | VARCHAR(100) | Тип значения для расчёта (процент, фиксированная сумма) | +| `value` | FLOAT | Значение для расчёта оклада | +| `game_value` | INTEGER | Значение для геймификации (баллы) | + +--- + +## Описание полей + +### `value` — Значение для расчёта + +Используется при расчёте заработной платы. Может быть: +- Процентом от оклада (например, 0.15 = +15%) +- Фиксированной суммой (например, 500 рублей) + +### `game_value` — Баллы геймификации + +Количество баллов, начисляемых сотруднику при достижении показателя. Используется для рейтинга и мотивации. + +### `value_type_add` — Тип расчёта + +| Значение | Описание | +|----------|----------| +| `percent` | Процент от базовой ставки | +| `fixed` | Фиксированная сумма | +| `multiply` | Множитель | + +--- + +## Примеры использования + +### Создание ставки + +```php +$rate = new RateDict(); +$rate->name = 'Перевыполнение плана +15%'; +$rate->value_type_add = 'percent'; +$rate->value = 0.15; +$rate->game_value = 50; +$rate->save(); +``` + +### Получение всех ставок + +```php +$rates = RateDict::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($rates as $rate) { + $percent = $rate->value * 100; + echo "{$rate->name}: +{$percent}% к окладу, {$rate->game_value} баллов\n"; +} +``` + +### Расчёт надбавки к зарплате + +```php +$baseSalary = 50000; +$rateId = 1; + +$rate = RateDict::findOne($rateId); + +if ($rate) { + switch ($rate->value_type_add) { + case 'percent': + $bonus = $baseSalary * $rate->value; + break; + case 'fixed': + $bonus = $rate->value; + break; + case 'multiply': + $bonus = $baseSalary * ($rate->value - 1); + break; + default: + $bonus = 0; + } + + echo "Надбавка по ставке '{$rate->name}': {$bonus} руб."; +} +``` + +### Начисление баллов геймификации + +```php +$adminId = 15; +$rateId = 2; + +$rate = RateDict::findOne($rateId); + +if ($rate && $rate->game_value > 0) { + $balance = new EmployeeBalance(); + $balance->admin_id = $adminId; + $balance->name = "Начислено за: {$rate->name}"; + $balance->points = $rate->game_value; + $balance->entity_type = 'rate'; + $balance->entity_id = $rate->id; + $balance->created_at = date('Y-m-d H:i:s'); + $balance->save(); +} +``` + +### Получение ставок с высокими баллами + +```php +$topRates = RateDict::find() + ->where(['>', 'game_value', 30]) + ->orderBy(['game_value' => SORT_DESC]) + ->all(); +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + rate_dict { + int id PK + string name + string value_type_add + float value + int game_value + } + + rate_dict ||--o{ rate_category_admin_group : "used_in" +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, строка, макс. 100 символов | +| `game_value` | Обязательное, целое число | +| `value` | Число (float) | +| `value_type_add` | Строка, макс. 100 символов | + +--- + +## Связанные модели + +- **[RateCategoryAdminGroup](./RateCategoryAdminGroup.md)** — привязка ставок к категориям и должностям +- **[RateStoreCategory](./RateStoreCategory.md)** — категории магазинов для ставок +- **[EmployeeBalance](./EmployeeBalance.md)** — баланс баллов сотрудников + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/RateStoreCategory.md b/erp24/docs/models/RateStoreCategory.md new file mode 100644 index 00000000..23fae221 --- /dev/null +++ b/erp24/docs/models/RateStoreCategory.md @@ -0,0 +1,275 @@ +# Модель RateStoreCategory + + +## Mindmap + +```mermaid +mindmap + root((RateStoreCategory)) + Таблица БД + rate_store_category + Свойства + store_id + int + category_id + int + Связи + Store + 1:1 CityStore + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `RateStoreCategory` связывает магазины с категориями для системы расчёта ставок и зарплат. Поддерживает темпоральное версионирование: каждая запись имеет период действия и признак активности. Используется для определения категории магазина при расчёте норм смены и KPI сотрудников. + +**Файл модели:** `erp24/records/RateStoreCategory.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `rate_store_category` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `store_id` | INTEGER | ID магазина (FK → city_store.id) | +| `category_id` | INTEGER | ID категории | +| `date_from` | DATE | Дата начала действия | +| `date_to` | DATE | Дата окончания действия | +| `is_active` | INTEGER | Признак активности (1/0) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `store_id`, `category_id`, `is_active` +- **Темпоральное версионирование** через `date_from` и `date_to` +- Магазин может менять категорию со временем +- История изменений категорий сохраняется + +--- + +## Категории магазинов + +| ID | Название | Описание | +|----|----------|----------| +| 1 | A | Высокий трафик, большие продажи | +| 2 | B | Средний трафик | +| 3 | C | Низкий трафик | +| 4 | D | Новые/тестовые точки | + +--- + +## Связи (Relations) + +### `getStore(): ActiveQuery` + +Возвращает связанный магазин. + +**Тип связи:** `hasOne` +**Связанная модель:** CityStore + +```php +$rateCategory = RateStoreCategory::findOne(['store_id' => 5]); +echo "Магазин: {$rateCategory->store->name}"; +``` + +--- + +## Методы модели + +### `getAllStoreCategory(): array` + +Возвращает все записи связей магазинов с категориями. + +```php +$all = RateStoreCategory::getAllStoreCategory(); +``` + +### `getStoreCategory($storeId): ?int` + +Возвращает ID категории для указанного магазина. + +```php +$categoryId = RateStoreCategory::getStoreCategory(5); +echo "Категория магазина: {$categoryId}"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + rate_store_category }o--|| city_store : "store" + + rate_store_category { + int store_id PK,FK + int category_id PK + date date_from + date date_to + int is_active PK + } + + city_store { + int id PK + string name + int city_id + } +``` + +--- + +## Примеры использования + +### Назначение категории магазину + +```php +$storeCategory = new RateStoreCategory(); +$storeCategory->store_id = $storeId; +$storeCategory->category_id = 1; // Категория A +$storeCategory->date_from = date('Y-m-d'); +$storeCategory->date_to = '2099-12-31'; +$storeCategory->is_active = 1; +$storeCategory->save(); +``` + +### Получение текущей категории магазина + +```php +$categoryId = RateStoreCategory::getStoreCategory($storeId); + +if ($categoryId) { + echo "Магазин относится к категории {$categoryId}"; +} else { + echo "Категория не назначена"; +} +``` + +### Изменение категории магазина + +```php +$storeId = 5; +$newCategoryId = 2; +$today = date('Y-m-d'); + +// Закрываем текущую категорию +RateStoreCategory::updateAll( + [ + 'date_to' => date('Y-m-d', strtotime('-1 day')), + 'is_active' => 0 + ], + [ + 'store_id' => $storeId, + 'is_active' => 1 + ] +); + +// Создаём новую категорию +$newCategory = new RateStoreCategory(); +$newCategory->store_id = $storeId; +$newCategory->category_id = $newCategoryId; +$newCategory->date_from = $today; +$newCategory->date_to = '2099-12-31'; +$newCategory->is_active = 1; +$newCategory->save(); +``` + +### История категорий магазина + +```php +$history = RateStoreCategory::find() + ->where(['store_id' => $storeId]) + ->orderBy(['date_from' => SORT_DESC]) + ->all(); + +echo "История категорий магазина:\n"; +foreach ($history as $record) { + $status = $record->is_active ? 'активна' : 'закрыта'; + echo "Категория {$record->category_id}: {$record->date_from} - {$record->date_to} ({$status})\n"; +} +``` + +### Получение магазинов по категории + +```php +$categoryId = 1; + +$stores = RateStoreCategory::find() + ->alias('rsc') + ->innerJoin('city_store cs', 'cs.id = rsc.store_id') + ->select(['cs.id', 'cs.name']) + ->where([ + 'rsc.category_id' => $categoryId, + 'rsc.is_active' => 1 + ]) + ->asArray() + ->all(); + +echo "Магазины категории A:\n"; +foreach ($stores as $store) { + echo "- {$store['name']}\n"; +} +``` + +### Статистика по категориям + +```php +$stats = RateStoreCategory::find() + ->select(['category_id', 'COUNT(*) as count']) + ->where(['is_active' => 1]) + ->groupBy('category_id') + ->asArray() + ->all(); + +echo "Распределение магазинов по категориям:\n"; +foreach ($stats as $stat) { + echo "Категория {$stat['category_id']}: {$stat['count']} магазинов\n"; +} +``` + +### Получение категории на определённую дату + +```php +$storeId = 5; +$date = '2025-06-15'; + +$category = RateStoreCategory::find() + ->where(['store_id' => $storeId]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere(['>=', 'date_to', $date]) + ->one(); + +if ($category) { + echo "Категория на {$date}: {$category->category_id}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `store_id` | Обязательное, целое число | +| `category_id` | Обязательное, целое число | +| `date_from` | Обязательное | +| `date_to` | Обязательное | +| `is_active` | Целое число | +| `store_id + category_id + is_active` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазины +- **[RateCategoryAdminGroup](./RateCategoryAdminGroup.md)** — нормы по категориям и должностям +- **[RateDict](./RateDict.md)** — справочник ставок + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/RateStoreCategorySearch.md b/erp24/docs/models/RateStoreCategorySearch.md new file mode 100644 index 00000000..4b979233 --- /dev/null +++ b/erp24/docs/models/RateStoreCategorySearch.md @@ -0,0 +1,171 @@ +# Класс: RateStoreCategorySearch + + +## Mindmap + +```mermaid +mindmap + root((RateStoreCategorySearch)) + Таблица БД + ActiveRecord + Наследование + extends RateStoreCategory +``` + +## Назначение +Search-модель для поиска и фильтрации связей магазинов с категориями рейтинга в ERP24. Простая модель с JOIN к магазинам и увеличенным размером страницы (100 записей). + +## Пространство имён +`yii_app\records` + +## Родительский класс +`RateStoreCategory` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `store_id`, `category_id` — integer + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к магазинам. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с joinWith для связи store +2. Оборачивает в ActiveDataProvider с кастомной пагинацией: + - forcePageParam: false + - pageSizeParam: false + - pageSize: 100 +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: store_id, category_id + +## Диаграмма связей + +```mermaid +erDiagram + RateStoreCategory { + int store_id FK + int category_id FK + } + + CityStore { + int id PK + varchar name + } + + RateCategory { + int id PK + varchar name + } + + RateStoreCategory }o--|| CityStore : "store_id" + RateStoreCategory }o--|| RateCategory : "category_id" +``` + +## Диаграмма пагинации + +```mermaid +flowchart TD + A[ActiveDataProvider] --> B[pagination] + B --> C[forcePageParam: false] + B --> D[pageSizeParam: false] + B --> E[pageSize: 100] + + C --> F[Страница без параметра в URL] + D --> F + E --> G[100 записей на странице] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new RateStoreCategorySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по магазину +```php +$searchModel = new RateStoreCategorySearch(); +$dataProvider = $searchModel->search([ + 'RateStoreCategorySearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Поиск по категории +```php +$searchModel = new RateStoreCategorySearch(); +$dataProvider = $searchModel->search([ + 'RateStoreCategorySearch' => [ + 'category_id' => 3, + ] +]); +``` + +### Поиск связи магазин-категория +```php +$searchModel = new RateStoreCategorySearch(); +$dataProvider = $searchModel->search([ + 'RateStoreCategorySearch' => [ + 'store_id' => 5, + 'category_id' => 3, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + [ + 'attribute' => 'store_id', + 'value' => 'store.name', + ], + [ + 'attribute' => 'category_id', + 'value' => 'category.name', + ], + ], +]) ?> +``` + +## Связанные модели + +- [RateStoreCategory](./RateStoreCategory.md) — базовая модель связи +- [CityStore](./CityStore.md) — магазины +- [RateCategory](./RateCategory.md) — категории рейтинга + +## Особенности реализации + +1. **Таблица связи**: Связь many-to-many между магазинами и категориями +2. **joinWith store**: Eager loading магазина +3. **Увеличенная пагинация**: 100 записей на странице +4. **Скрытые параметры пагинации**: forcePageParam и pageSizeParam отключены +5. **Простая фильтрация**: Только по store_id и category_id diff --git a/erp24/docs/models/ReferralStatus.md b/erp24/docs/models/ReferralStatus.md new file mode 100644 index 00000000..298b2b75 --- /dev/null +++ b/erp24/docs/models/ReferralStatus.md @@ -0,0 +1,477 @@ +# Model: ReferralStatus + + +## Mindmap + +```mermaid +mindmap + root((ReferralStatus)) + Таблица БД + referral_status + Свойства + id + int + check_id + string + user_id + int + referral_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель отслеживания реферальных покупок. Фиксирует факт совершения покупки приведенным клиентом (другом) и связывает её с рефер ером (кто привел). Используется для начисления реферальных бонусов и аналитики эффективности реферальной программы. + +Каждая запись представляет одну покупку, совершенную по реферальной ссылке, и содержит информацию о чеке, покупателе и рефере. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`referral_status` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `check_id` | string(36) | **GUID чека** из таблицы sales (покупка друга) | +| `user_id` | int | **ID друга** из таблицы users (кто совершил покупку) | +| `referral_id` | int | **ID реферера** из таблицы users (кто привел друга) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['check_id', 'user_id', 'referral_id'] +``` + +### Целочисленные поля +```php +['user_id', 'referral_id'] // integer +``` + +### Строковые поля +```php +['check_id'] // max:36 (GUID формат) +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'check_id' => 'Check ID', + 'user_id' => 'User ID', + 'referral_id' => 'Referral ID', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['id' => 'user_id']` +**Описание:** Друг, который совершил покупку + +**Пример:** +```php +$referralStatus = ReferralStatus::findOne($id); +$friend = $referralStatus->user; +echo "Покупатель: {$friend->name}"; +``` + +### getReferral() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['id' => 'referral_id']` +**Описание:** Реферер, который привел друга + +**Пример:** +```php +$referralStatus = ReferralStatus::findOne($id); +$referrer = $referralStatus->referral; +echo "Реферер: {$referrer->name}"; +``` + +### getCheck() +**Тип:** `hasOne` +**Модель:** `Sales` +**Ключ:** `['id' => 'check_id']` +**Описание:** Чек покупки + +**Пример:** +```php +$referralStatus = ReferralStatus::findOne($id); +$check = $referralStatus->check; +echo "Сумма покупки: {$check->summ}"; +``` + +--- + +## Примеры использования + +### Создание записи при реферальной покупке + +```php +use yii_app\records\ReferralStatus; + +// При совершении покупки другом +$checkId = $sale->id; // GUID чека +$friendId = $friend->id; // ID друга +$referrerId = $friend->referral_id; // ID реферера + +if ($referrerId) { + $referralStatus = new ReferralStatus(); + $referralStatus->check_id = $checkId; + $referralStatus->user_id = $friendId; + $referralStatus->referral_id = $referrerId; + + if ($referralStatus->save()) { + echo "Реферальная покупка зафиксирована"; + + // Начисление бонусов рефереру + $purchaseAmount = $sale->summ; + $referrer = Users::findOne($referrerId); + + // Определение процента реферальных бонусов из уровня + $level = BonusLevels::find() + ->where(['active' => 1, 'alias' => $referrer->bonus_level]) + ->one(); + + if ($level) { + $referralBonus = $purchaseAmount * ($level->referal_rate / 100); + + $referrer->balans += $referralBonus; + $referrer->save(); + + // Запись в историю + $bonusRecord = new UsersBonus(); + $bonusRecord->phone = $referrer->phone; + $bonusRecord->name = "Реферальный бонус за покупку друга"; + $bonusRecord->date = date('Y-m-d H:i:s'); + $bonusRecord->tip = 'credit'; + $bonusRecord->bonus = $referralBonus; + $bonusRecord->check_id = $checkId; + $bonusRecord->referal_id = $friendId; + $bonusRecord->save(); + } + } +} +``` + +### Получение всех реферальных покупок реферера + +```php +$referrerId = 123; + +$referralPurchases = ReferralStatus::find() + ->where(['referral_id' => $referrerId]) + ->with(['user', 'check']) + ->all(); + +echo "Реферальных покупок: " . count($referralPurchases) . "\n"; + +foreach ($referralPurchases as $purchase) { + echo "Друг: {$purchase->user->name}, сумма: {$purchase->check->summ}\n"; +} +``` + +### Статистика реферальной программы + +```php +$referrerId = 123; + +$stats = ReferralStatus::find() + ->select([ + 'COUNT(*) as purchases_count', + 'COUNT(DISTINCT user_id) as friends_count' + ]) + ->where(['referral_id' => $referrerId]) + ->asArray() + ->one(); + +// Общая сумма покупок друзей +$totalAmount = ReferralStatus::find() + ->joinWith('check') + ->where(['referral_id' => $referrerId]) + ->sum('sales.summ'); + +echo "Приведено друзей: {$stats['friends_count']}\n"; +echo "Покупок друзьями: {$stats['purchases_count']}\n"; +echo "Общая сумма покупок: {$totalAmount} руб.\n"; +``` + +### Проверка, была ли покупка реферальной + +```php +$checkId = 'a1b2c3d4-e5f6-7890-1234-567890abcdef'; + +$isReferral = ReferralStatus::find() + ->where(['check_id' => $checkId]) + ->exists(); + +if ($isReferral) { + $referralData = ReferralStatus::find() + ->where(['check_id' => $checkId]) + ->one(); + + echo "Реферальная покупка: друг {$referralData->user_id}, реферер {$referralData->referral_id}"; +} +``` + +### Топ-10 рефереров по количеству приведенных покупок + +```php +$topReferrers = ReferralStatus::find() + ->select([ + 'referral_id', + 'COUNT(*) as purchases_count' + ]) + ->groupBy('referral_id') + ->orderBy(['purchases_count' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); + +foreach ($topReferrers as $key => $referrer) { + $user = Users::findOne($referrer['referral_id']); + echo ($key + 1) . ". {$user->name}: {$referrer['purchases_count']} покупок\n"; +} +``` + +### История реферальных покупок за период + +```php +$referrerId = 123; +$startDate = '2025-01-01'; +$endDate = '2025-01-31'; + +$purchases = ReferralStatus::find() + ->joinWith('check') + ->where(['referral_id' => $referrerId]) + ->andWhere(['>=', 'sales.date', $startDate]) + ->andWhere(['<=', 'sales.date', $endDate]) + ->all(); + +echo "Реферальных покупок в январе: " . count($purchases); +``` + +### Анализ конверсии приведенных друзей + +```php +$referrerId = 123; + +// Всего приведено друзей +$friendsCount = Users::find() + ->where(['referral_id' => $referrerId]) + ->count(); + +// Сколько из них совершили покупки +$buyingFriendsCount = ReferralStatus::find() + ->select('user_id') + ->where(['referral_id' => $referrerId]) + ->distinct() + ->count(); + +$conversionRate = $friendsCount > 0 ? ($buyingFriendsCount / $friendsCount) * 100 : 0; + +echo "Приведено друзей: {$friendsCount}\n"; +echo "Совершили покупки: {$buyingFriendsCount}\n"; +echo "Конверсия: " . round($conversionRate, 2) . "%\n"; +``` + +--- + +## Бизнес-логика + +### Механизм работы реферальной программы + +1. **Регистрация друга** + - Новый клиент регистрируется по реферальной ссылке + - В `Users` устанавливается `referral_id` реферера + +2. **Покупка другом** + - Друг совершает покупку + - Создается запись в `Sales` + - Создается запись в `ReferralStatus` + +3. **Начисление бонусов рефереру** + - Рассчитывается бонус по формуле: `сумма * referal_rate` + - Бонусы начисляются на счет реферера + - Создается запись в `UsersBonus` с `referal_id` + +### Связь полей + +- **check_id** - GUID чека из `Sales.id` +- **user_id** - ID друга из `Users.id` +- **referral_id** - ID реферера из `Users.id`, соответствует `Users.referral_id` у друга + +### Условия начисления + +Реферальные бонусы начисляются: +- При каждой покупке друга (не только первой) +- Процент зависит от уровня реферера (`BonusLevels.referal_rate`) +- Бонусы начисляются сразу после покупки + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + ReferralStatus { + int id PK + string check_id FK + int user_id FK + int referral_id FK + } + + Users ||--o{ ReferralStatus : "friend purchases" + Users ||--o{ ReferralStatus : "referrer gets bonuses" + Sales ||--o| ReferralStatus : "purchase" + UsersBonus }o--|| ReferralStatus : "bonus record" + + Users { + int id PK + string phone + string name + int referral_id FK + } + + Sales { + string id PK + bigint phone + decimal summ + datetime date + } + + UsersBonus { + int id PK + string phone + float bonus + int referal_id FK + string check_id FK + } +``` + +--- + +## Диаграмма процесса реферальной покупки + +```mermaid +sequenceDiagram + participant Friend as Друг + participant Sale as Продажа + participant RS as ReferralStatus + participant Referrer as Реферер + participant UB as UsersBonus + + Friend->>Sale: Совершает покупку + Sale-->>RS: Создание записи + RS->>Referrer: Получение данных реферера + Referrer->>Referrer: Определение уровня и referal_rate + Referrer->>Referrer: Расчет бонусов + Referrer->>Referrer: Начисление balans + Referrer->>UB: Запись в историю бонусов + UB-->>Referrer: Подтверждение + + Note over Friend,UB: Друг ID=user_id, Реферер ID=referral_id +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE referral_status ADD PRIMARY KEY (id); + +-- Индекс для поиска по чеку +CREATE INDEX idx_referral_status_check_id ON referral_status(check_id); + +-- Индекс для поиска покупок друга +CREATE INDEX idx_referral_status_user_id ON referral_status(user_id); + +-- Индекс для поиска покупок приведенных рефером +CREATE INDEX idx_referral_status_referral_id ON referral_status(referral_id); + +-- Составной индекс для статистики +CREATE INDEX idx_referral_status_referral_user +ON referral_status(referral_id, user_id); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты (друзья и рефереры) +- [Sales](Sales.md) - Продажи +- [UsersBonus](UsersBonus.md) - Движения бонусов +- [BonusLevels](BonusLevels.md) - Уровни бонусной программы + +--- + +## Связанные сервисы + +- **ReferralService** - Управление реферальной программой +- **BonusService** - Начисление бонусов +- **AnalyticsService** - Аналитика эффективности + +--- + +## API Endpoints + +- `GET /api2/referral/stats` - Статистика реферальной программы +- `GET /api2/referral/purchases` - История реферальных покупок +- `GET /api2/referral/friends` - Список приведенных друзей + +--- + +## Замечания + +1. **Один чек - одна запись** - каждая покупка друга создает новую запись. + +2. **check_id GUID** - использует GUID из таблицы Sales. + +3. **Двойная связь с Users** - user_id (друг) и referral_id (реферер). + +4. **Начисление при каждой покупке** - не только первая покупка друга. + +5. **Процент от уровня** - `referal_rate` берется из уровня реферера. + +6. **История в UsersBonus** - запись содержит referal_id для отслеживания. + +7. **Аналитика** - используется для расчета ROI реферальной программы. + +8. **Отсутствие дат** - даты берутся из связанного чека (Sales.date). + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/RegulationGroup.md b/erp24/docs/models/RegulationGroup.md new file mode 100644 index 00000000..6b73b83c --- /dev/null +++ b/erp24/docs/models/RegulationGroup.md @@ -0,0 +1,153 @@ +# Class: RegulationGroup + + +## Mindmap + +```mermaid +mindmap + root((RegulationGroup)) + Таблица БД + regulation_group + Свойства + id + int + posit + int + Связи + Regulations + 1:N Regulations + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель группы (категории) регламентов в системе ERP24. Используется для иерархической организации внутренних документов компании. Поддерживает вложенность через parent_id, позволяя создавать древовидную структуру категорий. Каждая группа может содержать несколько регламентов и подгрупп. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`regulation_group` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `name` | string(250) | Название группы | +| `parent_id` | int | **FK** ID родительской группы (NULL для корневых) | +| `posit` | int | Позиция в списке (для сортировки) | + +--- + +## Отношения (Relations) + +### getRegulations() + +**Тип:** `hasMany` +**Модель:** `Regulations` +**Ключ:** `['group_id' => 'id']` +**Описание:** Все регламенты группы + +**Логика:** +Связь один-ко-многим с моделью Regulations. Возвращает все регламенты, принадлежащие данной группе. Используется для отображения списка документов в категории. + +**Вызовы сторонних методов:** +- `Regulations::hasMany()` - построение связи с таблицей regulations + +**Пример:** +```php +$group = RegulationGroup::findOne($id); +echo "Группа: {$group->name}\n"; +echo "Количество регламентов: " . count($group->regulations) . "\n"; + +foreach ($group->regulations as $regulation) { + echo "- {$regulation->name}\n"; +} +``` + +--- + +## Правила валидации + +```php +['parent_id', 'posit'] // integer +['name'] // string, max: 250 +``` + +--- + +## Примеры использования + +### Создание корневой группы + +```php +use yii_app\records\RegulationGroup; + +$group = new RegulationGroup(); +$group->name = 'Кассовые операции'; +$group->parent_id = null; // Корневая группа +$group->posit = 1; +$group->save(); +``` + +--- + +### Создание подгруппы + +```php +$subgroup = new RegulationGroup(); +$subgroup->name = 'Работа с наличными'; +$subgroup->parent_id = $parentGroupId; // ID родительской группы +$subgroup->posit = 1; +$subgroup->save(); +``` + +--- + +### Получение дерева категорий + +```php +// Корневые группы +$rootGroups = RegulationGroup::find() + ->where(['parent_id' => null]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($rootGroups as $root) { + echo "{$root->name}\n"; + + // Подгруппы + $subgroups = RegulationGroup::find() + ->where(['parent_id' => $root->id]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + + foreach ($subgroups as $sub) { + echo " └─ {$sub->name}\n"; + } +} +``` + +--- + +## Связанные документы + +- [Regulations.md](./Regulations.md) — регламенты +- [RegulationsPassed.md](./RegulationsPassed.md) — прохождение регламентов +- [RegulationsPoll.md](./RegulationsPoll.md) — вопросы по регламентам diff --git a/erp24/docs/models/Regulations.md b/erp24/docs/models/Regulations.md index 52a23cec..4fa5852c 100644 --- a/erp24/docs/models/Regulations.md +++ b/erp24/docs/models/Regulations.md @@ -1,5 +1,30 @@ # Class: Regulations + +## Mindmap + +```mermaid +mindmap + root((Regulations)) + Таблица БД + regulations + Свойства + id + int + group_id + int + name + string + content + string + created_at + string + created_by + int + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель Regulations представляет регламент (внутренний нормативный документ) компании в системе ERP24. Регламенты содержат правила, инструкции и стандарты работы, которые должны знать сотрудники. Интегрируется с системой тестирования для проверки знаний. diff --git a/erp24/docs/models/RegulationsPassed.md b/erp24/docs/models/RegulationsPassed.md new file mode 100644 index 00000000..f408e10f --- /dev/null +++ b/erp24/docs/models/RegulationsPassed.md @@ -0,0 +1,139 @@ +# Class: RegulationsPassed + + +## Mindmap + +```mermaid +mindmap + root((RegulationsPassed)) + Таблица БД + regulations_passed + Свойства + regulation_id + int + admin_id + int + status + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель результата прохождения регламента сотрудником в системе ERP24. Фиксирует факт изучения и прохождения теста по внутреннему документу компании. Хранит статус прохождения (с ошибками или без) и временную метку. + +Используется для контроля знания сотрудниками актуальных регламентов компании, обязательного ознакомления с новыми версиями документов, отчётности по обучению персонала. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`regulations_passed` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `regulation_id` | int | **PK, FK** ID регламента из таблицы regulations | +| `admin_id` | int | **PK, FK** ID сотрудника | +| `status` | int | **Статус прохождения:** 0 - с ошибками, 1 - без ошибок | +| `created_at` | datetime | **Время прохождения** теста по регламенту | + +--- + +## Правила валидации + +```php +['regulation_id', 'admin_id', 'status', 'created_at'] // required +['regulation_id', 'admin_id', 'status'] // integer +['created_at'] // safe (datetime) +['regulation_id', 'admin_id'] // unique composite (составной PK) +``` + +--- + +## Примеры использования + +### Фиксация прохождения регламента + +```php +use yii_app\records\RegulationsPassed; + +$passed = new RegulationsPassed(); +$passed->regulation_id = $regulationId; +$passed->admin_id = $employeeId; +$passed->status = 1; // Без ошибок +$passed->created_at = date('Y-m-d H:i:s'); +$passed->save(); +``` + +--- + +### Проверка прохождения + +```php +$hasPassed = RegulationsPassed::find() + ->where([ + 'regulation_id' => $regulationId, + 'admin_id' => $adminId + ]) + ->exists(); + +if ($hasPassed) { + echo "Регламент уже пройден"; +} else { + echo "Требуется изучить регламент"; +} +``` + +--- + +### Статистика прохождения + +```php +$regulationId = 5; + +// Всего прошли +$totalPassed = RegulationsPassed::find() + ->where(['regulation_id' => $regulationId]) + ->count(); + +// Без ошибок +$perfect = RegulationsPassed::find() + ->where(['regulation_id' => $regulationId, 'status' => 1]) + ->count(); + +// С ошибками +$withErrors = RegulationsPassed::find() + ->where(['regulation_id' => $regulationId, 'status' => 0]) + ->count(); + +echo "Всего прошли: {$totalPassed}\n"; +echo "Без ошибок: {$perfect}\n"; +echo "С ошибками: {$withErrors}\n"; +``` + +--- + +## Связанные документы + +- [Regulations.md](./Regulations.md) — регламенты +- [RegulationGroup.md](./RegulationGroup.md) — группы регламентов +- [Admin.md](./Admin.md) — модель сотрудников diff --git a/erp24/docs/models/RegulationsPoll.md b/erp24/docs/models/RegulationsPoll.md new file mode 100644 index 00000000..a3b1e792 --- /dev/null +++ b/erp24/docs/models/RegulationsPoll.md @@ -0,0 +1,147 @@ +# Class: RegulationsPoll + + +## Mindmap + +```mermaid +mindmap + root((RegulationsPoll)) + Таблица БД + regulations_poll + Свойства + id + int + regulation_id + int + name + string + type_option + string + posit + int + created_at + string + Связи + Answers + 1:N RegulationsPollAnswers + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель вопроса теста по регламенту в системе ERP24. После изучения регламента сотрудник проходит тест для проверки понимания документа. Каждый регламент может иметь несколько вопросов с вариантами ответов. + +Вопросы используются для контроля качества усвоения регламентов, выявления проблемных мест в документации, статистики понимания правил сотрудниками. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`regulations_poll` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `regulation_id` | int | **FK** ID регламента из таблицы regulations | +| `name` | text | **Текст вопроса** | +| `type_option` | string | **Тип вопроса:** radio или checkbox | +| `posit` | int | Позиция в списке вопросов (для сортировки) | +| `created_at` | datetime | Дата создания вопроса | + +--- + +## Отношения (Relations) + +### getAnswers() + +**Тип:** `hasMany` +**Модель:** `RegulationsPollAnswers` +**Ключ:** `['poll_id' => 'id']` +**Описание:** Все варианты ответа на вопрос + +**Логика:** +Связь один-ко-многим с моделью RegulationsPollAnswers. Возвращает все варианты ответа для данного вопроса, включая правильные и неправильные. + +**Вызовы сторонних методов:** +- `RegulationsPollAnswers::hasMany()` - построение связи с таблицей regulations_poll_answers + +**Пример:** +```php +$poll = RegulationsPoll::findOne($id); +foreach ($poll->answers as $answer) { + echo "- {$answer->name}"; + if ($answer->is_correct) { + echo " ✓"; + } + echo "\n"; +} +``` + +--- + +## Правила валидации + +```php +['regulation_id', 'name', 'type_option', 'created_at'] // required +['regulation_id', 'posit'] // integer +['name', 'type_option'] // string (text) +['created_at'] // safe (datetime) +``` + +--- + +## Примеры использования + +### Создание вопроса + +```php +use yii_app\records\RegulationsPoll; + +$poll = new RegulationsPoll(); +$poll->regulation_id = $regulationId; +$poll->name = 'Какая максимальная сумма наличных в кассе?'; +$poll->type_option = 'radio'; +$poll->posit = 1; +$poll->created_at = date('Y-m-d H:i:s'); +$poll->save(); +``` + +--- + +### Получение вопросов регламента + +```php +$polls = RegulationsPoll::find() + ->where(['regulation_id' => $regulationId]) + ->with('answers') + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($polls as $poll) { + echo "{$poll->posit}. {$poll->name}\n"; +} +``` + +--- + +## Связанные документы + +- [Regulations.md](./Regulations.md) — регламенты +- [RegulationsPassed.md](./RegulationsPassed.md) — прохождение регламентов diff --git a/erp24/docs/models/RegulationsPollAnswers.md b/erp24/docs/models/RegulationsPollAnswers.md new file mode 100644 index 00000000..0f29f626 --- /dev/null +++ b/erp24/docs/models/RegulationsPollAnswers.md @@ -0,0 +1,167 @@ +# Модель RegulationsPollAnswers + + +## Mindmap + +```mermaid +mindmap + root((RegulationsPollAnswers)) + Таблица БД + regulations_poll_answers + Свойства + id + int + poll_id + int + name + string + posit + int + is_correct + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `RegulationsPollAnswers` представляет варианты ответов на вопросы тестов по регламентам. Хранит текст ответа, порядок отображения и признак правильности. Используется для проверки знаний сотрудников после изучения регламентов. + +**Файл модели:** `erp24/records/RegulationsPollAnswers.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `regulations_poll_answers` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `poll_id` | INTEGER | ID вопроса (FK → regulations_poll.id) | +| `name` | VARCHAR(250) | Текст варианта ответа | +| `posit` | INTEGER | Позиция/порядок отображения | +| `is_correct` | INTEGER | Признак правильного ответа (0/1) | + +--- + +## Описание полей + +### `is_correct` — Правильность ответа + +| Значение | Описание | +|----------|----------| +| 0 | Неправильный ответ | +| 1 | Правильный ответ | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + regulations_poll_answers }o--|| regulations_poll : "belongs_to" + + regulations_poll_answers { + int id PK + int poll_id FK + string name + int posit + int is_correct + } + + regulations_poll { + int id PK + int regulation_id FK + string name + } +``` + +--- + +## Примеры использования + +### Создание вариантов ответа + +```php +$questionId = 5; + +$answers = [ + ['name' => 'В течение 24 часов', 'is_correct' => 0, 'posit' => 1], + ['name' => 'В течение 48 часов', 'is_correct' => 1, 'posit' => 2], + ['name' => 'В течение 72 часов', 'is_correct' => 0, 'posit' => 3], +]; + +foreach ($answers as $data) { + $answer = new RegulationsPollAnswers(); + $answer->poll_id = $questionId; + $answer->name = $data['name']; + $answer->is_correct = $data['is_correct']; + $answer->posit = $data['posit']; + $answer->save(); +} +``` + +### Получение ответов для вопроса + +```php +$answers = RegulationsPollAnswers::find() + ->where(['poll_id' => $questionId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($answers as $answer) { + $marker = $answer->is_correct ? '[✓]' : '[ ]'; + echo "{$marker} {$answer->name}\n"; +} +``` + +### Получение правильного ответа + +```php +$correctAnswer = RegulationsPollAnswers::findOne([ + 'poll_id' => $questionId, + 'is_correct' => 1 +]); + +if ($correctAnswer) { + echo "Правильный ответ: {$correctAnswer->name}"; +} +``` + +### Проверка выбранного ответа + +```php +$selectedId = 10; +$answer = RegulationsPollAnswers::findOne($selectedId); + +if ($answer && $answer->is_correct) { + echo "Верно!"; +} else { + echo "Неверно."; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `poll_id` | Обязательное, целое число | +| `name` | Обязательное, строка, макс. 250 символов | +| `is_correct` | Обязательное, целое число | +| `posit` | Целое число | + +--- + +## Связанные модели + +- **[RegulationsPoll](./RegulationsPoll.md)** — вопросы тестов +- **[Regulations](./Regulations.md)** — регламенты + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/RegulationsSearch.md b/erp24/docs/models/RegulationsSearch.md new file mode 100644 index 00000000..9da96151 --- /dev/null +++ b/erp24/docs/models/RegulationsSearch.md @@ -0,0 +1,193 @@ +# Класс: RegulationsSearch + + +## Mindmap + +```mermaid +mindmap + root((RegulationsSearch)) + Таблица БД + ActiveRecord + Наследование + extends Regulations +``` + +## Назначение +Search-модель для поиска и фильтрации регламентов (инструкций) в ERP24. Стандартная Gii-модель для поиска по названию, содержимому, группе и автору. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Regulations` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `group_id`, `created_by` — integer +- `name`, `content`, `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска регламентов. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Regulations::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, group_id, created_at, created_by, updated_at + - like: name, content + +**Примечание:** Использует `like` вместо `ilike` (регистрозависимый поиск). + +## Диаграмма связей + +```mermaid +erDiagram + Regulations { + int id PK + varchar name + text content + int group_id FK + int created_by FK + datetime created_at + datetime updated_at + } + + RegulationsGroup { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + Regulations }o--|| RegulationsGroup : "group_id" + Regulations }o--|| Admin : "created_by" +``` + +## Диаграмма структуры регламента + +```mermaid +flowchart TD + A[Regulations] --> B[Основные данные] + B --> C[name - название] + B --> D[content - содержимое HTML/Markdown] + + A --> E[Классификация] + E --> F[group_id - группа регламентов] + + A --> G[Аудит] + G --> H[created_by - автор] + G --> I[created_at - дата создания] + G --> J[updated_at - дата обновления] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new RegulationsSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new RegulationsSearch(); +$dataProvider = $searchModel->search([ + 'RegulationsSearch' => [ + 'name' => 'Инструкция', + ] +]); +``` + +### Поиск по содержимому +```php +$searchModel = new RegulationsSearch(); +$dataProvider = $searchModel->search([ + 'RegulationsSearch' => [ + 'content' => 'флорист', + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new RegulationsSearch(); +$dataProvider = $searchModel->search([ + 'RegulationsSearch' => [ + 'group_id' => 5, // ID группы регламентов + ] +]); +``` + +### Поиск по автору +```php +$searchModel = new RegulationsSearch(); +$dataProvider = $searchModel->search([ + 'RegulationsSearch' => [ + 'created_by' => 10, // ID администратора + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'attribute' => 'group_id', + 'value' => 'group.name', + ], + [ + 'attribute' => 'created_by', + 'value' => 'createdBy.name', + ], + 'created_at:datetime', + 'updated_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [Regulations](./Regulations.md) — базовая модель регламентов +- [RegulationsGroup](./RegulationsGroup.md) — группы регламентов +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Стандартная Gii-модель**: Без кастомного formName +2. **like вместо ilike**: Регистрозависимый поиск +3. **Полнотекстовый поиск**: like по content для поиска в содержимом +4. **Группировка**: group_id для категоризации регламентов +5. **Аудит**: created_by, created_at, updated_at для отслеживания изменений diff --git a/erp24/docs/models/ReplacementInvoice.md b/erp24/docs/models/ReplacementInvoice.md new file mode 100644 index 00000000..6de04bef --- /dev/null +++ b/erp24/docs/models/ReplacementInvoice.md @@ -0,0 +1,331 @@ +# Модель ReplacementInvoice + + +## Mindmap + +```mermaid +mindmap + root((ReplacementInvoice)) + Таблица БД + replacement_invoice + Свойства + id + int + guid + string + status + int + created_admin_id + int + store_id + int + store_guid + string + Связи + ReplacementProducts + 1:N ReplacementInvoiceProducts + CreatedAdmin + 1:1 Admin + Store + 1:1 CityStore + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ReplacementInvoice` представляет накладные пересорта (замены товаров). Автоматически создаётся на основе данных о выравнивании остатков товаров при передаче смены, когда один товар заменяется другим. + +**Файл модели:** `erp24/records/ReplacementInvoice.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `replacement_invoice` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | Уникальный GUID документа для 1С | +| `shift_transfer_id` | INTEGER | ID записи передачи смены (FK) | +| `status` | INTEGER | Статус документа | +| `created_admin_id` | INTEGER | ID создателя документа (FK) | +| `updated_admin_id` | INTEGER | ID редактора документа (FK) | +| `confirm_admin_id` | INTEGER | ID подтвердившего документ (FK) | +| `store_id` | INTEGER | ID магазина в ERP (FK) | +| `store_guid` | VARCHAR(100) | GUID магазина из 1С | +| `number` | VARCHAR(100) | Номер документа | +| `number_1c` | VARCHAR(100) | Номер документа в 1С | +| `date` | TIMESTAMP | Дата документа | +| `comment` | TEXT | Комментарий | +| `quantity` | NUMERIC | Общее количество товаров | +| `summ` | NUMERIC | Сумма розничная | +| `summ_self_cost` | NUMERIC | Сумма себестоимости | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `send_at` | TIMESTAMP | Дата отправки в 1С | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Автоматическое заполнение дат создания и обновления. + +### BlameableBehavior + +Автоматическое заполнение ID администраторов при создании и обновлении. + +--- + +## Методы модели + +### Связи (Relations) + +#### `getReplacementProducts()` + +Возвращает позиции товаров накладной пересорта. + +```php +$products = $invoice->replacementProducts; // ReplacementInvoiceProducts[] +``` + +**Тип:** hasMany +**Связанная модель:** `ReplacementInvoiceProducts` +**FK:** `id` → `replacement_invoice_id` + +#### `getCreatedAdmin()` + +Возвращает администратора, создавшего документ. + +```php +$admin = $invoice->createdAdmin; // Admin +``` + +**Тип:** hasOne +**Связанная модель:** `Admin` +**FK:** `created_admin_id` → `id` + +#### `getStore()` + +Возвращает магазин документа. + +```php +$store = $invoice->store; // CityStore +``` + +**Тип:** hasOne +**Связанная модель:** `CityStore` +**FK:** `store_id` → `id` + +--- + +### Статические методы + +#### `setData($shiftTransfer): void` + +Создаёт накладную пересорта на основе данных передачи смены. + +**Параметры:** +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** void + +**Логика:** +1. Получает `store_id` из `CityStore::getAllActiveGuidId()` по `store_guid` +2. Генерирует GUID через `DataHelper::createGuidMy()` +3. Создаёт новый экземпляр `ReplacementInvoice` с полями: + - `guid` - сгенерированный GUID + - `shift_transfer_id` - ID передачи смены + - `status = WriteOffsErp::STATUS_CREATED` + - `created_admin_id` - из `end_shift_admin_id` + - `updated_admin_id` - из `start_shift_admin_id` + - `confirm_admin_id` - из `start_shift_admin_id` + - `store_id`, `store_guid` - из передачи смены + - `date` - дата передачи смены + - `comment` - комментарий передачи смены + - `created_at` - дата начала смены +4. Сохраняет документ +5. Генерирует номер: `'ЕРП_ПС_' . date("Y-m-d_H-i") . '_' . $model->id` +6. Вызывает `ReplacementInvoiceProducts::setData()` для создания позиций +7. Проверяет наличие созданных позиций, если их нет - удаляет документ +8. Рассчитывает итоги из `EqualizationRemains`: + - `quantity = SUM(product_replacement_count)` + - `summ = SUM(balance)` + - `summ_self_cost = SUM(balance_self_cost)` +9. Обновляет поля документа рассчитанными значениями + +**Вызовы сторонних методов:** +- `CityStore::getAllActiveGuidId()` - получение соответствия GUID и ID магазинов +- `DataHelper::createGuidMy()` - генерация GUID +- `ReplacementInvoiceProducts::setData()` - создание позиций товаров +- `EqualizationRemains::find()` - агрегация сумм выравниваний + +**Пример:** +```php +$shiftTransfer = ShiftTransfer::findOne($id); +ReplacementInvoice::setData($shiftTransfer); +// Создаётся накладная пересорта с автоматическим заполнением товаров +``` + +**Исключения:** +- Выбрасывает `\Exception` при ошибках сохранения + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `guid` | Обязательное, уникальное, макс. 100 символов | +| `created_admin_id` | Обязательное, целое число | +| `store_id` | Обязательное, целое число | +| `store_guid` | Обязательное, макс. 100 символов | +| `date` | Обязательное, безопасное | +| `created_at` | Обязательное, безопасное | +| `status`, `updated_admin_id`, `confirm_admin_id`, `shift_transfer_id` | Целое число, по умолчанию null | +| `comment`, `number_1c` | Текстовые | +| `summ`, `summ_self_cost`, `quantity` | Числовые | +| `guid`, `store_guid`, `number`, `number_1c` | Макс. 100 символов | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + replacement_invoice }o--|| shift_transfer : "belongs_to" + replacement_invoice ||--o{ replacement_invoice_products : "has_many" + replacement_invoice }o--|| admin : "created_by" + replacement_invoice }o--|| city_store : "store" + replacement_invoice_products }o--|| products_1c : "product" + replacement_invoice_products }o--|| products_1c : "replacement" + equalization_remains ||--o{ replacement_invoice : "source_data" + + replacement_invoice { + int id PK + string guid UK + int shift_transfer_id FK + int status + int store_id FK + string store_guid + string number + timestamp date + numeric quantity + numeric summ + numeric summ_self_cost + } + + shift_transfer { + int id PK + string store_guid + timestamp date + int start_shift_admin_id + int end_shift_admin_id + } + + replacement_invoice_products { + int id PK + int replacement_invoice_id FK + string product_id FK + string product_replacement_id FK + numeric product_count + } + + equalization_remains { + int id PK + int shift_transfer_id + string product_id + string product_replacement_id + numeric product_replacement_count + } +``` + +--- + +## Примеры использования + +### Автоматическое создание накладной пересорта + +```php +$shiftTransfer = ShiftTransfer::findOne(['id' => $shiftTransferId]); + +try { + ReplacementInvoice::setData($shiftTransfer); + echo "Накладная пересорта создана\n"; +} catch (\Exception $e) { + echo "Ошибка создания накладной: {$e->getMessage()}\n"; +} +``` + +### Получение накладных магазина + +```php +$invoices = ReplacementInvoice::find() + ->where(['store_id' => $storeId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($invoices as $invoice) { + echo "Накладная: {$invoice->number}\n"; + echo "Дата: {$invoice->date}\n"; + echo "Количество: {$invoice->quantity}\n"; + echo "Сумма: {$invoice->summ}\n\n"; +} +``` + +### Просмотр деталей накладной + +```php +$invoice = ReplacementInvoice::findOne($id); + +echo "Номер: {$invoice->number}\n"; +echo "Магазин: " . $invoice->store->name . "\n"; +echo "Количество позиций: {$invoice->quantity}\n"; +echo "Сумма розничная: {$invoice->summ}\n"; +echo "Себестоимость: {$invoice->summ_self_cost}\n"; + +// Администраторы +echo "Создал: " . $invoice->createdAdmin->name . "\n"; + +// Позиции пересорта +$products = $invoice->replacementProducts; +foreach ($products as $product) { + echo "Заменено: {$product->product_id} → {$product->product_replacement_id}\n"; + echo "Количество: {$product->product_count}\n"; +} +``` + +--- + +## Связанные модели + +- **ReplacementInvoiceProducts** — позиции товаров накладной пересорта +- **ShiftTransfer** — передача смены магазина +- **EqualizationRemains** — выравнивания остатков товаров (замены) +- **[CityStore](./CityStore.md)** — справочник магазинов +- **[Admin](./Admin.md)** — администраторы системы +- **[WriteOffsErp](./WriteOffsErp.md)** — документы списаний (используется статус) +- **[Products1c](./Products1c.md)** — справочник товаров + +--- + +## Примечания + +1. Документ создаётся **автоматически** при передаче смены, если обнаружены замены товаров +2. Номер документа генерируется в формате: `ЕРП_ПС_YYYY-MM-DD_HH-MM_ID` +3. ПС = Пересорт (замена товаров) +4. Если при создании не обнаружено товаров для замены, документ удаляется +5. Использует behaviors для автоматического заполнения дат и ID администраторов +6. Связан с системой передачи смен (`ShiftTransfer`) +7. Данные берутся из таблицы `EqualizationRemains` (выравнивания остатков) +8. Хранит три ID администраторов: создатель, редактор, подтвердивший +9. Подготовлен для интеграции с 1С (поля `guid`, `number_1c`, `send_at`) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/ReplacementInvoiceProducts.md b/erp24/docs/models/ReplacementInvoiceProducts.md new file mode 100644 index 00000000..49e4ceed --- /dev/null +++ b/erp24/docs/models/ReplacementInvoiceProducts.md @@ -0,0 +1,316 @@ +# Класс: ReplacementInvoiceProducts + + +## Mindmap + +```mermaid +mindmap + root((ReplacementInvoiceProducts)) + Таблица БД + replacement_invoice_products + Свойства + id + int + replacement_invoice_id + int + name + string + product_id + string + direction_id + int + quantity + float + Связи + UpdatedAdmin + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель товаров в документе замены (накладной замены) в ERP24. Хранит информацию о списываемых и приходуемых товарах при операциях замены с полным учётом цен и себестоимости. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`replacement_invoice_products` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Описание | +|-----------|----------| +| `TimestampBehavior` | Автоматическое заполнение created_at/updated_at | +| `BlameableBehavior` | Автоматическое заполнение created_admin_id/updated_admin_id | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `replacement_invoice_id` | int | FK на документ замены | +| `name` | varchar(100) | Название товара | +| `product_id` | varchar(100) | GUID товара из products_1c | +| `direction_id` | int | Направление: 1=списание, 2=приход | +| `quantity` | float | Количество | +| `price` | float | Цена | +| `price_retail` | float / null | Розничная цена | +| `price_self_cost` | float / null | Себестоимость | +| `summ` | float | Сумма | +| `summ_retail` | float / null | Сумма в розничных ценах | +| `summ_self_cost` | float / null | Сумма себестоимости | +| `active_product` | int | Активность (1=активен, 0=удалён) | +| `created_at` | datetime | Дата создания | +| `updated_at` | datetime / null | Дата обновления | +| `deleted_at` | datetime / null | Дата удаления | +| `created_admin_id` | int | FK на создателя (Admin) | +| `updated_admin_id` | int / null | FK на редактора (Admin) | +| `deleted_admin_id` | int / null | FK на удалившего (Admin) | + +## Константы + +### Статусы активности +```php +public const ACTIVE = 1; // Активная запись +public const DELETE = 0; // Удалённая запись +``` + +### Направления операции +```php +public const EXPENDING = 1; // Списываемый товар (товар-замена) +public const INCOMING = 2; // Приходуемый товар (заменяемый товар) +``` + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getUpdatedAdmin()` | hasOne | Admin | Редактор записи | + +## Статические методы + +### setData($replacementInvoice, $shiftTransfer) +**Описание:** Создаёт товарные позиции в документе замены из данных передачи смены. + +**Параметры:** +- `$replacementInvoice` — документ замены +- `$shiftTransfer` — передача смены + +**Логика работы:** +1. Получает все записи EqualizationRemains для передачи смены +2. Для каждой записи создаёт две позиции: + - INCOMING (direction_id=2) — заменяемый товар + - EXPENDING (direction_id=1) — товар-замена +3. Заполняет цены и суммы из исходных данных +4. Сохраняет обе записи в БД + +## Диаграмма связей + +```mermaid +erDiagram + ReplacementInvoiceProducts { + int id PK + int replacement_invoice_id FK + varchar name + varchar product_id FK + int direction_id + float quantity + float price + float price_retail + float price_self_cost + float summ + float summ_retail + float summ_self_cost + int active_product + datetime created_at + int created_admin_id FK + } + + ReplacementInvoice { + int id PK + varchar number + datetime date + } + + Products1c { + varchar id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + ReplacementInvoice ||--o{ ReplacementInvoiceProducts : "replacement_invoice_id" + Products1c ||--o{ ReplacementInvoiceProducts : "product_id" + Admin ||--o{ ReplacementInvoiceProducts : "created_admin_id" +``` + +## Диаграмма операции замены + +```mermaid +flowchart TD + A[Документ замены] --> B[Товар A
    direction_id = 2
    INCOMING] + A --> C[Товар B
    direction_id = 1
    EXPENDING] + + B -->|Приход| D[Склад: +quantity товара A] + C -->|Расход| E[Склад: -quantity товара B] + + subgraph Баланс операции + F[Сумма прихода = Сумма расхода] + end +``` + +## Примеры использования + +### Создание позиции документа замены +```php +// Списываемый товар (замена) +$expending = new ReplacementInvoiceProducts(); +$expending->replacement_invoice_id = $invoice->id; +$expending->name = $replacementProduct->name; +$expending->product_id = $replacementProduct->id; +$expending->direction_id = ReplacementInvoiceProducts::EXPENDING; +$expending->quantity = 5; +$expending->price = 100.00; +$expending->price_retail = 120.00; +$expending->price_self_cost = 80.00; +$expending->summ = 500.00; +$expending->summ_retail = 600.00; +$expending->summ_self_cost = 400.00; +$expending->active_product = ReplacementInvoiceProducts::ACTIVE; +$expending->save(); + +// Приходуемый товар (заменяемый) +$incoming = new ReplacementInvoiceProducts(); +$incoming->replacement_invoice_id = $invoice->id; +$incoming->name = $originalProduct->name; +$incoming->product_id = $originalProduct->id; +$incoming->direction_id = ReplacementInvoiceProducts::INCOMING; +// ... аналогичное заполнение +$incoming->save(); +``` + +### Получение товаров документа замены +```php +$products = ReplacementInvoiceProducts::find() + ->where([ + 'replacement_invoice_id' => $invoiceId, + 'active_product' => ReplacementInvoiceProducts::ACTIVE + ]) + ->all(); + +foreach ($products as $product) { + $direction = $product->direction_id == ReplacementInvoiceProducts::EXPENDING + ? 'Списание' + : 'Приход'; + echo "{$direction}: {$product->name} x {$product->quantity}\n"; +} +``` + +### Получение списываемых товаров +```php +$expendingProducts = ReplacementInvoiceProducts::find() + ->where([ + 'replacement_invoice_id' => $invoiceId, + 'direction_id' => ReplacementInvoiceProducts::EXPENDING, + 'active_product' => ReplacementInvoiceProducts::ACTIVE + ]) + ->all(); +``` + +### Расчёт итогов документа +```php +$totals = ReplacementInvoiceProducts::find() + ->select([ + 'direction_id', + 'SUM(summ) as total_summ', + 'SUM(summ_retail) as total_retail', + 'SUM(summ_self_cost) as total_self_cost' + ]) + ->where([ + 'replacement_invoice_id' => $invoiceId, + 'active_product' => ReplacementInvoiceProducts::ACTIVE + ]) + ->groupBy('direction_id') + ->asArray() + ->all(); + +foreach ($totals as $total) { + $direction = $total['direction_id'] == 1 ? 'Списано' : 'Оприходовано'; + echo "{$direction}: {$total['total_summ']} руб.\n"; +} +``` + +### Мягкое удаление позиции +```php +$product = ReplacementInvoiceProducts::findOne($productId); +if ($product) { + $product->active_product = ReplacementInvoiceProducts::DELETE; + $product->deleted_at = date('Y-m-d H:i:s'); + $product->deleted_admin_id = Yii::$app->user->id; + $product->save(); +} +``` + +### Создание из EqualizationRemains +```php +// Автоматическое создание позиций +ReplacementInvoiceProducts::setData($replacementInvoice, $shiftTransfer); +``` + +### Проверка баланса документа +```php +$expSum = ReplacementInvoiceProducts::find() + ->where([ + 'replacement_invoice_id' => $invoiceId, + 'direction_id' => ReplacementInvoiceProducts::EXPENDING, + 'active_product' => ReplacementInvoiceProducts::ACTIVE + ]) + ->sum('summ'); + +$incSum = ReplacementInvoiceProducts::find() + ->where([ + 'replacement_invoice_id' => $invoiceId, + 'direction_id' => ReplacementInvoiceProducts::INCOMING, + 'active_product' => ReplacementInvoiceProducts::ACTIVE + ]) + ->sum('summ'); + +$balance = $expSum - $incSum; +echo "Баланс документа: {$balance} руб."; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `replacement_invoice_id` | required, integer | +| `name` | required, string (max 100) | +| `product_id` | required, string (max 100) | +| `direction_id` | required, integer | +| `quantity` | required, number | +| `price` | required, number | +| `summ` | required, number | +| `active_product` | integer | + +## Связанные модели + +- [ReplacementInvoice](./ReplacementInvoice.md) — документы замены +- [Products1c](./Products1c.md) — товары +- [Admin](./Admin.md) — администраторы +- [EqualizationRemains](./EqualizationRemains.md) — остатки для выравнивания + +## Особенности реализации + +1. **Двойная запись**: Каждая замена создаёт пару записей (приход + расход) +2. **Три вида цен**: price, price_retail, price_self_cost для полного учёта +3. **Soft Delete**: Мягкое удаление через active_product и deleted_at +4. **Автоматический аудит**: TimestampBehavior и BlameableBehavior +5. **Связь с передачей смены**: Метод setData() для импорта из EqualizationRemains +6. **Баланс операции**: Сумма списаний должна соответствовать сумме приходов diff --git a/erp24/docs/models/Report.md b/erp24/docs/models/Report.md new file mode 100644 index 00000000..39afcf6b --- /dev/null +++ b/erp24/docs/models/Report.md @@ -0,0 +1,266 @@ +# Класс: Report + + +## Mindmap + +```mermaid +mindmap + root((Report)) + Таблица БД + report + Свойства + id + int + year + int + month + int + row + int + keyRow + string + unit + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель отчётов с понедельной и подневной детализацией в ERP24. Хранит план/факт показатели по месяцам с разбивкой на 6 недель и 42 дня для детального анализа KPI. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`report` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +### Основные поля +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `year` | int | Год отчёта | +| `month` | int | Месяц отчёта (1-12) | +| `row` | int | Номер строки в отчёте | +| `keyRow` | varchar(500) | Композитный ключ (разделы через `\|`) | +| `unit` | varchar(20) | Единица измерения (piece, ruble, percent, auto) | +| `isAuto` | int | Тип заполнения: 0=ручное, 1=авто | + +### Месячные план/факт +| Поле | Тип | Описание | +|------|-----|----------| +| `monthPlan` | float | План на месяц | +| `monthFakt` | float | Факт за месяц | + +### Понедельные план/факт (6 недель) +| Поля | Описание | +|------|----------| +| `week1Plan`, `week1Fakt` | Неделя 1 | +| `week2Plan`, `week2Fakt` | Неделя 2 | +| `week3Plan`, `week3Fakt` | Неделя 3 | +| `week4Plan`, `week4Fakt` | Неделя 4 | +| `week5Plan`, `week5Fakt` | Неделя 5 | +| `week6Plan`, `week6Fakt` | Неделя 6 | + +### Подневные данные (42 дня) +Поля `week{N}day{D}` где N=1-6 (неделя), D=1-7 (день): +- `week1day1` ... `week1day7` — дни первой недели +- `week2day1` ... `week2day7` — дни второй недели +- ... и так далее до `week6day7` + +## Формат keyRow + +Композитный ключ `keyRow` состоит из названий разделов, разделённых вертикальной чертой: + +``` +Маркетинг|Сео|Количество лидов +Продажи|Розница|Выручка +Финансы|ФОТ|Процент от выручки +``` + +## Единицы измерения (unit) + +| Значение | Описание | +|----------|----------| +| `piece` | Штуки (количество) | +| `ruble` | Рубли (суммы) | +| `percent` | Проценты | +| `auto` | Автоматическое вычисление | + +## Диаграмма структуры данных + +```mermaid +erDiagram + Report { + int id PK + int year + int month + int row + varchar keyRow + varchar unit + int isAuto + float monthPlan + float monthFakt + float week1Plan + float week1Fakt + float week1day1 + float week1day2 + } +``` + +## Диаграмма иерархии отчёта + +```mermaid +flowchart TD + A[Отчёт за месяц] --> B[monthPlan / monthFakt] + A --> C[Недели 1-6] + C --> D[week1Plan / week1Fakt] + C --> E[week2Plan / week2Fakt] + C --> F[...] + D --> G[week1day1...week1day7] + E --> H[week2day1...week2day7] +``` + +## Примеры использования + +### Создание строки отчёта +```php +$report = new Report(); +$report->year = 2024; +$report->month = 12; +$report->row = 1; +$report->keyRow = 'Маркетинг|Сео|Количество лидов'; +$report->unit = 'piece'; +$report->isAuto = 0; +$report->monthPlan = 1000; +$report->monthFakt = 950; +$report->week1Plan = 250; +$report->week1Fakt = 240; +// ... заполнение остальных полей +$report->save(); +``` + +### Получение отчёта за месяц +```php +$reports = Report::find() + ->where(['year' => 2024, 'month' => 12]) + ->orderBy(['row' => SORT_ASC]) + ->all(); + +foreach ($reports as $report) { + $sections = explode('|', $report->keyRow); + $indicator = end($sections); + $planFaktRatio = $report->monthPlan > 0 + ? round($report->monthFakt / $report->monthPlan * 100, 1) + : 0; + echo "{$indicator}: {$report->monthFakt} / {$report->monthPlan} ({$planFaktRatio}%)\n"; +} +``` + +### Фильтрация по разделу +```php +// Все показатели раздела "Маркетинг" +$marketingReports = Report::find() + ->where(['year' => 2024, 'month' => 12]) + ->andWhere(['like', 'keyRow', 'Маркетинг|%', false]) + ->all(); +``` + +### Подневная детализация недели +```php +$report = Report::findOne(['keyRow' => 'Продажи|Розница|Выручка', 'year' => 2024, 'month' => 12]); + +if ($report) { + echo "Неделя 1:\n"; + for ($day = 1; $day <= 7; $day++) { + $field = "week1day{$day}"; + echo " День {$day}: {$report->$field}\n"; + } +} +``` + +### Расчёт выполнения плана по неделям +```php +$report = Report::findOne($id); + +$weeklyPerformance = []; +for ($week = 1; $week <= 6; $week++) { + $planField = "week{$week}Plan"; + $faktField = "week{$week}Fakt"; + + if ($report->$planField > 0) { + $weeklyPerformance[$week] = round($report->$faktField / $report->$planField * 100, 1); + } +} +``` + +### Автоматические vs ручные показатели +```php +$autoReports = Report::find() + ->where(['year' => 2024, 'month' => 12, 'isAuto' => 1]) + ->all(); + +$manualReports = Report::find() + ->where(['year' => 2024, 'month' => 12, 'isAuto' => 0]) + ->all(); +``` + +### Агрегация по единицам измерения +```php +$sumByUnit = Report::find() + ->select(['unit', 'SUM(monthFakt) as total']) + ->where(['year' => 2024, 'month' => 12]) + ->andWhere(['in', 'unit', ['ruble', 'piece']]) + ->groupBy('unit') + ->asArray() + ->all(); +``` + +### Построение древовидной структуры +```php +$reports = Report::find() + ->where(['year' => 2024, 'month' => 12]) + ->orderBy(['keyRow' => SORT_ASC]) + ->all(); + +$tree = []; +foreach ($reports as $report) { + $path = explode('|', $report->keyRow); + $current = &$tree; + + foreach ($path as $section) { + if (!isset($current[$section])) { + $current[$section] = ['_children' => [], '_data' => null]; + } + $current = &$current[$section]['_children']; + } + $current['_data'] = $report; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `year` | required, integer | +| `month` | required, integer | +| `row` | required, integer | +| `keyRow` | required, string (max 500) | +| `unit` | required, string (max 20) | +| `isAuto` | integer | +| `monthPlan`, `monthFakt` | number | +| `week{N}Plan`, `week{N}Fakt` | number | +| `week{N}day{D}` | number | + +## Особенности реализации + +1. **Иерархический ключ**: keyRow позволяет строить древовидные отчёты +2. **Полная детализация**: 42 дня (6 недель по 7 дней) для подневного анализа +3. **План/факт на всех уровнях**: Месяц и недели имеют пары plan/fakt +4. **Гибкие единицы**: piece, ruble, percent, auto +5. **Авто/ручное заполнение**: isAuto для разделения автоматических и ручных данных +6. **Большое количество полей**: 56 числовых полей для хранения данных diff --git a/erp24/docs/models/Reports.md b/erp24/docs/models/Reports.md new file mode 100644 index 00000000..7c9c04e2 --- /dev/null +++ b/erp24/docs/models/Reports.md @@ -0,0 +1,363 @@ +# Class: Reports + + +## Mindmap + +```mermaid +mindmap + root((Reports)) + Таблица БД + reports + Свойства + id + int + year + int + month + int + field_id + int + date + string + value + float + Связи + Field + 1:1 ReportsFields + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель Reports хранит детализированные данные отчётов по месяцам в системе ERP24. Содержит значения плановых и фактических показателей по различным параметрам (поля отчётов) с разбивкой по неделям и месяцам. Используется для построения управленческих отчётов и анализа выполнения плановых показателей. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +reports +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка +- `ReportsFields` - справочник полей отчётов + +## Свойства (Properties) + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `id` | `int` | Уникальный идентификатор записи (PRIMARY KEY, AUTO_INCREMENT) | Нет (авто) | +| `year` | `int` | Год создания отчёта (4 цифры, например 2025) | Да | +| `month` | `int` | Месяц создания отчёта (1-12) | Да | +| `field_id` | `int` | ID параметра отчёта из таблицы reports_fields (FK) | Да | +| `field_suffix` | `string` | Суффикс поля: "Plan" - план, "Fakt" - факт (до 100 символов) | Нет | +| `date` | `string` | Дата, к которой относится значение ячейки (до 20 символов, например: "week1Plan", "week2Fakt", "monthPlan") | Да | +| `value` | `float` | Значение ячейки в отчёте (числовое с плавающей точкой) | Нет | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['year', 'month', 'field_id', 'date'], 'required'], + [['year', 'month', 'field_id'], 'integer'], + [['value'], 'number'], + [['field_suffix'], 'string', 'max' => 100], + [['date'], 'string', 'max' => 20], + ]; +} +``` + +### Описание правил: +1. **required**: Поля `year`, `month`, `field_id`, `date` обязательны +2. **integer**: Год, месяц и ID поля должны быть целыми числами +3. **number**: Значение может быть числом с плавающей точкой +4. **string max=100**: Суффикс поля ограничен 100 символами +5. **string max=20**: Дата ограничена 20 символами + +## Методы + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'reports'` + +**Пример:** +```php +$tableName = Reports::tableName(); +// Результат: 'reports' +``` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +**Пример:** +```php +$labels = (new Reports())->attributeLabels(); +// Результат: +// [ +// 'id' => 'ID', +// 'year' => 'Year', +// 'month' => 'Month', +// 'field_id' => 'Field ID', +// 'field_suffix' => 'Field Suffix', +// 'date' => 'Date', +// 'value' => 'Value', +// ] +``` + +--- + +### getField() + +**Описание:** Получает связь "многие к одному" с моделью ReportsFields. Возвращает метаданные параметра отчёта (название, единицы измерения, формулу расчёта). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного ReportsFields + +**Логика работы:** +1. Создаёт связь hasOne с моделью ReportsFields +2. Связывает по полю `field_id` в Reports с `id` в ReportsFields +3. Возвращает объект ActiveQuery для ленивой загрузки + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 ActiveRecord для связи "многие к одному" +- `ReportsFields::class` - получение имени класса (PHP 8 синтаксис) + +**Пример:** +```php +$report = Reports::findOne(['year' => 2025, 'month' => 1, 'field_id' => 5, 'date' => 'week1Plan']); +$field = $report->field; + +if ($field) { + echo "Параметр: {$field->field_name}"; + echo "Единицы: {$field->unit}"; + echo "Формула: {$field->formula}"; +} + +// Результат: +// Параметр: Выручка +// Единицы: ruble +// Формула: SUM(sales.summ) +``` + +## Связи (Relations) + +```mermaid +erDiagram + REPORTS }o--|| REPORTS_FIELDS : "belongs to" + REPORTS_FIELDS }o--o| REPORTS_GROUPS : "belongs to" + + REPORTS { + int id PK + int year + int month + int field_id FK + string field_suffix + string date + float value + } + + REPORTS_FIELDS { + int id PK + string field_alias + string field_name + string unit + int parent_id FK + string formula + int priority + int posit + } + + REPORTS_GROUPS { + int id PK + string group_name + int parent_id FK + string store_guid + int posit + } +``` + +## Примеры использования + +### Сохранение планового показателя + +```php +$report = new Reports(); +$report->year = 2025; +$report->month = 1; +$report->field_id = 10; // ID поля "Выручка" +$report->field_suffix = 'Plan'; +$report->date = 'week1Plan'; // План на 1-ю неделю +$report->value = 500000.00; + +if ($report->save()) { + echo "Плановый показатель сохранён"; +} else { + print_r($report->errors); +} +``` + +### Сохранение фактического показателя + +```php +$report = new Reports(); +$report->year = 2025; +$report->month = 1; +$report->field_id = 10; +$report->field_suffix = 'Fakt'; +$report->date = 'week1Fakt'; // Факт за 1-ю неделю +$report->value = 520000.00; +$report->save(); +``` + +### Получение отчёта за месяц с полями + +```php +$reports = Reports::find() + ->where([ + 'year' => 2025, + 'month' => 1 + ]) + ->with('field') // Жадная загрузка метаданных полей + ->orderBy(['field_id' => SORT_ASC, 'date' => SORT_ASC]) + ->all(); + +foreach ($reports as $report) { + echo "{$report->field->field_name} ({$report->date}): {$report->value} {$report->field->unit}\n"; +} +``` + +### Сравнение плана и факта + +```php +$plan = Reports::find() + ->where([ + 'year' => 2025, + 'month' => 1, + 'field_id' => 10, + 'field_suffix' => 'Plan', + 'date' => 'monthPlan' + ]) + ->one(); + +$fact = Reports::find() + ->where([ + 'year' => 2025, + 'month' => 1, + 'field_id' => 10, + 'field_suffix' => 'Fakt', + 'date' => 'monthFakt' + ]) + ->one(); + +if ($plan && $fact) { + $percent = ($fact->value / $plan->value) * 100; + echo "Выполнение плана: " . number_format($percent, 2) . "%"; +} +``` + +### Получение данных по всем неделям месяца + +```php +$weeklyData = Reports::find() + ->where([ + 'year' => 2025, + 'month' => 1, + 'field_id' => 15, + 'field_suffix' => 'Fakt' + ]) + ->andWhere(['LIKE', 'date', 'week%Fakt']) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +foreach ($weeklyData as $week) { + echo "{$week->date}: {$week->value}\n"; +} +``` + +### Агрегация данных по периоду + +```php +$totalRevenue = Reports::find() + ->select(['total' => 'SUM(value)']) + ->where([ + 'year' => 2025, + 'field_id' => 10, + 'field_suffix' => 'Fakt' + ]) + ->andWhere(['>=', 'month', 1]) + ->andWhere(['<=', 'month', 3]) + ->scalar(); + +echo "Выручка за квартал: " . number_format($totalRevenue, 2) . " руб."; +``` + +## Поток данных + +```mermaid +flowchart TD + A[Ввод плановых показателей] --> B[Reports::save - Plan] + C[Сбор фактических данных] --> D[Расчёт метрик] + D --> E[Reports::save - Fakt] + B --> F[(Таблица reports)] + E --> F + F --> G[Запрос отчёта] + G --> H[Reports::find + field] + H --> I[Загрузка метаданных из ReportsFields] + I --> J[Группировка по ReportsGroups] + J --> K[Расчёт отклонений план/факт] + K --> L[Рендеринг отчёта] + L --> M[Визуализация и экспорт] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [ReportsFields](./ReportsFields.md) | Model | Справочник параметров отчётов | +| [ReportsGroups](./ReportsGroups.md) | Model | Группировка параметров отчётов | +| `ReportsService` | Service | Бизнес-логика работы с отчётами | +| `ReportsController` | Controller | Управление отчётами | +| `ReportsCalculatorJob` | Job | Фоновый расчёт отчётных показателей | + +## Примечания + +1. **Структура date**: Поле `date` содержит метки типа "week1Plan", "week2Fakt", "monthPlan", "monthFakt" для идентификации периода и типа показателя +2. **field_suffix**: Разделяет плановые (Plan) и фактические (Fakt) значения +3. **Гибкость структуры**: Модель позволяет хранить данные с разной детализацией (недели, месяцы) +4. **Производительность**: Рекомендуется создать индекс на (year, month, field_id, date) для быстрых выборок +5. **Единицы измерения**: Хранятся в связанной таблице ReportsFields (ruble, piece, percent) + +--- + +**Связанная документация:** +- [ReportsFields](./ReportsFields.md) +- [ReportsGroups](./ReportsGroups.md) +- [Архитектура системы отчётов](../architecture/reports-system.md) +- [Руководство по созданию отчётов](../guides/reports-guide.md) diff --git a/erp24/docs/models/ReportsFields.md b/erp24/docs/models/ReportsFields.md new file mode 100644 index 00000000..512b52c2 --- /dev/null +++ b/erp24/docs/models/ReportsFields.md @@ -0,0 +1,290 @@ +# Class: ReportsFields + + +## Mindmap + +```mermaid +mindmap + root((ReportsFields)) + Таблица БД + reports_fields + Свойства + id + int + field_alias + string + field_name + string + unit + string + priority + int + posit + int + Связи + ParentGroup + 1:1 ReportsGroups + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель ReportsFields представляет справочник параметров (полей) отчётов в системе ERP24. Содержит метаданные каждого параметра: системное имя, отображаемое название, единицы измерения, формулу расчёта, приоритет вычисления и позицию при выводе. Используется для динамической генерации структуры отчётов. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +reports_fields +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка +- `ReportsGroups` - модель групп отчётов + +## Свойства (Properties) + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `id` | `int` | Уникальный идентификатор параметра (PRIMARY KEY, AUTO_INCREMENT) | Нет (авто) | +| `field_alias` | `string` | Системное название параметра отчёта (до 100 символов), используется в коде | Да | +| `field_name` | `string` | Человекочитаемое название параметра отчёта (до 100 символов) | Да | +| `unit` | `string` | Единицы измерения value (piece - штуки, ruble - рубли, percent - проценты, до 20 символов) | Да | +| `parent_id` | `int` | ID родительской группы (FK → reports_groups.id) | Нет | +| `formula` | `string` | Формула, по которой вычисляется данная строка параметра (до 255 символов) | Нет | +| `priority` | `int` | Приоритет вычисления. Чем выше, тем раньше вычисляется значение строки | Нет (по умолчанию 0) | +| `posit` | `int` | Позиция параметра внутри группы при выводе отчёта | Нет (по умолчанию 0) | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['field_alias', 'field_name', 'unit'], 'required'], + [['parent_id', 'priority', 'posit'], 'integer'], + [['field_alias', 'field_name'], 'string', 'max' => 100], + [['unit'], 'string', 'max' => 20], + [['formula'], 'string', 'max' => 255], + ]; +} +``` + +### Описание правил: +1. **required**: Обязательны `field_alias`, `field_name`, `unit` +2. **integer**: `parent_id`, `priority`, `posit` - целые числа +3. **string max=100**: Системное и отображаемое имя до 100 символов +4. **string max=20**: Единица измерения до 20 символов +5. **string max=255**: Формула до 255 символов + +## Методы + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'reports_fields'` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +--- + +### getParentGroup() + +**Описание:** Получает связь "многие к одному" с моделью ReportsGroups. Возвращает родительскую группу, к которой принадлежит данный параметр отчёта. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения связанного ReportsGroups + +**Логика работы:** +1. Создаёт связь hasOne с моделью ReportsGroups +2. Связывает по полю `parent_id` в ReportsFields с `id` в ReportsGroups +3. Позволяет получить метаданные группы (название, позицию, родительскую группу) + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "многие к одному" +- `ReportsGroups::class` - получение имени класса + +**Пример:** +```php +$field = ReportsFields::findOne(['field_alias' => 'revenue']); +$group = $field->parentGroup; + +if ($group) { + echo "Группа: {$group->group_name}"; + echo "Позиция группы: {$group->posit}"; +} +``` + +## Связи (Relations) + +```mermaid +erDiagram + REPORTS_FIELDS }o--o| REPORTS_GROUPS : "belongs to" + REPORTS_FIELDS ||--o{ REPORTS : "has many" + + REPORTS_FIELDS { + int id PK + string field_alias + string field_name + string unit + int parent_id FK + string formula + int priority + int posit + } + + REPORTS_GROUPS { + int id PK + string group_name + int parent_id FK + string store_guid + int posit + } + + REPORTS { + int id PK + int year + int month + int field_id FK + string field_suffix + string date + float value + } +``` + +## Примеры использования + +### Создание параметра отчёта + +```php +$field = new ReportsFields(); +$field->field_alias = 'revenue'; +$field->field_name = 'Выручка'; +$field->unit = 'ruble'; +$field->parent_id = 1; // ID группы "Финансы" +$field->formula = 'SUM(sales.summ)'; +$field->priority = 10; +$field->posit = 1; + +if ($field->save()) { + echo "Параметр создан с ID: {$field->id}"; +} +``` + +### Получение всех параметров отчёта с группами + +```php +$fields = ReportsFields::find() + ->with('parentGroup') + ->orderBy(['parent_id' => SORT_ASC, 'posit' => SORT_ASC]) + ->all(); + +foreach ($fields as $field) { + $groupName = $field->parentGroup ? $field->parentGroup->group_name : 'Без группы'; + echo "{$groupName} > {$field->field_name} ({$field->unit})\n"; +} +``` + +### Параметры для расчёта (сортировка по приоритету) + +```php +$fieldsForCalculation = ReportsFields::find() + ->where(['IS NOT', 'formula', null]) + ->orderBy(['priority' => SORT_DESC]) // Сначала высокий приоритет + ->all(); + +foreach ($fieldsForCalculation as $field) { + echo "Рассчитываем: {$field->field_name}\n"; + echo "Формула: {$field->formula}\n"; + // Выполнение расчёта по формуле... +} +``` + +### Получение параметров группы + +```php +$groupId = 5; +$fields = ReportsFields::find() + ->where(['parent_id' => $groupId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +echo "Параметры группы:\n"; +foreach ($fields as $field) { + echo "{$field->posit}. {$field->field_name}\n"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[Создание структуры отчёта] --> B[ReportsFields::find] + B --> C[Загрузка с parentGroup] + C --> D[Группировка по parent_id] + D --> E[Сортировка по posit] + E --> F{Для каждого параметра} + F --> G{Есть формула?} + G -->|Да| H[Расчёт по приоритету] + G -->|Нет| I[Ввод вручную] + H --> J[Reports::save] + I --> J + J --> K[Отображение в отчёте с unit] +``` + +## Константы единиц измерения + +```php +// Рекомендуемые значения для поля unit +const UNIT_PIECE = 'piece'; // Штуки (количество) +const UNIT_RUBLE = 'ruble'; // Рубли (деньги) +const UNIT_PERCENT = 'percent'; // Проценты +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Reports](./Reports.md) | Model | Данные отчётов | +| [ReportsGroups](./ReportsGroups.md) | Model | Группы параметров | +| `ReportsService` | Service | Бизнес-логика отчётов | +| `FormulaCalculator` | Helper | Расчёт значений по формулам | + +## Примечания + +1. **Приоритет расчёта**: Параметры с формулами должны рассчитываться в порядке убывания приоритета +2. **Позиция вывода**: Поле `posit` определяет порядок строк в отчёте (меньшее значение - выше) +3. **Формулы**: Содержат текстовое описание расчёта, не являются исполняемым кодом +4. **Единицы измерения**: Влияют на форматирование значений при отображении + +--- + +**Связанная документация:** +- [Reports](./Reports.md) +- [ReportsGroups](./ReportsGroups.md) +- [Архитектура системы отчётов](../architecture/reports-system.md) diff --git a/erp24/docs/models/ReportsGroups.md b/erp24/docs/models/ReportsGroups.md new file mode 100644 index 00000000..653339aa --- /dev/null +++ b/erp24/docs/models/ReportsGroups.md @@ -0,0 +1,322 @@ +# Class: ReportsGroups + + +## Mindmap + +```mermaid +mindmap + root((ReportsGroups)) + Таблица БД + reports_groups + Свойства + id + int + group_name + string + posit + int + Связи + ParentGroup + 1:1 ReportsGroups + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель ReportsGroups представляет иерархическую систему группировки параметров отчётов в системе ERP24. Позволяет организовать параметры отчётов в древовидную структуру с поддержкой вложенности и привязки к конкретным магазинам. Используется для структурирования и визуальной организации отчётов. + +## Пространство имён + +```php +namespace yii_app\records; +``` + +## Родительский класс + +```php +\yii\db\ActiveRecord +``` + +## Таблица БД + +``` +reports_groups +``` + +## Использования (Dependencies) + +- `Yii` - главный класс фреймворка + +## Свойства (Properties) + +| Имя | Тип | Описание | Обязательное | +|-----|-----|----------|--------------| +| `id` | `int` | Уникальный идентификатор группы (PRIMARY KEY, AUTO_INCREMENT) | Нет (авто) | +| `group_name` | `string` | Название группы (до 100 символов) | Да | +| `parent_id` | `int` | ID родительской группы для создания иерархии (FK → reports_groups.id, рекурсивная связь) | Нет | +| `store_guid` | `string` | GUID магазина из таблицы products_1c, если группа к нему относится, или пустая строка (до 36 символов) | Нет | +| `posit` | `int` | Позиция группы при выводе отчёта (сортировка) | Нет (по умолчанию 0) | + +## Правила валидации (Rules) + +```php +public function rules() +{ + return [ + [['group_name'], 'required'], + [['parent_id', 'posit'], 'integer'], + [['group_name'], 'string', 'max' => 100], + [['store_guid'], 'string', 'max' => 36], + ]; +} +``` + +### Описание правил: +1. **required**: Поле `group_name` обязательно +2. **integer**: `parent_id` и `posit` - целые числа +3. **string max=100**: Название группы до 100 символов +4. **string max=36**: GUID магазина до 36 символов (формат UUID) + +## Методы + +### tableName() + +**Описание:** Возвращает имя таблицы в базе данных. + +**Параметры:** Нет + +**Возвращает:** `string` - имя таблицы `'reports_groups'` + +--- + +### attributeLabels() + +**Описание:** Возвращает человекочитаемые названия атрибутов модели. + +**Параметры:** Нет + +**Возвращает:** `array` - ассоциативный массив [атрибут => метка] + +--- + +### getParentGroup() + +**Описание:** Получает связь "многие к одному" с самой собой (рекурсивная связь). Возвращает родительскую группу для построения иерархии. + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос для получения родительской ReportsGroups + +**Логика работы:** +1. Создаёт рекурсивную связь hasOne с ReportsGroups +2. Связывает по полю `parent_id` в текущей группе с `id` в родительской группе +3. Позволяет построить дерево групп любой вложенности + +**Вызовы сторонних методов:** +- `$this->hasOne()` - метод Yii2 для связи "многие к одному" +- `ReportsGroups::class` - получение имени класса (PHP 8 синтаксис) + +**Пример:** +```php +$group = ReportsGroups::findOne(['group_name' => 'Персонал']); +$parentGroup = $group->parentGroup; + +if ($parentGroup) { + echo "Родительская группа: {$parentGroup->group_name}"; +} else { + echo "Это группа верхнего уровня"; +} + +// Построение полного пути +$path = []; +$current = $group; +while ($current) { + array_unshift($path, $current->group_name); + $current = $current->parentGroup; +} +echo "Путь: " . implode(' > ', $path); +// Результат: "Операционные показатели > Персонал" +``` + +## Связи (Relations) + +```mermaid +erDiagram + REPORTS_GROUPS ||--o{ REPORTS_GROUPS : "self reference parent" + REPORTS_GROUPS ||--o{ REPORTS_FIELDS : "has many fields" + REPORTS_GROUPS }o--o| PRODUCTS_1C : "belongs to store" + + REPORTS_GROUPS { + int id PK + string group_name + int parent_id FK + string store_guid FK + int posit + } + + REPORTS_FIELDS { + int id PK + string field_alias + string field_name + string unit + int parent_id FK + string formula + int priority + int posit + } + + PRODUCTS_1C { + string guid PK + string name + string store_name + } +``` + +## Примеры использования + +### Создание группы верхнего уровня + +```php +$group = new ReportsGroups(); +$group->group_name = 'Финансовые показатели'; +$group->parent_id = null; // Группа верхнего уровня +$group->store_guid = ''; +$group->posit = 1; + +if ($group->save()) { + echo "Группа создана с ID: {$group->id}"; +} +``` + +### Создание вложенной группы + +```php +$parentGroup = ReportsGroups::findOne(['group_name' => 'Финансовые показатели']); + +$childGroup = new ReportsGroups(); +$childGroup->group_name = 'Выручка по магазинам'; +$childGroup->parent_id = $parentGroup->id; // Вложена в родительскую +$childGroup->store_guid = ''; +$childGroup->posit = 1; +$childGroup->save(); +``` + +### Создание группы для конкретного магазина + +```php +$group = new ReportsGroups(); +$group->group_name = 'ТЦ Мега'; +$group->parent_id = 5; // ID группы "Магазины" +$group->store_guid = '550e8400-e29b-41d4-a716-446655440000'; // GUID магазина +$group->posit = 2; +$group->save(); +``` + +### Получение иерархической структуры групп + +```php +// Получение групп верхнего уровня +$topLevelGroups = ReportsGroups::find() + ->where(['parent_id' => null]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($topLevelGroups as $group) { + echo $group->group_name . "\n"; + + // Получение дочерних групп + $childGroups = ReportsGroups::find() + ->where(['parent_id' => $group->id]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + + foreach ($childGroups as $child) { + echo " - {$child->group_name}\n"; + } +} +``` + +### Построение полного дерева групп + +```php +function buildTree($parentId = null) +{ + $groups = ReportsGroups::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + + $tree = []; + foreach ($groups as $group) { + $tree[] = [ + 'id' => $group->id, + 'name' => $group->group_name, + 'children' => buildTree($group->id) // Рекурсия + ]; + } + + return $tree; +} + +$fullTree = buildTree(); +print_r($fullTree); +``` + +### Фильтрация групп по магазину + +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +$storeGroups = ReportsGroups::find() + ->where(['store_guid' => $storeGuid]) + ->orWhere(['store_guid' => '']) // Общие группы + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($storeGroups as $group) { + echo $group->group_name . "\n"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[Запрос отчёта] --> B[ReportsGroups::find - верхний уровень] + B --> C[Сортировка по posit] + C --> D{Для каждой группы} + D --> E[Загрузка parentGroup] + E --> F[Получение дочерних групп рекурсивно] + F --> G[ReportsFields::find по parent_id] + G --> H[Сортировка полей по posit] + H --> I[Reports::find - данные по полям] + I --> J[Построение иерархической структуры отчёта] + J --> K[Рендеринг с группировкой] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Reports](./Reports.md) | Model | Данные отчётов | +| [ReportsFields](./ReportsFields.md) | Model | Параметры отчётов | +| `Products1c` | Model | Справочник магазинов из 1С | +| `ReportsService` | Service | Бизнес-логика отчётов | +| `ReportsController` | Controller | Управление отчётами | + +## Примечания + +1. **Иерархическая структура**: Поддерживается неограниченная вложенность групп через `parent_id` +2. **Привязка к магазинам**: Поле `store_guid` позволяет создавать группы для конкретных магазинов +3. **Позиция вывода**: Поле `posit` определяет порядок отображения групп (меньшее значение - выше) +4. **Рекурсивная связь**: `getParentGroup()` позволяет построить полный путь от группы до корня +5. **Общие группы**: Если `store_guid` пустой, группа применяется ко всем магазинам + +--- + +**Связанная документация:** +- [Reports](./Reports.md) +- [ReportsFields](./ReportsFields.md) +- [Архитектура системы отчётов](../architecture/reports-system.md) +- [Руководство по структуре отчётов](../guides/reports-structure.md) diff --git a/erp24/docs/models/RnpAlias.md b/erp24/docs/models/RnpAlias.md new file mode 100644 index 00000000..993f296a --- /dev/null +++ b/erp24/docs/models/RnpAlias.md @@ -0,0 +1,223 @@ +# Класс: RnpAlias + + +## Mindmap + +```mermaid +mindmap + root((RnpAlias)) + Таблица БД + rnp_alias + Свойства + id + int + Связи + Indices + 1:N RnpIndex + RnpDatas + 1:N RnpData + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник алиасов (названий показателей) для системы РНП (Расчёт Нормативных Показателей) в ERP24. Определяет типы метрик, используемых при расчёте мотивации и KPI. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`rnp_alias` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `alias` | varchar(255) / null | Название показателя (уникальное) | + +## Константы (ID показателей) + +```php +public const USERS_COUNT_ID = 30; // Количество пользователей +public const FIRST_MINUS_USER_BONUS_ID = 31; // Первый минус бонуса пользователя +public const SECOND_MINUS_USER_BONUS_ID = 32; // Второй минус бонуса пользователя +public const MATRIX_BASE_SUMM_ID = 27; // Сумма базовой матрицы +public const MATRIX_SEASON_SUMM_ID = 28; // Сумма сезонной матрицы +public const MATRIX_NEW_SUMM_ID = 29; // Сумма новой матрицы +``` + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getIndices()` | hasMany via | RnpIndex | Индексы через таблицу rnp_data | +| `getRnpDatas()` | hasMany | RnpData | Значения показателя | + +## Диаграмма связей + +```mermaid +erDiagram + RnpAlias { + int id PK + varchar alias UK + } + + RnpIndex { + int id PK + int cluster_id + int store_id + date date + int shift_type + } + + RnpData { + int index_id PK,FK + int alias_id PK,FK + float value + } + + RnpAlias ||--o{ RnpData : "alias_id" + RnpIndex ||--o{ RnpData : "index_id" + RnpAlias }o--o{ RnpIndex : "via rnp_data" +``` + +## Диаграмма структуры РНП + +```mermaid +flowchart TD + subgraph Справочники + A[RnpAlias
    Типы показателей] + end + + subgraph Индексация + B[RnpIndex
    Кластер + Магазин + Дата + Смена] + end + + subgraph Данные + C[RnpData
    index_id + alias_id -> value] + end + + A --> C + B --> C + C -->|Значение метрики| D[Расчёт мотивации] +``` + +## Примеры использования + +### Создание алиаса показателя +```php +$alias = new RnpAlias(); +$alias->alias = 'Количество продаж'; +$alias->save(); +``` + +### Получение всех алиасов +```php +$aliases = RnpAlias::find() + ->orderBy(['alias' => SORT_ASC]) + ->all(); + +foreach ($aliases as $a) { + echo "{$a->id}: {$a->alias}\n"; +} +``` + +### Использование констант +```php +// Получение количества пользователей +$usersCountAlias = RnpAlias::findOne(RnpAlias::USERS_COUNT_ID); + +// Получение данных по матрицам +$matrixAliases = RnpAlias::find() + ->where(['id' => [ + RnpAlias::MATRIX_BASE_SUMM_ID, + RnpAlias::MATRIX_SEASON_SUMM_ID, + RnpAlias::MATRIX_NEW_SUMM_ID + ]]) + ->all(); +``` + +### Получение значений показателя +```php +$alias = RnpAlias::findOne(RnpAlias::MATRIX_BASE_SUMM_ID); + +if ($alias) { + $values = $alias->rnpDatas; + + foreach ($values as $data) { + $index = $data->index; + echo "Магазин {$index->store_id}, дата {$index->date}: {$data->value}\n"; + } +} +``` + +### Формирование списка для выбора +```php +$aliasesList = ArrayHelper::map( + RnpAlias::find()->orderBy(['alias' => SORT_ASC])->all(), + 'id', + 'alias' +); + +echo Html::dropDownList('alias_id', null, $aliasesList); +``` + +### Получение индексов для показателя +```php +$alias = RnpAlias::findOne($aliasId); + +if ($alias) { + $indices = $alias->indices; + + foreach ($indices as $index) { + echo "Кластер: {$index->cluster_id}, Магазин: {$index->store_id}\n"; + } +} +``` + +### Статистика по показателям +```php +$stats = RnpData::find() + ->select(['alias_id', 'COUNT(*) as count', 'AVG(value) as avg_value']) + ->groupBy('alias_id') + ->asArray() + ->all(); + +$aliases = ArrayHelper::index(RnpAlias::find()->all(), 'id'); + +foreach ($stats as $stat) { + $aliasName = $aliases[$stat['alias_id']]->alias ?? 'Unknown'; + echo "{$aliasName}: {$stat['count']} записей, среднее: {$stat['avg_value']}\n"; +} +``` + +### Поиск по названию +```php +$searchAlias = RnpAlias::find() + ->where(['like', 'alias', 'матриц']) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `alias` | string (max 255), unique | + +## Связанные модели + +- [RnpIndex](./RnpIndex.md) — индексы (координаты данных) +- [RnpData](./RnpData.md) — значения показателей + +## Особенности реализации + +1. **Справочник метрик**: Определяет типы показателей для расчётов +2. **Предопределённые константы**: ID для часто используемых показателей +3. **Уникальность alias**: Каждое название показателя уникально +4. **Many-to-Many**: Связь с RnpIndex через промежуточную таблицу RnpData +5. **Расширяемость**: Новые показатели добавляются без изменения кода +6. **Интеграция с мотивацией**: Используется при расчёте KPI и бонусов diff --git a/erp24/docs/models/RnpData.md b/erp24/docs/models/RnpData.md new file mode 100644 index 00000000..6f2eeae8 --- /dev/null +++ b/erp24/docs/models/RnpData.md @@ -0,0 +1,273 @@ +# Класс: RnpData + + +## Mindmap + +```mermaid +mindmap + root((RnpData)) + Таблица БД + rnp_data + Свойства + index_id + int + alias_id + int + value + float + alias + RnpAlias + index + RnpIndex + Связи + Alias + 1:1 RnpAlias + Index + 1:1 RnpIndex + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель данных РНП (Расчёт Нормативных Показателей) в ERP24. Хранит значения метрик на пересечении индекса (кластер/магазин/дата/смена) и типа показателя. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`rnp_data` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `index_id` | int | FK на индекс (RnpIndex), часть составного PK | +| `alias_id` | int | FK на алиас показателя (RnpAlias), часть составного PK | +| `value` | float | Значение показателя | + +## Составной первичный ключ + +Уникальная комбинация `[index_id, alias_id]` обеспечивает одно значение для каждой пары индекс-показатель. + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getAlias()` | hasOne | RnpAlias | Тип показателя | +| `getIndex()` | hasOne | RnpIndex | Индекс (координаты) | + +## Диаграмма связей + +```mermaid +erDiagram + RnpData { + int index_id PK,FK + int alias_id PK,FK + float value + } + + RnpIndex { + int id PK + int cluster_id + int store_id + date date + int shift_type + } + + RnpAlias { + int id PK + varchar alias + } + + RnpIndex ||--o{ RnpData : "index_id" + RnpAlias ||--o{ RnpData : "alias_id" +``` + +## Диаграмма куба данных + +```mermaid +flowchart TD + subgraph Измерения + A[Кластер] + B[Магазин] + C[Дата] + D[Смена] + E[Показатель] + end + + subgraph Координаты + F[RnpIndex
    cluster + store + date + shift] + G[RnpAlias
    alias_id] + end + + subgraph Факт + H[RnpData
    value] + end + + A --> F + B --> F + C --> F + D --> F + E --> G + F --> H + G --> H +``` + +## Примеры использования + +### Создание записи данных +```php +$data = new RnpData(); +$data->index_id = $index->id; +$data->alias_id = RnpAlias::MATRIX_BASE_SUMM_ID; +$data->value = 150000.50; +$data->save(); +``` + +### Получение значения по индексу и алиасу +```php +$data = RnpData::find() + ->where([ + 'index_id' => $indexId, + 'alias_id' => $aliasId + ]) + ->one(); + +if ($data) { + echo "Значение: {$data->value}"; +} +``` + +### Получение всех данных для индекса +```php +$indexData = RnpData::find() + ->where(['index_id' => $indexId]) + ->with(['alias']) + ->all(); + +foreach ($indexData as $data) { + echo "{$data->alias->alias}: {$data->value}\n"; +} +``` + +### Получение всех значений показателя +```php +$aliasData = RnpData::find() + ->where(['alias_id' => RnpAlias::USERS_COUNT_ID]) + ->with(['index']) + ->all(); + +foreach ($aliasData as $data) { + $index = $data->index; + echo "Магазин {$index->store_id}, {$index->date}: {$data->value}\n"; +} +``` + +### Агрегация по магазинам +```php +$storeStats = RnpData::find() + ->alias('d') + ->select([ + 'i.store_id', + 'SUM(d.value) as total' + ]) + ->innerJoin('rnp_index i', 'd.index_id = i.id') + ->where(['d.alias_id' => $aliasId]) + ->groupBy('i.store_id') + ->asArray() + ->all(); +``` + +### Обновление или создание значения +```php +$data = RnpData::find() + ->where([ + 'index_id' => $indexId, + 'alias_id' => $aliasId + ]) + ->one(); + +if (!$data) { + $data = new RnpData(); + $data->index_id = $indexId; + $data->alias_id = $aliasId; +} + +$data->value = $newValue; +$data->save(); +``` + +### Массовый импорт данных +```php +$dataRows = [ + ['index_id' => 1, 'alias_id' => 27, 'value' => 100000], + ['index_id' => 1, 'alias_id' => 28, 'value' => 50000], + ['index_id' => 2, 'alias_id' => 27, 'value' => 120000], +]; + +foreach ($dataRows as $row) { + $data = new RnpData(); + $data->setAttributes($row); + $data->save(); +} +``` + +### Сравнение показателей по сменам +```php +$comparison = RnpData::find() + ->alias('d') + ->select([ + 'i.shift_type', + 'd.alias_id', + 'AVG(d.value) as avg_value' + ]) + ->innerJoin('rnp_index i', 'd.index_id = i.id') + ->where(['d.alias_id' => $aliasId]) + ->groupBy(['i.shift_type', 'd.alias_id']) + ->asArray() + ->all(); +``` + +### Получение данных за период +```php +$periodData = RnpData::find() + ->alias('d') + ->innerJoin('rnp_index i', 'd.index_id = i.id') + ->where(['d.alias_id' => $aliasId]) + ->andWhere(['>=', 'i.date', '2024-12-01']) + ->andWhere(['<=', 'i.date', '2024-12-31']) + ->with(['index', 'alias']) + ->all(); +``` + +### Удаление данных для индекса +```php +RnpData::deleteAll(['index_id' => $indexId]); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `index_id` | required, integer, exists в RnpIndex | +| `alias_id` | required, integer, exists в RnpAlias | +| `value` | required, number | + +**Уникальное ограничение:** `[index_id, alias_id]` + +## Связанные модели + +- [RnpIndex](./RnpIndex.md) — индексы (координаты) +- [RnpAlias](./RnpAlias.md) — типы показателей + +## Особенности реализации + +1. **Составной PK**: index_id + alias_id обеспечивает уникальность +2. **Куб данных**: Реализует многомерную модель данных +3. **Внешние ключи**: Валидация существования индекса и алиаса +4. **Связующая таблица**: Соединяет RnpIndex и RnpAlias +5. **Числовые значения**: float для хранения метрик +6. **Эффективные запросы**: JOIN с RnpIndex для фильтрации по координатам diff --git a/erp24/docs/models/RnpIndex.md b/erp24/docs/models/RnpIndex.md new file mode 100644 index 00000000..4575b60b --- /dev/null +++ b/erp24/docs/models/RnpIndex.md @@ -0,0 +1,294 @@ +# Класс: RnpIndex + + +## Mindmap + +```mermaid +mindmap + root((RnpIndex)) + Таблица БД + rnp_index + Свойства + id + int + Связи + Aliases + 1:N RnpAlias + RnpDatas + 1:N RnpData + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель индексов РНП (Расчёт Нормативных Показателей) в ERP24. Определяет координаты данных: кластер, магазин, дата и тип смены для многомерного хранения метрик. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`rnp_index` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `cluster_id` | int / null | ID кластера магазинов | +| `store_id` | int / null | ID магазина | +| `date` | date / null | Дата показателей | +| `shift_type` | int / null | Тип смены (1=день, 2=ночь) | + +## Составное уникальное ограничение + +Комбинация `[cluster_id, store_id, date, shift_type]` уникальна — для каждой комбинации координат существует только один индекс. + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getAliases()` | hasMany via | RnpAlias | Алиасы через таблицу rnp_data | +| `getRnpDatas()` | hasMany | RnpData | Данные для этого индекса | + +## Диаграмма связей + +```mermaid +erDiagram + RnpIndex { + int id PK + int cluster_id + int store_id + date date + int shift_type + } + + RnpAlias { + int id PK + varchar alias + } + + RnpData { + int index_id PK,FK + int alias_id PK,FK + float value + } + + Cluster { + int id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + RnpIndex ||--o{ RnpData : "index_id" + RnpAlias ||--o{ RnpData : "alias_id" + Cluster ||--o{ RnpIndex : "cluster_id" + CityStore ||--o{ RnpIndex : "store_id" +``` + +## Диаграмма координатной системы + +```mermaid +flowchart TD + subgraph Измерения индекса + A[cluster_id
    Кластер магазинов] + B[store_id
    Конкретный магазин] + C[date
    Дата] + D[shift_type
    Смена] + end + + E[RnpIndex] --> A + E --> B + E --> C + E --> D + + E -->|Связь| F[RnpData
    Значения метрик] +``` + +## Примеры использования + +### Создание индекса +```php +$index = new RnpIndex(); +$index->cluster_id = 5; +$index->store_id = 123; +$index->date = '2024-12-15'; +$index->shift_type = 1; // День +$index->save(); +``` + +### Получение или создание индекса +```php +function getOrCreateIndex($clusterId, $storeId, $date, $shiftType) +{ + $index = RnpIndex::find() + ->where([ + 'cluster_id' => $clusterId, + 'store_id' => $storeId, + 'date' => $date, + 'shift_type' => $shiftType + ]) + ->one(); + + if (!$index) { + $index = new RnpIndex(); + $index->cluster_id = $clusterId; + $index->store_id = $storeId; + $index->date = $date; + $index->shift_type = $shiftType; + $index->save(); + } + + return $index; +} +``` + +### Получение индексов магазина за период +```php +$indices = RnpIndex::find() + ->where(['store_id' => $storeId]) + ->andWhere(['>=', 'date', '2024-12-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->orderBy(['date' => SORT_ASC, 'shift_type' => SORT_ASC]) + ->all(); +``` + +### Получение всех алиасов для индекса +```php +$index = RnpIndex::findOne($indexId); + +if ($index) { + $aliases = $index->aliases; + + foreach ($aliases as $alias) { + echo "{$alias->alias}\n"; + } +} +``` + +### Получение данных индекса +```php +$index = RnpIndex::find() + ->where([ + 'store_id' => $storeId, + 'date' => '2024-12-15', + 'shift_type' => 1 + ]) + ->with(['rnpDatas', 'rnpDatas.alias']) + ->one(); + +if ($index) { + foreach ($index->rnpDatas as $data) { + echo "{$data->alias->alias}: {$data->value}\n"; + } +} +``` + +### Агрегация по кластерам +```php +$clusterStats = RnpIndex::find() + ->select(['cluster_id', 'COUNT(*) as count']) + ->where(['>=', 'date', '2024-12-01']) + ->groupBy('cluster_id') + ->asArray() + ->all(); +``` + +### Получение индексов по сменам +```php +$dayShiftIndices = RnpIndex::find() + ->where([ + 'store_id' => $storeId, + 'shift_type' => 1 + ]) + ->all(); + +$nightShiftIndices = RnpIndex::find() + ->where([ + 'store_id' => $storeId, + 'shift_type' => 2 + ]) + ->all(); +``` + +### Проверка существования индекса +```php +$exists = RnpIndex::find() + ->where([ + 'cluster_id' => $clusterId, + 'store_id' => $storeId, + 'date' => $date, + 'shift_type' => $shiftType + ]) + ->exists(); + +if (!$exists) { + // Создать новый индекс +} +``` + +### Удаление индекса с данными +```php +$index = RnpIndex::findOne($indexId); + +if ($index) { + // Сначала удаляем связанные данные + RnpData::deleteAll(['index_id' => $index->id]); + + // Затем сам индекс + $index->delete(); +} +``` + +### Построение матрицы данных +```php +$indices = RnpIndex::find() + ->where(['store_id' => $storeId]) + ->andWhere(['>=', 'date', '2024-12-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->with(['rnpDatas']) + ->all(); + +$matrix = []; +foreach ($indices as $index) { + $key = "{$index->date}_{$index->shift_type}"; + $matrix[$key] = []; + + foreach ($index->rnpDatas as $data) { + $matrix[$key][$data->alias_id] = $data->value; + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `cluster_id` | integer | +| `store_id` | integer | +| `date` | safe | +| `shift_type` | integer | + +**Уникальное ограничение:** `[cluster_id, store_id, date, shift_type]` + +## Связанные модели + +- [RnpData](./RnpData.md) — значения показателей +- [RnpAlias](./RnpAlias.md) — типы показателей +- [Cluster](./Cluster.md) — кластеры магазинов +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Многомерная модель**: 4 измерения (кластер, магазин, дата, смена) +2. **Nullable поля**: Все координаты могут быть null для агрегированных данных +3. **Уникальность комбинации**: Одна запись на каждую комбинацию координат +4. **Many-to-Many**: Связь с RnpAlias через RnpData +5. **Иерархия**: cluster_id для группировки магазинов +6. **Разделение по сменам**: shift_type для дневных и ночных показателей diff --git a/erp24/docs/models/Sales.md b/erp24/docs/models/Sales.md index 4d5ee708..2ff8dc37 100644 --- a/erp24/docs/models/Sales.md +++ b/erp24/docs/models/Sales.md @@ -1,5 +1,41 @@ # Class: Sales + +## Mindmap + +```mermaid +mindmap + root((Sales)) + Таблица БД + sales + Свойства + id + string + date + string + operation + string + status + string + summ + float + skidka + float + Связи + Admin + 1:1 Admin + Store + 1:1 CityStore + StoreByGuid + 1:1 Products1c + SellerByGuid + 1:1 Products1c + SellerById + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель чека продажи в системе ERP24. Представляет собой заголовок чека с общей информацией о продаже: дата, сумма, скидка, способ оплаты, продавец, магазин и клиент. Связана с товарами через модель `SalesProducts`. diff --git a/erp24/docs/models/SalesGroupSearch.md b/erp24/docs/models/SalesGroupSearch.md new file mode 100644 index 00000000..9147cd07 --- /dev/null +++ b/erp24/docs/models/SalesGroupSearch.md @@ -0,0 +1,264 @@ +# Класс: SalesGroupSearch + + +## Mindmap + +```mermaid +mindmap + root((SalesGroupSearch)) + Таблица БД + ActiveRecord + Наследование + extends Sales +``` + +## Назначение +Search-модель для группового анализа продаж в ERP24. Комплексная аналитическая модель с агрегацией по кластерам, магазинам и сменам, поддержкой YoY-сравнения, почасовой статистикой и анализом матрицы продуктов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Sales` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$date_start_str` | string | Дата начала периода (default: первый день месяца) | +| `$date_end_str` | string | Дата окончания периода (default: последний день месяца) | +| `$mode_group` | int | Режим группировки (default: 0) | +| `$mode_select_matrix_category` | int | Режим выбора категорий матрицы (default: 0) | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска с дефолтными значениями. + +**Возвращает:** `array` — массив правил + +**Дефолтные значения:** +- `date_start_str` — первый день текущего месяца +- `date_end_str` — последний день текущего месяца +- `mode_select_matrix_category` — 0 +- `mode_group` — 0 + +### search($params): ActiveQuery +**Описание:** Основной метод поиска с комплексной агрегацией продаж. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveQuery` — запрос (не DataProvider!) + +**Логика:** +1. JOIN store_dynamic, admin, city_store для кластеров и прав доступа +2. Фильтрация по правам пользователя (admin.store_arr) +3. Исключение возвратов (OPERATION_RETURN) +4. Агрегация: SUM, COUNT, AVG по продажам +5. Почасовая статистика (count_sales_0..count_sales_23) +6. Статистика бонусной программы (users_bonus, users) +7. Матрица продаж (mode_select_matrix_category) +8. GROUP BY: cluster_id, store_id, shift_type, date + +**Выходные поля:** +- cluster_id, store_id, store_name, shift_type, date +- sales_summ, count_all_sales, avg_sales_summ +- count_anonymous_customer, count_users_1c +- users_first_minus_balance, users_minus_balance +- count_sales_0..count_sales_23 (почасовая статистика) +- other_matrix_summ, matrix_base_summ, matrix_season_summ, matrix_new_summ (при mode_select_matrix_category=1) + +### searchPreviousYear($params): ActiveQuery +**Описание:** Поиск продаж за аналогичный период прошлого года (YoY). + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveQuery` — запрос + +**Логика:** +- Сдвиг date_start_str и date_end_str на год назад +- Упрощённая агрегация без бонусов и матрицы +- Те же GROUP BY и ORDER BY + +### searchThisMonth($params): ActiveQuery +**Описание:** Поиск продаж за текущий месяц с планами. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveQuery` — запрос + +**Логика:** +- LEFT JOIN plan_store для сравнения с планом +- Фиксированный период: текущий месяц +- Выходные поля: plan_sales для сравнения факт/план + +## Диаграмма связей + +```mermaid +erDiagram + Sales { + int id PK + int store_id FK + datetime date + decimal summ + int operation + varchar sales_check + } + + StoreDynamic { + int store_id FK + int value_int + date date_from + date date_to + } + + UsersBonus { + int id PK + int user_id FK + int check_id FK + varchar tip + date date + } + + Users { + int id PK + int source + } + + PlanStore { + int store_id FK + int year + int month + int day + decimal plan_sales + } + + Sales }o--|| StoreDynamic : "кластер" + Sales ||--o| UsersBonus : "бонусы" + UsersBonus }o--|| Users : "пользователь" + Sales }o--o| PlanStore : "план" +``` + +## Диаграмма потока данных + +```mermaid +flowchart TD + A[search] --> B[JOIN store_dynamic] + B --> C[JOIN admin - права доступа] + C --> D[JOIN city_store] + + D --> E[Исключение возвратов] + E --> F[LEFT JOIN users_bonus] + F --> G[LEFT JOIN users] + G --> H[LEFT JOIN matrix_sales] + + H --> I[SELECT агрегация] + I --> J[sales_summ, count_all_sales] + I --> K[count_sales_0..23 почасовая] + I --> L[users_first_minus_balance] + I --> M[matrix_base/season/new_summ] + + J --> N[GROUP BY cluster, store, shift, date] + K --> N + L --> N + M --> N +``` + +## Выходные поля агрегации + +```sql +SELECT + cluster_id, -- ID кластера из store_dynamic + store_id, -- ID магазина + store_name, -- Название магазина + shift_type, -- Тип смены: 1=дневная, 2=ночная + date, -- Дата (с учётом ночной смены) + sales_summ, -- SUM(summ) + count_all_sales, -- COUNT(id) + avg_sales_summ, -- AVG(summ) + count_anonymous_customer, -- Анонимные клиенты + count_users_1c, -- Клиенты из 1С + users_first_minus_balance, -- Первое списание бонусов + users_minus_balance, -- Повторное списание + count_sales_8..count_sales_7, -- Почасовая статистика (24 поля) + other_matrix_summ, -- Продажи вне матрицы + matrix_base_summ, -- Базовая матрица + matrix_season_summ, -- Сезонная матрица + matrix_new_summ -- Новая матрица +``` + +## Примеры использования + +### Анализ продаж за период +```php +$searchModel = new SalesGroupSearch(); +$query = $searchModel->search([ + 'date_start_str' => '2024-01-01', + 'date_end_str' => '2024-01-31', +]); +$sales = $query->all(); +``` + +### YoY сравнение +```php +$searchModel = new SalesGroupSearch(); +$searchModel->load($params); + +// Текущий период +$currentSales = $searchModel->search($params)->all(); + +// Прошлый год +$previousYearSales = $searchModel->searchPreviousYear($params)->all(); + +// Сравнение +foreach ($currentSales as $sale) { + $yoySale = findBySameDate($previousYearSales, $sale->date); + $growth = ($sale->sales_summ - $yoySale->sales_summ) / $yoySale->sales_summ * 100; +} +``` + +### Анализ с матрицей категорий +```php +$searchModel = new SalesGroupSearch(); +$query = $searchModel->search([ + 'date_start_str' => '2024-03-01', + 'date_end_str' => '2024-03-31', + 'mode_select_matrix_category' => 1, // Включить анализ матрицы +]); +``` + +### Сравнение с планом +```php +$searchModel = new SalesGroupSearch(); +$query = $searchModel->searchThisMonth([]); +$salesWithPlan = $query->all(); + +foreach ($salesWithPlan as $sale) { + $planCompletion = $sale->sales_summ / $sale->plan_sales * 100; +} +``` + +## Связанные модели + +- [Sales](./Sales.md) — базовая модель продаж +- [StoreDynamic](./StoreDynamic.md) — динамические параметры магазинов +- [CityStore](./CityStore.md) — магазины +- [UsersBonus](./UsersBonus.md) — бонусы пользователей +- [Users](./Users.md) — пользователи +- [PlanStore](./PlanStore.md) — планы магазинов +- [SalesProducts](./SalesProducts.md) — товары в чеке +- [Products1c](./Products1c.md) — товары 1С + +## Особенности реализации + +1. **Возвращает ActiveQuery**: Не DataProvider, требует ->all() +2. **Права доступа**: Фильтрация по admin.store_arr текущего пользователя +3. **Ночные смены**: Продажи до 8:00 относятся к предыдущему дню +4. **Исключение возвратов**: NOT IN по sales_check с OPERATION_RETURN +5. **Почасовая статистика**: 24 поля count_sales_0..count_sales_23 +6. **Матрица продаж**: Анализ по категориям (Базовая/Сезонная/Новая) +7. **YoY сравнение**: searchPreviousYear() для прошлого года +8. **План/факт**: searchThisMonth() с JOIN plan_store diff --git a/erp24/docs/models/SalesHistory.md b/erp24/docs/models/SalesHistory.md new file mode 100644 index 00000000..f2e1170d --- /dev/null +++ b/erp24/docs/models/SalesHistory.md @@ -0,0 +1,612 @@ +# >45;L SalesHistory + + +## Mindmap + +```mermaid +mindmap + root((SalesHistory)) + Таблица БД + sales_history + Свойства + id + int + crc_sum_sales + string + crc_sum_sales_check + string + crc_sum_sales_products + string + date_from + string + date_to + string + Наследование + extends yiidbActiveRecord +``` + +## 07=0G5=85 + +>45;L `SalesHistory` ?@54AB02;O5B 8AB>@8N 87<5=5=89 G5:>2 ?@>406. -B> A8AB5<0 25@A8>=8@>20=8O, :>B>@0O D8:A8@C5B :064>5 87<5=5=85 2 G5:0E 8 ?>78F8OE B>20@>2 G5@57 <5E0=87< CRC-AC<<. 0640O 70?8AL 2 8AB>@88 - MB> A=8<>: A>AB>O=8O G5:0 2 >?@545;Q==K9 <><5=B 2@5<5=8. + +>45;L 8A?>;L7C5BAO 4;O: +- BA;56820=8O 87<5=5=89 G5:>2 ?>A;5 8E A>740=8O +- C48B0 87<5=5=89 40==KE ?@>406 +- =0;870 8AB>@88 <>48D8:0F89 G5:0 +- %@0=5=8O ?@54K4CI8E 25@A89 40==KE + +**$09; <>45;8:** `erp24/records/SalesHistory.php` +**Namespace:** `yii_app\records` +**"01;8F0 :** `sales_history` +** >48B5;LA:89 :;0AA:** `yii\db\ActiveRecord` + +--- + +## >;O B01;8FK + +| >;5 | "8? | ?8A0=85 | +|------|-----|----------| +| `id` | INTEGER | 5@28G=K9 :;NG (02B>8=:@5<5=B) | +| `crc_sum_sales` | VARCHAR(100) | CRC-AC<<0 2A53> G5:0 (G5: + B>20@K) | +| `crc_sum_sales_check` | VARCHAR(100) | CRC-AC<<0 B>;L:> G5:0 (157 B>20@>2) | +| `crc_sum_sales_products` | VARCHAR(100) | CRC-AC<<0 B>;L:> B>20@>2 2 G5:5 | +| `date_from` | DATETIME | 0B0 =0G0;0 459AB28O 25@A88 | +| `date_to` | DATETIME | 0B0 >:>=G0=8O 459AB28O 25@A88 | +| `active` | INTEGER | :B82=>ABL 70?8A8 (0 - CAB0@52H0O, 1 - 0:BC0;L=0O) | +| `check_guid` | STRING(36) | GUID G5:0 2 1C (FK `sales.id`) | +| `date` | DATETIME | 0B0 8 2@5740=8O G5:0 | +| `operation` | VARCHAR(35) | "8? >?5@0F88: "@>4060" 8;8 ">72@0B" | +| `status` | VARCHAR(45) | !B0BCA G5:0 B5:AB>< | +| `summ` | FLOAT | !C<<0 G5:0 | +| `skidka` | FLOAT | !:84:0 ?> G5:C | +| `number` | VARCHAR(255) | ><5@ G5:0 B5:AB>< 2 1! | +| `admin_id` | INTEGER | ID A>B@C4=8:0, A>7402H53> G5: (FK `admin.id`) | +| `seller_id` | STRING(36) | GUID A>B@C4=8:0 2 1! | +| `store_id_1c` | STRING(36) | GUID <03078=0 2 1! | +| `store_id` | INTEGER | ID <03078=0 2 ERP (FK `city_store.id`) | +| `payments` | TEXT | JSON <0AA82 A 8=D>@<0F859 > ?;0B560E | +| `pay_arr` | VARCHAR(15) | ID B8?>2 ?;0B5659 | +| `phone` | INTEGER | ><5@ B5;5D>=0 :;85=B0 | +| `sales_check` | STRING(36) | ID G5:0 2>72@0B0 | +| `order_id` | STRING(36) | ><5@ 70:070 A A09B0 | +| `terminal_id` | STRING(36) | GUID B5@<8=0;0 | +| `terminal` | VARCHAR(255) | 0720=85 B5@<8=0;0/:0AAK | +| `kkm_id` | STRING(36) | GUID CAB@>9AB20  | +| `status_check` | INTEGER | !B0BCA ?>4B25@645=8O (0/1) | +| `held` | INTEGER | $;03 ?@>2545=8O G5:0 (0/1) | +| `delivery_date` | DATETIME | 0B0 4>AB02:8 | +| `pickup` | INTEGER | @87=0: A0<>2K2>70 (0/1) | +| `matrix` | INTEGER | @>F5=B <0B@8FK | +| `date_up` | DATETIME | 0B0 >1=>2;5=8O <0B@8G=>AB8 | +| `update_source` | INTEGER | AB>G=8: >1=>2;5=8O (1 - 251EC: >B 1!) | + +--- + +## 5B>4K <>45;8 + +### 5B>4K C?@02;5=8O 40B0<8 25@A8>=8@>20=8O + +#### `initDate(): void` + +=8F80;878@C5B 2@5<5==K5 @0<:8 =>2>9 25@A88 8AB>@88. + +**>38:0:** +- #AB0=02;8205B `date_from` 2 B5:CICN 40BC 8 2@55 1C4CI55 ("2100-01-01 00:00:00") +- >20O 25@A8O AG8B05BAO 0:B82=>9 15AA@>G=> 4> <><5=B0 87<5=5=8O + +**K7K205BAO:** ?@8 A>740=88 =>2>9 70?8A8 8AB>@88 + +**@8<5@:** +```php +$history = new SalesHistory(); +$history->initDate(); +// date_from = "2025-12-11 14:30:00" +// date_to = "2100-01-01 00:00:00" +``` + +--- + +#### `setDisableSalesHistory(): void` + +50:B828@C5B B5:CICN 25@A8N 8AB>@88. + +**>38:0:** +- #AB0=02;8205B `date_to` 2 B5:CICN 40BC 8 2@54 459AB28O 25@A88) +- #AB0=02;8205B `active = 0` (?><5G05B :0: =50:B82=CN) +- 5@A8O AB0=>28BAO 8AB>@8G5A:8< A=8<:>< A GQB:8<8 2@5<5==K<8 @0<:0<8 + +**K7K205BAO:** ?@8 A>740=88 =>2>9 25@A88 4;O B>3> 65 G5:0 + +**@8<5@:** +```php +$oldHistory = SalesHistory::findOne($id); +$oldHistory->setDisableSalesHistory(); +$oldHistory->save(); +// date_to = "2025-12-11 15:00:00" +// active = 0 +``` + +--- + +### !B0B8G5A:85 <5B>4K C?@02;5=8O 8AB>@859 + +#### `disableCurrentHistory(SalesHistory $saleHistoryRow): void` + +0:@K205B (450:B828@C5B) B5:CICN 0:B82=CN 25@A8N 8AB>@88. + +**0@0<5B@K:** +- `$saleHistoryRow` - >1J5:B B5:CI59 0:B82=>9 25@A88 8AB>@88 + +**>38:0:** +- K7K205B <5B>4 `setDisableSalesHistory()` C ?5@540==>9 70?8A8 +- !>E@0=O5B 87<5=5=8O 2  +- A?>;L7C5BAO ?5@54 A>740=85< =>2>9 25@A88 8AB>@88 + +**@8<5@:** +```php +$currentHistory = SalesHistory::find() + ->where(['check_guid' => $checkId, 'active' => 1]) + ->one(); + +if ($currentHistory) { + SalesHistory::disableCurrentHistory($currentHistory); +} +``` + +--- + +#### `createHistory($saleRowId, array $saleRowDataPrepared, $crcSumSalesRow, $crcSumSalesCheck, $crcSumSalesProductsRow, $salesProductsOriginal): void` + +!>740QB =>2CN 25@A8N 8AB>@88 4;O G5:0. + +**0@0<5B@K:** +- `$saleRowId` (string) - GUID G5:0 +- `$saleRowDataPrepared` (array) - ?>43>B>2;5==K5 40==K5 G5:0 +- `$crcSumSalesRow` (string) - CRC-AC<<0 2A53> G5:0 +- `$crcSumSalesCheck` (string) - CRC-AC<<0 B>;L:> 40==KE G5:0 +- `$crcSumSalesProductsRow` (string) - CRC-AC<<0 B>20@>2 +- `$salesProductsOriginal` (array) - <0AA82 B>20@>2 G5:0 + +**>38:0:** +1. !>740QB =>2K9 >1J5:B SalesHistory +2. 03@C605B 2 =53> 40==K5 G5:0 G5@57 MultipleModel::loadMultipleFromArray() +3. =8F80;878@C5B 40BK 25@A8>=8@>20=8O (initDate()) +4. #AB0=02;8205B active = 1 +5. 0?8AK205B 2A5 B@8 CRC-AC<E@0=O5B 70?8AL 8AB>@88 G5:0 + - !>740QB 70?8A8 8AB>@88 B>20@>2 (SalesProductsHistory) A ?@82O7:>9 : 8AB>@88 G5:0 + - 0;848@C5B 8 A>E@0=O5B 2A5 B>20@K +8. $8:A8@C5B B@0=70:F8N ?@8 CA?5E5 8;8 >B:0BK205B ?@8 >H81:5 + +**K7>2K AB>@>==8E <5B>4>2:** +- `MultipleModel::loadMultipleFromArray()` - 703@C7:0 40==KE 2 <>45;L +- `MultipleModel::validateMultiple()` - 20;840F8O <=>65AB25==KE <>45;59 +- `MultipleModel::createMultipleModelFromArray()` - A>740=85 <>45;59 B>20@>2 +- `Yii::$app->db->beginTransaction()` - =0G0;> B@0=70:F88 +- `$transaction->commit()` - D8:A0F8O B@0=70:F88 +- `$transaction->rollBack()` - >B:0B B@0=70:F88 + +**@8<5@:** +```php +$checkData = Sales::findOne($checkGuid)->attributes; +$productsData = SalesProducts::find() + ->where(['check_id' => $checkGuid]) + ->all(); + +$crcCheck = md5(json_encode($checkData)); +$crcProducts = md5(json_encode($productsData)); +$crcTotal = md5($crcCheck . $crcProducts); + +SalesHistory::createHistory( + $checkGuid, + [$checkData], + $crcTotal, + $crcCheck, + $crcProducts, + [$checkGuid => $productsData] +); +``` + +--- + +#### `setSaleHistory(string $dateFrom, string $dateTo): array` + +;02=K9 <5B>4 A8=E@>=870F88 8AB>@88 ?@>406 70 ?5@8>4. + +**0@0<5B@K:** +- `$dateFrom` (string) - 40B0 =0G0;0 ?5@8>40 (D>@<0B: "Y-m-d H:i:s") +- `$dateTo` (string) - 40B0 >:>=G0=8O ?5@8>40 + +**>72@0I05B:** <0AA82 A @57C;LB0B0<8 A8=E@>=870F88: +- `infoText` - :@0B:0O AB0B8AB8:0 (4>102;5=>, >1=>2;5=>, 157 87<5=5=89) +- `infoTextFull` - 45B0;L=0O 8=D>@<0F8O ?> :064>H81:8, 5A;8 =5 2A5 70?8A8 >1@01>B0=K + +**>38:0:** +1. >;CG05B 2A5 G5:8 87 `sales` 70 C:070==K9 ?5@8>4 +2. 03@C605B 2A5 B>20@K MB8E G5:>2 87 `sales_products` +3. @C??8@C5B B>20@K ?> check_id +4. ;O :064>3> G5:0: + - KG8A;O5B CRC-AC<B JSON 40==KE G5:0 157 ?>;O date) + - CRC B>20@>2 (md5 >B JSON B>20@>2 157 ?>;O id) + - CRC 8B>3>20O (md5 >B :>=:0B5=0F88 42CE ?@54K4CI8E) + - I5B 0:B82=CN 70?8AL 2 8AB>@88 ?> check_guid + - A;8 70?8A8 =5B - A>740QB =>2CN (addRows++) + - A;8 CRC 87<5=8;0AL: + - 0:@K205B AB0@CN 25@A8N (disableCurrentHistory) + - !>740QB =>2CN 25@A8N (createHistory) + - !GQBG8: updateRows++ + - A;8 CRC A>2?0405B - =8G53> =5 45;05B (stableRows++) +5. $>@<8@C5B 8B>3>2CN AB0B8AB8:C +6. @>25@O5B, GB> 2A5 G5:8 >1@01>B0=K + +**K7>2K AB>@>==8E <5B>4>2:** +- `Sales::find()` - ?>8A: G5:>2 70 ?5@8>4 +- `SalesProducts::find()` - ?>8A: B>20@>2 G5:>2 +- `ArrayHelper::getColumn()` - 872;5G5=85 :>;>=:8 87 <0AA820 +- `json_encode()` - A5@80;870F8O 2 JSON +- `md5()` - 2KG8A;5=85 MD5 E5H0 +- `SalesHistory::find()` - ?>8A: B5:CI59 25@A88 2 8AB>@88 +- `SalesHistory::createHistory()` - A>740=85 =>2>9 25@A88 +- `SalesHistory::disableCurrentHistory()` - 70:@KB85 AB0@>9 25@A88 + +**@8<5@:** +```php +$result = SalesHistory::setSaleHistory( + '2025-12-01 00:00:00', + '2025-12-11 23:59:59' +); + +echo $result['infoText']; +// "A53> 4>102;5=> 150 A53> >1=>28;>AL 20 0==K5 =5 87<5=8;8AL 1000" + +echo $result['infoTextFull']; +// 5B0;L=0O 8=D>@<0F8O ?> :064>4K ?>43>B>2:8 40==KE + +#### `saleRowDataPrepared(array $saleRow): array` + +@5>1@07C5B 40==K5 G5:0 87 D>@<0B0 Sales 2 D>@<0B SalesHistory. + +**0@0<5B@K:** +- `$saleRow` (array) - 40==K5 G5:0 87 B01;8FK sales + +**>72@0I05B:** ?@5>1@07>20==K9 <0AA82 40==KE + +**>38:0:** +5@58<5=>2K205B :;NG8 <0AA820 A>3;0A=> :>=D83C@0F88: +- `id` `check_guid` (2 8AB>@88 ?5@28G=K9 :;NG - id, 0 GUID G5:0 - check_guid) + +AB0;L=K5 ?>;O :>?8@CNBAO 157 87<5=5=89. + +**@8<5@:** +```php +$saleData = [ + 'id' => 'guid-G5:0', + 'date' => '2025-12-11', + 'summ' => 1500.00, + 'operation' => '@>4060' +]; + +$historyData = SalesHistory::saleRowDataPrepared($saleData); +// $historyData = [ +// 'check_guid' => 'guid-G5:0', +// 'date' => '2025-12-11', +// 'summ' => 1500.00, +// 'operation' => '@>4060' +// ] +``` + +--- + +### 5BB5@K 8 A5BB5@K + +>45;L A>45@68B ?>;=K9 =01>@ 35BB5@>2 8 A5BB5@>2 4;O 2A5E ?>;59: + +#### CRC-AC<38G=> <>45;8 Sales) +```php +getCheckGuid(): string +setCheckGuid(string $check_guid): void + +getDate(): string +setDate(string $date): void + +getOperation(): string +setOperation(string $operation): void + +getSumm(): float +setSumm(float $summ): void + +getSkidka(): float +setSkidka(float $skidka): void + +// 8 B.4. 4;O 2A5E ?>;59 +``` + +#### :B82=>ABL +```php +getActive(): string +setActive(): void // CAB0=02;8205B active = 1 +disableActive(): void // CAB0=02;8205B active = 0 +``` + +--- + +## @8=F8? @01>BK A8AB5=8@>20=8O + +### ;3>@8B< >BA;56820=8O 87<5=5=89 + +1. **KG8A;5=85 CRC-AC<<** + - ;O G5:0: md5(json_encode(40==K5_G5:0_157_date)) + - ;O B>20@>2: md5(json_encode(B>20@K_157_id)) + - B>3>20O: md5(crc_G5:0 + crc_B>20@>2) + +2. **@>25@:0 87<5=5=89** + - @8 A8=E@>=870F88 A@02=8205BAO 8B>3>20O CRC A `crc_sum_sales` 0:B82=>9 70?8A8 + - A;8 A>2?0405B - 40==K5 =5 87<5=8;8AL + - A;8 =5 A>2?0405B - 70D8:A8@>20=> 87<5=5=85 + +3. **!>740=85 =>2>9 25@A88** + - !B0@0O 25@A8O: date_to = NOW, active = 0 + - >20O 25@A8O: date_from = NOW, date_to = "2100-01-01", active = 1 + - >2K5 CRC-AC<2CN 25@A8N + +4. **%@0=5=85 25@A89** + - A5 25@A88 E@0=OBAO 2 `sales_history` + - ">20@K 25@A89 - 2 `sales_products_history` + - :B82=0O 25@A8O 8<55B `active = 1` 8 `date_to` 2 1C4CI5< + - AB>@8G5A:85 25@A88 8<5NB `active = 0` 8 70:@KBK9 ?5@8>4 + +### @8<5@K 87<5=5=89, :>B>@K5 >BA;56820NBAO + +- 7<5=5=85 AC<;8G5AB20 B>20@>2 +- >102;5=85/C40;5=85 B>20@>2 +- 7<5=5=85 A:84:8 +- 7<5=5=85 A?>A>1>2 >?;0BK +- 7<5=5=85 AB0BCA0 G5:0 +- N1K5 4@C385 87<5=5=8O 2 40==KE + +--- + +## 803@0<<0 A2O759 + +```mermaid +erDiagram + sales_history ||--|| sales : "tracks_changes" + sales_history ||--o{ sales_products_history : "has_versions" + sales_history }o--|| city_store : "belongs_to" + sales_history }o--|| admin : "created_by" + + sales_history { + int id PK + string check_guid FK + string crc_sum_sales + string crc_sum_sales_check + string crc_sum_sales_products + datetime date_from + datetime date_to + int active + float summ + float skidka + int store_id FK + int admin_id FK + } + + sales { + string id PK + datetime date + float summ + } + + sales_products_history { + int id PK + int sales_history_id FK + string product_guid + float kol + float summa + } +``` + +--- + +## @8<5@K 8A?>;L7>20=8O + +### >;CG5=85 B5:CI59 0:B82=>9 25@A88 G5:0 + +```php +$activeHistory = SalesHistory::find() + ->where(['check_guid' => $checkGuid]) + ->andWhere(['active' => 1]) + ->one(); + +if ($activeHistory) { + echo ":BC0;L=0O 25@A8O A " . $activeHistory->date_from; + echo "CRC: " . $activeHistory->crc_sum_sales; +} +``` + +--- + +### >;CG5=85 2A59 8AB>@88 87<5=5=89 G5:0 + +```php +$history = SalesHistory::find() + ->where(['check_guid' => $checkGuid]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $version) { + echo "5@A8O >B " . $version->date_from; + if ($version->active) { + echo " (B5:CI0O)"; + } else { + echo " 4> " . $version->date_to; + } + echo ": AC<<0 " . $version->summ . " @C1.\n"; +} +``` + +--- + +### !8=E@>=870F8O 8AB>@88 70 A53>4=O + +```php +$today = date('Y-m-d 00:00:00'); +$now = date('Y-m-d H:i:s'); + +$result = SalesHistory::setSaleHistory($today, $now); + +echo ">102;5=> =>2KE G5:>2: " . $result['addRows'] . "\n"; +echo "1=>2;5=> G5:>2: " . $result['updateRows'] . "\n"; +echo "57 87<5=5=89: " . $result['stableRows'] . "\n"; + +if ($result['infoError']) { + echo "H81:8: " . $result['infoError']; +} +``` + +--- + +### >8A: G5:>2 A 87<5=5=8O<8 70 ?5@8>4 + +```php +// '5:8, :>B>@K5 8<5NB 1>;55 >4=>9 25@A88 +$changedChecks = SalesHistory::find() + ->select('check_guid') + ->groupBy('check_guid') + ->having('COUNT(*) > 1') + ->column(); + +echo "0945=> G5:>2 A 87<5=5=8O<8: " . count($changedChecks); + +foreach ($changedChecks as $checkGuid) { + $versions = SalesHistory::find() + ->where(['check_guid' => $checkGuid]) + ->count(); + echo "'5: {$checkGuid}: {$versions} 25@A89\n"; +} +``` + +--- + +### !@02=5=85 42CE 25@A89 G5:0 + +```php +$versions = SalesHistory::find() + ->where(['check_guid' => $checkGuid]) + ->orderBy(['date_from' => SORT_ASC]) + ->limit(2) + ->all(); + +if (count($versions) >= 2) { + $old = $versions[0]; + $new = $versions[1]; + + echo "7<5=5=8O A " . $old->date_from . " ?> " . $new->date_from . ":\n"; + + if ($old->summ != $new->summ) { + echo "!C<<0: {$old->summ} {$new->summ}\n"; + } + + if ($old->skidka != $new->skidka) { + echo "!:84:0: {$old->skidka} {$new->skidka}\n"; + } + + if ($old->crc_sum_sales_products != $new->crc_sum_sales_products) { + echo "!>AB02 B>20@>2 87<5=8;AO\n"; + } +} +``` + +--- + +## 0;840F8O + +1O70B5;L=K5 ?>;O: +- `crc_sum_sales`, `crc_sum_sales_check`, `crc_sum_sales_products` +- `date_from`, `check_guid`, `date` +- `operation`, `status`, `summ`, `skidka`, `number` +- `admin_id`, `seller_id`, `store_id_1c`, `store_id` +- `payments`, `pay_arr`, `held` + +"8?K ?>;59: +- Integer: `active`, `admin_id`, `store_id`, `phone`, `status_check`, `held`, `pickup`, `matrix`, `update_source` +- Float: `summ`, `skidka` +- String (max 36): `date_from`, `date_to`, `check_guid`, `seller_id`, `store_id_1c`, `sales_check`, `order_id`, `terminal_id`, `kkm_id` +- String (max 100): CRC-AC<45;8 + +- **[Sales](./Sales.md)**  >A=>2=0O <>45;L G5:>2 (8AB>G=8: 40==KE) +- **SalesProductsHistory**  8AB>@8O B>20@>2 G5:>2 +- **[Admin](./Admin.md)**  A>B@C4=8:8 +- **[CityStore](./CityStore.md)**  <03078=K +- **MultipleModel**  2A?><>30B5;L=K9 :;0AA 4;O @01>BK A <=>65AB25==K<8 <>45;O<8 + +--- + +## A>15==>AB8 @01>BK + +### CRC-AC< G5:0** (`crc_sum_sales`) - :>=B@>;L=0O AC<<0 2A5E 40==KE (G5: + B>20@K) +- **CRC B>;L:> G5:0** (`crc_sum_sales_check`) - 87<5=5=8O 2 H0?:5 G5:0 (AC<<0, A:84:0, AB0BCA 8 B.4.) +- **CRC B>20@>2** (`crc_sum_sales_products`) - 87<5=5=8O 2 A>AB025 8;8 :>;8G5AB25 B>20@>2 + +-B> ?>72>;O5B 1KAB@> >?@545;8BL, GB> 8<5==> 87<5=8;>AL 2 G5:5. + +### 5@A8>=8@>20=85 + +- 0640O 70?8AL 2 8AB>@88 - ?>;=0O :>?8O 40==KE G5:0 =0 <><5=B 87<5=5=8O +- :B82=0O 25@A8O (`active = 1`) 8<55B >B:@KBK9 ?5@8>4 (`date_to` 2 40;Q:>< 1C4CI5<) +- @8 87<5=5=88 AB0@0O 25@A8O 70:@K205BAO, A>740QBAO =>20O +- AB>@8O =8:>340 =5 C40;O5BAO, B>;L:> ?><5G05BAO :0: =50:B82=0O + +### @>872>48B5;L=>ABL + +- !8=E@>=870F8O 2K?>;=O5BAO ?0:5B=> G5@57 `setSaleHistory()` +- A?>;L7CNBAO B@0=70:F88 4;O 0B><0@=>AB8 >?5@0F89 +- CRC-AC<48= @07 ?@8 A8=E@>=870F88 +- =45:A0F8O ?> `check_guid` 8 `active` 4;O 1KAB@>3> ?>8A:0 + +--- + +**5@A8O:** 1.0 +**0B0:** 2025-12-11 diff --git a/erp24/docs/models/SalesHistorySearch.md b/erp24/docs/models/SalesHistorySearch.md new file mode 100644 index 00000000..0f1cdc22 --- /dev/null +++ b/erp24/docs/models/SalesHistorySearch.md @@ -0,0 +1,175 @@ +# Класс: SalesHistorySearch + + +## Mindmap + +```mermaid +mindmap + root((SalesHistorySearch)) + Таблица БД + ActiveRecord + Наследование + extends SalesHistory +``` + +## Назначение +Search-модель для поиска и фильтрации истории продаж в ERP24. Стандартная Gii-модель с поддержкой множества атрибутов для поиска по архивным данным чеков. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`SalesHistory` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `admin_id`, `store_id`, `phone`, `status_check`, `held`, `pickup`, `matrix`, `update_source` — integer +- `crc_summ_sales`, `crc_summ_sales_check`, `crc_summ_sales_products`, `date_from`, `date_to`, `guid`, `date`, `operation`, `status`, `number`, `seller_id`, `store_id_1c`, `payments`, `pay_arr`, `sales_history_check`, `order_id`, `terminal_id`, `terminal`, `kkm_id`, `delivery_date`, `date_up` — safe +- `summ`, `skidka` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска истории продаж. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос SalesHistory::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, date, summ, skidka, admin_id, store_id, phone, status_check, held, delivery_date, pickup, matrix, date_up, update_source + - like: crc_summ_*, date_from, date_to, guid, operation, status, number, seller_id, store_id_1c, payments, pay_arr, sales_history_check, order_id, terminal_id, terminal, kkm_id + +## Диаграмма структуры истории + +```mermaid +erDiagram + SalesHistory { + int id PK + varchar guid + datetime date + decimal summ + decimal skidka + int admin_id FK + int store_id FK + int phone + varchar operation + varchar status + varchar number + varchar seller_id + varchar store_id_1c + varchar payments + varchar pay_arr + varchar sales_history_check + varchar order_id + varchar terminal_id + varchar terminal + varchar kkm_id + datetime delivery_date + datetime date_up + int status_check + int held + int pickup + int matrix + int update_source + varchar crc_summ_sales + varchar crc_summ_sales_check + varchar crc_summ_sales_products + datetime date_from + datetime date_to + } +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SalesHistorySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по магазину +```php +$searchModel = new SalesHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesHistorySearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Поиск по GUID +```php +$searchModel = new SalesHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesHistorySearch' => [ + 'guid' => 'abc-123', + ] +]); +``` + +### Поиск по терминалу +```php +$searchModel = new SalesHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesHistorySearch' => [ + 'terminal' => 'TERMINAL-01', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'guid', + 'date:datetime', + 'summ:decimal', + 'store_id', + 'operation', + 'status', + 'terminal', + ], +]) ?> +``` + +## Связанные модели + +- [SalesHistory](./SalesHistory.md) — базовая модель истории +- [Sales](./Sales.md) — текущие продажи +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Архивная таблица**: История продаж для аудита +2. **CRC контроль**: crc_summ_sales, crc_summ_sales_check, crc_summ_sales_products +3. **Период**: date_from, date_to для диапазона +4. **Терминалы**: terminal_id, terminal, kkm_id для идентификации кассы +5. **like вместо ilike**: Регистрозависимый поиск +6. **Статусы**: status, status_check, held для отслеживания diff --git a/erp24/docs/models/SalesItems.md b/erp24/docs/models/SalesItems.md new file mode 100644 index 00000000..0521d40f --- /dev/null +++ b/erp24/docs/models/SalesItems.md @@ -0,0 +1,302 @@ +# >45;L SalesItems + + +## Mindmap + +```mermaid +mindmap + root((SalesItems)) + Таблица БД + sales_items + Свойства + id + int + check_id + string + phone + int + lid_id + int + store_id + int + store_id_1c + string + Наследование + extends yiidbActiveRecord +``` + +## 07=0G5=85 + +>45;L `SalesItems` ?@54AB02;O5B B>20@=K5 ?>78F88 (items) 2 G5:5 ?@>4068. -B> 0;LB5@=0B82=0O 8;8 CAB0@52H0O 25@A8O <>45;8 SalesProducts, E@0=OI0O 8=D>@<0F8N > ?@>40==KE B>20@0E. + +**@8<5G0=85:**  ?@>5:B5 >A=>2=>9 <>45;LN 4;O B>20@>2 2 G5:0E O2;O5BAO **SalesProducts**. >45;L SalesItems <>65B 8A?>;L7>20BLAO 4;O A?5F8D8G5A:8E 7040G 8;8 O2;OBLAO legacy-:>4><. + +**$09; <>45;8:** `erp24/records/SalesItems.php` +**Namespace:** `yii_app\records` +**"01;8F0 :** `sales_items` +** >48B5;LA:89 :;0AA:** `yii\db\ActiveRecord` + +--- + +## >;O B01;8FK + +| >;5 | "8? | ?8A0=85 | +|------|-----|----------| +| `id` | INTEGER | 5@28G=K9 :;NG (02B>8=:@5<5=B) | +| `check_id` | STRING(36) | GUID G5:0 (FK `sales.id`) | +| `phone` | INTEGER | ><5@ B5;5D>=0 :;85=B0 | +| `lid_id` | INTEGER | ID ;840/70O2:8 | +| `store_id` | INTEGER | ID <03078=0 (FK `city_store.id`) | +| `store_id_1c` | STRING(36) | GUID <03078=0 2 1! | +| `admin_id` | INTEGER | ID A>B@C4=8:0 (FK `admin.id`) | +| `seller_id` | STRING(36) | GUID ?@>402F0 2 1! | +| `item_id` | INTEGER | ID B>20@0 | +| `complect_id` | INTEGER | ID :>20@0 2 1! | +| `name` | VARCHAR(200) | 0720=85 B>20@0 | +| `kol` | FLOAT | >;8G5AB2> B>20@0 | +| `summa` | FLOAT | !C<<0 ?>78F88 | +| `skidka` | FLOAT | !:84:0 =0 ?>78F8N | +| `date` | DATETIME | 0B0 ?@>4068 | +| `referal_id` | INTEGER | ID @5D5@0;0 | +| `color_id` | INTEGER | ID F25B0 B>20@0 | +| `vozvrat` | INTEGER | @87=0: 2>72@0B0 (0/1) | + +--- + +## 5B>4K <>45;8 + +### @028;0 20;840F88 + +#### `rules(): array` + +?@545;O5B ?@028;0 20;840F88 4;O ?>;59 <>45;8. + +**1O70B5;L=K5 ?>;O:** +- `check_id` - GUID G5:0 +- `phone` - =><5@ B5;5D>=0 :;85=B0 +- `lid_id` - ID ;840 +- `admin_id` - ID A>B@C4=8:0 +- `seller_id` - GUID ?@>402F0 +- `item_id` - ID B>20@0 +- `complect_id` - ID :>20@0 2 1! +- `kol` - :>;8G5AB2> +- `summa` - AC<<0 +- `skidka` - A:84:0 +- `date` - 40B0 +- `referal_id` - ID @5D5@0;0 +- `color_id` - ID F25B0 +- `vozvrat` - ?@87=0: 2>72@0B0 + +**'8A;>2K5 ?>;O:** +- Integer: `phone`, `lid_id`, `store_id`, `admin_id`, `item_id`, `complect_id`, `referal_id`, `color_id`, `vozvrat` +- Float: `kol`, `summa`, `skidka` + +**!B@>:>2K5 ?>;O:** +- `check_id`, `store_id_1c`, `seller_id`, `id_1c` - <0:A8;>2 (GUID) +- `name` - <0:A8;>2 + +**57>?0A=K5 ?>;O:** +- `date` - 20;848@C5BAO :0: safe (02B><0B8G5A:>5 ?@5>1@07>20=85 B8?0) + +**>72@0I05B:** <0AA82 ?@028; 20;840F88 + +**@8<5@:** +```php +$item = new SalesItems(); +$item->attributes = $data; +if ($item->validate()) { + $item->save(); +} +``` + +--- + +### 5B:8 0B@81CB>2 + +#### `attributeLabels(): array` + +>72@0I05B <5B:8 (labels) 4;O ?>;59 <>45;8, 8A?>;L7C5@<0E 8 ?@54AB02;5=8OE. + +**>72@0I05B:** <0AA82 <5B>: 0B@81CB>2 + +**@8<5@:** +```php +$labels = $item->attributeLabels(); +echo $labels['check_id']; // "Check ID" +echo $labels['name']; // "Name" +``` + +--- + +## @8<5@K 8A?>;L7>20=8O + +### >;CG5=85 2A5E B>20@>2 G5:0 + +```php +$items = SalesItems::find() + ->where(['check_id' => $checkGuid]) + ->all(); + +foreach ($items as $item) { + echo $item->name . " x " . $item->kol . " = " . $item->summa . " @C1.\n"; +} +``` + +--- + +### >4AGQB AC<where(['check_id' => $checkGuid]) + ->sum('summa'); + +echo "!C<<0 G5:0: " . $checkTotal . " @C1."; +``` + +--- + +### >;CG5=85 2>72@0B=KE ?>78F89 + +```php +$returns = SalesItems::find() + ->where(['vozvrat' => 1]) + ->andWhere(['>=', 'date', date('Y-m-d')]) + ->all(); + +echo ">72@0B>2 A53>4=O: " . count($returns); +``` + +--- + +### >8A: B>20@>2 :;85=B0 + +```php +$customerItems = SalesItems::find() + ->where(['phone' => $customerPhone]) + ->orderBy(['date' => SORT_DESC]) + ->limit(20) + ->all(); + +foreach ($customerItems as $item) { + echo $item->date . ": " . $item->name . " - " . $item->summa . " @C1.\n"; +} +``` + +--- + +### $8;LB@0F8O ?> <03078=C + +```php +$storeItems = SalesItems::find() + ->where(['store_id' => $storeId]) + ->andWhere(['>=', 'date', date('Y-m-01')]) // B5:CI89 <5AOF + ->all(); + +$totalSales = array_sum(array_column($storeItems, 'summa')); +echo "@>4068 <03078=0 70 <5AOF: " . $totalSales . " @C1."; +``` + +--- + +### >8A: ?> B>20@C + +```php +$productSales = SalesItems::find() + ->where(['id_1c' => $productGuid]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +$totalQty = array_sum(array_column($productSales, 'kol')); +echo "@>40=> 548=8F B>20@0: " . $totalQty; +``` + +--- + +## 803@0<<0 A2O759 + +```mermaid +erDiagram + sales_items }o--|| sales : "belongs_to_check" + sales_items }o--|| city_store : "belongs_to_store" + sales_items }o--|| admin : "sold_by" + + sales_items { + int id PK + string check_id FK + int store_id FK + int admin_id FK + string id_1c + string name + float kol + float summa + float skidka + int vozvrat + } + + sales { + string id PK + datetime date + float summ + } + + city_store { + int id PK + string name + } + + admin { + int id PK + string name + } +``` + +--- + +## 0;840F8O + +>45;L 8A?>;L7C5B AB0=40@B=K5 ?@028;0 20;840F88 Yii2: + +- **1O70B5;L=K5 ?>;O** - 1>;LH8=AB2> ?>;59 required +- **GUID ?>;O** >3@0=8G5=K 36 A8<2>;0<8 +- **'8A;>2K5 ?>;O** ?@>25@ONBAO =0 :>@@5:B=>ABL B8?0 +- **0720=85 B>20@0** >3@0=8G5=> 200 A8<2>;0<8 +- **0B0** 20;848@C5BAO :0: 157>?0A=>5 ?>;5 + +--- + +## !2O70==K5 <>45;8 + +- **[Sales](./Sales.md)**  G5:8 ?@>406 +- **[SalesProducts](./SalesProducts.md)**  >A=>2=0O <>45;L B>20@>2 2 G5:0E (@5:><5=4C5BAO) +- **[CityStore](./CityStore.md)**  <03078=K +- **[Admin](./Admin.md)**  A>B@C4=8:8 +- **[Products1c](./Products1c.md)**  A?@02>G=8: B>20@>2 87 1! + +--- + +## B;8G8O >B SalesProducts + +| %0@0:B5@8AB8:0 | SalesItems | SalesProducts | +|----------------|------------|---------------| +| A?>;L7>20=85 | #AB0@52H0O/0;LB5@=0B82=0O | A=>2=0O <>45;L | +| !2O78 | 8=8<0;L=K5 | >;=K5 A2O78 A G5:0<8, B>20@0<8 | +| >;O | 07>2K9 =01>@ | 0AH8@5==K9 =01>@ | +| >:C<5=B0F8O | 8=8<0;L=0O | >;=0O | +| 5:><5=40F8O | 5 @5:><5=4C5BAO 4;O =>2KE 7040G | 5:><5=4C5BAO | + +--- + +## @8<5G0=8O + +1. **;O =>2KE 7040G 8A?>;L7C9B5 SalesProducts** - MB> >A=>2=0O <>45;L 4;O @01>BK A B>20@0<8 2 G5:0E +2. **SalesItems <>65B A>45@60BL legacy-40==K5** - ?@>25@O9B5 0:BC0;L=>ABL 40==KE +3. **5B >?@545;Q==KE A2O759 (relations)** - <>45;L =5 8<55B <5B>4>2 4;O A2O759 A 4@C38<8 <>45;O<8 +4. **8=8<0;8AB8G=0O 20;840F8O** - <=>385 ?>;O 70:><<5=B8@>20=K 2 rules + +--- + +**5@A8O:** 1.0 +**0B0:** 2025-12-11 diff --git a/erp24/docs/models/SalesMetrics.md b/erp24/docs/models/SalesMetrics.md new file mode 100644 index 00000000..9204f280 --- /dev/null +++ b/erp24/docs/models/SalesMetrics.md @@ -0,0 +1,317 @@ +# Class: SalesMetrics + +## Mindmap + +```mermaid +mindmap + root((SalesMetrics)) + Таблица БД + Metrics наследник + Свойства + alias + array 26 метрик + listSelectedQuery + array SQL + Метрики + sales_sum + Сумма продаж + count_sales + Кол-во чеков + count_sales_in_N_hour + 24 почасовых + Связи + Sales + 1:N данные + Наследование + extends Metrics +``` + +## Назначение + +Класс SalesMetrics наследует абстрактный класс Metrics и реализует расчёт метрик продаж по магазинам с разбивкой по сменам и дням в системе ERP24. Агрегирует данные из таблицы `sales` (продажи), вычисляет сумму продаж, количество чеков и почасовое распределение продаж. Сохраняет результаты в нормализованную структуру `rnp_*` таблиц для последующего анализа и отображения на дашбордах. + +## Пространство имён + +```php +namespace yii_app\records\metrics; +``` + +## Родительский класс + +```php +\yii_app\records\metrics\Metrics +``` + +## Использования (Dependencies) + +- `Exception` - базовый класс исключений PHP +- `yii\db\Expression` - SQL выражения Yii2 +- `yii_app\records\RnpData` - модель данных метрик +- `yii_app\records\Sales` - модель продаж + +## Свойства (Properties) + +### Защищённые массивы псевдонимов + +```php +protected array $alias = [ + 'sales_sum', // Сумма продаж + 'count_sales', // Количество чеков + 'count_sales_in_0_hour', // Чеков с 00:00 до 00:59 + 'count_sales_in_1_hour', // Чеков с 01:00 до 01:59 + // ... до 23 часа + 'count_sales_in_23_hour', // Чеков с 23:00 до 23:59 +]; +``` + +Всего 26 метрик: 1 сумма + 1 количество + 24 почасовых метрики. + +### Исключения по типам смен + +```php +protected array $listOfExceptionsByShiftType = [ + 1 => [ // Дневная смена (08:00-20:00) - исключаем ночные часы + 'count_sales_in_0_hour', 'count_sales_in_1_hour', ..., 'count_sales_in_7_hour', + 'count_sales_in_20_hour', ..., 'count_sales_in_23_hour', + ], + 2 => [ // Ночная смена (20:00-08:00) - исключаем дневные часы + 'count_sales_in_8_hour', ..., 'count_sales_in_19_hour', + ], + 4 => [], // Полный день - включает все часы +]; +``` + +### Список SELECT выражений + +```php +protected array $listSelectedQuery = [ + 'sales_sum' => 'SUM(IF(sales.sales_check = \'\', sales.summ, 0)) - SUM(IF(sales.sales_check != \'\', sales.summ, 0))', + 'count_sales' => 'COUNT(sales.id)', + 'count_sales_in_0_hour' => 'SUM(if (TIME_FORMAT(sales.date, \'%H:%i:%s\') >= \'00:00:00\' AND <= \'00:59:59\', 1, 0))', + // ... аналогично для всех 24 часов +]; +``` + +## Методы + +### getQueryDataDay() + +**Описание:** Реализует абстрактный метод родителя. Возвращает запрос для расчёта метрик продаж за полный день (shift_type = 4). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос к таблице sales с агрегацией по дням + +**Логика работы:** +1. Создаёт запрос к таблице `sales` +2. Выполняет SELECT с агрегирующими функциями (SUM, COUNT) для всех метрик +3. JOIN с `city_store` для привязки к магазинам +4. JOIN с `store_dynamic` для определения кластера магазина на дату продажи +5. Фильтрует по: + - Диапазону дат (dateStart, dateEnd) + - Кластеру (если указан) + - Магазину (если указан) + - Типу операции ('Продажа') + - Отсутствию order_id (обычные продажи, не из заказов) +6. Группирует по дате, магазину и store_dynamic_id +7. Сортирует по дате и магазину + +**Вызовы сторонних методов:** +- `Sales::find()` - создание запроса +- `->innerJoin()` - соединение таблиц +- `->andFilterWhere()` - условная фильтрация +- `->andWhere()` - фильтрация +- `->groupBy()` - группировка +- `->orderBy()` - сортировка +- `new Expression()` - SQL выражения для дат + +**Пример результирующего запроса:** +```sql +SELECT + DATE_FORMAT(sales.date, '%Y-%m-%d') AS date, + city_store.id AS store_id, + store_dynamic.id AS store_dynamic_id, + 4 AS shift_type, + SUM(IF(sales.sales_check = '', sales.summ, 0)) - SUM(IF(sales.sales_check != '', sales.summ, 0)) AS sales_sum, + COUNT(sales.id) AS count_sales, + SUM(IF(HOUR(sales.date) = 0, 1, 0)) AS count_sales_in_0_hour, + ... +FROM sales +INNER JOIN city_store ON sales.store_id = city_store.id +INNER JOIN store_dynamic ON ... +WHERE DATE_FORMAT(sales.date, '%Y-%m-%d') BETWEEN '2025-01-01' AND '2025-01-31' + AND sales.operation = 'Продажа' + AND sales.order_id = '' +GROUP BY DATE_FORMAT(sales.date, '%Y-%m-%d'), city_store.id, store_dynamic.id +ORDER BY date ASC, store_id ASC +``` + +--- + +### getQueryDataShifts() + +**Описание:** Реализует абстрактный метод родителя. Возвращает запрос для расчёта метрик продаж по сменам (shift_type = 1 или 2). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос к таблице sales с агрегацией по сменам + +**Логика работы:** +1. Создаёт запрос к таблице `sales` +2. В SELECT определяет дату смены: + - Если час < 8: дата = дата продажи - 1 день (ночная смена относится к предыдущему дню) + - Если час >= 8: дата = дата продажи +3. Определяет тип смены: + - Если час >= 8 AND < 20: shift_type = 1 (дневная) + - Иначе: shift_type = 2 (ночная) +4. Выполняет аналогичные JOIN и фильтрацию как в getQueryDataDay() +5. Группирует по дате смены, типу смены, магазину +6. Сортирует по дате, типу смены, магазину + +**Вызовы сторонних методов:** +- Аналогично `getQueryDataDay()` + +**Пример результирующего запроса:** +```sql +SELECT + IF(HOUR(sales.date) < 8, DATE_FORMAT(DATE_SUB(sales.date, INTERVAL 1 DAY), '%Y-%m-%d'), DATE_FORMAT(sales.date, '%Y-%m-%d')) AS date, + city_store.id AS store_id, + store_dynamic.id AS store_dynamic_id, + IF(HOUR(sales.date) >= 8 AND HOUR(sales.date) < 20, 1, 2) AS shift_type, + SUM(...) AS sales_sum, + ... +FROM sales +INNER JOIN city_store ON ... +INNER JOIN store_dynamic ON ... +WHERE ... +GROUP BY date, shift_type, city_store.id, store_dynamic.id +ORDER BY date ASC, shift_type ASC, store_id ASC +``` + +**Особенность:** Продажи с 00:00 до 07:59 относятся к ночной смене (shift_type = 2) предыдущего календарного дня. + +## Связи (Relations) + +```mermaid +erDiagram + SALES_METRICS --|> METRICS : extends + SALES_METRICS --> SALES : reads from + SALES_METRICS --> CITY_STORE : joins with + SALES_METRICS --> STORE_DYNAMIC : joins with + SALES_METRICS --> RNP_INDEX : writes to + SALES_METRICS --> RNP_DATA : writes to + SALES_METRICS --> RNP_ALIAS : uses + + SALES { + int id PK + int store_id FK + datetime date + float summ + string operation + string sales_check + string order_id + } +``` + +## Примеры использования + +### Расчёт метрик продаж за месяц + +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +$result = $metrics->insertData(); +echo $result; +// |Время поиска: 3.125 sec.|Время записи индексов: 0.450 sec.|Время записи данных: 1.850 sec.| +``` + +### Расчёт для конкретного кластера + +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-15'; +$metrics->dateEnd = '2025-01-15'; +$metrics->cluster = 1; // Только для кластера 1 + +$metrics->insertData(); +``` + +### Расчёт для одного магазина + +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; +$metrics->store = 25; // Магазин ID 25 + +$metrics->insertData(); +``` + +### Получение рассчитанных данных + +```php +$metrics = new SalesMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +$data = $metrics->getDataArray(1, 5, 1); // Кластер 1, Магазин 5, Дневная смена + +foreach ($data as $row) { + echo "{$row['date']} - {$row['alias']}: {$row['value']}\n"; +} +// Результат: +// 2025-01-01 - sales_sum: 150000.50 +// 2025-01-01 - count_sales: 320 +// 2025-01-01 - count_sales_in_8_hour: 25 +// ... +``` + +## Поток данных + +```mermaid +flowchart TD + A[SalesMetrics::insertData] --> B[Валидация дат] + B --> C[getQueryDataCollection] + C --> D[getQueryDataShifts - смены 1,2] + C --> E[getQueryDataDay - смена 4] + D --> F[Sales: агрегация по shift] + E --> G[Sales: агрегация по day] + F --> H[IF shift_type != 4, data_shifts] + G --> H + H --> I[Batch 1000 записей] + I --> J[Расчёт индексов RnpIndex] + J --> K[Маппинг RnpAlias] + K --> L[Фильтрация исключений по сменам] + L --> M[RnpData::deleteAll старые] + M --> N[RnpData::batchInsert новые] + N --> O[Возврат статистики] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Metrics](./Metrics.md) | Abstract | Базовый класс метрик | +| [Sales](./Sales.md) | Model | Модель продаж | +| [FotMetrics](./FotMetrics.md) | Model | Метрики ФОТ | +| [WriteOffsMetrics](./WriteOffsMetrics.md) | Model | Метрики списаний | +| `SalesMetricsJob` | Job | Фоновый расчёт метрик продаж | +| `RnpData` | Model | Хранение данных метрик | + +## Примечания + +1. **Расчёт суммы продаж**: Учитывает возвраты через поле `sales_check` (продажи с пустым sales_check - возвраты вычитаются) +2. **Смены**: Ночная смена начинается в 20:00 и заканчивается в 08:00 следующего дня +3. **Почасовые метрики**: Позволяют анализировать пиковые часы продаж +4. **Исключения**: Почасовые метрики исключаются для нерелевантных смен (дневные часы не попадают в ночную смену) +5. **Производительность**: Используется batch обработка и индексы для оптимизации + +--- + +**Связанная документация:** +- [Metrics](./Metrics.md) +- [FotMetrics](./FotMetrics.md) +- [WriteOffsMetrics](./WriteOffsMetrics.md) +- [Архитектура системы метрик](../architecture/metrics-system.md) diff --git a/erp24/docs/models/SalesProducts.md b/erp24/docs/models/SalesProducts.md index 88bf7056..c800071b 100644 --- a/erp24/docs/models/SalesProducts.md +++ b/erp24/docs/models/SalesProducts.md @@ -1,5 +1,37 @@ # Class: SalesProducts + +## Mindmap + +```mermaid +mindmap + root((SalesProducts)) + Таблица БД + sales_products + Свойства + check_id + string + product_id + string + quantity + float + price + float + discount + float + summ + float + Связи + Seller + 1:1 Products1c + Product + 1:1 Products1c + Sale + 1:1 Sales + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель для управления товарами в чеках продаж. Хранит информацию о каждом товаре в чеке: количество, цену, скидку, сумму, продавца. Используется для детализации чеков и анализа продаж по товарам и продавцам. diff --git a/erp24/docs/models/SalesProductsHistory.md b/erp24/docs/models/SalesProductsHistory.md new file mode 100644 index 00000000..233e2419 --- /dev/null +++ b/erp24/docs/models/SalesProductsHistory.md @@ -0,0 +1,278 @@ +# Класс: SalesProductsHistory + + +## Mindmap + +```mermaid +mindmap + root((SalesProductsHistory)) + Таблица БД + sales_products_history + Свойства + id + int + sales_history_id + int + check_id + string + product_id + string + sales_product_id + string + type_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель истории товарных позиций чеков продаж в ERP24. Хранит исторические снимки товаров в чеках для отслеживания изменений и аудита продаж. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`sales_products_history` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `sales_history_id` | int | FK на историю чека (SalesHistory) | +| `check_id` | varchar(36) | GUID чека из таблицы sales | +| `product_id` | varchar(36) | GUID товара из products_1c | +| `sales_product_id` | int | ID позиции из SalesProducts | +| `type_id` | int | Тип товара: 1=товар, 2=матричный букет, 3=комплект | +| `component_parent_id` | varchar(36) | GUID родительского товара (для комплектов) | +| `seller_id` | varchar(36) / null | GUID флориста (для начисления ЗП) | +| `assemble_id` | varchar(40) / null | GUID сборки из 1С | +| `quantity` | float | Количество | +| `color` | varchar(145) / null | Цвет (характеристика из 1С) | +| `price` | float | Цена за единицу | +| `discount` | float | Скидка в рублях на позицию | +| `summ` | float | Сумма позиции | + +## Типы товаров (type_id) + +| Значение | Описание | +|----------|----------| +| `1` | Обычный товар | +| `2` | Матричный букет | +| `3` | Комплект | + +## Диаграмма связей + +```mermaid +erDiagram + SalesProductsHistory { + int id PK + int sales_history_id FK + varchar check_id FK + varchar product_id FK + int sales_product_id + int type_id + varchar component_parent_id + varchar seller_id FK + varchar assemble_id + float quantity + varchar color + float price + float discount + float summ + } + + SalesHistory { + int id PK + varchar check_id FK + } + + Sales { + varchar guid PK + } + + Products1c { + varchar id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + SalesHistory ||--o{ SalesProductsHistory : "sales_history_id" + Sales ||--o{ SalesProductsHistory : "check_id" + Products1c ||--o{ SalesProductsHistory : "product_id" + Admin ||--o{ SalesProductsHistory : "seller_id" +``` + +## Диаграмма версионирования + +```mermaid +flowchart TD + A[Чек Sales] --> B[Изменение чека] + B --> C[Создание SalesHistory] + C --> D[Копирование SalesProducts в SalesProductsHistory] + D --> E[sales_history_id = новый ID] + + subgraph История + F[SalesProductsHistory v1] + G[SalesProductsHistory v2] + H[SalesProductsHistory v3] + end + + E --> F + E --> G + E --> H +``` + +## Примеры использования + +### Получение истории товаров чека +```php +$historyProducts = SalesProductsHistory::find() + ->where(['check_id' => $checkGuid]) + ->orderBy(['sales_history_id' => SORT_DESC]) + ->all(); + +foreach ($historyProducts as $product) { + echo "{$product->product_id}: {$product->quantity} x {$product->price}\n"; +} +``` + +### Получение товаров конкретной версии чека +```php +$versionProducts = SalesProductsHistory::find() + ->where(['sales_history_id' => $historyId]) + ->all(); + +$total = 0; +foreach ($versionProducts as $product) { + $total += $product->summ; + echo "{$product->product_id}: {$product->summ} руб.\n"; +} +echo "Итого: {$total} руб."; +``` + +### Фильтрация по типу товара +```php +// Только матричные букеты +$bouquets = SalesProductsHistory::find() + ->where([ + 'sales_history_id' => $historyId, + 'type_id' => 2 + ]) + ->all(); +``` + +### Продажи флориста +```php +$floristSales = SalesProductsHistory::find() + ->where(['seller_id' => $floristGuid]) + ->andWhere(['>=', 'sales_history_id', $startHistoryId]) + ->sum('summ'); + +echo "Продажи флориста: {$floristSales} руб."; +``` + +### Компоненты комплекта +```php +// Получить комплект +$kit = SalesProductsHistory::find() + ->where([ + 'sales_history_id' => $historyId, + 'type_id' => 3 + ]) + ->one(); + +// Получить компоненты комплекта +$components = SalesProductsHistory::find() + ->where([ + 'sales_history_id' => $historyId, + 'component_parent_id' => $kit->product_id + ]) + ->all(); +``` + +### Сравнение версий чека +```php +$version1 = SalesProductsHistory::find() + ->where(['sales_history_id' => $historyId1]) + ->indexBy('product_id') + ->all(); + +$version2 = SalesProductsHistory::find() + ->where(['sales_history_id' => $historyId2]) + ->indexBy('product_id') + ->all(); + +// Найти изменения +foreach ($version2 as $productId => $v2Product) { + if (isset($version1[$productId])) { + $v1Product = $version1[$productId]; + if ($v1Product->quantity != $v2Product->quantity) { + echo "Изменено количество {$productId}: {$v1Product->quantity} -> {$v2Product->quantity}\n"; + } + } else { + echo "Добавлен товар: {$productId}\n"; + } +} +``` + +### Статистика по цветам +```php +$colorStats = SalesProductsHistory::find() + ->select(['color', 'SUM(quantity) as total_qty']) + ->where(['not', ['color' => null]]) + ->groupBy('color') + ->orderBy(['total_qty' => SORT_DESC]) + ->asArray() + ->all(); +``` + +### Общая сумма скидок +```php +$totalDiscount = SalesProductsHistory::find() + ->where(['sales_history_id' => $historyId]) + ->sum('discount'); + +echo "Общая скидка: {$totalDiscount} руб."; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `sales_history_id` | required, integer | +| `check_id` | required, string (max 36) | +| `product_id` | required, string (max 36) | +| `type_id` | integer | +| `component_parent_id` | string (max 36) | +| `seller_id` | string (max 36) | +| `assemble_id` | string (max 40) | +| `quantity` | required, number | +| `color` | string (max 145) | +| `price` | required, number | +| `discount` | required, number | +| `summ` | required, number | + +## Связанные модели + +- [SalesHistory](./SalesHistory.md) — история чеков +- [Sales](./Sales.md) — чеки продаж +- [SalesProducts](./SalesProducts.md) — текущие товары чеков +- [Products1c](./Products1c.md) — товары +- [Admin](./Admin.md) — флористы (seller_id) + +## Особенности реализации + +1. **Снимки данных**: Хранит исторические версии товарных позиций +2. **Связь с версией**: sales_history_id привязывает к конкретной версии чека +3. **Типизация товаров**: type_id для различения товаров, букетов и комплектов +4. **Иерархия**: component_parent_id для компонентов комплектов +5. **Учёт флориста**: seller_id для начисления зарплаты создателю букета +6. **Интеграция с 1С**: GUID для связи с товарами и сборками diff --git a/erp24/docs/models/SalesProductsHistorySearch.md b/erp24/docs/models/SalesProductsHistorySearch.md new file mode 100644 index 00000000..1ea34f7b --- /dev/null +++ b/erp24/docs/models/SalesProductsHistorySearch.md @@ -0,0 +1,192 @@ +# Класс: SalesProductsHistorySearch + + +## Mindmap + +```mermaid +mindmap + root((SalesProductsHistorySearch)) + Таблица БД + ActiveRecord + Наследование + extends SalesProductsHistory +``` + +## Назначение +Search-модель для поиска и фильтрации истории товаров в продажах ERP24. Стандартная Gii-модель для поиска по архивным данным товарных позиций в чеках. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`SalesProductsHistory` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `sales_history_id`, `type_id` — integer +- `check_id`, `product_id`, `component_parent_id`, `seller_id`, `assemble_id`, `color` — safe +- `quantity`, `price`, `discount`, `summ` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска истории товаров. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос SalesProductsHistory::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, sales_history_id, type_id, quantity, price, discount, summ + - like: check_id, product_id, component_parent_id, seller_id, assemble_id, color + +## Диаграмма связей + +```mermaid +erDiagram + SalesProductsHistory { + int id PK + int sales_history_id FK + varchar check_id + varchar product_id FK + varchar component_parent_id + varchar seller_id + varchar assemble_id + varchar color + int type_id + decimal quantity + decimal price + decimal discount + decimal summ + } + + SalesHistory { + int id PK + varchar guid + } + + Products1c { + varchar id PK + varchar name + } + + SalesProductsHistory }o--|| SalesHistory : "sales_history_id" + SalesProductsHistory }o--|| Products1c : "product_id" +``` + +## Диаграмма ценообразования + +```mermaid +flowchart LR + A[price] --> B[Цена за единицу] + C[quantity] --> D[Количество] + E[discount] --> F[Скидка] + + B --> G[summ = price * quantity - discount] + D --> G + F --> G +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SalesProductsHistorySearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по ID истории продажи +```php +$searchModel = new SalesProductsHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesProductsHistorySearch' => [ + 'sales_history_id' => 12345, + ] +]); +``` + +### Поиск по товару +```php +$searchModel = new SalesProductsHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesProductsHistorySearch' => [ + 'product_id' => 'abc-123-product', + ] +]); +``` + +### Поиск по цвету +```php +$searchModel = new SalesProductsHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesProductsHistorySearch' => [ + 'color' => 'красный', + ] +]); +``` + +### Поиск по сумме +```php +$searchModel = new SalesProductsHistorySearch(); +$dataProvider = $searchModel->search([ + 'SalesProductsHistorySearch' => [ + 'summ' => 2500, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'sales_history_id', + 'product_id', + 'quantity', + 'price:decimal', + 'discount:decimal', + 'summ:decimal', + 'color', + ], +]) ?> +``` + +## Связанные модели + +- [SalesProductsHistory](./SalesProductsHistory.md) — базовая модель +- [SalesHistory](./SalesHistory.md) — история продаж +- [Products1c](./Products1c.md) — товары 1С +- [SalesProducts](./SalesProducts.md) — текущие товары в продажах + +## Особенности реализации + +1. **Архивная таблица**: История товаров для аудита +2. **Компоненты букета**: component_parent_id для состава +3. **Сборка**: assemble_id для идентификации сборки +4. **Цвет**: color для характеристики товара +5. **like вместо ilike**: Регистрозависимый поиск +6. **Ценовая детализация**: price, quantity, discount, summ diff --git a/erp24/docs/models/SalesProductsUpdate.md b/erp24/docs/models/SalesProductsUpdate.md new file mode 100644 index 00000000..e044db35 --- /dev/null +++ b/erp24/docs/models/SalesProductsUpdate.md @@ -0,0 +1,275 @@ +# Класс: SalesProductsUpdate + + +## Mindmap + +```mermaid +mindmap + root((SalesProductsUpdate)) + Таблица БД + sales_products_update + Свойства + check_id + string + product_id + string + type_id + int + component_parent_id + string + quantity + float + color + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель обновлений товарных позиций чеков продаж в ERP24. Используется как промежуточная таблица для синхронизации изменений товаров в чеках с составным первичным ключом. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`sales_products_update` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `check_id` | varchar(36) | GUID чека из таблицы sales (часть PK) | +| `product_id` | varchar(36) | GUID товара из products_1c (часть PK) | +| `type_id` | int | Тип товара: 1=товар, 2=матричный букет, 3=комплект | +| `component_parent_id` | varchar(36) | GUID родительского товара (для комплектов) | +| `seller_id` | varchar(36) / null | GUID флориста | +| `quantity` | float | Количество | +| `color` | varchar(145) | Цвет (характеристика из 1С) | +| `price` | float | Цена за единицу | +| `discount` | float | Скидка в рублях | +| `summ` | float | Сумма позиции | + +## Составной первичный ключ + +Уникальная комбинация `[check_id, product_id]` — один товар может быть в чеке только один раз. + +## Типы товаров (type_id) + +| Значение | Описание | +|----------|----------| +| `1` | Обычный товар | +| `2` | Матричный букет | +| `3` | Комплект | + +## Диаграмма связей + +```mermaid +erDiagram + SalesProductsUpdate { + varchar check_id PK,FK + varchar product_id PK,FK + int type_id + varchar component_parent_id + varchar seller_id FK + float quantity + varchar color + float price + float discount + float summ + } + + Sales { + varchar guid PK + } + + Products1c { + varchar id PK + varchar name + } + + SalesProducts { + int id PK + varchar check_id FK + varchar product_id FK + } + + Sales ||--o{ SalesProductsUpdate : "check_id" + Products1c ||--o{ SalesProductsUpdate : "product_id" + SalesProducts ||--|| SalesProductsUpdate : "sync" +``` + +## Диаграмма процесса обновления + +```mermaid +flowchart TD + A[Изменение чека] --> B[Запись в SalesProductsUpdate] + B --> C[Триггер/Задача синхронизации] + C --> D{Запись существует
    в SalesProducts?} + D -->|Да| E[UPDATE SalesProducts] + D -->|Нет| F[INSERT SalesProducts] + E --> G[Удаление из SalesProductsUpdate] + F --> G + G --> H[Синхронизация завершена] +``` + +## Примеры использования + +### Создание записи обновления +```php +$update = new SalesProductsUpdate(); +$update->check_id = $checkGuid; +$update->product_id = $productGuid; +$update->type_id = 1; // Обычный товар +$update->quantity = 2; +$update->price = 500.00; +$update->discount = 50.00; +$update->summ = 950.00; +$update->save(); +``` + +### Получение ожидающих обновлений +```php +$pendingUpdates = SalesProductsUpdate::find() + ->orderBy(['check_id' => SORT_ASC]) + ->all(); + +echo "Ожидает обновления: " . count($pendingUpdates) . " позиций"; +``` + +### Обновление или создание позиции +```php +$update = SalesProductsUpdate::find() + ->where([ + 'check_id' => $checkGuid, + 'product_id' => $productGuid + ]) + ->one(); + +if (!$update) { + $update = new SalesProductsUpdate(); + $update->check_id = $checkGuid; + $update->product_id = $productGuid; +} + +$update->quantity = $newQuantity; +$update->price = $newPrice; +$update->discount = $newDiscount; +$update->summ = $newQuantity * $newPrice - $newDiscount; +$update->save(); +``` + +### Синхронизация с SalesProducts +```php +$updates = SalesProductsUpdate::find()->all(); + +foreach ($updates as $update) { + $salesProduct = SalesProducts::find() + ->where([ + 'check_id' => $update->check_id, + 'product_id' => $update->product_id + ]) + ->one(); + + if ($salesProduct) { + // Обновляем существующую позицию + $salesProduct->quantity = $update->quantity; + $salesProduct->price = $update->price; + $salesProduct->discount = $update->discount; + $salesProduct->summ = $update->summ; + $salesProduct->save(); + } else { + // Создаём новую позицию + $salesProduct = new SalesProducts(); + $salesProduct->check_id = $update->check_id; + $salesProduct->product_id = $update->product_id; + $salesProduct->type_id = $update->type_id; + $salesProduct->quantity = $update->quantity; + $salesProduct->price = $update->price; + $salesProduct->discount = $update->discount; + $salesProduct->summ = $update->summ; + $salesProduct->save(); + } + + // Удаляем обработанное обновление + $update->delete(); +} +``` + +### Получение обновлений для чека +```php +$checkUpdates = SalesProductsUpdate::find() + ->where(['check_id' => $checkGuid]) + ->all(); + +$total = 0; +foreach ($checkUpdates as $update) { + $total += $update->summ; + echo "{$update->product_id}: {$update->quantity} x {$update->price}\n"; +} +echo "Итого к обновлению: {$total} руб."; +``` + +### Удаление обновлений чека +```php +SalesProductsUpdate::deleteAll(['check_id' => $checkGuid]); +``` + +### Массовая вставка обновлений +```php +$updates = [ + ['check_id' => $checkGuid, 'product_id' => 'guid1', 'quantity' => 1, 'price' => 100, 'discount' => 0, 'summ' => 100], + ['check_id' => $checkGuid, 'product_id' => 'guid2', 'quantity' => 2, 'price' => 200, 'discount' => 20, 'summ' => 380], +]; + +foreach ($updates as $data) { + $update = new SalesProductsUpdate(); + $update->setAttributes($data); + $update->save(); +} +``` + +### Статистика ожидающих обновлений +```php +$stats = SalesProductsUpdate::find() + ->select(['COUNT(DISTINCT check_id) as checks', 'COUNT(*) as products']) + ->asArray() + ->one(); + +echo "Чеков к обновлению: {$stats['checks']}\n"; +echo "Позиций к обновлению: {$stats['products']}\n"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `check_id` | required, string (max 36) | +| `product_id` | required, string (max 36) | +| `type_id` | integer | +| `component_parent_id` | string (max 36) | +| `seller_id` | string (max 36) | +| `quantity` | required, number | +| `color` | string (max 145) | +| `price` | required, number | +| `discount` | required, number | +| `summ` | required, number | + +**Уникальное ограничение:** `[check_id, product_id]` + +## Связанные модели + +- [Sales](./Sales.md) — чеки продаж +- [SalesProducts](./SalesProducts.md) — основная таблица товаров чеков +- [Products1c](./Products1c.md) — товары + +## Особенности реализации + +1. **Составной PK**: check_id + product_id для уникальности +2. **Промежуточная таблица**: Используется для буферизации изменений +3. **Аналогичная структура**: Повторяет структуру SalesProducts +4. **Синхронизация**: Данные переносятся в SalesProducts и удаляются +5. **Типизация товаров**: type_id для различения товаров, букетов и комплектов +6. **GUID ключи**: Совместимость с 1С diff --git a/erp24/docs/models/SalesSearch.md b/erp24/docs/models/SalesSearch.md new file mode 100644 index 00000000..1e916230 --- /dev/null +++ b/erp24/docs/models/SalesSearch.md @@ -0,0 +1,215 @@ +# Класс: SalesSearch + + +## Mindmap + +```mermaid +mindmap + root((SalesSearch)) + Таблица БД + ActiveRecord + Наследование + extends Sales +``` + +## Назначение +Search-модель для поиска и фильтрации продаж в ERP24. Модель с JOIN к администратору и магазину, увеличенной пагинацией и фильтром по дате "от". + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Sales` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `date`, `operation`, `status`, `number`, `seller_id`, `store_id_1c`, `payments`, `pay_arr`, `sales_check`, `order_id`, `terminal_id`, `terminal`, `kkm_id`, `date_up` — safe +- `summ`, `skidka` — number +- `admin_id`, `store_id`, `phone`, `status_check`, `held`, `matrix` — integer + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к admin и store. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с joinWith для admin и store +2. Настраивает пагинацию: pageSize=100, скрытые параметры +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: summ, skidka, admin_id, store_id, phone, status_check, held, matrix, date_up + - **>=** для date (от указанной даты) + - like: id, operation, status, number, seller_id, store_id_1c, payments, pay_arr, sales_check, order_id, terminal_id, terminal, kkm_id + +## Диаграмма связей + +```mermaid +erDiagram + Sales { + int id PK + datetime date + decimal summ + decimal skidka + int admin_id FK + int store_id FK + int phone + varchar operation + varchar status + varchar number + varchar seller_id + varchar store_id_1c + varchar payments + varchar pay_arr + varchar sales_check + varchar order_id + varchar terminal_id + varchar terminal + varchar kkm_id + int status_check + int held + int matrix + datetime date_up + } + + Admin { + int id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + Sales }o--|| Admin : "admin_id" + Sales }o--|| CityStore : "store_id" +``` + +## Диаграмма фильтра по дате + +```mermaid +flowchart TD + A[Фильтр date] --> B{Условие} + B -->|>=| C[sales.date >= $this->date] + C --> D[Продажи от указанной даты] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SalesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск продаж от даты +```php +$searchModel = new SalesSearch(); +$dataProvider = $searchModel->search([ + 'SalesSearch' => [ + 'date' => '2024-01-01', // Продажи от этой даты + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new SalesSearch(); +$dataProvider = $searchModel->search([ + 'SalesSearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Поиск по кассиру +```php +$searchModel = new SalesSearch(); +$dataProvider = $searchModel->search([ + 'SalesSearch' => [ + 'admin_id' => 10, + ] +]); +``` + +### Поиск по номеру чека +```php +$searchModel = new SalesSearch(); +$dataProvider = $searchModel->search([ + 'SalesSearch' => [ + 'sales_check' => 'CHECK-12345', + ] +]); +``` + +### Поиск проведённых продаж +```php +$searchModel = new SalesSearch(); +$dataProvider = $searchModel->search([ + 'SalesSearch' => [ + 'held' => 1, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'date:datetime', + 'summ:decimal', + [ + 'attribute' => 'store_id', + 'value' => 'store.name', + ], + [ + 'attribute' => 'admin_id', + 'value' => 'admin.name', + ], + 'operation', + 'status', + 'sales_check', + ], +]) ?> +``` + +## Связанные модели + +- [Sales](./Sales.md) — базовая модель продаж +- [Admin](./Admin.md) — администраторы/кассиры +- [CityStore](./CityStore.md) — магазины +- [SalesProducts](./SalesProducts.md) — товары в чеке + +## Особенности реализации + +1. **joinWith**: Связи admin и store загружаются через JOIN +2. **Увеличенная пагинация**: pageSize=100 +3. **Скрытые параметры**: forcePageParam и pageSizeParam отключены +4. **Фильтр даты >=**: Поиск продаж от указанной даты, а не точное совпадение +5. **like вместо ilike**: Регистрозависимый поиск +6. **Алиас таблицы**: sales.* для избежания конфликтов с JOIN diff --git a/erp24/docs/models/SalesUpdate.md b/erp24/docs/models/SalesUpdate.md new file mode 100644 index 00000000..004780f2 --- /dev/null +++ b/erp24/docs/models/SalesUpdate.md @@ -0,0 +1,227 @@ +# Класс: SalesUpdate + + +## Mindmap + +```mermaid +mindmap + root((SalesUpdate)) + Таблица БД + sales_update + Свойства + id + string + date + string + operation + string + status + string + summ + float + skidka + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель обновлений чеков продаж в ERP24. Используется как промежуточная таблица для синхронизации чеков из 1С с основной таблицей Sales. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`sales_update` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | varchar(36) | GUID чека в 1С (PK) | +| `date` | datetime | Дата и время чека | +| `operation` | varchar(35) | Тип операции: Возврат или Продажа | +| `status` | varchar(45) | Статус чека текстом | +| `summ` | float | Сумма чека | +| `skidka` | float | Скидка чека | +| `number` | varchar(225) | Название чека в 1С | +| `admin_id` | int | FK на продавца (Admin) | +| `seller_id` | varchar(36) | GUID продавца в 1С | +| `store_id_1c` | varchar(36) | GUID магазина в 1С | +| `store_id` | int / null | FK на магазин ERP (CityStore) | +| `payments` | text | JSON массив информации о платежах | +| `pay_arr` | varchar(15) | ID типов платежей | +| `phone` | int / null | Телефон клиента бонусной программы | +| `sales_check` | varchar(36) | GUID чека возврата (если чек возвратный) | +| `order_id` | varchar(36) | Номер заказа с сайта | +| `terminal_id` | varchar(36) | GUID терминала из 1С | +| `terminal` | varchar(255) | Название кассы | +| `kkm_id` | varchar(36) | GUID ККМ из 1С | +| `status_check` | int | Статус подтверждения чека | +| `held` | int | Проведён | +| `matrix` | int | % матрицы (>0 = матричный букет) | +| `date_up` | datetime / null | Дата обновления расчёта | +| `delivery_date` | datetime / null | Дата доставки | +| `pickup` | int / null | Самовывоз | + +## Диаграмма связей + +```mermaid +erDiagram + SalesUpdate { + varchar id PK + datetime date + varchar operation + varchar status + float summ + float skidka + int admin_id FK + varchar store_id_1c + int store_id FK + text payments + varchar sales_check + varchar order_id + } + + Sales { + varchar guid PK + } + + Admin { + int id PK + } + + CityStore { + int id PK + } + + SalesUpdate ||--|| Sales : "sync" + Admin ||--o{ SalesUpdate : "admin_id" + CityStore ||--o{ SalesUpdate : "store_id" +``` + +## Диаграмма синхронизации + +```mermaid +flowchart TD + A[1С: Новый/изменённый чек] --> B[Выгрузка в SalesUpdate] + B --> C[Триггер синхронизации] + C --> D{Чек существует в Sales?} + D -->|Да| E[UPDATE Sales] + D -->|Нет| F[INSERT Sales] + E --> G[Удаление из SalesUpdate] + F --> G + G --> H[Синхронизация SalesProductsUpdate] +``` + +## Примеры использования + +### Получение ожидающих обновлений +```php +$pendingUpdates = SalesUpdate::find() + ->orderBy(['date' => SORT_ASC]) + ->all(); + +echo "Чеков к обновлению: " . count($pendingUpdates); +``` + +### Синхронизация с Sales +```php +$updates = SalesUpdate::find()->all(); + +foreach ($updates as $update) { + $sale = Sales::findOne($update->id); + + if ($sale) { + $sale->setAttributes($update->attributes); + $sale->save(); + } else { + $sale = new Sales(); + $sale->setAttributes($update->attributes); + $sale->guid = $update->id; + $sale->save(); + } + + $update->delete(); +} +``` + +### Фильтрация по типу операции +```php +// Только продажи +$sales = SalesUpdate::find() + ->where(['operation' => 'Продажа']) + ->all(); + +// Только возвраты +$returns = SalesUpdate::find() + ->where(['operation' => 'Возврат']) + ->all(); +``` + +### Поиск по магазину +```php +$storeUpdates = SalesUpdate::find() + ->where(['store_id' => $storeId]) + ->orWhere(['store_id_1c' => $store1cGuid]) + ->all(); +``` + +### Анализ платежей +```php +$update = SalesUpdate::findOne($checkId); + +if ($update && $update->payments) { + $payments = json_decode($update->payments, true); + + foreach ($payments as $payment) { + echo "Тип: {$payment['type']}, Сумма: {$payment['amount']}\n"; + } +} +``` + +### Статистика по статусам +```php +$statusStats = SalesUpdate::find() + ->select(['status', 'COUNT(*) as count']) + ->groupBy('status') + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `id` | required, string (max 36), unique | +| `date` | required, safe | +| `operation` | required, string (max 35) | +| `status` | required, string (max 45) | +| `summ` | required, number | +| `skidka` | required, number | +| `number` | required, string (max 225) | +| `admin_id` | required, integer | +| `store_id_1c` | required, string (max 36) | +| `payments` | required, string | +| `held` | required, integer | + +**Уникальное ограничение:** `[date, operation, store_id_1c, id]` + +## Связанные модели + +- [Sales](./Sales.md) — основная таблица чеков +- [SalesProductsUpdate](./SalesProductsUpdate.md) — товары чеков для синхронизации +- [Admin](./Admin.md) — продавцы +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Промежуточная таблица**: Буфер для синхронизации с 1С +2. **GUID первичный ключ**: Совместимость с идентификаторами 1С +3. **JSON платежи**: Гибкое хранение информации о способах оплаты +4. **Двойная привязка к магазину**: store_id (ERP) и store_id_1c (1С) +5. **Признак матричного букета**: matrix > 0 указывает на матричный заказ +6. **Возвратные чеки**: sales_check содержит GUID оригинального чека diff --git a/erp24/docs/models/SalesWriteOffsPlan.md b/erp24/docs/models/SalesWriteOffsPlan.md new file mode 100644 index 00000000..70cc6457 --- /dev/null +++ b/erp24/docs/models/SalesWriteOffsPlan.md @@ -0,0 +1,258 @@ +# Класс: SalesWriteOffsPlan + + +## Mindmap + +```mermaid +mindmap + root((SalesWriteOffsPlan)) + Таблица БД + sales_write_offs_plan + Свойства + id + int + year + int + month + int + store_id + int + created_at + string + updated_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель планов продаж и списаний по магазинам в ERP24. Хранит помесячные планы по различным каналам продаж: офлайн, онлайн-магазин, маркетплейс и плановые списания. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`sales_write_offs_plan` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Описание | +|-----------|----------| +| `TimestampBehavior` | Автоматическое заполнение created_at/updated_at | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `year` | int | Год плана | +| `month` | int | Месяц плана (1-12) | +| `store_id` | int | FK на магазин (CityStore) | +| `total_sales_plan` | float / null | Общий план продаж | +| `write_offs_plan` | float / null | План списаний | +| `offline_sales_plan` | float / null | План офлайн-продаж | +| `online_sales_shop_plan` | float / null | План онлайн-продаж магазина | +| `online_sales_marketplace_plan` | float / null | План продаж на маркетплейсе | +| `created_at` | datetime | Дата создания | +| `updated_at` | datetime | Дата обновления | +| `created_by` | int | FK на создателя (Admin) | +| `updated_by` | int | FK на редактора (Admin) | + +## Диаграмма связей + +```mermaid +erDiagram + SalesWriteOffsPlan { + int id PK + int year + int month + int store_id FK + float total_sales_plan + float write_offs_plan + float offline_sales_plan + float online_sales_shop_plan + float online_sales_marketplace_plan + datetime created_at + datetime updated_at + int created_by FK + int updated_by FK + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + CityStore ||--o{ SalesWriteOffsPlan : "store_id" + Admin ||--o{ SalesWriteOffsPlan : "created_by" + Admin ||--o{ SalesWriteOffsPlan : "updated_by" +``` + +## Диаграмма структуры плана + +```mermaid +flowchart TD + A[total_sales_plan
    Общий план продаж] --> B[offline_sales_plan
    Офлайн продажи] + A --> C[online_sales_shop_plan
    Интернет-магазин] + A --> D[online_sales_marketplace_plan
    Маркетплейсы] + + E[write_offs_plan
    План списаний] --> F[Брак] + E --> G[Утилизация] + E --> H[Прочие потери] +``` + +## Примеры использования + +### Создание плана на месяц +```php +$plan = new SalesWriteOffsPlan(); +$plan->year = 2024; +$plan->month = 12; +$plan->store_id = $storeId; +$plan->total_sales_plan = 1000000; +$plan->write_offs_plan = 50000; +$plan->offline_sales_plan = 600000; +$plan->online_sales_shop_plan = 300000; +$plan->online_sales_marketplace_plan = 100000; +$plan->created_by = Yii::$app->user->id; +$plan->updated_by = Yii::$app->user->id; +$plan->save(); +``` + +### Получение плана магазина на месяц +```php +$plan = SalesWriteOffsPlan::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->one(); + +if ($plan) { + echo "План продаж: {$plan->total_sales_plan} руб.\n"; + echo "План списаний: {$plan->write_offs_plan} руб.\n"; +} +``` + +### Суммарный план по всем магазинам +```php +$totalPlan = SalesWriteOffsPlan::find() + ->select([ + 'SUM(total_sales_plan) as total_sales', + 'SUM(write_offs_plan) as total_writeoffs', + 'SUM(offline_sales_plan) as total_offline', + 'SUM(online_sales_shop_plan) as total_online_shop', + 'SUM(online_sales_marketplace_plan) as total_marketplace' + ]) + ->where(['year' => 2024, 'month' => 12]) + ->asArray() + ->one(); + +echo "Общий план продаж: {$totalPlan['total_sales']} руб."; +``` + +### Сравнение каналов продаж +```php +$plans = SalesWriteOffsPlan::find() + ->where(['year' => 2024, 'month' => 12]) + ->all(); + +$channels = [ + 'offline' => 0, + 'online_shop' => 0, + 'marketplace' => 0 +]; + +foreach ($plans as $plan) { + $channels['offline'] += $plan->offline_sales_plan ?? 0; + $channels['online_shop'] += $plan->online_sales_shop_plan ?? 0; + $channels['marketplace'] += $plan->online_sales_marketplace_plan ?? 0; +} + +$total = array_sum($channels); +foreach ($channels as $channel => $sum) { + $percent = $total > 0 ? round($sum / $total * 100, 1) : 0; + echo "{$channel}: {$sum} руб. ({$percent}%)\n"; +} +``` + +### Динамика планов за год +```php +$yearPlans = SalesWriteOffsPlan::find() + ->where(['store_id' => $storeId, 'year' => 2024]) + ->orderBy(['month' => SORT_ASC]) + ->all(); + +foreach ($yearPlans as $plan) { + echo "{$plan->month}/2024: {$plan->total_sales_plan} руб.\n"; +} +``` + +### Обновление плана +```php +$plan = SalesWriteOffsPlan::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->one(); + +if ($plan) { + $plan->total_sales_plan = 1200000; + $plan->updated_by = Yii::$app->user->id; + $plan->save(); +} +``` + +### Расчёт % списаний от продаж +```php +$plans = SalesWriteOffsPlan::find() + ->where(['year' => 2024]) + ->all(); + +foreach ($plans as $plan) { + if ($plan->total_sales_plan > 0) { + $writeoffPercent = round($plan->write_offs_plan / $plan->total_sales_plan * 100, 2); + echo "Магазин {$plan->store_id}: списания {$writeoffPercent}%\n"; + } +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `year` | required, integer | +| `month` | required, integer | +| `store_id` | required, integer | +| `total_sales_plan` | number | +| `write_offs_plan` | number | +| `offline_sales_plan` | number | +| `online_sales_shop_plan` | number | +| `online_sales_marketplace_plan` | number | +| `created_by` | required, integer | +| `updated_by` | required, integer | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Помесячное планирование**: Планы задаются на каждый месяц отдельно +2. **Мультиканальность**: Разделение планов по каналам продаж +3. **План списаний**: Отдельный учёт планируемых потерь +4. **TimestampBehavior**: Автоматические временные метки +5. **Аудит**: Фиксация создателя и редактора плана +6. **Nullable суммы**: Планы могут быть не заданы (null) diff --git a/erp24/docs/models/SchedulerTask.md b/erp24/docs/models/SchedulerTask.md new file mode 100644 index 00000000..505b8191 --- /dev/null +++ b/erp24/docs/models/SchedulerTask.md @@ -0,0 +1,659 @@ +# Class: SchedulerTask + +## 🧠 Mindmap: Модель SchedulerTask + +```mermaid +mindmap + root((SchedulerTask)) + Идентификация + id PK автоинкремент + task_num уникальный номер + name имя задачи + alias алиас для поиска + Расписание + date_start дата начала + date_stop дата окончания + frequency_minuet частота в минутах + frequency_hour частота в часах + date_time время последнего запуска + Управление + active активна/неактивна + force_task принудительный запуск + access_from_db доступ из БД + start_count_by_day счетчик запусков + Описание + description описание задачи +``` + +--- + +## Назначение + +Модель задач планировщика (Scheduler) в системе ERP24. Хранит конфигурацию фоновых задач, которые выполняются по расписанию. Каждая задача имеет уникальный номер, настройки частоты запуска, период активности и счетчик запусков за день. + +Используется планировщиком для определения, какие задачи нужно запустить, с какой частотой и в какой период. Поддерживает гибкую настройку расписания через минуты и часы, а также возможность принудительного запуска. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`scheduler_task` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ, автоинкремент | +| `task_num` | int | **Уникальный номер задачи** (обязательное, уникальное) | +| `name` | string(100) | **Имя задачи** (человекочитаемое название) | +| `alias` | string(100) | **Алиас задачи** (для поиска и идентификации в коде) | + +### Расписание выполнения + +| Имя | Тип | Описание | +|-----|-----|----------| +| `date_start` | string(100) | **Дата начала активности** (формат даты) | +| `date_stop` | string(100) | **Дата окончания активности** (формат даты) | +| `frequency_minuet` | int | **Частота запуска в минутах** (null = не используется) | +| `frequency_hour` | int | **Частота запуска в часах** (null = не используется) | +| `date_time` | string(100) | **Время последнего запуска** (обязательное) | + +### Управление задачей + +| Имя | Тип | Описание | +|-----|-----|----------| +| `active` | int | **Активна ли задача**: 0 - нет, 1 - да | +| `force_task` | int | **Принудительный запуск**: 1 - запустить немедленно | +| `access_from_db` | int | **Доступ из БД**: можно ли управлять из интерфейса | +| `start_count_by_day` | int | **Счетчик запусков за день** | + +### Описание + +| Имя | Тип | Описание | +|-----|-----|----------| +| `description` | text | **Описание задачи** (что делает, для чего) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'task_num', // уникальный номер + 'date_time' // время последнего запуска +] +``` + +### Типы данных +```php +[ + 'task_num', + 'frequency_minuet', + 'frequency_hour', + 'force_task', + 'active', + 'access_from_db', + 'start_count_by_day' +] // integer + +['description'] // text + +[ + 'name', + 'date_start', + 'date_stop', + 'alias', + 'date_time' +] // string max:100 +``` + +### Уникальность +```php +['task_num'] // unique - каждая задача имеет уникальный номер +``` + +--- + +## Методы + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех свойств: + +#### getTaskNum() / setTaskNum(int $task_num) +**Описание:** Получение/установка уникального номера задачи + +**Логика работы:** +- `getTaskNum()`: возвращает значение поля task_num (int) +- `setTaskNum()`: устанавливает значение поля task_num, принимает int + +**Пример:** +```php +$task = new SchedulerTask(); +$task->setTaskNum(100); +echo $task->getTaskNum(); // 100 +``` + +--- + +#### getName() / setName(?string $name) +**Описание:** Получение/установка имени задачи + +**Логика работы:** +- `getName()`: возвращает значение поля name (string|null) +- `setName()`: устанавливает значение поля name, принимает string|null + +**Пример:** +```php +$task->setName('Daily import from 1C'); +echo $task->getName(); // "Daily import from 1C" +``` + +--- + +#### getDateStart() / setDateStart(?string $date_start) +**Описание:** Получение/установка даты начала активности + +**Логика работы:** +- `getDateStart()`: возвращает значение поля date_start (string|null) +- `setDateStart()`: устанавливает дату начала, принимает string|null + +**Пример:** +```php +$task->setDateStart('2025-01-01 00:00:00'); +echo $task->getDateStart(); // "2025-01-01 00:00:00" +``` + +--- + +#### getDateStop() / setDateStop(?string $date_stop) +**Описание:** Получение/установка даты окончания активности + +**Логика работы:** +- `getDateStop()`: возвращает значение поля date_stop (string|null) +- `setDateStop()`: устанавливает дату окончания, принимает string|null + +**Пример:** +```php +$task->setDateStop('2025-12-31 23:59:59'); +echo $task->getDateStop(); // "2025-12-31 23:59:59" +``` + +--- + +#### getFrequencyMinuet() / setFrequencyMinuet(?int $frequency_minuet) +**Описание:** Получение/установка частоты запуска в минутах + +**Логика работы:** +- `getFrequencyMinuet()`: возвращает частоту в минутах (int|null) +- `setFrequencyMinuet()`: устанавливает частоту, принимает int|null + +**Пример:** +```php +$task->setFrequencyMinuet(30); // каждые 30 минут +echo $task->getFrequencyMinuet(); // 30 +``` + +--- + +#### getFrequencyHour() / setFrequencyHour(?int $frequency_hour) +**Описание:** Получение/установка частоты запуска в часах + +**Логика работы:** +- `getFrequencyHour()`: возвращает частоту в часах (int|null) +- `setFrequencyHour()`: устанавливает частоту, принимает int|null + +**Пример:** +```php +$task->setFrequencyHour(2); // каждые 2 часа +echo $task->getFrequencyHour(); // 2 +``` + +--- + +#### getAlias() / setAlias(?string $alias) +**Описание:** Получение/установка алиаса задачи + +**Логика работы:** +- `getAlias()`: возвращает алиас (string|null) +- `setAlias()`: устанавливает алиас для идентификации в коде, принимает string|null + +**Пример:** +```php +$task->setAlias('import_1c_daily'); +echo $task->getAlias(); // "import_1c_daily" +``` + +--- + +#### getDescription() / setDescription(?string $description) +**Описание:** Получение/установка описания задачи + +**Логика работы:** +- `getDescription()`: возвращает описание (string|null) +- `setDescription()`: устанавливает текстовое описание, принимает string|null + +**Пример:** +```php +$task->setDescription('Ежедневный импорт заказов из 1С'); +echo $task->getDescription(); // "Ежедневный импорт заказов из 1С" +``` + +--- + +#### getForceTask() / setForceTask(?int $force_task) +**Описание:** Получение/установка флага принудительного запуска + +**Логика работы:** +- `getForceTask()`: возвращает флаг (int|null) +- `setForceTask()`: устанавливает флаг принудительного запуска (1 = запустить немедленно), принимает int|null + +**Пример:** +```php +$task->setForceTask(1); // принудительный запуск +echo $task->getForceTask(); // 1 +``` + +--- + +#### getActive() / setActive(int $active) +**Описание:** Получение/установка статуса активности + +**Логика работы:** +- `getActive()`: возвращает статус активности (int) +- `setActive()`: устанавливает активна ли задача (0/1), принимает int + +**Пример:** +```php +$task->setActive(1); // активировать задачу +echo $task->getActive(); // 1 +``` + +--- + +#### getAccessFromDb() / setAccessFromDb(int $access_from_db) +**Описание:** Получение/установка флага доступа из БД + +**Логика работы:** +- `getAccessFromDb()`: возвращает флаг доступа (int) +- `setAccessFromDb()`: определяет, можно ли управлять задачей через интерфейс БД, принимает int + +**Пример:** +```php +$task->setAccessFromDb(1); // разрешить управление +echo $task->getAccessFromDb(); // 1 +``` + +--- + +#### getStartCountByDay() / setStartCountByDay(int $start_count_by_day) +**Описание:** Получение/установка счетчика запусков за день + +**Логика работы:** +- `getStartCountByDay()`: возвращает количество запусков (int) +- `setStartCountByDay()`: устанавливает счетчик запусков за текущий день, принимает int + +**Пример:** +```php +$task->setStartCountByDay(5); // запускалась 5 раз сегодня +echo $task->getStartCountByDay(); // 5 +``` + +--- + +#### getDateTime() / setDateTime() +**Описание:** Получение/установка времени последнего запуска + +**Логика работы:** +- `getDateTime()`: возвращает время последнего запуска (string) +- `setDateTime()`: устанавливает текущее время как время последнего запуска (без параметров) + +**Особенность:** метод `setDateTime()` автоматически устанавливает текущее время через `date('Y-m-d H:i:s')` + +**Пример:** +```php +$task->setDateTime(); // устанавливает текущее время +echo $task->getDateTime(); // "2025-01-11 15:30:45" +``` + +--- + +### Стандартные методы ActiveRecord + +#### tableName() +**Тип:** `static` +**Возвращает:** `string` — 'scheduler_task' + +#### rules() +**Тип:** `public` +**Возвращает:** `array` — правила валидации + +#### attributeLabels() +**Тип:** `public` +**Возвращает:** `array` — метки атрибутов на английском + +--- + +## Примеры использования + +### Создание новой задачи + +```php +use yii_app\records\SchedulerTask; + +$task = new SchedulerTask(); +$task->setTaskNum(1001); +$task->setName('Import orders from 1C'); +$task->setAlias('import_1c_orders'); +$task->setDescription('Импорт заказов из 1С каждые 30 минут'); +$task->setFrequencyMinuet(30); +$task->setActive(1); +$task->setAccessFromDb(1); +$task->setStartCountByDay(0); +$task->setDateTime(); // текущее время +$task->save(); + +echo "Task created with ID: {$task->id}\n"; +``` + +### Получение активных задач + +```php +$activeTasks = SchedulerTask::find() + ->where(['active' => 1]) + ->orderBy(['task_num' => SORT_ASC]) + ->all(); + +echo "Active tasks:\n"; +foreach ($activeTasks as $task) { + echo "#{$task->getTaskNum()}: {$task->getName()}\n"; +} +``` + +### Проверка необходимости запуска задачи + +```php +$task = SchedulerTask::findOne(['task_num' => 1001]); + +if ($task->getActive()) { + $lastRun = strtotime($task->getDateTime()); + $now = time(); + $frequencySeconds = ($task->getFrequencyMinuet() ?? 0) * 60; + + if ($frequencySeconds > 0 && ($now - $lastRun) >= $frequencySeconds) { + echo "Task should run now\n"; + // Запуск задачи + $this->executeTask($task); + + // Обновление времени и счетчика + $task->setDateTime(); + $task->setStartCountByDay($task->getStartCountByDay() + 1); + $task->save(); + } +} +``` + +### Принудительный запуск задачи + +```php +// Установка флага принудительного запуска +$task = SchedulerTask::findOne(['alias' => 'import_1c_orders']); +$task->setForceTask(1); +$task->save(); + +echo "Task marked for forced execution\n"; + +// В планировщике +$forcedTasks = SchedulerTask::find() + ->where(['force_task' => 1, 'active' => 1]) + ->all(); + +foreach ($forcedTasks as $task) { + echo "Forcing task: {$task->getName()}\n"; + $this->executeTask($task); + + // Сброс флага после выполнения + $task->setForceTask(0); + $task->setDateTime(); + $task->save(); +} +``` + +### Сброс счетчика запусков в начале дня + +```php +// Cron задача в 00:00 +$tasks = SchedulerTask::find()->all(); + +foreach ($tasks as $task) { + $task->setStartCountByDay(0); + $task->save(); +} + +echo "Daily counters reset\n"; +``` + +### Деактивация задачи по расписанию + +```php +$task = SchedulerTask::findOne(['task_num' => 1001]); + +// Проверка даты окончания +if ($task->getDateStop()) { + $stopDate = strtotime($task->getDateStop()); + $now = time(); + + if ($now >= $stopDate) { + $task->setActive(0); + $task->save(); + echo "Task automatically deactivated (date_stop reached)\n"; + } +} +``` + +### Статистика запусков + +```php +$tasks = SchedulerTask::find() + ->where(['active' => 1]) + ->all(); + +echo "Task execution statistics:\n"; +foreach ($tasks as $task) { + echo "{$task->getName()}:\n"; + echo " Runs today: {$task->getStartCountByDay()}\n"; + echo " Last run: {$task->getDateTime()}\n"; + echo " Frequency: "; + + if ($task->getFrequencyMinuet()) { + echo "{$task->getFrequencyMinuet()} minutes\n"; + } elseif ($task->getFrequencyHour()) { + echo "{$task->getFrequencyHour()} hours\n"; + } else { + echo "Manual\n"; + } +} +``` + +### Поиск задачи по алиасу + +```php +$alias = 'import_1c_orders'; +$task = SchedulerTask::find() + ->where(['alias' => $alias]) + ->one(); + +if ($task) { + echo "Found task: {$task->getName()}\n"; + echo "Status: " . ($task->getActive() ? 'Active' : 'Inactive') . "\n"; + echo "Description: {$task->getDescription()}\n"; +} else { + echo "Task with alias '{$alias}' not found\n"; +} +``` + +### Обновление частоты запуска + +```php +$task = SchedulerTask::findOne(['task_num' => 1001]); + +// Изменение с 30 минут на 1 час +$task->setFrequencyMinuet(null); // убираем минуты +$task->setFrequencyHour(1); // устанавливаем часы +$task->save(); + +echo "Task frequency updated\n"; +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + SchedulerTask { + int id PK + int task_num UK "Unique task number" + string name "Task name" + string alias "Task alias" + string date_start "Start date" + string date_stop "Stop date" + int frequency_minuet "Minutes interval" + int frequency_hour "Hours interval" + text description "Task description" + int force_task "Force run flag" + int active "Active flag" + int access_from_db "DB access flag" + int start_count_by_day "Daily counter" + string date_time "Last run time" + } +``` + +--- + +## Бизнес-логика + +### Планирование задач + +Планировщик использует следующую логику: + +1. **Выборка активных задач**: `active = 1` +2. **Проверка периода активности**: текущее время между date_start и date_stop +3. **Проверка частоты**: время с последнего запуска >= frequency +4. **Принудительный запуск**: `force_task = 1` игнорирует частоту +5. **Выполнение**: запуск задачи +6. **Обновление**: date_time, start_count_by_day, сброс force_task + +### Типы расписаний + +**По минутам:** +```php +frequency_minuet = 15 // каждые 15 минут +frequency_hour = null +``` + +**По часам:** +```php +frequency_minuet = null +frequency_hour = 2 // каждые 2 часа +``` + +**Ручной запуск:** +```php +frequency_minuet = null +frequency_hour = null +force_task = 1 // только принудительно +``` + +### Управление активностью + +**Временная активность:** +```php +date_start = '2025-01-01 00:00:00' +date_stop = '2025-12-31 23:59:59' +active = 1 +``` + +**Постоянная задача:** +```php +date_start = null +date_stop = null +active = 1 +``` + +### Счетчик запусков + +Поле `start_count_by_day` используется для: +- Мониторинга активности задач +- Ограничения количества запусков за день +- Диагностики проблем (если счетчик слишком большой/маленький) + +--- + +## Связи с другими моделями + +### Логическая связь +- **SchedulerTaskLog** — логи выполнения задач (через task_num) + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (id) + +-- Уникальный номер задачи +UNIQUE INDEX uk_scheduler_task_num ON scheduler_task(task_num); + +-- Поиск активных задач +CREATE INDEX idx_scheduler_task_active ON scheduler_task(active); + +-- Поиск по алиасу +CREATE INDEX idx_scheduler_task_alias ON scheduler_task(alias); + +-- Поиск задач для принудительного запуска +CREATE INDEX idx_scheduler_task_force ON scheduler_task(force_task, active); +``` + +--- + +## Замечания + +1. **task_num** — уникальный номер, основной идентификатор задачи +2. **Частота** — используется либо minutes, либо hours, не оба одновременно +3. **date_time** — обновляется после каждого запуска +4. **start_count_by_day** — сбрасывается в 00:00 каждый день +5. **force_task** — игнорирует расписание и запускает немедленно +6. **access_from_db** — определяет возможность управления через интерфейс +7. **Геттеры/сеттеры** — полный набор для всех свойств +8. **setDateTime()** — автоматически устанавливает текущее время +9. **date_start/date_stop** — опциональны, для временных задач +10. **active** — главный переключатель вкл/выкл задачи + +--- + +## Связанные документы + +- [SchedulerTaskLog.md](./SchedulerTaskLog.md) — логи выполнения задач +- [ApiCron.md](./ApiCron.md) — задачи Cron для API +- [ScriptLauncherLog.md](./ScriptLauncherLog.md) — логи запуска скриптов diff --git a/erp24/docs/models/SchedulerTaskCounter.md b/erp24/docs/models/SchedulerTaskCounter.md new file mode 100644 index 00000000..97c827b8 --- /dev/null +++ b/erp24/docs/models/SchedulerTaskCounter.md @@ -0,0 +1,264 @@ +# Модель SchedulerTaskCounter + + +## Mindmap + +```mermaid +mindmap + root((SchedulerTaskCounter)) + Таблица БД + scheduler_task_counter + Свойства + id + int + task_id + int + date + string + year + int + month + int + value + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `SchedulerTaskCounter` хранит агрегированные счётчики выполнения задач планировщика по месяцам. Фиксирует количество запусков и связь с последним логом. Используется для построения отчётов и мониторинга активности cron-задач. + +**Файл модели:** `erp24/records/SchedulerTaskCounter.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `scheduler_task_counter` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `task_id` | INTEGER | ID задачи (FK → scheduler_task.id) | +| `date` | VARCHAR(100) | Период в формате YYYY-MM | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `value` | INTEGER | Количество выполнений | +| `last_update` | VARCHAR(100) | Время последнего обновления | +| `last_lod_id` | INTEGER | ID последнего лога | + +--- + +## Описание полей + +### `date` — Период + +Формируется автоматически из `year` и `month` в формате `YYYY-MM` (например, `2025-12`). + +### `value` — Счётчик + +Количество успешных выполнений задачи за указанный месяц. + +### `last_lod_id` — Последний лог + +Ссылка на запись в `scheduler_task_log` для быстрого доступа к последнему результату. + +--- + +## Методы модели + +| Метод | Описание | +|-------|----------| +| `setTaskId(int $task_id)` | Устанавливает ID задачи | +| `setDate()` | Формирует `date` из `year` и `month` | +| `setYear(int $year)` | Устанавливает год | +| `setMonth(int $month)` | Устанавливает месяц | +| `setValue(int $value)` | Устанавливает счётчик | +| `setLastUpdate()` | Устанавливает текущее время | +| `setLastLodId(int $last_lod_id)` | Устанавливает ID последнего лога | + +--- + +## Примеры использования + +### Создание/обновление счётчика + +```php +$taskId = 5; +$year = (int) date('Y'); +$month = (int) date('n'); + +$counter = SchedulerTaskCounter::findOne([ + 'task_id' => $taskId, + 'year' => $year, + 'month' => $month +]); + +if (!$counter) { + $counter = new SchedulerTaskCounter(); + $counter->setTaskId($taskId) + ->setYear($year) + ->setMonth($month) + ->setValue(0) + ->setDate(); +} + +$counter->setValue($counter->value + 1) + ->setLastUpdate() + ->setLastLodId($logId); +$counter->save(); +``` + +### Получение статистики по задаче + +```php +$taskId = 5; + +$stats = SchedulerTaskCounter::find() + ->where(['task_id' => $taskId]) + ->orderBy(['year' => SORT_DESC, 'month' => SORT_DESC]) + ->limit(12) + ->all(); + +echo "Статистика за последние месяцы:\n"; +foreach ($stats as $stat) { + echo "{$stat->date}: {$stat->value} запусков\n"; +} +``` + +### Общая статистика за период + +```php +$year = 2025; + +$total = SchedulerTaskCounter::find() + ->where(['year' => $year]) + ->sum('value'); + +echo "Всего выполнений за {$year} год: {$total}"; +``` + +### Статистика по всем задачам + +```php +$stats = SchedulerTaskCounter::find() + ->alias('stc') + ->innerJoin('scheduler_task st', 'st.id = stc.task_id') + ->select([ + 'st.name', + 'SUM(stc.value) as total', + 'MAX(stc.last_update) as last_run' + ]) + ->groupBy('stc.task_id') + ->orderBy(['total' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['total']} (посл.: {$stat['last_run']})\n"; +} +``` + +### Очистка старых данных + +```php +$oldYear = date('Y') - 2; + +$deleted = SchedulerTaskCounter::deleteAll([ + '<', 'year', $oldYear +]); + +echo "Удалено {$deleted} записей старше {$oldYear} года"; +``` + +### Инициализация счётчика при первом запуске + +```php +function incrementTaskCounter(int $taskId, int $logId): void +{ + $year = (int) date('Y'); + $month = (int) date('n'); + + $counter = SchedulerTaskCounter::findOne([ + 'task_id' => $taskId, + 'year' => $year, + 'month' => $month + ]); + + if ($counter) { + $counter->setValue($counter->value + 1); + } else { + $counter = new SchedulerTaskCounter(); + $counter->setTaskId($taskId) + ->setYear($year) + ->setMonth($month) + ->setValue(1) + ->setDate(); + } + + $counter->setLastUpdate() + ->setLastLodId($logId); + $counter->save(); +} +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + scheduler_task_counter }o--|| scheduler_task : "belongs_to" + scheduler_task_counter }o--o| scheduler_task_log : "last_log" + + scheduler_task_counter { + int id PK + int task_id FK + string date + int year + int month + int value + string last_update + int last_lod_id FK + } + + scheduler_task { + int id PK + string name + string alias + } + + scheduler_task_log { + int id PK + int task_num + string result + } +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `task_id` | Обязательное, целое число | +| `date` | Обязательное | +| `year` | Обязательное, целое число | +| `month` | Обязательное, целое число | +| `value` | Обязательное, целое число | +| `last_update` | Обязательное | +| `last_lod_id` | Обязательное, целое число | + +--- + +## Связанные модели + +- **[SchedulerTask](./SchedulerTask.md)** — задачи планировщика +- **[SchedulerTaskLog](./SchedulerTaskLog.md)** — логи выполнения + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/SchedulerTaskLog.md b/erp24/docs/models/SchedulerTaskLog.md new file mode 100644 index 00000000..09e80dad --- /dev/null +++ b/erp24/docs/models/SchedulerTaskLog.md @@ -0,0 +1,723 @@ +# Class: SchedulerTaskLog + +## 🧠 Mindmap: Модель SchedulerTaskLog + +```mermaid +mindmap + root((SchedulerTaskLog)) + Идентификация + id PK автоинкремент + task_num номер задачи FK + Задача + name имя задачи + alias алиас + description описание + Выполнение + date_start время начала + date_stop время окончания + info дополнительная информация + Результаты + result текстовый результат + result_number числовой результат + log детальный лог + error ошибки + Временные метки + date дата создания лога +``` + +--- + +## Назначение + +Модель логирования выполнения задач планировщика в системе ERP24. Сохраняет детальную информацию о каждом запуске задачи: время начала и окончания, результаты выполнения, ошибки и дополнительные метаданные. + +Используется для мониторинга работы планировщика, диагностики проблем, анализа производительности задач и аудита выполненных операций. Каждый запуск задачи создает отдельную запись лога. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`scheduler_task_log` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ, автоинкремент | +| `task_num` | int | **Номер задачи** (логическая связь с scheduler_task, обязательное) | + +### Данные задачи + +| Имя | Тип | Описание | +|-----|-----|----------| +| `name` | string(100) | **Имя задачи** (копируется из scheduler_task) | +| `alias` | string(100) | **Алиас задачи** (для идентификации) | +| `description` | text | **Описание задачи** | + +### Время выполнения + +| Имя | Тип | Описание | +|-----|-----|----------| +| `date_start` | string(100) | **Время начала выполнения** | +| `date_stop` | string(100) | **Время окончания выполнения** | +| `date` | string(100) | **Дата создания лога** (обязательное) | + +### Результаты выполнения + +| Имя | Тип | Описание | +|-----|-----|----------| +| `result` | text | **Текстовый результат** выполнения | +| `result_number` | float | **Числовой результат** (количество обработанных записей, сумма и т.д.) | +| `error` | text | **Сообщения об ошибках** | +| `log` | text | **Детальный лог выполнения** | + +### Дополнительная информация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `info` | text | **Дополнительная информация** (JSON, метаданные) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'task_num', // номер задачи + 'date' // дата создания +] +``` + +### Типы данных +```php +['task_num'] // integer +['result_number'] // number (float) +['description', 'info', 'result', 'error', 'log'] // text +['date_start', 'date_stop', 'name', 'alias', 'date'] // string max:100 +``` + +--- + +## Методы + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех свойств, все методы возвращают `$this` для поддержки method chaining: + +#### getTaskNum() / setTaskNum(int $task_num) +**Описание:** Получение/установка номера задачи + +**Логика работы:** +- `getTaskNum()`: возвращает значение поля task_num (int) +- `setTaskNum()`: устанавливает номер задачи, возвращает $this для chaining + +**Пример:** +```php +$log = new SchedulerTaskLog(); +$log->setTaskNum(1001)->setName('Import Task'); +echo $log->getTaskNum(); // 1001 +``` + +--- + +#### getDateStart() / setDateStart(?string $date_start) +**Описание:** Получение/установка времени начала выполнения + +**Логика работы:** +- `getDateStart()`: возвращает время начала (string|null) +- `setDateStart()`: устанавливает время начала, возвращает $this + +**Пример:** +```php +$log->setDateStart(date('Y-m-d H:i:s')); +echo $log->getDateStart(); // "2025-01-11 15:30:00" +``` + +--- + +#### getDateStop() / setDateStop(?string $date_stop) +**Описание:** Получение/установка времени окончания выполнения + +**Логика работы:** +- `getDateStop()`: возвращает время окончания (string|null) +- `setDateStop()`: устанавливает время окончания, возвращает $this + +**Пример:** +```php +$log->setDateStop(date('Y-m-d H:i:s')); +echo $log->getDateStop(); // "2025-01-11 15:35:00" +``` + +--- + +#### getName() / setName(?string $name) +**Описание:** Получение/установка имени задачи + +**Логика работы:** +- `getName()`: возвращает имя (string|null) +- `setName()`: устанавливает имя, возвращает $this + +**Пример:** +```php +$log->setName('Daily data import'); +echo $log->getName(); // "Daily data import" +``` + +--- + +#### getAlias() / setAlias(?string $alias) +**Описание:** Получение/установка алиаса задачи + +**Логика работы:** +- `getAlias()`: возвращает алиас (string|null) +- `setAlias()`: устанавливает алиас, возвращает $this + +**Пример:** +```php +$log->setAlias('import_daily'); +echo $log->getAlias(); // "import_daily" +``` + +--- + +#### getDescription() / setDescription(?string $description) +**Описание:** Получение/установка описания задачи + +**Логика работы:** +- `getDescription()`: возвращает описание (string|null) +- `setDescription()`: устанавливает описание, возвращает $this + +**Пример:** +```php +$log->setDescription('Импорт данных из 1С'); +echo $log->getDescription(); +``` + +--- + +#### getResult() / setResult(?string $result) +**Описание:** Получение/установка текстового результата + +**Логика работы:** +- `getResult()`: возвращает результат (string|null) +- `setResult()`: устанавливает текстовый результат выполнения, возвращает $this + +**Пример:** +```php +$log->setResult('Successfully imported 150 records'); +echo $log->getResult(); +``` + +--- + +#### getResultNumber() / setResultNumber(?float $result_number) +**Описание:** Получение/установка числового результата + +**Логика работы:** +- `getResultNumber()`: возвращает числовое значение (float|null) +- `setResultNumber()`: устанавливает числовой результат (количество обработанных записей, сумма и т.д.), возвращает $this + +**Пример:** +```php +$log->setResultNumber(150.5); +echo $log->getResultNumber(); // 150.5 +``` + +--- + +#### getError() / setError(?string $error) +**Описание:** Получение/установка сообщений об ошибках + +**Логика работы:** +- `getError()`: возвращает текст ошибок (string|null) +- `setError()`: устанавливает сообщения об ошибках, возвращает $this + +**Пример:** +```php +$log->setError('Database connection timeout'); +echo $log->getError(); +``` + +--- + +#### getInfo() / setInfo(string $info) +**Описание:** Получение/установка дополнительной информации + +**Логика работы:** +- `getInfo()`: возвращает дополнительную информацию (string|null) +- `setInfo()`: устанавливает доп. информацию (обычно JSON), возвращает $this + +**Особенность:** в PHPDoc возвращаемый тип указан как string|null, но параметр setInfo не nullable + +**Пример:** +```php +$info = json_encode(['total' => 150, 'errors' => 2, 'skipped' => 5]); +$log->setInfo($info); +echo $log->getInfo(); +``` + +--- + +#### getLog() / setLog(?string $log) +**Описание:** Получение/установка детального лога + +**Логика работы:** +- `getLog()`: возвращает детальный лог (string|null) +- `setLog()`: устанавливает полный лог выполнения, возвращает $this + +**Пример:** +```php +$detailedLog = "Step 1: Connect to DB\nStep 2: Fetch data\nStep 3: Process"; +$log->setLog($detailedLog); +echo $log->getLog(); +``` + +--- + +#### getDate() / setDate(string $date) +**Описание:** Получение/установка даты создания лога + +**Логика работы:** +- `getDate()`: возвращает дату создания (string) +- `setDate()`: устанавливает дату создания, возвращает $this + +**Пример:** +```php +$log->setDate(date('Y-m-d H:i:s')); +echo $log->getDate(); +``` + +--- + +### Стандартные методы ActiveRecord + +#### tableName() +**Тип:** `static` +**Возвращает:** `string` — 'scheduler_task_log' + +#### rules() +**Тип:** `public` +**Возвращает:** `array` — правила валидации + +#### attributeLabels() +**Тип:** `public` +**Возвращает:** `array` — метки атрибутов на английском + +--- + +## Примеры использования + +### Создание лога при запуске задачи + +```php +use yii_app\records\SchedulerTaskLog; +use yii_app\records\SchedulerTask; + +$task = SchedulerTask::findOne(['task_num' => 1001]); + +// Создаем запись лога +$log = new SchedulerTaskLog(); +$log->setTaskNum($task->getTaskNum()) + ->setName($task->getName()) + ->setAlias($task->getAlias()) + ->setDescription($task->getDescription()) + ->setDateStart(date('Y-m-d H:i:s')) + ->setDate(date('Y-m-d H:i:s')) + ->save(); + +echo "Log created with ID: {$log->id}\n"; + +// Выполнение задачи... +try { + $result = $this->executeTask($task); + $processedCount = count($result); + + // Обновление лога после выполнения + $log->setDateStop(date('Y-m-d H:i:s')) + ->setResult("Successfully processed {$processedCount} items") + ->setResultNumber($processedCount) + ->save(); + +} catch (\Exception $e) { + // Логирование ошибки + $log->setDateStop(date('Y-m-d H:i:s')) + ->setError($e->getMessage()) + ->setLog($e->getTraceAsString()) + ->save(); +} +``` + +### Детальное логирование с этапами + +```php +$log = new SchedulerTaskLog(); +$log->setTaskNum(1001) + ->setName('Import orders') + ->setDateStart(date('Y-m-d H:i:s')) + ->setDate(date('Y-m-d H:i:s')); + +$detailedLog = []; + +// Этап 1 +$detailedLog[] = "[" . date('H:i:s') . "] Starting import"; +$log->setLog(implode("\n", $detailedLog))->save(); + +// Этап 2 +$detailedLog[] = "[" . date('H:i:s') . "] Connecting to 1C API"; +$log->setLog(implode("\n", $detailedLog))->save(); + +// Этап 3 +$detailedLog[] = "[" . date('H:i:s') . "] Fetching orders"; +$orders = $this->fetch1COrders(); +$detailedLog[] = "[" . date('H:i:s') . "] Fetched " . count($orders) . " orders"; +$log->setLog(implode("\n", $detailedLog))->save(); + +// Завершение +$log->setDateStop(date('Y-m-d H:i:s')) + ->setResult('Import completed') + ->setResultNumber(count($orders)) + ->save(); +``` + +### Получение логов задачи за период + +```php +$taskNum = 1001; +$dateFrom = date('Y-m-d', strtotime('-7 days')); + +$logs = SchedulerTaskLog::find() + ->where(['task_num' => $taskNum]) + ->andWhere(['>=', 'date', $dateFrom]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +echo "Logs for task {$taskNum} (last 7 days):\n"; +foreach ($logs as $log) { + $duration = 'N/A'; + if ($log->getDateStart() && $log->getDateStop()) { + $start = strtotime($log->getDateStart()); + $stop = strtotime($log->getDateStop()); + $duration = round(($stop - $start), 2) . 's'; + } + + echo "[{$log->getDate()}] "; + echo "Duration: {$duration}, "; + echo "Result: " . ($log->getResult() ?: 'N/A') . ", "; + echo "Records: " . ($log->getResultNumber() ?: 0) . "\n"; + + if ($log->getError()) { + echo " ERROR: {$log->getError()}\n"; + } +} +``` + +### Поиск логов с ошибками + +```php +$errorLogs = SchedulerTaskLog::find() + ->where(['not', ['error' => null]]) + ->andWhere(['<>', 'error', '']) + ->orderBy(['date' => SORT_DESC]) + ->limit(20) + ->all(); + +echo "Recent errors:\n"; +foreach ($errorLogs as $log) { + echo "Task: {$log->getName()} (#{$log->getTaskNum()})\n"; + echo "Date: {$log->getDate()}\n"; + echo "Error: {$log->getError()}\n\n"; +} +``` + +### Статистика выполнения задачи + +```php +$taskNum = 1001; +$logs = SchedulerTaskLog::find() + ->where(['task_num' => $taskNum]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-30 days'))]) + ->all(); + +$totalRuns = count($logs); +$successfulRuns = 0; +$failedRuns = 0; +$totalDuration = 0; +$totalProcessed = 0; + +foreach ($logs as $log) { + if ($log->getError()) { + $failedRuns++; + } else { + $successfulRuns++; + } + + if ($log->getDateStart() && $log->getDateStop()) { + $duration = strtotime($log->getDateStop()) - strtotime($log->getDateStart()); + $totalDuration += $duration; + } + + if ($log->getResultNumber()) { + $totalProcessed += $log->getResultNumber(); + } +} + +echo "Task statistics (last 30 days):\n"; +echo "Total runs: {$totalRuns}\n"; +echo "Successful: {$successfulRuns}\n"; +echo "Failed: {$failedRuns}\n"; +echo "Success rate: " . round(($successfulRuns / $totalRuns) * 100, 2) . "%\n"; +echo "Average duration: " . round($totalDuration / $totalRuns, 2) . " seconds\n"; +echo "Total processed: {$totalProcessed} records\n"; +echo "Average per run: " . round($totalProcessed / $totalRuns, 2) . " records\n"; +``` + +### Очистка старых логов + +```php +// Удаление логов старше 90 дней +$date = date('Y-m-d', strtotime('-90 days')); +$deleted = SchedulerTaskLog::deleteAll(['<', 'date', $date]); + +echo "Deleted {$deleted} old log records\n"; +``` + +### Экспорт логов в отчет + +```php +$taskNum = 1001; +$logs = SchedulerTaskLog::find() + ->where(['task_num' => $taskNum]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-1 day'))]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$report = "Task Execution Report\n"; +$report .= "Task: " . ($logs[0] ? $logs[0]->getName() : 'N/A') . "\n"; +$report .= "Period: Last 24 hours\n"; +$report .= "Total runs: " . count($logs) . "\n\n"; + +foreach ($logs as $log) { + $report .= str_repeat("=", 70) . "\n"; + $report .= "Run Date: {$log->getDate()}\n"; + $report .= "Start: {$log->getDateStart()}\n"; + $report .= "Stop: {$log->getDateStop()}\n"; + $report .= "Result: {$log->getResult()}\n"; + $report .= "Processed: {$log->getResultNumber()} records\n"; + + if ($log->getError()) { + $report .= "\nERROR:\n{$log->getError()}\n"; + } + + if ($log->getLog()) { + $report .= "\nDetailed Log:\n{$log->getLog()}\n"; + } + + $report .= "\n"; +} + +file_put_contents("task_{$taskNum}_report.txt", $report); +echo "Report exported\n"; +``` + +### Логирование с метаданными в info + +```php +$log = new SchedulerTaskLog(); +$log->setTaskNum(1001) + ->setName('Product sync') + ->setDateStart(date('Y-m-d H:i:s')) + ->setDate(date('Y-m-d H:i:s')); + +// Сохранение метаданных +$metadata = [ + 'source' => '1C', + 'categories' => ['flowers', 'gifts'], + 'api_version' => '2.0', + 'server' => gethostname() +]; +$log->setInfo(json_encode($metadata)); + +// После выполнения +$log->setDateStop(date('Y-m-d H:i:s')) + ->setResult('Sync completed') + ->setResultNumber(250) + ->save(); + +// Чтение метаданных +$info = json_decode($log->getInfo(), true); +echo "Source: {$info['source']}\n"; +echo "Categories: " . implode(', ', $info['categories']) . "\n"; +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + SchedulerTaskLog }o--|| SchedulerTask : "logs for task" + + SchedulerTaskLog { + int id PK + int task_num FK "Logical FK" + string name "Task name" + string alias "Task alias" + text description "Task description" + string date_start "Start time" + string date_stop "End time" + text result "Text result" + float result_number "Numeric result" + text error "Error messages" + text log "Detailed log" + text info "Additional info" + string date "Log creation date" + } + + SchedulerTask { + int id PK + int task_num UK + string name + string alias + int active + } +``` + +--- + +## Бизнес-логика + +### Жизненный цикл лога + +1. **Создание**: при запуске задачи создается запись с date_start и date +2. **Обновление**: во время выполнения может обновляться log и info +3. **Завершение**: заполняется date_stop, result, result_number +4. **Ошибка**: заполняется error и log (stack trace) +5. **Архивация**: старые логи периодически удаляются + +### Использование полей + +**result** (text): +- Текстовое описание результата +- Сообщения об успехе/неудаче +- Краткая сводка + +**result_number** (float): +- Количество обработанных записей +- Суммы (финансовые операции) +- Проценты выполнения +- Любые числовые метрики + +**error** (text): +- Сообщения об ошибках +- Exception messages +- Краткое описание проблемы + +**log** (text): +- Детальный лог выполнения +- Stack traces +- Пошаговое описание +- Debug информация + +**info** (text): +- JSON с метаданными +- Параметры запуска +- Конфигурация +- Контекст выполнения + +### Method Chaining + +Все сеттеры возвращают `$this`, что позволяет использовать fluent interface: + +```php +$log->setTaskNum(1001) + ->setName('Import') + ->setDateStart(date('Y-m-d H:i:s')) + ->setResult('Success') + ->setResultNumber(100) + ->save(); +``` + +--- + +## Связи с другими моделями + +### Логическая связь +- **SchedulerTask** — задача, для которой создан лог (через task_num) + +**Примечание:** внешний ключ не объявлен в модели, связь только логическая + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +PRIMARY KEY (id) + +-- Поиск логов по задаче +CREATE INDEX idx_scheduler_task_log_task_num ON scheduler_task_log(task_num); + +-- Поиск по дате +CREATE INDEX idx_scheduler_task_log_date ON scheduler_task_log(date); + +-- Поиск логов с ошибками +CREATE INDEX idx_scheduler_task_log_error ON scheduler_task_log(error) WHERE error IS NOT NULL; + +-- Композитный для аналитики +CREATE INDEX idx_scheduler_task_log_task_date ON scheduler_task_log(task_num, date); +``` + +### Оптимизация запросов + +```php +// Плохо: загрузка всех логов с большими текстовыми полями +$logs = SchedulerTaskLog::find()->all(); + +// Хорошо: выборка только нужных полей +$logs = SchedulerTaskLog::find() + ->select(['id', 'task_num', 'name', 'date', 'result_number']) + ->where(['task_num' => 1001]) + ->all(); +``` + +--- + +## Замечания + +1. **task_num** — логическая связь с SchedulerTask (не foreign key) +2. **Method chaining** — все сеттеры возвращают $this +3. **Копирование данных** — name, alias, description копируются из task +4. **Числовой результат** — float для поддержки дробных значений +5. **info** — обычно хранится JSON с метаданными +6. **log** — может содержать большие тексты (stack traces) +7. **date** — обязательное поле, дата создания лога +8. **date_start/date_stop** — для вычисления времени выполнения +9. **Архивация** — требуется регулярная очистка старых записей +10. **Мониторинг** — используется для отслеживания здоровья планировщика + +--- + +## Связанные документы + +- [SchedulerTask.md](./SchedulerTask.md) — задачи планировщика +- [ApiCron.md](./ApiCron.md) — задачи Cron для API +- [ScriptLauncherLog.md](./ScriptLauncherLog.md) — логи запуска скриптов diff --git a/erp24/docs/models/SchedulerTaskLogSearch.md b/erp24/docs/models/SchedulerTaskLogSearch.md new file mode 100644 index 00000000..78762ccb --- /dev/null +++ b/erp24/docs/models/SchedulerTaskLogSearch.md @@ -0,0 +1,216 @@ +# Класс: SchedulerTaskLogSearch + + +## Mindmap + +```mermaid +mindmap + root((SchedulerTaskLogSearch)) + Таблица БД + ActiveRecord + Наследование + extends SchedulerTaskLog +``` + +## Назначение +Search-модель для поиска и фильтрации логов задач планировщика в ERP24. Модель с дополнительным свойством status для фильтрации по статусу выполнения (пропущено/все/выполнено). + +## Пространство имён +`yii_app\records` + +## Родительский класс +`SchedulerTaskLog` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$status` | int | Статус выполнения: -1 (пропущено), 0 (все), 1 (выполнено) | +| `$statusArray` | array | Справочник статусов для фильтра | + +## Справочник statusArray + +```php +[ + -1 => 'Пропущено', // date_start IS NULL AND date_stop IS NULL + 0 => 'Все', // Без фильтра по статусу + 1 => 'Выполнено' // date_start IS NOT NULL AND date_stop IS NOT NULL +] +``` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска с дефолтным status. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `task_num`, `status` — integer +- `date_start`, `date_stop`, `name`, `alias`, `description`, `result`, `error`, `log`, `date` — safe +- `status` default: 0 + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с фильтрацией по статусу выполнения. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос SchedulerTaskLog::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, task_num + - like: date_start, date_stop, name, alias, description, result, error, log, date +5. Дополнительная логика по status: + - status = -1: date_start IS NULL AND date_stop IS NULL (пропущенные) + - status = 1: date_start IS NOT NULL AND date_stop IS NOT NULL (выполненные) + - status = 0: без фильтра + +## Диаграмма логики статуса + +```mermaid +flowchart TD + A[status?] --> B{Значение} + + B -->|-1 Пропущено| C[date_start IS NULL] + C --> D[AND date_stop IS NULL] + + B -->|0 Все| E[Без фильтра] + + B -->|1 Выполнено| F[date_start IS NOT NULL] + F --> G[AND date_stop IS NOT NULL] +``` + +## Диаграмма структуры лога + +```mermaid +erDiagram + SchedulerTaskLog { + int id PK + int task_num FK + varchar name + varchar alias + varchar description + datetime date_start + datetime date_stop + varchar result + varchar error + text log + date date + } + + SchedulerTask { + int id PK + int task_num + varchar name + } + + SchedulerTaskLog }o--|| SchedulerTask : "task_num" +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SchedulerTaskLogSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск выполненных задач +```php +$searchModel = new SchedulerTaskLogSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskLogSearch' => [ + 'status' => 1, // Выполнено + ] +]); +``` + +### Поиск пропущенных задач +```php +$searchModel = new SchedulerTaskLogSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskLogSearch' => [ + 'status' => -1, // Пропущено + ] +]); +``` + +### Поиск по названию задачи +```php +$searchModel = new SchedulerTaskLogSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskLogSearch' => [ + 'name' => 'sync', + ] +]); +``` + +### Поиск по ошибке +```php +$searchModel = new SchedulerTaskLogSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskLogSearch' => [ + 'error' => 'timeout', + ] +]); +``` + +### GridView с выпадающим фильтром статуса +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'task_num', + 'name', + 'alias', + 'date_start:datetime', + 'date_stop:datetime', + [ + 'attribute' => 'status', + 'filter' => $searchModel->statusArray, + 'value' => function($model) use ($searchModel) { + if ($model->date_start === null && $model->date_stop === null) { + return 'Пропущено'; + } + return 'Выполнено'; + }, + ], + 'result', + 'error', + ], +]) ?> +``` + +## Связанные модели + +- [SchedulerTaskLog](./SchedulerTaskLog.md) — базовая модель логов +- [SchedulerTask](./SchedulerTask.md) — задачи планировщика + +## Особенности реализации + +1. **Виртуальный статус**: status определяется по наличию date_start/date_stop +2. **Справочник statusArray**: Для выпадающего фильтра +3. **Дефолтный статус**: 0 (все записи) +4. **Логика пропуска**: Если оба date_start и date_stop NULL — задача пропущена +5. **like вместо ilike**: Регистрозависимый поиск +6. **Логирование**: result, error, log для диагностики diff --git a/erp24/docs/models/SchedulerTaskSearch.md b/erp24/docs/models/SchedulerTaskSearch.md new file mode 100644 index 00000000..725f3721 --- /dev/null +++ b/erp24/docs/models/SchedulerTaskSearch.md @@ -0,0 +1,199 @@ +# Класс: SchedulerTaskSearch + + +## Mindmap + +```mermaid +mindmap + root((SchedulerTaskSearch)) + Таблица БД + ActiveRecord + Наследование + extends SchedulerTask +``` + +## Назначение +Search-модель для поиска и фильтрации задач планировщика в ERP24. Стандартная Gii-модель для управления CRON-задачами системы. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`SchedulerTask` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `task_num`, `frequency_minuet`, `frequency_hour`, `force_task`, `active`, `access_from_db`, `start_count_by_day` — integer +- `name`, `date_start`, `date_stop`, `alias`, `description`, `date_time` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска задач. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос SchedulerTask::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, task_num, frequency_minuet, frequency_hour, force_task, active, access_from_db, start_count_by_day + - like: name, date_start, date_stop, alias, description, date_time + +## Диаграмма структуры задачи + +```mermaid +erDiagram + SchedulerTask { + int id PK + int task_num + varchar name + varchar alias + varchar description + int frequency_minuet + int frequency_hour + datetime date_start + datetime date_stop + datetime date_time + int force_task + int active + int access_from_db + int start_count_by_day + } +``` + +## Диаграмма частоты выполнения + +```mermaid +flowchart TD + A[Планировщик] --> B{Частота} + + B --> C[frequency_minuet] + C --> D[Каждые N минут] + + B --> E[frequency_hour] + E --> F[Каждые N часов] + + G[Ограничения] --> H[date_start - начало] + G --> I[date_stop - окончание] + G --> J[start_count_by_day - лимит/день] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SchedulerTaskSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск активных задач +```php +$searchModel = new SchedulerTaskSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskSearch' => [ + 'active' => 1, + ] +]); +``` + +### Поиск по названию +```php +$searchModel = new SchedulerTaskSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskSearch' => [ + 'name' => 'sync', + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new SchedulerTaskSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskSearch' => [ + 'alias' => 'sync-1c', + ] +]); +``` + +### Поиск задач с принудительным запуском +```php +$searchModel = new SchedulerTaskSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskSearch' => [ + 'force_task' => 1, + ] +]); +``` + +### Поиск по частоте в минутах +```php +$searchModel = new SchedulerTaskSearch(); +$dataProvider = $searchModel->search([ + 'SchedulerTaskSearch' => [ + 'frequency_minuet' => 5, // Каждые 5 минут + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'task_num', + 'name', + 'alias', + 'frequency_minuet', + 'frequency_hour', + [ + 'attribute' => 'active', + 'value' => function($model) { + return $model->active ? 'Да' : 'Нет'; + }, + 'filter' => [0 => 'Нет', 1 => 'Да'], + ], + 'date_start:datetime', + 'date_stop:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [SchedulerTask](./SchedulerTask.md) — базовая модель задач +- [SchedulerTaskLog](./SchedulerTaskLog.md) — логи выполнения + +## Особенности реализации + +1. **Частота выполнения**: frequency_minuet, frequency_hour +2. **Принудительный запуск**: force_task для немедленного выполнения +3. **Активность**: active для включения/выключения +4. **Доступ из БД**: access_from_db для динамической конфигурации +5. **Лимит запусков**: start_count_by_day для ограничения +6. **Период действия**: date_start, date_stop +7. **like вместо ilike**: Регистрозависимый поиск diff --git a/erp24/docs/models/ScriptLauncherLog.md b/erp24/docs/models/ScriptLauncherLog.md new file mode 100644 index 00000000..5696a82e --- /dev/null +++ b/erp24/docs/models/ScriptLauncherLog.md @@ -0,0 +1,272 @@ +# Класс: ScriptLauncherLog + + +## Mindmap + +```mermaid +mindmap + root((ScriptLauncherLog)) + Таблица БД + script_launcher_log + Свойства + id + int + name + string + date_start + string + count_start + int + year + int + month + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель логирования запуска и выполнения скриптов в ERP24. Отслеживает прогресс выполнения фоновых задач, консольных команд и scheduled jobs с детальной информацией о статусе и ошибках. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`script_launcher_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +### Идентификация +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `source` | varchar(100) / null | Источник запуска | +| `category` | varchar(100) / null | Категория скрипта | +| `prefix` | varchar(100) / null | Префикс | +| `name` | varchar(200) | Название скрипта | +| `file` | varchar(200) / null | Путь к файлу | +| `context` | varchar(200) / null | Контекст выполнения | + +### Информация и сообщения +| Поле | Тип | Описание | +|------|-----|----------| +| `info` | text / null | Дополнительная информация | +| `message` | text / null | Сообщение | +| `error_message` | text / null | Текст ошибки | + +### Время выполнения +| Поле | Тип | Описание | +|------|-----|----------| +| `date_start` | varchar(100) | Дата и время старта | +| `date_finish` | varchar(100) / null | Дата и время завершения | +| `date` | varchar(100) | Дата (YYYY-MM-DD) | +| `created_at` | int | Unix timestamp создания | + +### Прогресс выполнения +| Поле | Тип | Описание | +|------|-----|----------| +| `count_start` | int | Начальное количество элементов | +| `count_current` | int / null | Текущий обработанный элемент | +| `count_remain` | int / null | Оставшееся количество | +| `count_finish` | int / null | Финальное количество | +| `current_work` | varchar(200) / null | Текущая операция | +| `progress` | int / null | Прогресс в процентах (0-100) | + +### Статус и ошибки +| Поле | Тип | Описание | +|------|-----|----------| +| `status` | int / null | Статус выполнения | +| `active` | int / null | Активен ли скрипт | +| `error_count` | int / null | Количество ошибок | + +### Дата (составная) +| Поле | Тип | Описание | +|------|-----|----------| +| `year` | int | Год | +| `month` | int | Месяц | +| `day` | int | День | + +## Значения по умолчанию (конструктор) + +```php +$this->date_start = date('Y-m-d H:i:s'); +$this->count_start = 0; +$this->count_current = 0; +$this->progress = 0; +$this->status = 1; +$this->active = 1; +$this->error_count = 0; +$this->year = (int) date('Y'); +$this->month = (int) date('n'); +$this->day = 1; +$this->date = date('Y-m-d'); +$this->created_at = time(); +``` + +## Диаграмма жизненного цикла + +```mermaid +flowchart TD + A[Запуск скрипта] --> B[Создание ScriptLauncherLog] + B --> C[status=1, active=1, progress=0] + C --> D[Выполнение итераций] + D --> E[Обновление count_current, progress] + E --> F{Ошибка?} + F -->|Да| G[error_count++, error_message] + F -->|Нет| H{Завершено?} + G --> H + H -->|Нет| D + H -->|Да| I[date_finish, count_finish] + I --> J[status=0, active=0] +``` + +## Диаграмма связей + +```mermaid +erDiagram + ScriptLauncherLog { + int id PK + varchar source + varchar category + varchar name + varchar file + varchar date_start + varchar date_finish + int count_start + int count_current + int progress + int status + int active + int error_count + text error_message + } +``` + +## Примеры использования + +### Логирование начала скрипта +```php +$log = new ScriptLauncherLog(); +$log->name = 'ImportProducts'; +$log->category = 'sync'; +$log->source = 'console'; +$log->file = 'commands/ImportController.php'; +$log->count_start = count($products); +$log->save(); +``` + +### Обновление прогресса +```php +foreach ($items as $index => $item) { + // Обработка item + processItem($item); + + // Обновление прогресса + $log->count_current = $index + 1; + $log->count_remain = count($items) - $index - 1; + $log->progress = round(($index + 1) / count($items) * 100); + $log->current_work = "Обработка: {$item->name}"; + $log->save(); +} +``` + +### Завершение скрипта +```php +$log->date_finish = date('Y-m-d H:i:s'); +$log->count_finish = $processedCount; +$log->status = 0; +$log->active = 0; +$log->save(); +``` + +### Логирование ошибки +```php +try { + processItem($item); +} catch (\Exception $e) { + $log->error_count++; + $log->error_message .= date('H:i:s') . ": {$e->getMessage()}\n"; + $log->save(); +} +``` + +### Получение активных скриптов +```php +$activeScripts = ScriptLauncherLog::find() + ->where(['active' => 1]) + ->orderBy(['date_start' => SORT_DESC]) + ->all(); + +foreach ($activeScripts as $script) { + echo "{$script->name}: {$script->progress}%\n"; +} +``` + +### История запусков за день +```php +$todayLogs = ScriptLauncherLog::find() + ->where(['date' => date('Y-m-d')]) + ->orderBy(['date_start' => SORT_DESC]) + ->all(); +``` + +### Поиск скриптов с ошибками +```php +$errorScripts = ScriptLauncherLog::find() + ->where(['>', 'error_count', 0]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-7 days'))]) + ->orderBy(['error_count' => SORT_DESC]) + ->all(); + +foreach ($errorScripts as $script) { + echo "{$script->name}: {$script->error_count} ошибок\n"; + echo "Последняя ошибка: {$script->error_message}\n"; +} +``` + +### Статистика по категориям +```php +$categoryStats = ScriptLauncherLog::find() + ->select(['category', 'COUNT(*) as runs', 'SUM(error_count) as errors']) + ->where(['>=', 'date', date('Y-m-01')]) + ->groupBy('category') + ->asArray() + ->all(); +``` + +### Мониторинг зависших скриптов +```php +$stuckScripts = ScriptLauncherLog::find() + ->where(['active' => 1]) + ->andWhere(['<', 'date_start', date('Y-m-d H:i:s', strtotime('-1 hour'))]) + ->all(); + +foreach ($stuckScripts as $script) { + Yii::warning("Возможно зависший скрипт: {$script->name}"); +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 200) | +| `date_start` | required, string (max 100) | +| `count_start` | required, integer | +| `year` | required, integer | +| `month` | required, integer | +| `day` | required, integer | +| `date` | required, string (max 100) | +| `created_at` | required, integer | + +## Особенности реализации + +1. **Инициализация в конструкторе**: Значения по умолчанию устанавливаются автоматически +2. **Прогресс в реальном времени**: count_current и progress для отслеживания +3. **Детальное логирование ошибок**: error_count и error_message +4. **Временные метки**: date_start и date_finish для анализа длительности +5. **Составная дата**: year/month/day для быстрой фильтрации +6. **Статус активности**: active для мониторинга запущенных скриптов diff --git a/erp24/docs/models/SelfCostProduct.md b/erp24/docs/models/SelfCostProduct.md new file mode 100644 index 00000000..5ec1c4e1 --- /dev/null +++ b/erp24/docs/models/SelfCostProduct.md @@ -0,0 +1,552 @@ +# Модель SelfCostProduct + + +## Mindmap + +```mermaid +mindmap + root((SelfCostProduct)) + Таблица БД + self_cost_product + Свойства + id + int + date + string + store_id + int + product_guid + string + price + float + Связи + Product1C + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `SelfCostProduct` представляет историю себестоимости товаров по магазинам и датам. Хранит фактические закупочные цены товаров для каждого магазина в конкретную дату. Используется для расчета медианной себестоимости букетов, анализа изменения цен поставщиков и оценки рентабельности. Модель ведет историю изменения цен, что позволяет проводить статистический анализ. + +**Файл модели:** `erp24/records/SelfCostProduct.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `self_cost_product` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `date` | DATE | Дата фиксации себестоимости | +| `store_id` | INTEGER | ID магазина из таблицы `store` | +| `product_guid` | VARCHAR(255) | GUID товара из таблицы товаров | +| `price` | FLOAT | Себестоимость товара на указанную дату | +| `updated_at` | TIMESTAMP | Дата внесения записи в систему (nullable) | + +--- + +## Методы модели + +### `tableName(): string` (static) + +Возвращает имя таблицы в базе данных. + +**Возвращает:** `'self_cost_product'` + +**Логика работы:** +Стандартный метод ActiveRecord для определения связи модели с таблицей БД. + +**Пример:** +```php +$tableName = SelfCostProduct::tableName(); +// 'self_cost_product' +``` + +--- + +### `rules(): array` + +Определяет правила валидации для полей модели. + +**Возвращает:** Массив правил валидации + +**Логика работы:** +1. Устанавливает обязательные поля: `date`, `store_id`, `product_guid`, `price` +2. Устанавливает значение по умолчанию `null` для `store_id` +3. Проверяет типы данных: + - DATE для `date` и `updated_at` (safe) + - INTEGER для `store_id` + - FLOAT для `price` + - VARCHAR(255) для `product_guid` + +**Правила валидации:** +- **required**: `date`, `store_id`, `product_guid`, `price` +- **safe (date)**: `date`, `updated_at` +- **default (null)**: `store_id` +- **integer**: `store_id` +- **number (float)**: `price` +- **string (max 255)**: `product_guid` + +**Пример:** +```php +$selfCost = new SelfCostProduct(); +$selfCost->date = '2025-12-11'; +$selfCost->store_id = 1; +$selfCost->product_guid = 'abc-123-456-guid'; +$selfCost->price = 125.50; + +if ($selfCost->validate()) { + $selfCost->save(); +} +``` + +--- + +### `attributeLabels(): array` + +Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** Ассоциативный массив [атрибут => метка] + +**Логика работы:** +Определяет названия полей на русском языке для использования в формах и сообщениях об ошибках. + +**Метки:** +- `id` → "ID" +- `date` → "Дата" +- `store_id` → "ID магазина" +- `product_guid` → "Guid Товара" +- `price` → "Цена" +- `updated_at` → "Обновлено" + +**Пример:** +```php +$label = $selfCost->getAttributeLabel('store_id'); +// "ID магазина" +``` + +--- + +### `getProduct1C(): ActiveQuery` + +Возвращает связь с товаром из 1С. + +**Возвращает:** ActiveQuery для связанной модели `Products1c` + +**Тип связи:** hasOne (многие к одному) + +**Логика работы:** +1. Устанавливает связь через внешний ключ `product_guid` → `id` +2. Добавляет условие фильтрации по типу товара: `tip = 'products'` +3. Возвращает только товары (не букеты и не другие сущности) + +**Пример:** +```php +$selfCost = SelfCostProduct::findOne($id); +$product = $selfCost->product1C; + +if ($product) { + echo "Товар: {$product->name}"; +} +``` + +**Связанные таблицы:** +- **Текущая таблица:** `self_cost_product` +- **Связанная таблица:** `products_1c` +- **Внешний ключ:** `product_guid` (FK) → `id` (PK) +- **Условие:** `tip = 'products'` + +--- + +## Связи (Relations) + +### `getProduct1C()` + +Товар из 1С. + +```php +$product = $selfCost->product1C; // Products1c +``` + +**Тип:** hasOne +**Связанная модель:** `Products1c` +**FK:** `product_guid` → `id` +**Условие:** `tip = 'products'` +**Описание:** Возвращает товар, к которому относится себестоимость + +--- + +## Примеры использования + +### Создание записи себестоимости + +```php +$selfCost = new SelfCostProduct(); +$selfCost->date = date('Y-m-d'); +$selfCost->store_id = 1; // ID магазина +$selfCost->product_guid = 'abc-123-456-guid'; +$selfCost->price = 99.50; + +if ($selfCost->save()) { + echo "Себестоимость зафиксирована"; +} +``` + +### Получение истории себестоимости товара + +```php +$history = SelfCostProduct::find() + ->where(['product_guid' => $productGuid]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($history as $record) { + echo "{$record->date}: {$record->price} руб. (Магазин {$record->store_id})" . PHP_EOL; +} +``` + +### Расчет медианной себестоимости за период + +```php +$productGuid = 'abc-123-456-guid'; +$startDate = date('Y-m-d', strtotime('-14 days')); + +$pricesData = SelfCostProduct::find() + ->where(['product_guid' => $productGuid]) + ->andWhere(['>=', 'date', $startDate]) + ->orderBy('price') + ->select(['price']) + ->column(); + +if (!empty($pricesData)) { + $count = count($pricesData); + $middle = (int)($count / 2); + + $median = ($count % 2 === 0) + ? ($pricesData[$middle - 1] + $pricesData[$middle]) / 2 + : $pricesData[$middle]; + + echo "Медианная себестоимость: {$median} руб."; +} +``` + +### Получение средней себестоимости за последние 14 дней + +```php +$averageCost = SelfCostProduct::find() + ->where(['product_guid' => $productGuid]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-14 days'))]) + ->average('price'); + +echo "Средняя себестоимость: {$averageCost} руб."; +``` + +### Получение себестоимости по магазину + +```php +$storeCosts = SelfCostProduct::find() + ->where([ + 'store_id' => $storeId, + 'date' => date('Y-m-d') + ]) + ->all(); + +foreach ($storeCosts as $cost) { + $product = $cost->product1C; + echo "{$product->name}: {$cost->price} руб." . PHP_EOL; +} +``` + +### Импорт себестоимости из закупки + +```php +$purchaseItems = [ + ['guid' => 'guid-1', 'price' => 100.00], + ['guid' => 'guid-2', 'price' => 150.00], + ['guid' => 'guid-3', 'price' => 200.00], +]; + +$date = date('Y-m-d'); +$storeId = 1; + +foreach ($purchaseItems as $item) { + $selfCost = new SelfCostProduct(); + $selfCost->date = $date; + $selfCost->store_id = $storeId; + $selfCost->product_guid = $item['guid']; + $selfCost->price = $item['price']; + $selfCost->save(); +} +``` + +### Анализ изменения себестоимости + +```php +$trendData = SelfCostProduct::find() + ->where(['product_guid' => $productGuid]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-30 days'))]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$prices = array_map(fn($item) => $item->price, $trendData); +$firstPrice = $prices[0]; +$lastPrice = end($prices); + +$change = (($lastPrice / $firstPrice) - 1) * 100; +echo "Изменение за месяц: " . ($change >= 0 ? '+' : '') . "{$change}%"; +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `date` | Обязательное, дата (safe) | +| `store_id` | Обязательное, целое число, default = null | +| `product_guid` | Обязательное, макс. 255 символов | +| `price` | Обязательное, число с плавающей точкой | +| `updated_at` | Безопасное присвоение (safe), timestamp | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары из 1С (связь по `product_guid`) +- **Store** — магазины (связь по `store_id`) +- **[BouquetComposition](./BouquetComposition.md)** — использует модель для расчета себестоимости букетов +- **[BouquetCompositionProducts](./BouquetCompositionProducts.md)** — использует для расчета рентабельности + +--- + +## Диаграмма связей + +```mermaid +erDiagram + products_1c ||--o{ self_cost_product : "has_cost_history" + store ||--o{ self_cost_product : "records_costs" + + products_1c { + string id PK + string name + string tip + } + + self_cost_product { + int id PK + date date + int store_id FK + string product_guid FK + float price + timestamp updated_at + } + + store { + int id PK + string name + int region_id + } +``` + +--- + +## Диаграмма потока данных + +```mermaid +graph TD + A[Закупка товаров] -->|Фиксация цен| B[SelfCostProduct] + B --> C[История по дате] + B --> D[История по магазину] + C --> E[Расчет медианы] + C --> F[Расчет средней] + E --> G[Себестоимость букета] + F --> G + D --> H[Анализ по магазинам] + G --> I[BouquetComposition] + B --> J[Рентабельность товара] + J --> K[BouquetCompositionProducts] +``` + +--- + +## Особенности реализации + +### Историчность данных + +Модель ведет полную историю изменения себестоимости: +- Каждая запись привязана к конкретной дате +- Не удаляются старые записи +- Позволяет анализировать тренды + +### Магазино-специфичность + +Себестоимость фиксируется отдельно для каждого магазина: +- Разные поставщики → разные цены +- Разная логистика → разная себестоимость +- Региональные особенности + +### Автоматическое updated_at + +Поле `updated_at` автоматически заполняется при создании/обновлении записи (на уровне БД через DEFAULT или trigger). + +--- + +## Использование в BouquetComposition + +Модель активно используется для расчета себестоимости букетов: + +```php +// Из BouquetComposition::getSelfCost() +$pricesData = SelfCostProduct::find() + ->where(['product_guid' => $productGuids]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-14 days'))]) + ->orderBy('price') + ->select(['product_guid', 'price']) + ->asArray() + ->all(); + +// Группировка по товарам +$pricesByProduct = []; +foreach ($pricesData as $row) { + $pricesByProduct[$row['product_guid']][] = $row['price']; +} + +// Расчет медианы для каждого товара +foreach ($compositionProducts as $item) { + $prices = $pricesByProduct[$item['product_guid']] ?? []; + $count = count($prices); + + if ($count == 0) { + continue; + } + + sort($prices); + $middle = (int)($count / 2); + $median = ($count % 2 === 0) + ? ($prices[$middle - 1] + $prices[$middle]) / 2 + : $prices[$middle]; + + $selfCost += $median * $item['count']; +} +``` + +**Логика:** +1. Получаем цены за последние 14 дней +2. Группируем по товарам +3. Вычисляем медиану для каждого товара +4. Умножаем на количество в букете +5. Суммируем + +--- + +## Использование в BouquetCompositionProducts + +Модель используется для расчета рентабельности товара: + +```php +// Из BouquetCompositionProducts::getProfitability() +$averageCost = SelfCostProduct::find() + ->where(['product_guid' => $this->product_guid]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-14 days'))]) + ->average('price'); + +if ($averageCost === null) { + return 0; +} + +// Получаем розничные цены +$retailPrices = PricesDynamic::find() + ->where(['product_id' => $this->product_guid]) + ->andWhere(['active' => 1]) + ->all(); + +// Рассчитываем рентабельность +foreach ($retailPrices as $price) { + if ($price['price'] > 0) { + $grossProfit = $price['price'] - $averageCost; + $profitability = ($grossProfit / $price['price']) * 100; + $profitMargins[] = $profitability; + } +} + +// Медиана рентабельности +$medianProfitability = calculateMedian($profitMargins); +``` + +--- + +## Статистический анализ + +### Расчет медианы (используется в системе) + +Медиана используется вместо среднего значения, так как: +- Устойчива к выбросам (аномальным ценам) +- Отражает "типичную" цену закупки +- Не искажается редкими дорогими закупками + +**Формула медианы:** +- Если количество нечетное: средний элемент +- Если количество четное: среднее двух средних элементов + +### Временное окно 14 дней + +Система использует окно в 14 дней для: +- Актуальности данных (свежие цены) +- Достаточной выборки для статистики +- Баланс между свежестью и стабильностью + +--- + +## Сценарии использования + +### 1. Ежедневная фиксация себестоимости + +```php +// Автоматическая задача (cron) +$date = date('Y-m-d'); +$purchases = getPurchasesForDate($date); + +foreach ($purchases as $purchase) { + foreach ($purchase['items'] as $item) { + $selfCost = new SelfCostProduct(); + $selfCost->date = $date; + $selfCost->store_id = $purchase['store_id']; + $selfCost->product_guid = $item['product_guid']; + $selfCost->price = $item['purchase_price']; + $selfCost->save(); + } +} +``` + +### 2. Отчет по динамике себестоимости + +```php +$products = ['guid-1', 'guid-2', 'guid-3']; +$period = 30; // дней + +foreach ($products as $guid) { + $records = SelfCostProduct::find() + ->where(['product_guid' => $guid]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime("-{$period} days"))]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + + $prices = array_column($records, 'price'); + $avgPrice = array_sum($prices) / count($prices); + $minPrice = min($prices); + $maxPrice = max($prices); + + echo "Товар {$guid}:" . PHP_EOL; + echo "Средняя: {$avgPrice}, Мин: {$minPrice}, Макс: {$maxPrice}" . PHP_EOL; +} +``` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/SelfCostProductDynamic.md b/erp24/docs/models/SelfCostProductDynamic.md new file mode 100644 index 00000000..fca97478 --- /dev/null +++ b/erp24/docs/models/SelfCostProductDynamic.md @@ -0,0 +1,282 @@ +# Класс: SelfCostProductDynamic + + +## Mindmap + +```mermaid +mindmap + root((SelfCostProductDynamic)) + Таблица БД + self_cost_product_dynamic + Свойства + id + int + store_id + int + product_guid + string + price + float + date_from + string + date_to + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель динамической себестоимости товаров в ERP24. Хранит исторические значения себестоимости товаров по магазинам с временным версионированием для корректного расчёта маржинальности. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`self_cost_product_dynamic` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Описание | +|-----------|----------| +| `TimestampBehavior` | Автоматическое заполнение created_at/updated_at | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_id` | int | FK на магазин (CityStore) | +| `product_guid` | varchar(255) | GUID товара из Products1c | +| `price` | float | Себестоимость товара | +| `date_from` | datetime | Дата начала действия цены | +| `date_to` | datetime | Дата окончания действия цены | +| `created_at` | datetime | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | + +## Диаграмма связей + +```mermaid +erDiagram + SelfCostProductDynamic { + int id PK + int store_id FK + varchar product_guid FK + float price + datetime date_from + datetime date_to + datetime created_at + datetime updated_at + } + + CityStore { + int id PK + varchar name + } + + Products1c { + varchar id PK + varchar name + } + + CityStore ||--o{ SelfCostProductDynamic : "store_id" + Products1c ||--o{ SelfCostProductDynamic : "product_guid" +``` + +## Диаграмма временного версионирования + +```mermaid +gantt + title История себестоимости товара + dateFormat YYYY-MM-DD + section Товар A + Цена 100 руб :done, 2024-01-01, 2024-03-31 + Цена 120 руб :active, 2024-04-01, 2024-06-30 + Цена 115 руб :2024-07-01, 2024-12-31 +``` + +## Примеры использования + +### Создание записи себестоимости +```php +$selfCost = new SelfCostProductDynamic(); +$selfCost->store_id = $storeId; +$selfCost->product_guid = $productGuid; +$selfCost->price = 150.50; +$selfCost->date_from = date('Y-m-d H:i:s'); +$selfCost->date_to = '9999-12-31 23:59:59'; // Бессрочно +$selfCost->save(); +``` + +### Получение актуальной себестоимости +```php +function getCurrentSelfCost($productGuid, $storeId, $date = null) +{ + $date = $date ?? date('Y-m-d H:i:s'); + + return SelfCostProductDynamic::find() + ->where([ + 'product_guid' => $productGuid, + 'store_id' => $storeId + ]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere(['>=', 'date_to', $date]) + ->orderBy(['date_from' => SORT_DESC]) + ->one(); +} + +$selfCost = getCurrentSelfCost($productGuid, $storeId); +if ($selfCost) { + echo "Себестоимость: {$selfCost->price} руб."; +} +``` + +### Изменение себестоимости +```php +// Закрываем текущую цену +$currentCost = SelfCostProductDynamic::find() + ->where([ + 'product_guid' => $productGuid, + 'store_id' => $storeId + ]) + ->andWhere(['date_to' => '9999-12-31 23:59:59']) + ->one(); + +if ($currentCost) { + $currentCost->date_to = date('Y-m-d H:i:s'); + $currentCost->save(); +} + +// Создаём новую цену +$newCost = new SelfCostProductDynamic(); +$newCost->store_id = $storeId; +$newCost->product_guid = $productGuid; +$newCost->price = $newPrice; +$newCost->date_from = date('Y-m-d H:i:s'); +$newCost->date_to = '9999-12-31 23:59:59'; +$newCost->save(); +``` + +### История себестоимости товара +```php +$history = SelfCostProductDynamic::find() + ->where([ + 'product_guid' => $productGuid, + 'store_id' => $storeId + ]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +foreach ($history as $record) { + echo "{$record->date_from} - {$record->date_to}: {$record->price} руб.\n"; +} +``` + +### Себестоимость на дату +```php +function getSelfCostAtDate($productGuid, $storeId, $date) +{ + $cost = SelfCostProductDynamic::find() + ->where([ + 'product_guid' => $productGuid, + 'store_id' => $storeId + ]) + ->andWhere(['<=', 'date_from', $date]) + ->andWhere(['>=', 'date_to', $date]) + ->one(); + + return $cost ? $cost->price : null; +} + +$costOnDate = getSelfCostAtDate($productGuid, $storeId, '2024-06-15'); +``` + +### Расчёт маржинальности +```php +function calculateMargin($salePrice, $productGuid, $storeId, $saleDate) +{ + $selfCost = getSelfCostAtDate($productGuid, $storeId, $saleDate); + + if ($selfCost && $salePrice > 0) { + $margin = ($salePrice - $selfCost) / $salePrice * 100; + return round($margin, 2); + } + + return null; +} + +$margin = calculateMargin(500, $productGuid, $storeId, '2024-12-15'); +echo "Маржинальность: {$margin}%"; +``` + +### Массовое обновление себестоимости +```php +$newPrices = [ + 'guid1' => 100, + 'guid2' => 150, + 'guid3' => 200, +]; + +$now = date('Y-m-d H:i:s'); + +foreach ($newPrices as $guid => $price) { + // Закрываем текущие цены + SelfCostProductDynamic::updateAll( + ['date_to' => $now], + [ + 'and', + ['product_guid' => $guid], + ['store_id' => $storeId], + ['date_to' => '9999-12-31 23:59:59'] + ] + ); + + // Создаём новые + $cost = new SelfCostProductDynamic(); + $cost->store_id = $storeId; + $cost->product_guid = $guid; + $cost->price = $price; + $cost->date_from = $now; + $cost->date_to = '9999-12-31 23:59:59'; + $cost->save(); +} +``` + +### Статистика изменений себестоимости +```php +$changeStats = SelfCostProductDynamic::find() + ->select([ + 'store_id', + 'COUNT(*) as changes' + ]) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('store_id') + ->asArray() + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, integer | +| `product_guid` | required, string (max 255) | +| `price` | required, number | +| `date_from` | required, safe | +| `date_to` | safe | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [Products1c](./Products1c.md) — товары + +## Особенности реализации + +1. **Временное версионирование**: date_from/date_to для истории цен +2. **Per-store цены**: Себестоимость может различаться по магазинам +3. **TimestampBehavior**: Автоматические метки created_at/updated_at +4. **Бессрочная цена**: date_to = '9999-12-31' для текущей цены +5. **Интеграция с 1С**: product_guid для связи с товарами +6. **Расчёт маржинальности**: Основа для финансовой аналитики diff --git a/erp24/docs/models/SelfCostProductSearch.md b/erp24/docs/models/SelfCostProductSearch.md new file mode 100644 index 00000000..6e6f49a2 --- /dev/null +++ b/erp24/docs/models/SelfCostProductSearch.md @@ -0,0 +1,209 @@ +# Класс: SelfCostProductSearch + + +## Mindmap + +```mermaid +mindmap + root((SelfCostProductSearch)) + Таблица БД + ActiveRecord + Наследование + extends SelfCostProduct +``` + +## Назначение +Search-модель для поиска и фильтрации себестоимости товаров в ERP24. Модель с JOIN к products_1c, кастомной сортировкой по названию товара и фильтром по типу "products". + +## Пространство имён +`yii_app\records` + +## Родительский класс +`SelfCostProduct` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$product_name` | string | Название товара для поиска по связанной таблице products_1c | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `store_id` — integer +- `date`, `product_guid`, `product_name`, `updated_at` — safe +- `price` — number + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к товарам и кастомной сортировкой. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с алиасом `scp` для основной таблицы +2. Выполняет joinWith для product1C (алиас p), без eager loading (false) +3. Добавляет базовый фильтр: p.tip = 'products' +4. Настраивает пагинацию: pageSize=20 +5. Устанавливает сортировку по умолчанию: date DESC +6. Добавляет кастомный атрибут сортировки: product_name (p.name) +7. Применяет фильтры: + - Точное совпадение: id, store_id, price, date + - like: p.name (product_name), product_guid + +## Диаграмма связей + +```mermaid +erDiagram + SelfCostProduct { + int id PK + int store_id FK + varchar product_guid FK + decimal price + date date + datetime updated_at + } + + Products1c { + varchar id PK + varchar name + varchar tip + } + + CityStore { + int id PK + varchar name + } + + SelfCostProduct }o--|| Products1c : "product_guid -> id" + SelfCostProduct }o--|| CityStore : "store_id" +``` + +## Диаграмма логики поиска + +```mermaid +flowchart TD + A[SelfCostProductSearch] --> B[joinWith product1C] + B --> C[WHERE p.tip = 'products'] + + C --> D[Фильтры] + D --> E[id, store_id, price, date] + D --> F[like p.name] + D --> G[like product_guid] + + H[Сортировка] --> I[defaultOrder: date DESC] + H --> J[product_name -> p.name] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new SelfCostProductSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию товара +```php +$searchModel = new SelfCostProductSearch(); +$dataProvider = $searchModel->search([ + 'SelfCostProductSearch' => [ + 'product_name' => 'Роза', + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new SelfCostProductSearch(); +$dataProvider = $searchModel->search([ + 'SelfCostProductSearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Поиск по GUID товара +```php +$searchModel = new SelfCostProductSearch(); +$dataProvider = $searchModel->search([ + 'SelfCostProductSearch' => [ + 'product_guid' => 'abc-123-def', + ] +]); +``` + +### Поиск по дате +```php +$searchModel = new SelfCostProductSearch(); +$dataProvider = $searchModel->search([ + 'SelfCostProductSearch' => [ + 'date' => '2024-01-15', + ] +]); +``` + +### Поиск по цене +```php +$searchModel = new SelfCostProductSearch(); +$dataProvider = $searchModel->search([ + 'SelfCostProductSearch' => [ + 'price' => 150.50, + ] +]); +``` + +### GridView с сортировкой по товару +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'product_name', + 'value' => 'product1C.name', + ], + 'product_guid', + [ + 'attribute' => 'store_id', + 'value' => 'store.name', + ], + 'price:decimal', + 'date:date', + 'updated_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [SelfCostProduct](./SelfCostProduct.md) — базовая модель себестоимости +- [Products1c](./Products1c.md) — товары 1С +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Фильтр по типу**: Только товары с tip = 'products' +2. **Алиасы таблиц**: scp для SelfCostProduct, p для products_1c +3. **joinWith без eager**: joinWith(..., false) для оптимизации +4. **Кастомная сортировка**: product_name сортирует по p.name +5. **Сортировка по умолчанию**: date DESC (новые записи сверху) +6. **Пагинация**: 20 записей на странице +7. **like поиск**: Регистрозависимый для названия и GUID +8. **Пустой результат при ошибке**: WHERE 0=1 при невалидных параметрах diff --git a/erp24/docs/models/SentKogort.md b/erp24/docs/models/SentKogort.md new file mode 100644 index 00000000..cc34ce66 --- /dev/null +++ b/erp24/docs/models/SentKogort.md @@ -0,0 +1,335 @@ +# Модель SentKogort + + +## Mindmap + +```mermaid +mindmap + root((SentKogort)) + Таблица БД + sent_kogort + Свойства + id + int + phone + string + kogort_date + string + target_date + string + kogort_unixtime + int + kogort_number + int + Связи + User + 1:1 Users + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `SentKogort` представляет записи отправленных когортных рассылок. Хранит информацию о клиентах, включённых в маркетинговые когорты: телефон, даты, статус отправки сообщений, факт контакта и совершения покупки. Используется для отслеживания эффективности когортных кампаний и интеграции с LPTracker. + +**Файл модели:** `erp24/records/SentKogort.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `sent_kogort` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `phone` | VARCHAR(15) | Телефон клиента | +| `kogort_date` | DATE | Дата формирования когорты | +| `target_date` | DATE | Целевая дата (дата покупки/события) | +| `kogort_unixtime` | INTEGER | UNIX-время когорты | +| `kogort_number` | INTEGER | Тип когорты (1-target, 2-whatsapp, 3-call) | +| `status` | INTEGER | Статус обработки записи | +| `contact` | INTEGER | Флаг контакта с клиентом | +| `purchase` | INTEGER | Флаг совершения покупки | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | + +--- + +## Константы + +### Типы когорт (KOGORT_NUMBERS) + +```php +public const KOGORT_NUMBERS = [ + 'target' => 1, // Целевая когорта + 'whatsapp' => 2, // WhatsApp рассылка + 'call' => 3, // Обзвон +]; + +public const CALL = 3; // Алиас для типа "обзвон" +``` + +### Статусы LPTracker + +```php +public const READY_TO_UPLOAD_LPTRACKER_STATUS = 1; // Готов к загрузке в LPTracker +public const SUCCESS_UPLOAD_TO_LPTRACKER_STATUS = 11; // Успешно загружен в LPTracker +public const ERROR_UPLOAD_TO_LPTRACKER_STATUS = 22; // Ошибка загрузки в LPTracker +``` + +### Статусы обработки (STATUSES) + +```php +public const STATUSES = [ + 'created' => 1, // Создана запись в когорте + 'first' => 2, // Отправлено первое сообщение в чатбот + 'second' => 3, // Отправлено второе сообщение +]; +``` + +--- + +## Описание полей + +### `phone` — Телефон клиента + +Номер телефона клиента в когорте. + +**Ограничения:** максимум 15 символов + +### `kogort_date` — Дата когорты + +Дата, когда была сформирована когорта для данного клиента. + +### `target_date` — Целевая дата + +Дата, к которой привязана когорта (например, годовщина первой покупки). + +### `kogort_number` — Тип когорты + +Определяет способ коммуникации с клиентом: +- `1` — целевая когорта (target) +- `2` — WhatsApp рассылка +- `3` — телефонный обзвон + +### `status` — Статус обработки + +Текущий статус записи в процессе обработки когорты. + +### `contact` — Контакт + +Флаг, указывающий был ли установлен контакт с клиентом. + +### `purchase` — Покупка + +Флаг, указывающий совершил ли клиент покупку после контакта. + +--- + +## Методы модели + +### `getUser(): ActiveQuery` + +Возвращает связь с клиентом по номеру телефона. + +**Возвращает:** `ActiveQuery` — запрос к модели Users + +**Пример:** +```php +$kogort = SentKogort::findOne($id); +$user = $kogort->user; +if ($user) { + echo "Клиент: {$user->name}"; + echo "Бонусы: {$user->bonus}"; +} +``` + +--- + +### `afterSave($insert, $changedAttributes)` + +Автоматически обновляет `updated_at` при изменении записи. + +**Логика:** +- При обновлении (не вставке) устанавливает `updated_at` текущим временем + +--- + +## Диаграмма связей + +```mermaid +erDiagram + sent_kogort }o--|| users : "linked_by_phone" + sent_kogort ||--o{ lptracker_logs : "integration" + + sent_kogort { + int id PK + string phone + date kogort_date + date target_date + int kogort_unixtime + int kogort_number + int status + int contact + int purchase + timestamp created_at + timestamp updated_at + } + + users { + string id PK + string phone + string name + int bonus + } +``` + +--- + +## Примеры использования + +### Создание записи когорты + +```php +$kogort = new SentKogort(); +$kogort->phone = '+79001234567'; +$kogort->kogort_date = date('Y-m-d'); +$kogort->target_date = '2025-12-25'; +$kogort->kogort_unixtime = time(); +$kogort->kogort_number = SentKogort::KOGORT_NUMBERS['whatsapp']; +$kogort->status = SentKogort::STATUSES['created']; +$kogort->created_at = date('Y-m-d H:i:s'); +$kogort->save(); +``` + +### Получение когорт для обзвона + +```php +$callKogorts = SentKogort::find() + ->where(['kogort_number' => SentKogort::CALL]) + ->andWhere(['status' => SentKogort::STATUSES['created']]) + ->andWhere(['contact' => null]) + ->all(); + +foreach ($callKogorts as $kogort) { + echo "Позвонить: {$kogort->phone}\n"; +} +``` + +### Обновление статуса после отправки сообщения + +```php +$kogort = SentKogort::findOne(['phone' => $phone, 'kogort_date' => $date]); +if ($kogort) { + $kogort->status = SentKogort::STATUSES['first']; + $kogort->save(); +} +``` + +### Отметка контакта и покупки + +```php +$kogort = SentKogort::findOne($id); +$kogort->contact = 1; +$kogort->purchase = 1; +$kogort->save(); +``` + +### Получение когорт для загрузки в LPTracker + +```php +$readyForUpload = SentKogort::find() + ->where(['status' => SentKogort::READY_TO_UPLOAD_LPTRACKER_STATUS]) + ->all(); + +foreach ($readyForUpload as $kogort) { + // Загрузка в LPTracker + try { + uploadToLPTracker($kogort); + $kogort->status = SentKogort::SUCCESS_UPLOAD_TO_LPTRACKER_STATUS; + } catch (Exception $e) { + $kogort->status = SentKogort::ERROR_UPLOAD_TO_LPTRACKER_STATUS; + } + $kogort->save(); +} +``` + +### Статистика эффективности когорт + +```php +$stats = SentKogort::find() + ->select([ + 'kogort_number', + 'COUNT(*) as total', + 'SUM(contact) as contacts', + 'SUM(purchase) as purchases' + ]) + ->where(['>=', 'kogort_date', '2025-01-01']) + ->groupBy('kogort_number') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $type = array_search($stat['kogort_number'], SentKogort::KOGORT_NUMBERS); + $conversionRate = $stat['total'] > 0 + ? round($stat['purchases'] / $stat['total'] * 100, 2) + : 0; + echo "{$type}: {$stat['total']} записей, конверсия {$conversionRate}%\n"; +} +``` + +### Получение клиента из когорты + +```php +$kogort = SentKogort::find() + ->with('user') + ->where(['id' => $id]) + ->one(); + +if ($kogort->user) { + echo "Клиент: {$kogort->user->name}"; + echo "История покупок: ..."; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `phone` | Обязательное, макс. 15 символов | +| `kogort_date` | Обязательное | +| `target_date` | Обязательное | +| `kogort_unixtime` | Обязательное, целое число | +| `kogort_number` | Обязательное, целое число | +| `created_at` | Обязательное | +| `status` | Целое число | +| `contact` | Целое число | +| `purchase` | Целое число | + +--- + +## Связанные модели + +- **[Users](./Users.md)** — клиенты (связь по телефону) +- **[KogortStopList](./KogortStopList.md)** — стоп-лист для исключения номеров +- **LPTrackerLog** — логи интеграции с LPTracker + +--- + +## Интеграция с LPTracker + +Модель поддерживает интеграцию с системой LPTracker для отслеживания лидов: + +1. Записи создаются со статусом `READY_TO_UPLOAD_LPTRACKER_STATUS` +2. Сервис загрузки обрабатывает записи и обновляет статус +3. При успехе — `SUCCESS_UPLOAD_TO_LPTRACKER_STATUS` +4. При ошибке — `ERROR_UPLOAD_TO_LPTRACKER_STATUS` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Shift.md b/erp24/docs/models/Shift.md new file mode 100644 index 00000000..093cb108 --- /dev/null +++ b/erp24/docs/models/Shift.md @@ -0,0 +1,525 @@ +# Class: Shift + + +## Mindmap + +```mermaid +mindmap + root((Shift)) + Таблица БД + timetable_shift + Свойства + id + string + name + string + short_name + string + start_time + string + duration + string + work_time + string + Связи + AdminGroups + 1:N AdminGroup + AdminGroupShift + 1:N AdminGroupShift + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `Shift` представляет справочник рабочих смен в системе ERP24. Определяет типы смен (утренние, дневные, вечерние, ночные) с параметрами времени начала, длительности и рабочего времени с учетом перерывов. Используется для планирования графика сотрудников, автоматического расчета рабочего времени и группировки смен по часам. Модель кэшируется для быстрого доступа и связана с должностями через промежуточную таблицу. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица базы данных + +`timetable_shift` + +## Константы + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `DAY` | 1 | Дневная смена | +| `NIGHT` | 2 | Ночная смена | + +## Свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | Уникальный идентификатор смены | +| `name` | string | Полное название смены | +| `short_name` | string | Короткое название для отображения | +| `start_time` | string(time) | Время начала смены (H:i:s) | +| `duration` | float | Полная длительность смены в часах (включая перерывы) | +| `work_time` | float | Рабочее время в часах (без перерывов) | +| `end_time` | string(time) | Время окончания смены (H:i:s) | +| `adminGroupShift` | AdminGroupShift | Связь с должностями | +| `adminGroups` | AdminGroup[] | Массив должностей, доступных для этой смены | +| `hours` | array | Массив часов, входящих в смену (приватное) | + +## Методы + +### `tableName()` + +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'timetable_shift' + +--- + +### `rules()` + +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- `id` - целое число +- `start_time` - время в формате H:i:s +- `duration` - дробное число от 1 до 24 часов + +**Пример:** +```php +$shift = new Shift(); +$shift->name = 'Утренняя смена'; +$shift->short_name = 'Утро'; +$shift->start_time = '09:00:00'; +$shift->duration = 9.0; +$shift->work_time = 8.0; // 1 час перерыв +$shift->validate(); +``` + +--- + +### `beforeValidate()` + +**Описание:** Автоматически рассчитывает время окончания смены перед валидацией. + +**Возвращает:** `bool` - результат родительского метода + +**Логика работы:** +1. Парсит `start_time` с помощью регулярного выражения, извлекая часы и минуты +2. Создает интервал длительности через `getDurationInterval()` +3. Вычисляет минуты окончания с учетом возможного переноса на следующий час +4. Вычисляет часы окончания с учетом суточного цикла (% 24) +5. Устанавливает `end_time` в формате "минуты:часы:00" + +**Вызовы сторонних методов:** +- `preg_match()` - парсинг времени +- `$this->getDurationInterval()` - создание интервала +- Математические операции для расчета времени + +**Пример:** +```php +$shift->start_time = '09:00:00'; +$shift->duration = 9.5; // 9 часов 30 минут +$shift->beforeValidate(); +// end_time будет вычислено как '18:30:00' +``` + +**Примечание:** В коде есть ошибка в формате `end_time`: `$endMinute . ':' . $endHour . ':00'` должно быть `$endHour . ':' . $endMinute . ':00'` + +--- + +### `getDurationInterval()` + +**Описание:** Преобразует дробное значение длительности в объект DateInterval. + +**Возвращает:** `\DateInterval` - интервал времени + +**Логика работы:** +1. Извлекает целую часть duration как количество часов +2. Вычисляет минуты из дробной части: `60 * (duration - floor(duration))` +3. Создает DateInterval в формате 'PT{hours}H{minutes}M' + +**Пример:** +```php +$shift->duration = 8.5; // 8 часов 30 минут +$interval = $shift->getDurationInterval(); +// Возвращает DateInterval: PT8H30M +``` + +--- + +### `getHours()` + +**Описание:** Возвращает массив всех часов, входящих в смену. + +**Возвращает:** `array` - массив часов (0-23) + +**Логика работы:** +1. Проверяет кэш в свойстве `$hours`, если заполнен - возвращает его +2. Парсит `start_time`, извлекая начальный час +3. Создает массив часов от начала смены до начала + duration +4. Использует операцию % 24 для обработки смен через полночь +5. Кэширует результат в `$hours` + +**Вызовы сторонних методов:** +- `preg_match()` - парсинг времени +- `empty()` - проверка кэша + +**Пример:** +```php +// Дневная смена с 09:00, длительность 9 часов +$shift->start_time = '09:00:00'; +$shift->duration = 9.0; +$hours = $shift->getHours(); +// Вернет: [9, 10, 11, 12, 13, 14, 15, 16, 17, 18] + +// Ночная смена с 22:00, длительность 9 часов +$shift->start_time = '22:00:00'; +$shift->duration = 9.0; +$hours = $shift->getHours(); +// Вернет: [22, 23, 0, 1, 2, 3, 4, 5, 6, 7] +``` + +--- + +### `all()` + +**Описание:** Возвращает кэшированный список всех смен, индексированный по ID. + +**Возвращает:** `Shift[]` - массив объектов Shift с ключами-ID + +**Логика работы:** +1. Проверяет статическую переменную `$all` +2. Если кэш не пуст - возвращает закэшированные данные +3. Если пуст - выполняет запрос `find()->indexBy('id')->all()` +4. Сохраняет результат в статическую переменную для повторного использования +5. Возвращает массив + +**Вызовы сторонних методов:** +- `self::find()` - создание запроса +- `->indexBy('id')` - индексация по ID +- `->all()` - выполнение запроса +- `empty()` - проверка кэша + +**Пример:** +```php +$shifts = Shift::all(); +// Возвращает: [1 => Shift{...}, 2 => Shift{...}, 3 => Shift{...}] + +$morningShift = $shifts[1]; +echo $morningShift->name; // 'Утренняя смена' + +// Повторный вызов использует кэш (без запроса к БД) +$shiftsAgain = Shift::all(); +``` + +--- + +### `getAdminGroups()` + +**Описание:** Определяет связь многие-ко-многим с должностями через промежуточную таблицу. + +**Возвращает:** `ActiveQuery` - запрос связи hasMany через via + +**Логика работы:** +Создает связь с таблицей `AdminGroup` через промежуточную таблицу `admin_group_shift`. Позволяет определить, какие должности могут работать в данную смену. + +**Пример:** +```php +$shift = Shift::findOne(1); +foreach ($shift->adminGroups as $group) { + echo $group->name . "\n"; + // 'Флорист' + // 'Администратор' +} +``` + +--- + +### `getAdminGroupShift()` + +**Описание:** Определяет связь с промежуточной таблицей связки смен и должностей. + +**Возвращает:** `ActiveQuery` - запрос связи hasMany + +**Логика работы:** +Создает связь один-ко-многим с таблицей `AdminGroupShift` через поле `shift_id`. Промежуточная таблица для связи many-to-many со должностями. + +--- + +### `getNames()` + +**Описание:** Возвращает массив коротких названий смен для выпадающих списков. + +**Возвращает:** `array` - [id => short_name] + +**Логика работы:** +Выполняет оптимизированный запрос: +- Выбирает только поля `short_name` и `id` +- Индексирует по `id` +- Преобразует в одномерный массив методом `column()` + +**Вызовы сторонних методов:** +- `self::find()` - создание запроса +- `->select()` - выбор полей +- `->indexBy()` - индексация +- `->column()` - получение массива значений + +**Пример:** +```php +$names = Shift::getNames(); +// Возвращает: [1 => 'Утро', 2 => 'День', 3 => 'Вечер', 4 => 'Ночь'] + +// Использование в форме +echo Html::dropDownList('shift_id', null, Shift::getNames()); +``` + +--- + +### `isNight($shiftId)` + +**Описание:** Проверяет, является ли смена ночной. + +**Параметры:** +- `$shiftId` (int) - ID смены для проверки + +**Возвращает:** `bool` - true если смена ночная + +**Логика работы:** +Сравнивает переданный ID с константой `NIGHT` (2). Строгое сравнение типов через приведение к int. + +**Пример:** +```php +if (Shift::isNight($shiftId)) { + $nightBonus = $salary * 0.2; // 20% надбавка за ночь + echo "Ночная смена, доплата: {$nightBonus}"; +} +``` + +--- + +## Примеры использования + +### 1. Создание новой смены + +```php +$shift = new Shift(); +$shift->name = 'Утренняя смена'; +$shift->short_name = 'Утро'; +$shift->start_time = '09:00:00'; +$shift->duration = 9.0; // 9 часов +$shift->work_time = 8.0; // 8 часов работы, 1 час перерыв + +if ($shift->save()) { + echo "Смена создана. ID: {$shift->id}"; + echo "Окончание: {$shift->end_time}"; // 18:00:00 +} +``` + +### 2. Получение всех смен для списка + +```php +$shifts = Shift::all(); +foreach ($shifts as $id => $shift) { + echo "{$id}: {$shift->name} ({$shift->start_time} - {$shift->end_time})\n"; + echo " Длительность: {$shift->duration}ч, работа: {$shift->work_time}ч\n"; + echo " Перерыв: " . ($shift->duration - $shift->work_time) . "ч\n\n"; +} +``` + +### 3. Использование в выпадающем списке + +```php +use yii\helpers\Html; + +echo Html::activeDropDownList( + $model, + 'shift_id', + Shift::getNames(), + ['prompt' => 'Выберите смену'] +); +``` + +### 4. Проверка доступности смены для должности + +```php +$shiftId = 1; +$adminGroupId = 3; // Флорист + +$shift = Shift::findOne($shiftId); +$isAvailable = false; + +foreach ($shift->adminGroups as $group) { + if ($group->id === $adminGroupId) { + $isAvailable = true; + break; + } +} + +if ($isAvailable) { + echo "Флористы могут работать в эту смену"; +} +``` + +### 5. Расчет времени с учетом смены + +```php +$shift = Shift::findOne(1); +$startDateTime = new DateTime('2025-11-27 ' . $shift->start_time); +$interval = $shift->getDurationInterval(); +$endDateTime = clone $startDateTime; +$endDateTime->add($interval); + +echo "Начало: " . $startDateTime->format('Y-m-d H:i:s') . "\n"; +echo "Окончание: " . $endDateTime->format('Y-m-d H:i:s') . "\n"; +echo "Рабочее время: {$shift->work_time} часов\n"; +echo "Перерыв: " . ($shift->duration - $shift->work_time) . " часов"; +``` + +### 6. Определение часов смены для графика + +```php +$shift = Shift::findOne(2); // Вечерняя смена +$hours = $shift->getHours(); + +// Построение графика занятости по часам +foreach ($hours as $hour) { + $employeesCount = Timetable::find() + ->where(['shift_id' => $shift->id]) + ->andWhere(['date' => '2025-11-27']) + ->count(); + + echo sprintf("%02d:00 - %d сотрудников\n", $hour, $employeesCount); +} +``` + +### 7. Расчет доплаты за ночную смену + +```php +$timetable = Timetable::findOne(1); +$baseSalary = $timetable->work_time * 250; // 250 руб/час + +if (Shift::isNight($timetable->shift_id)) { + $nightBonus = $baseSalary * 0.2; + $totalSalary = $baseSalary + $nightBonus; + echo "Базовая зарплата: {$baseSalary} руб\n"; + echo "Доплата за ночь: {$nightBonus} руб\n"; + echo "Итого: {$totalSalary} руб"; +} else { + echo "Зарплата: {$baseSalary} руб"; +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + SHIFT ||--o{ ADMIN_GROUP_SHIFT : "available for" + ADMIN_GROUP_SHIFT }o--|| ADMIN_GROUP : "position" + SHIFT ||--o{ TIMETABLE : "used in" + + SHIFT { + int id PK + string name "полное название" + string short_name "короткое название" + time start_time "начало" + float duration "полная длительность" + float work_time "рабочее время" + time end_time "окончание" + } + + ADMIN_GROUP_SHIFT { + int id PK + int shift_id FK + int admin_group_id FK + } + + ADMIN_GROUP { + int id PK + string name + } + + TIMETABLE { + int id PK + int shift_id FK + datetime datetime_start + datetime datetime_end + } +``` + +## Особенности реализации + +### 1. Кэширование + +Модель использует статическое кэширование: +- Метод `all()` кэширует результат в статической переменной +- Метод `getHours()` кэширует часы в свойстве объекта +- Повторные вызовы не обращаются к БД + +### 2. Автоматический расчет end_time + +Время окончания вычисляется автоматически: +- В методе `beforeValidate()` +- На основе `start_time` и `duration` +- Учитывает переход через полночь (% 24) + +### 3. Разделение времени + +Модель разделяет два типа времени: +- `duration` - полная длительность с перерывами +- `work_time` - чистое рабочее время +- Разница = длительность перерыва + +### 4. Ошибка в beforeValidate + +В коде есть ошибка формата `end_time`: +```php +// Текущий код (неправильно): +$this->end_time = $endMinute . ':' . $endHour . ':00'; + +// Должно быть: +$this->end_time = sprintf('%02d:%02d:00', $endHour, $endMinute); +``` + +## Связанные компоненты + +### Модели +- `Timetable` - использует Shift для планирования +- `TimetablePlan` - плановые смены +- `TimetableFact` - фактические смены +- `AdminGroup` - должности (связь many-to-many) +- `AdminGroupShift` - промежуточная таблица + +### Использование +- Планирование графиков работы +- Расчет рабочего времени с перерывами +- Группировка смен по часам +- Фильтрация доступных должностей + +## Примечания + +### Важные особенности + +1. **Статическое кэширование**: Метод `all()` выполняется один раз за запрос + +2. **Автоматические вычисления**: `end_time` рассчитывается автоматически + +3. **Константы типов**: Используйте `DAY` и `NIGHT` вместо магических чисел + +4. **Перерывы**: Разница между `duration` и `work_time` - это время перерыва + +### Рекомендации + +1. **Используйте all()**: Для получения списка смен используйте кэшированный метод + +2. **Проверяйте isNight()**: Для расчета доплат за ночные смены + +3. **Индексируйте**: Создайте индексы на `start_time` для быстрого поиска + +4. **Валидируйте duration**: Убедитесь, что `work_time <= duration` + +5. **Исправьте ошибку**: Поправьте формат `end_time` в `beforeValidate()` diff --git a/erp24/docs/models/ShiftRemains.md b/erp24/docs/models/ShiftRemains.md new file mode 100644 index 00000000..70008a9c --- /dev/null +++ b/erp24/docs/models/ShiftRemains.md @@ -0,0 +1,446 @@ +# Class: ShiftRemains + + +## Mindmap + +```mermaid +mindmap + root((ShiftRemains)) + Таблица БД + shift_remains + Свойства + id + int + shift_transfer_id + int + product_guid + string + retail_price + float + self_cost + float + remains_summ + float + Связи + Product + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ShiftRemains` представляет остатки товаров при передаче смены в системе ERP24. Хранит информацию о фактических остатках каждого товара, их стоимости и расхождениях с данными 1С. Используется для выявления недостач и излишков, расчета материальной ответственности и формирования документов передачи. Модель поддерживает два типа записей: временные (в процессе передачи) и архивные (после подтверждения). + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица базы данных + +`shift_remains` + +## Константы + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `TEMPORARY_RECORD` | 1 | Временная запись (в процессе передачи) | +| `ARCHIVE_RECORD` | 2 | Архивная запись (после подтверждения) | + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | - | Уникальный идентификатор записи | +| `shift_transfer_id` | int | да | ID смены по передаче | +| `group_label` | string(40) | нет | ID группы или алиас группы товаров (potted, wrap, matrix) | +| `product_guid` | string(36) | да | GUID продукта | +| `retail_price` | float | да | Цена розничная | +| `self_cost` | float | да | Себестоимость | +| `remains_summ` | float | да | Сумма остатков (недостача или излишек) | +| `remains_count` | float | да | Фактические остатки, количество | +| `fact_and_1c_diff` | float | да | Разница факт и по программе 1С (- недостача, + излишек, 0 - соответствие) | +| `remains_1c` | float | да | Остатки по 1С (текущие) | +| `type` | int | - | Тип записи (1=временная, 2=архивная) | + +## Связанные свойства (Relations) + +| Имя | Тип | Модель | Описание | +|-----|-----|--------|----------| +| `product` | Products1c | Products1c | Связанный товар | + +## Методы + +### `tableName()` + +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'shift_remains' + +--- + +### `rules()` + +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Все основные поля обязательны +- `shift_transfer_id` - целое число с значением по умолчанию null +- Все числовые поля (цены, суммы, количества) проверяются как number +- `group_label` - строка до 40 символов +- `product_guid` - строка ровно 36 символов (формат GUID) +- `type` по умолчанию устанавливается в `TEMPORARY_RECORD` (1) + +**Пример:** +```php +$remain = new ShiftRemains(); +$remain->shift_transfer_id = $transferId; +$remain->product_guid = $product->guid; +$remain->retail_price = 500.00; +$remain->self_cost = 300.00; +$remain->remains_count = 10; +$remain->remains_1c = 12; +$remain->fact_and_1c_diff = -2; // недостача 2 шт +$remain->remains_summ = -1000; // недостача на 1000 руб +$remain->validate(); +``` + +--- + +### `attributeLabels()` + +**Описание:** Возвращает метки для атрибутов модели (на английском). + +**Возвращает:** `array` - ассоциативный массив меток + +**Примечание:** Единственная метка на русском - 'type' => 'Тип записи' + +--- + +### `getProduct()` + +**Описание:** Определяет связь с товаром из справочника 1С. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `Products1c` через поле `product_guid`. Позволяет получить полную информацию о товаре (название, артикул, группа). + +**Пример:** +```php +$remain = ShiftRemains::findOne(1); +$product = $remain->product; +echo $product->name; // 'Роза Ред Наоми 60см' +echo $product->artikul; // '12345' +echo $product->price; // 500.00 +``` + +--- + +## Примеры использования + +### 1. Создание записи остатков при передаче + +```php +// При вводе фактических остатков +$transfer = ShiftTransfer::findOne($transferId); + +foreach ($products as $productData) { + $product = Products1c::findOne(['id' => $productData['guid']]); + + $remain = new ShiftRemains(); + $remain->shift_transfer_id = $transfer->id; + $remain->group_label = $product->group_alias; // 'potted', 'wrap', etc + $remain->product_guid = $product->id; + $remain->retail_price = $product->price; + $remain->self_cost = $product->self_cost; + $remain->remains_1c = $productData['count_1c']; // из 1С + $remain->remains_count = $productData['fact_count']; // факт от сотрудника + + // Расчет расхождения + $remain->fact_and_1c_diff = $remain->remains_count - $remain->remains_1c; + + // Расчет суммы расхождения по розничной цене + $remain->remains_summ = $remain->fact_and_1c_diff * $remain->retail_price; + + $remain->type = ShiftRemains::TEMPORARY_RECORD; + $remain->save(); +} +``` + +### 2. Анализ недостач и излишков + +```php +$transferId = 1; +$remains = ShiftRemains::find() + ->where(['shift_transfer_id' => $transferId]) + ->all(); + +$shortage = []; // недостачи +$surplus = []; // излишки +$matched = []; // соответствие + +foreach ($remains as $remain) { + if ($remain->fact_and_1c_diff < 0) { + $shortage[] = $remain; + } elseif ($remain->fact_and_1c_diff > 0) { + $surplus[] = $remain; + } else { + $matched[] = $remain; + } +} + +echo "Недостачи: " . count($shortage) . " позиций\n"; +echo "Излишки: " . count($surplus) . " позиций\n"; +echo "Соответствие: " . count($matched) . " позиций\n"; + +// Детальный отчет по недостачам +foreach ($shortage as $item) { + echo "{$item->product->name}: "; + echo "недостача {$item->fact_and_1c_diff} шт "; + echo "на сумму {$item->remains_summ} руб\n"; +} +``` + +### 3. Расчет общей суммы расхождений + +```php +$transferId = 1; +$remains = ShiftRemains::find() + ->where(['shift_transfer_id' => $transferId]) + ->all(); + +$totalShortageSum = 0; +$totalSurplusSum = 0; +$totalShortageCount = 0; +$totalSurplusCount = 0; + +foreach ($remains as $remain) { + if ($remain->fact_and_1c_diff < 0) { + // Недостача + $totalShortageSum += abs($remain->remains_summ); + $totalShortageCount += abs($remain->fact_and_1c_diff); + } elseif ($remain->fact_and_1c_diff > 0) { + // Излишек + $totalSurplusSum += $remain->remains_summ; + $totalSurplusCount += $remain->fact_and_1c_diff; + } +} + +echo "Недостача: {$totalShortageCount} шт на {$totalShortageSum} руб\n"; +echo "Излишек: {$totalSurplusCount} шт на {$totalSurplusSum} руб\n"; +echo "Итоговое расхождение: " . ($totalSurplusSum - $totalShortageSum) . " руб"; +``` + +### 4. Архивация записей после принятия + +```php +$transferId = 1; +$transfer = ShiftTransfer::findOne($transferId); + +if ($transfer->status_id === ShiftTransfer::STATUS_ID_ACCEPTED) { + // Переводим все временные записи в архивные + ShiftRemains::updateAll( + ['type' => ShiftRemains::ARCHIVE_RECORD], + ['shift_transfer_id' => $transferId, 'type' => ShiftRemains::TEMPORARY_RECORD] + ); + + echo "Записи переведены в архив"; +} +``` + +### 5. Фильтрация по группам товаров + +```php +$transferId = 1; + +// Только горшечка +$pottedRemains = ShiftRemains::find() + ->where([ + 'shift_transfer_id' => $transferId, + 'group_label' => 'potted' + ]) + ->all(); + +// Только срезка +$wrapRemains = ShiftRemains::find() + ->where([ + 'shift_transfer_id' => $transferId, + 'group_label' => 'wrap' + ]) + ->all(); + +// Несколько групп +$mainGroups = ShiftRemains::find() + ->where(['shift_transfer_id' => $transferId]) + ->andWhere(['in', 'group_label', ['potted', 'wrap', 'matrix']]) + ->all(); +``` + +### 6. Отчет по остаткам с товарами + +```php +$transferId = 1; +$remains = ShiftRemains::find() + ->where(['shift_transfer_id' => $transferId]) + ->with('product') // жадная загрузка товаров + ->all(); + +echo "Группа\tТовар\t1С\tФакт\tРазница\tСумма\n"; +echo str_repeat('-', 80) . "\n"; + +foreach ($remains as $remain) { + echo sprintf( + "%s\t%s\t%.1f\t%.1f\t%.1f\t%.2f\n", + $remain->group_label, + $remain->product->name, + $remain->remains_1c, + $remain->remains_count, + $remain->fact_and_1c_diff, + $remain->remains_summ + ); +} +``` + +### 7. Поиск критических расхождений + +```php +$transferId = 1; +$threshold = 5000; // критический порог в рублях + +$criticalRemains = ShiftRemains::find() + ->where(['shift_transfer_id' => $transferId]) + ->andWhere(['or', + ['<', 'remains_summ', -$threshold], // недостача > 5000 + ['>', 'remains_summ', $threshold] // излишек > 5000 + ]) + ->all(); + +if (!empty($criticalRemains)) { + echo "ВНИМАНИЕ! Критические расхождения:\n"; + foreach ($criticalRemains as $remain) { + $type = $remain->fact_and_1c_diff < 0 ? 'НЕДОСТАЧА' : 'ИЗЛИШЕК'; + echo "{$type}: {$remain->product->name} - {$remain->remains_summ} руб\n"; + } +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + SHIFT_REMAINS ||--|| SHIFT_TRANSFER : "belongs to" + SHIFT_REMAINS ||--|| PRODUCTS_1C : "references" + + SHIFT_REMAINS { + int id PK + int shift_transfer_id FK + string group_label "группа товара" + string product_guid FK "GUID товара" + float retail_price "розничная цена" + float self_cost "себестоимость" + float remains_count "факт кол-во" + float remains_1c "остатки 1С" + float fact_and_1c_diff "разница" + float remains_summ "сумма разницы" + int type "1=временная, 2=архив" + } + + SHIFT_TRANSFER { + int id PK + int status_id + string date + int end_shift_admin_id + } + + PRODUCTS_1C { + string id PK "GUID" + string name + float price + float self_cost + } +``` + +## Формулы расчета + +``` +fact_and_1c_diff = remains_count - remains_1c + +remains_summ = fact_and_1c_diff × retail_price + +Если fact_and_1c_diff < 0 → недостача (shortage) +Если fact_and_1c_diff > 0 → излишек (surplus) +Если fact_and_1c_diff = 0 → соответствие (matched) +``` + +## Особенности реализации + +### 1. Два типа записей + +Модель поддерживает два типа: +- **TEMPORARY_RECORD** (1) - временные записи в процессе передачи, могут редактироваться +- **ARCHIVE_RECORD** (2) - архивные записи после принятия, изменять нельзя + +### 2. Расчет расхождений + +Расхождения вычисляются по формуле: +- Положительное значение = излишек +- Отрицательное значение = недостача +- Ноль = полное соответствие + +### 3. Группировка товаров + +Товары группируются по `group_label`: +- `potted` - горшечные растения +- `wrap` - срезка +- `matrix` - матрица (аксессуары) +- `other_items` - прочие товары + +### 4. Связь с ценами + +Хранит два типа цен: +- `retail_price` - для расчета материальной ответственности +- `self_cost` - для управленческого учета + +## Связанные компоненты + +### Модели +- `ShiftTransfer` - передача смены (родитель) +- `Products1c` - справочник товаров +- `EqualizationRemains` - выравнивание остатков + +### Использование +- Контроль материальных ценностей +- Выявление недостач и излишков +- Расчет ответственности сотрудников +- Формирование документов передачи + +## Примечания + +### Важные особенности + +1. **Обязательная связь**: Каждая запись должна быть связана с передачей смены + +2. **GUID товара**: Используется GUID из 1С, а не числовой ID + +3. **Знак разницы**: Отрицательное значение `fact_and_1c_diff` означает недостачу + +4. **Архивация**: После принятия смены записи переводятся в архив + +### Рекомендации + +1. **Транзакции**: Создавайте записи в транзакции вместе с ShiftTransfer + +2. **Валидация**: Проверяйте корректность расчета `remains_summ` перед сохранением + +3. **Архивирование**: Не удаляйте архивные записи, они нужны для аудита + +4. **Индексы**: Создайте индексы на `shift_transfer_id` и `product_guid` + +5. **Группы**: Используйте стандартные алиасы групп для единообразия diff --git a/erp24/docs/models/ShiftTransfer.md b/erp24/docs/models/ShiftTransfer.md new file mode 100644 index 00000000..182d20a2 --- /dev/null +++ b/erp24/docs/models/ShiftTransfer.md @@ -0,0 +1,564 @@ +# Class: ShiftTransfer + + +## Mindmap + +```mermaid +mindmap + root((ShiftTransfer)) + Таблица БД + shift_transfer + Свойства + id + int + status_id + int + date + string + date_start + string + store_guid + string + end_shift_admin_id + int + Связи + ShiftRemains + 1:N ShiftRemains + EqualizationRemains + 1:N EqualizationRemains + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `ShiftTransfer` представляет процесс передачи смены между сотрудниками в системе ERP24. Реализует полный цикл приемки-передачи материальных ценностей при смене ответственного лица: от ввода фактических остатков товаров, через действия по замене (пересорт), до финального принятия смены с расчетом расхождений. Модель связана с остатками товаров (ShiftRemains) и используется для контроля материальной ответственности и выявления недостач/излишков. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица базы данных + +`shift_transfer` + +## Константы статусов + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_ID_INPUT_FACT_REMAINS` | 1 | Ввод фактических остатков | +| `STATUS_ID_TRANSFER_ACTIONS` | 2 | Действия по замене товара | +| `STATUS_OF_THE_FORMATION_OF_SURPLUSES_AND_SHORTAGES` | 5 | Формирования излишков и недостачи | +| `STATUS_ID_READY_TO_ACCEPT` | 3 | Готов к принятию | +| `STATUS_ID_ACCEPTED` | 4 | Принято | + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | - | Уникальный идентификатор передачи | +| `status_id` | int | да | ID статуса процесса передачи | +| `date` | string(36) | да | Дата передачи смены | +| `date_start` | string(datetime) | да | Время старта процедуры | +| `date_end` | string(datetime) | нет | Время принятия смены | +| `store_guid` | string(36) | да | GUID магазина | +| `end_shift_admin_id` | int | да | ID сотрудника, передающего смену | +| `start_shift_admin_id` | int | нет | ID сотрудника, принимающего смену | +| `goods_transfer_summ` | float | нет | Сумма, полученная в результате замены товара | +| `goods_transfer_count` | float | нет | Количество замен общее, шт | +| `discrepancy_pieces` | float | нет | Расхождение факта, шт | +| `discrepancy_rubles` | float | нет | Расхождение факта, руб | +| `comment` | string | нет | Комментарий принимающей стороны | +| `report` | string | нет | Отчет в формате HTML/TXT/JSON | +| `product_groups` | string | нет | Список alias выбранных групп товаров через запятую | +| `error_text` | string | нет | Текст ошибки при возникновении проблем | +| `shiftRemainsCopy` | mixed | - | Копия остатков (публичное) | +| `groups1` | array | - | Другие группы товаров (публичное) | +| `groups2` | array | - | Основная группа (публичное) | + +## Связанные свойства (Relations) + +| Имя | Тип | Модель | Описание | +|-----|-----|--------|----------| +| `shiftRemains` | ShiftRemains[] | ShiftRemains | Остатки товаров при передаче | +| `equalizationRemains` | EqualizationRemains[] | EqualizationRemains | Записи выравнивания остатков | + +## Методы + +### `statusNames()` + +**Описание:** Возвращает справочник названий статусов процесса передачи. + +**Возвращает:** `array` - массив [id => название] + +**Логика работы:** +Статический метод возвращает ассоциативный массив с названиями всех статусов передачи смены на русском языке. + +**Пример:** +```php +$statuses = ShiftTransfer::statusNames(); +echo $statuses[ShiftTransfer::STATUS_ID_ACCEPTED]; // 'Принято' +``` + +--- + +### `tableName()` + +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'shift_transfer' + +--- + +### `rules()` + +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +Устанавливает правила валидации: +- Обязательные поля: `status_id`, `date`, `date_start`, `store_guid`, `end_shift_admin_id` +- Целочисленные поля с значением по умолчанию null +- Числовые поля для сумм и количеств +- Строковые поля для комментариев и отчетов +- Безопасные поля для дат и групп товаров +- Ограничения по длине для GUID (36 символов) + +**Пример:** +```php +$transfer = new ShiftTransfer(); +$transfer->status_id = ShiftTransfer::STATUS_ID_INPUT_FACT_REMAINS; +$transfer->date = date('Y-m-d'); +$transfer->date_start = date('Y-m-d H:i:s'); +$transfer->store_guid = $store->guid; +$transfer->end_shift_admin_id = $_SESSION['admin_id']; +$transfer->validate(); +``` + +--- + +### `attributeLabels()` + +**Описание:** Возвращает человекочитаемые метки для атрибутов на русском языке. + +**Возвращает:** `array` - ассоциативный массив меток + +--- + +### `getShiftRemains()` + +**Описание:** Определяет связь с остатками товаров при передаче смены. + +**Возвращает:** `ActiveQuery` - запрос связи hasMany + +**Логика работы:** +Создает связь один-ко-многим с таблицей `ShiftRemains` через поле `shift_transfer_id`. Возвращает все записи об остатках товаров для данной передачи. + +**Пример:** +```php +$transfer = ShiftTransfer::findOne(1); +foreach ($transfer->shiftRemains as $remain) { + echo "{$remain->product->name}: {$remain->remains_count} шт\n"; + echo "Разница с 1С: {$remain->fact_and_1c_diff}\n"; +} +``` + +--- + +### `getEqualizationRemains()` + +**Описание:** Определяет связь с записями выравнивания остатков. + +**Возвращает:** `ActiveQuery` - запрос связи hasMany + +**Логика работы:** +Создает связь один-ко-многим с таблицей `EqualizationRemains` через поле `shift_transfer_id`. Используется для хранения действий по выравниванию недостач/излишков. + +--- + +### `setGroups()` + +**Описание:** Разбивает строку групп товаров на два массива. + +**Логика работы:** +1. Получает строку `product_groups`, разделяет по запятой +2. Фильтрует группы: + - `groups1` - все группы кроме 'other_items' + - `groups2` - только группа 'other_items' +3. Устанавливает публичные свойства `groups1` и `groups2` + +**Вызовы сторонних методов:** +- `explode()` - разделение строки +- `array_filter()` - фильтрация массива + +**Пример:** +```php +$transfer->product_groups = 'potted,wrap,other_items'; +$transfer->setGroups(); +// $transfer->groups1 = ['potted', 'wrap'] +// $transfer->groups2 = ['other_items'] +``` + +--- + +### `setProductGroups()` + +**Описание:** Объединяет массивы групп товаров обратно в строку. + +**Логика работы:** +1. Объединяет `groups1` и `groups2` в один массив +2. Обрабатывает пустые массивы (заменяет на []) +3. Объединяет элементы через запятую +4. Устанавливает результат в `product_groups` + +**Вызовы сторонних методов:** +- `array_merge()` - объединение массивов +- `implode()` - объединение в строку +- `empty()` - проверка пустоты + +**Пример:** +```php +$transfer->groups1 = ['potted', 'wrap']; +$transfer->groups2 = ['other_items']; +$transfer->setProductGroups(); +// $transfer->product_groups = 'potted,wrap,other_items' +``` + +--- + +## Примеры использования + +### 1. Создание новой передачи смены + +```php +$transfer = new ShiftTransfer(); +$transfer->status_id = ShiftTransfer::STATUS_ID_INPUT_FACT_REMAINS; +$transfer->date = date('Y-m-d'); +$transfer->date_start = date('Y-m-d H:i:s'); +$transfer->store_guid = $store->guid; +$transfer->end_shift_admin_id = $currentAdmin->id; // передающий +$transfer->product_groups = 'potted,wrap,matrix'; + +if ($transfer->save()) { + echo "Передача смены создана. ID: {$transfer->id}"; + // Переход к вводу фактических остатков +} +``` + +### 2. Ввод фактических остатков + +```php +$transfer = ShiftTransfer::findOne($id); + +// Проверка статуса +if ($transfer->status_id !== ShiftTransfer::STATUS_ID_INPUT_FACT_REMAINS) { + throw new Exception('Неверный статус для ввода остатков'); +} + +// Добавление остатков товаров +foreach ($products as $product) { + $remain = new ShiftRemains(); + $remain->shift_transfer_id = $transfer->id; + $remain->product_guid = $product['guid']; + $remain->retail_price = $product['price']; + $remain->self_cost = $product['cost']; + $remain->remains_count = $product['fact_count']; + $remain->remains_1c = $product['count_1c']; + $remain->fact_and_1c_diff = $product['fact_count'] - $product['count_1c']; + $remain->remains_summ = $remain->fact_and_1c_diff * $remain->retail_price; + $remain->save(); +} + +// Переход к следующему статусу +$transfer->status_id = ShiftTransfer::STATUS_ID_TRANSFER_ACTIONS; +$transfer->save(); +``` + +### 3. Процесс действий по замене товара (пересорт) + +```php +$transfer = ShiftTransfer::findOne($id); + +if ($transfer->status_id !== ShiftTransfer::STATUS_ID_TRANSFER_ACTIONS) { + throw new Exception('Неверный статус для действий по замене'); +} + +// Расчет общей суммы замен +$totalSumm = 0; +$totalCount = 0; + +foreach ($transfer->shiftRemains as $remain) { + if ($remain->fact_and_1c_diff != 0) { + // Обработка замены товара + $totalSumm += abs($remain->remains_summ); + $totalCount += abs($remain->fact_and_1c_diff); + } +} + +$transfer->goods_transfer_summ = $totalSumm; +$transfer->goods_transfer_count = $totalCount; +$transfer->status_id = ShiftTransfer::STATUS_OF_THE_FORMATION_OF_SURPLUSES_AND_SHORTAGES; +$transfer->save(); +``` + +### 4. Формирование излишков и недостач + +```php +$transfer = ShiftTransfer::findOne($id); + +$surplus = []; // излишки +$shortage = []; // недостачи +$discrepancyPieces = 0; +$discrepancyRubles = 0; + +foreach ($transfer->shiftRemains as $remain) { + if ($remain->fact_and_1c_diff > 0) { + $surplus[] = $remain; + $discrepancyPieces += $remain->fact_and_1c_diff; + $discrepancyRubles += $remain->remains_summ; + } elseif ($remain->fact_and_1c_diff < 0) { + $shortage[] = $remain; + $discrepancyPieces += abs($remain->fact_and_1c_diff); + $discrepancyRubles += abs($remain->remains_summ); + } +} + +$transfer->discrepancy_pieces = $discrepancyPieces; +$transfer->discrepancy_rubles = $discrepancyRubles; +$transfer->status_id = ShiftTransfer::STATUS_ID_READY_TO_ACCEPT; +$transfer->save(); + +// Генерация отчета +$report = "Излишки: " . count($surplus) . " позиций\n"; +$report .= "Недостачи: " . count($shortage) . " позиций\n"; +$report .= "Общее расхождение: {$discrepancyPieces} шт, {$discrepancyRubles} руб"; +$transfer->report = $report; +$transfer->save(); +``` + +### 5. Принятие смены + +```php +$transfer = ShiftTransfer::findOne($id); + +if ($transfer->status_id !== ShiftTransfer::STATUS_ID_READY_TO_ACCEPT) { + throw new Exception('Смена не готова к принятию'); +} + +// Принимающий сотрудник оставляет комментарий +$transfer->start_shift_admin_id = $newAdmin->id; +$transfer->comment = 'Принято с замечаниями: расхождение по горшечке'; +$transfer->date_end = date('Y-m-d H:i:s'); +$transfer->status_id = ShiftTransfer::STATUS_ID_ACCEPTED; + +if ($transfer->save()) { + echo "Смена принята"; + // Отправка уведомлений + // Создание документов в 1С +} +``` + +### 6. Отчет по передачам за период + +```php +$dateFrom = '2025-11-01'; +$dateTo = '2025-11-30'; + +$transfers = ShiftTransfer::find() + ->where(['>=', 'date', $dateFrom]) + ->andWhere(['<=', 'date', $dateTo]) + ->andWhere(['status_id' => ShiftTransfer::STATUS_ID_ACCEPTED]) + ->all(); + +foreach ($transfers as $transfer) { + echo "Дата: {$transfer->date}\n"; + echo "Магазин: {$transfer->store_guid}\n"; + echo "Передал: ID {$transfer->end_shift_admin_id}\n"; + echo "Принял: ID {$transfer->start_shift_admin_id}\n"; + echo "Расхождение: {$transfer->discrepancy_pieces} шт, {$transfer->discrepancy_rubles} руб\n"; + echo "Статус: " . ShiftTransfer::statusNames()[$transfer->status_id] . "\n\n"; +} +``` + +### 7. Работа с группами товаров + +```php +$transfer = ShiftTransfer::findOne($id); +$transfer->product_groups = 'potted,wrap,matrix,other_items'; +$transfer->setGroups(); + +echo "Основные группы:\n"; +foreach ($transfer->groups1 as $group) { + echo "- {$group}\n"; +} + +echo "Прочие товары:\n"; +foreach ($transfer->groups2 as $group) { + echo "- {$group}\n"; +} + +// Изменение групп +$transfer->groups1 = ['potted', 'wrap']; +$transfer->groups2 = ['other_items']; +$transfer->setProductGroups(); +echo "Новые группы: {$transfer->product_groups}"; +``` + +## Диаграмма процесса передачи смены + +```mermaid +stateDiagram-v2 + [*] --> InputRemains: Создание передачи + InputRemains --> TransferActions: Остатки введены + TransferActions --> Formation: Замены выполнены + Formation --> ReadyToAccept: Расхождения подсчитаны + ReadyToAccept --> Accepted: Смена принята + Accepted --> [*] + + note right of InputRemains + STATUS_ID_INPUT_FACT_REMAINS (1) + Ввод фактических остатков товаров + end note + + note right of TransferActions + STATUS_ID_TRANSFER_ACTIONS (2) + Действия по замене/пересорту + end note + + note right of Formation + STATUS_OF_THE_FORMATION... (5) + Расчет излишков и недостач + end note + + note right of ReadyToAccept + STATUS_ID_READY_TO_ACCEPT (3) + Ожидание принятия сменщиком + end note + + note right of Accepted + STATUS_ID_ACCEPTED (4) + Смена принята, процесс завершен + end note +``` + +## Диаграмма связей + +```mermaid +erDiagram + SHIFT_TRANSFER ||--o{ SHIFT_REMAINS : "has" + SHIFT_TRANSFER ||--o{ EQUALIZATION_REMAINS : "has" + SHIFT_TRANSFER ||--|| ADMIN : "transferred by" + SHIFT_TRANSFER ||--o| ADMIN : "accepted by" + + SHIFT_TRANSFER { + int id PK + int status_id "статус процесса" + string date "дата передачи" + datetime date_start "время начала" + datetime date_end "время принятия" + string store_guid "магазин" + int end_shift_admin_id FK "передающий" + int start_shift_admin_id FK "принимающий" + float goods_transfer_summ "сумма замен" + float goods_transfer_count "количество замен" + float discrepancy_pieces "расхождение шт" + float discrepancy_rubles "расхождение руб" + string comment "комментарий" + string report "отчет" + string product_groups "группы товаров" + } + + SHIFT_REMAINS { + int id PK + int shift_transfer_id FK + string product_guid + float remains_count + float fact_and_1c_diff + } + + EQUALIZATION_REMAINS { + int id PK + int shift_transfer_id FK + } + + ADMIN { + int id PK + string name_full + } +``` + +## Особенности реализации + +### 1. Многостадийный процесс + +Передача смены проходит через 5 статусов: +1. Ввод остатков +2. Действия по замене +3. Готов к принятию +4. Принято +5. Формирование расхождений + +### 2. Связь с остатками + +Каждая передача связана с множеством записей ShiftRemains: +- Хранит фактические остатки товаров +- Вычисляет расхождения с 1С +- Рассчитывает суммы недостач/излишков + +### 3. Разделение ролей + +Модель разделяет ответственность: +- `end_shift_admin_id` - передающий смену (заканчивающий) +- `start_shift_admin_id` - принимающий смену (начинающий) + +### 4. Группировка товаров + +Товары группируются для удобства: +- `groups1` - основные группы (горшечка, срезка, матрица) +- `groups2` - прочие товары (other_items) + +## Связанные компоненты + +### Модели +- `ShiftRemains` - остатки товаров +- `EqualizationRemains` - выравнивание +- `Admin` - сотрудники +- `Products1c` - товары + +### Контроллеры +- `ShiftTransferController` - управление передачами +- `RemainsController` - работа с остатками + +### Сервисы +- `TransferService` - бизнес-логика передач +- `RemainsService` - расчет остатков +- `1CService` - синхронизация с 1С + +## Примечания + +### Важные особенности + +1. **Последовательность статусов**: Нельзя пропускать этапы процесса + +2. **Обязательное принятие**: Смена должна быть принята другим сотрудником + +3. **Расчет расхождений**: Автоматический расчет недостач/излишков + +4. **Связь с 1С**: Данные об остатках берутся из 1С для сравнения + +### Рекомендации + +1. **Проверяйте статус**: Всегда проверяйте текущий статус перед операциями + +2. **Транзакции**: Используйте транзакции при изменении статуса и остатков + +3. **Валидация**: Проверяйте корректность остатков перед принятием + +4. **Аудит**: Логируйте все изменения статусов для аудита + +5. **Уведомления**: Отправляйте уведомления при смене статуса diff --git a/erp24/docs/models/ShipmentProviders.md b/erp24/docs/models/ShipmentProviders.md new file mode 100644 index 00000000..774c4797 --- /dev/null +++ b/erp24/docs/models/ShipmentProviders.md @@ -0,0 +1,213 @@ +# Класс: ShipmentProviders + + +## Mindmap + +```mermaid +mindmap + root((ShipmentProviders)) + Таблица БД + shipment_providers + Свойства + id + int + name + string + guid + string + valuta + string + contragent_id + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник поставщиков товаров в ERP24. Хранит информацию о поставщиках для учёта поставок, с привязкой к контрагентам в 1С и указанием валюты. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`shipment_providers` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(255) | Название поставщика | +| `guid` | varchar(36) | GUID поставщика | +| `valuta` | varchar(6) | Код валюты | +| `contragent_id` | varchar(36) | GUID контрагента из 1С | + +## Статические методы + +### getNames($orderBy = null) +**Описание:** Возвращает массив названий поставщиков для выпадающего списка. + +**Параметры:** +- `$orderBy` (string|null) — Порядок сортировки: `orderByNameASC`, `orderByNameDESC` + +**Возвращает:** `array` — массив [id => name] + +**Пример:** +```php +$providers = ShipmentProviders::getNames('orderByNameASC'); +// [1 => 'Поставщик А', 2 => 'Поставщик Б', ...] +``` + +## Диаграмма связей + +```mermaid +erDiagram + ShipmentProviders { + int id PK + varchar name + varchar guid UK + varchar valuta + varchar contragent_id FK + } + + Shipment { + int id PK + int provider_id FK + datetime date + } + + Contragent { + varchar id PK + varchar name + } + + ShipmentProviders ||--o{ Shipment : "provider_id" + Contragent ||--o{ ShipmentProviders : "contragent_id" +``` + +## Примеры использования + +### Создание поставщика +```php +$provider = new ShipmentProviders(); +$provider->name = 'ООО "Цветочная база"'; +$provider->guid = '12345678-1234-1234-1234-123456789abc'; +$provider->valuta = 'RUB'; +$provider->contragent_id = 'contragent-guid-from-1c'; +$provider->save(); +``` + +### Получение всех поставщиков +```php +$providers = ShipmentProviders::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($providers as $provider) { + echo "{$provider->name} ({$provider->valuta})\n"; +} +``` + +### Формирование списка для выбора +```php +// С сортировкой по имени +$providersList = ShipmentProviders::getNames('orderByNameASC'); + +echo Html::dropDownList('provider_id', null, $providersList, [ + 'prompt' => 'Выберите поставщика' +]); +``` + +### Поиск по GUID +```php +$provider = ShipmentProviders::find() + ->where(['guid' => $guid1c]) + ->one(); +``` + +### Поиск по контрагенту +```php +$provider = ShipmentProviders::find() + ->where(['contragent_id' => $contragentGuid]) + ->one(); + +if ($provider) { + echo "Поставщик: {$provider->name}"; +} +``` + +### Группировка по валютам +```php +$byValuta = ShipmentProviders::find() + ->select(['valuta', 'COUNT(*) as count']) + ->groupBy('valuta') + ->asArray() + ->all(); + +foreach ($byValuta as $group) { + echo "{$group['valuta']}: {$group['count']} поставщиков\n"; +} +``` + +### Поиск по названию +```php +$providers = ShipmentProviders::find() + ->where(['like', 'name', 'цвет']) + ->all(); +``` + +### Импорт из 1С +```php +$providersFrom1c = [ + ['guid' => 'guid1', 'name' => 'Поставщик 1', 'valuta' => 'RUB', 'contragent_id' => 'cg1'], + ['guid' => 'guid2', 'name' => 'Поставщик 2', 'valuta' => 'USD', 'contragent_id' => 'cg2'], +]; + +foreach ($providersFrom1c as $data) { + $existing = ShipmentProviders::find() + ->where(['guid' => $data['guid']]) + ->one(); + + if ($existing) { + $existing->setAttributes($data); + $existing->save(); + } else { + $provider = new ShipmentProviders(); + $provider->setAttributes($data); + $provider->save(); + } +} +``` + +### Фильтрация по валюте +```php +$rubProviders = ShipmentProviders::find() + ->where(['valuta' => 'RUB']) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `guid` | required, string (max 36) | +| `valuta` | required, string (max 6) | +| `contragent_id` | required, string (max 36) | + +## Связанные модели + +- [Shipment](./Shipment.md) — поставки +- Контрагенты 1С (contragent_id) + +## Особенности реализации + +1. **GUID идентификация**: guid для синхронизации с 1С +2. **Мультивалютность**: valuta для поставщиков в разных валютах +3. **Связь с контрагентом**: contragent_id для интеграции с 1С +4. **Статический метод getNames**: Удобное получение списка для UI +5. **Сортировка**: Поддержка ASC/DESC сортировки по имени diff --git a/erp24/docs/models/ShipmentProvidersSearch.md b/erp24/docs/models/ShipmentProvidersSearch.md new file mode 100644 index 00000000..fdc6a4f8 --- /dev/null +++ b/erp24/docs/models/ShipmentProvidersSearch.md @@ -0,0 +1,155 @@ +# Класс: ShipmentProvidersSearch + + +## Mindmap + +```mermaid +mindmap + root((ShipmentProvidersSearch)) + Таблица БД + ActiveRecord + Наследование + extends ShipmentProviders +``` + +## Назначение +Search-модель для поиска и фильтрации поставщиков доставки в ERP24. Стандартная Gii-модель для управления справочником логистических контрагентов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`ShipmentProviders` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name`, `guid`, `valuta`, `contragent_id` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска поставщиков доставки. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос ShipmentProviders::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id + - like: name, guid, valuta, contragent_id + +## Диаграмма структуры + +```mermaid +erDiagram + ShipmentProviders { + int id PK + varchar name + varchar guid + varchar valuta + varchar contragent_id + } +``` + +## Диаграмма поиска + +```mermaid +flowchart TD + A[ShipmentProvidersSearch] --> B[find] + B --> C[ActiveDataProvider] + + D[Фильтры] --> E[id - точное] + D --> F[name - like] + D --> G[guid - like] + D --> H[valuta - like] + D --> I[contragent_id - like] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new ShipmentProvidersSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new ShipmentProvidersSearch(); +$dataProvider = $searchModel->search([ + 'ShipmentProvidersSearch' => [ + 'name' => 'CDEK', + ] +]); +``` + +### Поиск по валюте +```php +$searchModel = new ShipmentProvidersSearch(); +$dataProvider = $searchModel->search([ + 'ShipmentProvidersSearch' => [ + 'valuta' => 'RUB', + ] +]); +``` + +### Поиск по контрагенту +```php +$searchModel = new ShipmentProvidersSearch(); +$dataProvider = $searchModel->search([ + 'ShipmentProvidersSearch' => [ + 'contragent_id' => 'abc-123', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'guid', + 'valuta', + 'contragent_id', + ], +]) ?> +``` + +## Связанные модели + +- [ShipmentProviders](./ShipmentProviders.md) — базовая модель поставщиков +- [StoreOrders](./StoreOrders.md) — заказы магазинов + +## Особенности реализации + +1. **Справочник доставки**: Поставщики логистических услуг +2. **GUID интеграции**: Идентификатор для внешних систем (1С) +3. **Валюта**: valuta для мультивалютных операций +4. **Контрагент**: contragent_id связь с 1С +5. **like вместо ilike**: Регистрозависимый поиск diff --git a/erp24/docs/models/StoreBalance.md b/erp24/docs/models/StoreBalance.md new file mode 100644 index 00000000..ff43c8cd --- /dev/null +++ b/erp24/docs/models/StoreBalance.md @@ -0,0 +1,325 @@ +# Class: StoreBalance + + +## Mindmap + +```mermaid +mindmap + root((StoreBalance)) + Таблица БД + store_balance + Свойства + id + int + store_id + string + shift_transfer_id + int + date + string + amount + float + status_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StoreBalance` представляет учёт балансовых операций магазина - недостачи, излишки, замены товаров и компенсации. Она используется для фиксации отклонений фактических остатков от учётных, контроля материальной ответственности и анализа причин расхождений. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_balance` + +## Константы + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_NEW` | 0 | Новая запись (требует проверки) | +| `REPLACEMENT_ACTIONS` | 2 | Тип записи: действия по замене товаров | + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи | +| `store_id` | string(36) | да | GUID магазина | +| `shift_transfer_id` | int | да | ID смены по передаче остатков | +| `date` | timestamp | да | Дата и время внесения записи | +| `amount` | float | да | Сумма остатков (недостача/излишек) | +| `status_id` | int | да | Статус записи (0=новая, 1=в работе, 2=проверено) | +| `comment` | text | нет | Комментарий смены | +| `json` | text | нет | JSON с товарами и операциями замены | +| `replace_articule` | text | нет | Артикулы заменённых товаров (ID1-ID2;ID3-ID4) | +| `comment_controler` | text | нет | Комментарий контролёра | +| `controler_id` | int | нет | ID сотрудника-контролёра | +| `type_id` | int | да | Тип записи (-1=недостача, 1=излишек, 2=замена, 5=компенсация) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_balance' + +--- + +### `setData($shiftTransfer)` +**Описание:** Статический метод для создания записей баланса на основе данных передачи смены. + +**Параметры:** +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** `void` + +**Логика работы:** + +1. **Обработка замен товаров (EqualizationRemains):** + - Проверяет наличие данных о замене товаров для смены + - Получает детальную информацию о заменах с ценами + - Формирует строку артикулов в формате "ID1=>ID2;ID3=>ID4" + - Рассчитывает общую сумму баланса + - Создаёт запись с типом `REPLACEMENT_ACTIONS` + +2. **Обработка приходов (WaybillIncoming):** + - Проверяет наличие накладных прихода для смены + - Получает список товаров прихода + - Суммирует общую стоимость прихода + - Создаёт запись о приходе + +3. **Обработка списаний (WaybillWriteOffs):** + - Проверяет наличие накладных списания для смены + - Получает список списанных товаров + - Рассчитывает сумму списания (со знаком минус) + - Создаёт запись о списании + +4. **Сохранение всех записей:** + - Сохраняет все созданные записи в транзакции + - Обрабатывает ошибки через Exception + +**Вызываемые методы:** +- `EqualizationRemains::find()` - поиск замен товаров +- `WaybillIncoming::find()` - поиск приходов +- `WaybillWriteOffs::find()` - поиск списаний +- `WaybillIncomingProducts::find()` - товары прихода +- `WaybillWriteOffsProducts::find()` - товары списания + +**Пример:** +```php +$shiftTransfer = ShiftTransfer::findOne(123); +StoreBalance::setData($shiftTransfer); +// Создастся 1-3 записи в зависимости от операций в смене +``` + +--- + +### `rules()` +**Описание:** Определяет правила валидации. + +**Правила:** +- Обязательные: store_id, shift_transfer_id, date, amount, status_id, type_id +- Целые числа: shift_transfer_id, status_id, controler_id, type_id +- Число: amount +- Строка (36): store_id (GUID) +- Текст: comment, json, replace_articule, comment_controler + +--- + +### `attributeLabels()` +**Описание:** Возвращает русские метки атрибутов. + +--- + +## Примеры использования + +### 1. Автоматическое создание записей баланса +```php +$shiftTransfer = ShiftTransfer::findOne(123); + +try { + StoreBalance::setData($shiftTransfer); + echo "Записи баланса созданы"; +} catch (Exception $e) { + echo "Ошибка: " . $e->getMessage(); +} +``` + +### 2. Получение недостач магазина +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +$shortages = StoreBalance::find() + ->where(['store_id' => $storeGuid]) + ->andWhere(['<', 'amount', 0]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +echo "Недостачи:\n"; +foreach ($shortages as $shortage) { + echo "Дата: {$shortage->date}, Сумма: {$shortage->amount} руб.\n"; +} +``` + +### 3. Обработка записи контролёром +```php +$balance = StoreBalance::findOne(10); + +if ($balance && $balance->status_id == StoreBalance::STATUS_NEW) { + $balance->status_id = 1; // В работе + $balance->controler_id = Yii::$app->user->id; + $balance->comment_controler = 'Требуется разбирательство с МОЛ'; + $balance->save(); +} +``` + +### 4. Анализ замен товаров +```php +$balance = StoreBalance::findOne([ + 'type_id' => StoreBalance::REPLACEMENT_ACTIONS +]); + +if ($balance && $balance->json) { + $replacements = json_decode($balance->json, true); + + foreach ($replacements as $item) { + echo "Товар: {$item['product_name']}\n"; + echo "Заменён на: {$item['product_replacement_name']}\n"; + echo "Баланс: {$item['balance']} руб.\n\n"; + } +} +``` + +### 5. Отчёт по балансу магазина за период +```php +$storeGuid = '550e8400-...'; +$dateFrom = '2024-01-01'; +$dateTo = '2024-01-31'; + +$balances = StoreBalance::find() + ->where(['store_id' => $storeGuid]) + ->andWhere(['between', 'date', $dateFrom, $dateTo]) + ->all(); + +$totalShortage = 0; +$totalSurplus = 0; + +foreach ($balances as $balance) { + if ($balance->amount < 0) { + $totalShortage += abs($balance->amount); + } else { + $totalSurplus += $balance->amount; + } +} + +echo "Недостача: {$totalShortage} руб.\n"; +echo "Излишек: {$totalSurplus} руб.\n"; +echo "Баланс: " . ($totalSurplus - $totalShortage) . " руб.\n"; +``` + +### 6. Получение непроверенных записей +```php +$newBalances = StoreBalance::find() + ->where(['status_id' => StoreBalance::STATUS_NEW]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +echo "Записи, требующие проверки:\n"; +foreach ($newBalances as $balance) { + echo "ID: {$balance->id}, Дата: {$balance->date}, Сумма: {$balance->amount}\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_BALANCE ||--|| SHIFT_TRANSFER : "created from" + STORE_BALANCE ||--o| ADMIN : "controlled by" + STORE_BALANCE ||--o{ EQUALIZATION_REMAINS : "has replacements" + STORE_BALANCE ||--o{ WAYBILL_INCOMING : "has incomings" + STORE_BALANCE ||--o{ WAYBILL_WRITE_OFFS : "has write-offs" + + STORE_BALANCE { + int id PK + string store_id FK "GUID магазина" + int shift_transfer_id FK "ID смены" + timestamp date "Дата" + float amount "Сумма" + int status_id "Статус" + text comment "Комментарий" + text json "JSON данные" + text replace_articule "Артикулы замен" + text comment_controler "Комментарий контролёра" + int controler_id FK "ID контролёра" + int type_id "Тип записи" + } + + SHIFT_TRANSFER { + int id PK + string store_guid FK + timestamp date + } + + ADMIN { + int id PK + string name_full + } +``` + +--- + +## Особенности реализации + +### Автоматическое создание записей +Метод `setData()` автоматически создаёт 1-3 записи баланса на основе операций смены (замены, приходы, списания). + +### JSON хранение деталей +Поле `json` хранит детальную информацию о товарах, ценах и количествах для последующего анализа. + +### Типы записей +- `-1` - недостача +- `1` - излишек +- `2` - замена товаров +- `5` - компенсация от контролёра + +### Статусы +- `0` (STATUS_NEW) - новая запись +- `1` - в работе +- `2` - проверено + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Контроль МОЛ** - материальная ответственность сотрудников +2. **Аудит** - проверка причин расхождений +3. **Аналитика** - выявление проблемных точек +4. **Финансы** - учёт потерь и компенсаций + +--- + +## Связанные компоненты + +- **Модели:** ShiftTransfer, EqualizationRemains, WaybillIncoming, WaybillWriteOffs, Admin +- **Сервисы:** BalanceService, ShiftService +- **Контроллеры:** StoreBalanceController + +--- + +## Примечания + +**Рекомендации:** +1. Регулярно проверять новые записи +2. Анализировать причины недостач +3. Контролировать процесс замен товаров +4. Вести учёт компенсаций diff --git a/erp24/docs/models/StoreCityList.md b/erp24/docs/models/StoreCityList.md new file mode 100644 index 00000000..87bcf10f --- /dev/null +++ b/erp24/docs/models/StoreCityList.md @@ -0,0 +1,287 @@ +# Класс: StoreCityList + + +## Mindmap + +```mermaid +mindmap + root((StoreCityList)) + Таблица БД + store_city_list + Свойства + id + int + name + string + type + string + created_at + string + parent + StoreCityList + Связи + Parent + 1:1 StoreCityList + StoreCityLists + 1:N StoreCityList + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Иерархический справочник географических объектов для магазинов в ERP24. Реализует древовидную структуру: Регион -> Город -> Район для географической классификации точек продаж. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`store_city_list` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `parent_id` | int / null | FK на родительский элемент | +| `name` | varchar(255) | Название (регион, город, район) | +| `type` | varchar(255) | Тип: region, city, district | +| `created_at` | datetime | Дата создания записи | + +## Константы типов + +```php +const TYPE_REGION = 'region'; // Регион +const TYPE_CITY = 'city'; // Город +const TYPE_DISTRICT = 'district'; // Район +``` + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getParent()` | hasOne | StoreCityList | Родительский элемент | +| `getStoreCityLists()` | hasMany | StoreCityList | Дочерние элементы | + +## Статические методы + +### getTypes() +**Описание:** Возвращает массив типов для выпадающего списка. + +**Возвращает:** `array` — ['region' => 'Region', 'city' => 'City', 'district' => 'District'] + +## Диаграмма связей + +```mermaid +erDiagram + StoreCityList { + int id PK + int parent_id FK + varchar name + varchar type + datetime created_at + } + + CityStore { + int id PK + int city_list_id FK + } + + StoreCityList ||--o{ StoreCityList : "parent_id" + StoreCityList ||--o{ CityStore : "city_list_id" +``` + +## Диаграмма иерархии + +```mermaid +flowchart TD + subgraph Уровень 1: Регионы + A[Московская область
    type=region] + B[Санкт-Петербург
    type=region] + end + + subgraph Уровень 2: Города + C[Москва
    type=city] + D[Химки
    type=city] + E[СПб Центр
    type=city] + end + + subgraph Уровень 3: Районы + F[ЦАО
    type=district] + G[САО
    type=district] + H[Левобережный
    type=district] + end + + A --> C + A --> D + B --> E + C --> F + C --> G + D --> H +``` + +## Примеры использования + +### Создание региона +```php +$region = new StoreCityList(); +$region->name = 'Московская область'; +$region->type = StoreCityList::TYPE_REGION; +$region->parent_id = null; // Корневой элемент +$region->save(); +``` + +### Создание города в регионе +```php +$city = new StoreCityList(); +$city->name = 'Москва'; +$city->type = StoreCityList::TYPE_CITY; +$city->parent_id = $region->id; +$city->save(); +``` + +### Создание района в городе +```php +$district = new StoreCityList(); +$district->name = 'ЦАО'; +$district->type = StoreCityList::TYPE_DISTRICT; +$district->parent_id = $city->id; +$district->save(); +``` + +### Получение всех регионов +```php +$regions = StoreCityList::find() + ->where(['type' => StoreCityList::TYPE_REGION]) + ->orderBy(['name' => SORT_ASC]) + ->all(); +``` + +### Получение городов региона +```php +$cities = StoreCityList::find() + ->where([ + 'parent_id' => $regionId, + 'type' => StoreCityList::TYPE_CITY + ]) + ->all(); +``` + +### Получение дочерних элементов +```php +$item = StoreCityList::findOne($id); + +if ($item) { + $children = $item->storeCityLists; + + foreach ($children as $child) { + echo "{$child->name} ({$child->type})\n"; + } +} +``` + +### Получение полного пути +```php +function getFullPath($item) +{ + $path = [$item->name]; + + while ($item->parent) { + $item = $item->parent; + array_unshift($path, $item->name); + } + + return implode(' / ', $path); +} + +$district = StoreCityList::findOne($districtId); +echo getFullPath($district); +// "Московская область / Москва / ЦАО" +``` + +### Построение дерева +```php +function buildTree($parentId = null) +{ + $items = StoreCityList::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + + $tree = []; + foreach ($items as $item) { + $node = [ + 'id' => $item->id, + 'name' => $item->name, + 'type' => $item->type, + 'children' => buildTree($item->id) + ]; + $tree[] = $node; + } + + return $tree; +} + +$fullTree = buildTree(); +``` + +### Формирование списка для выбора +```php +function buildSelectList($parentId = null, $prefix = '') +{ + $items = StoreCityList::find() + ->where(['parent_id' => $parentId]) + ->orderBy(['name' => SORT_ASC]) + ->all(); + + $list = []; + foreach ($items as $item) { + $list[$item->id] = $prefix . $item->name; + $childList = buildSelectList($item->id, $prefix . '— '); + $list = $list + $childList; + } + + return $list; +} + +$selectList = buildSelectList(); +echo Html::dropDownList('city_list_id', null, $selectList); +``` + +### Статистика по типам +```php +$stats = StoreCityList::find() + ->select(['type', 'COUNT(*) as count']) + ->groupBy('type') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $typeName = StoreCityList::getTypes()[$stat['type']] ?? 'Unknown'; + echo "{$typeName}: {$stat['count']}\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 255) | +| `type` | required, string (max 255), in [region, city, district] | +| `parent_id` | integer, exists в StoreCityList | +| `created_at` | safe | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Иерархическая структура**: parent_id для связи родитель-потомок +2. **Типизация**: type определяет уровень иерархии +3. **Самосвязь**: Ссылка на себя для построения дерева +4. **Три уровня**: region -> city -> district +5. **Валидация типа**: in-правило ограничивает допустимые значения +6. **Валидация parent_id**: Проверка существования родителя diff --git a/erp24/docs/models/StoreCityListSearch.md b/erp24/docs/models/StoreCityListSearch.md new file mode 100644 index 00000000..e49167dd --- /dev/null +++ b/erp24/docs/models/StoreCityListSearch.md @@ -0,0 +1,171 @@ +# Класс: StoreCityListSearch + + +## Mindmap + +```mermaid +mindmap + root((StoreCityListSearch)) + Таблица БД + ActiveRecord + Наследование + extends StoreCityList +``` + +## Назначение +Search-модель для поиска и фильтрации справочника городов магазинов в ERP24. Иерархическая структура с родительскими элементами, использующая ilike для регистронезависимого поиска. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`StoreCityList` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `parent_id` — integer +- `name`, `type`, `created_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска городов. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос StoreCityList::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, parent_id, created_at + - ilike: name, type (регистронезависимый поиск) + +## Диаграмма структуры + +```mermaid +erDiagram + StoreCityList { + int id PK + int parent_id FK + varchar name + varchar type + datetime created_at + } + + StoreCityList ||--o{ StoreCityList : "parent_id" +``` + +## Диаграмма иерархии + +```mermaid +flowchart TD + A[Россия] --> B[Москва] + A --> C[Санкт-Петербург] + A --> D[Регионы] + + B --> E[ЦАО] + B --> F[САО] + + D --> G[Краснодар] + D --> H[Новосибирск] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new StoreCityListSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию города +```php +$searchModel = new StoreCityListSearch(); +$dataProvider = $searchModel->search([ + 'StoreCityListSearch' => [ + 'name' => 'Москва', + ] +]); +``` + +### Поиск дочерних элементов +```php +$searchModel = new StoreCityListSearch(); +$dataProvider = $searchModel->search([ + 'StoreCityListSearch' => [ + 'parent_id' => 1, // Дочерние элементы России + ] +]); +``` + +### Поиск по типу +```php +$searchModel = new StoreCityListSearch(); +$dataProvider = $searchModel->search([ + 'StoreCityListSearch' => [ + 'type' => 'city', + ] +]); +``` + +### Поиск корневых элементов +```php +$searchModel = new StoreCityListSearch(); +$dataProvider = $searchModel->search([ + 'StoreCityListSearch' => [ + 'parent_id' => null, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + 'type', + [ + 'attribute' => 'parent_id', + 'value' => 'parent.name', + ], + 'created_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [StoreCityList](./StoreCityList.md) — базовая модель городов +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **Иерархическая структура**: parent_id для древовидной организации +2. **Типизация**: type для разграничения (страна/регион/город/район) +3. **ilike поиск**: Регистронезависимый поиск по названию и типу +4. **Самосвязь**: Модель ссылается сама на себя через parent_id +5. **created_at**: Точное совпадение по дате создания diff --git a/erp24/docs/models/StoreDynamic.md b/erp24/docs/models/StoreDynamic.md new file mode 100644 index 00000000..99d5cec3 --- /dev/null +++ b/erp24/docs/models/StoreDynamic.md @@ -0,0 +1,306 @@ +# Class: StoreDynamic + + +## Mindmap + +```mermaid +mindmap + root((StoreDynamic)) + Таблица БД + store_dynamic + Свойства + id + int + store_id + int + value_type + string + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StoreDynamic` представляет динамические параметры магазина с поддержкой версионирования во времени. Она хранит изменяемые характеристики магазинов (числовые или строковые значения) с указанием периода действия. Модель используется для отслеживания исторических изменений параметров магазинов и получения актуальных значений на конкретную дату. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_dynamic` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи | +| `store_id` | int | да | ID магазина (связь с city_store) | +| `value_type` | string(100) | да | Тип значения (int/string) | +| `value_int` | int | нет | Числовое значение параметра | +| `value_string` | string(255) | нет | Строковое значение параметра | +| `date_from` | string(100) | да | Дата начала действия значения | +| `date_to` | string(100) | нет | Дата окончания действия значения | +| `active` | int | да | Флаг активности записи (1=активна, 0=неактивна) | +| `category` | int | нет | Категория параметра (тип характеристики) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_dynamic' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Обязательные поля: `store_id`, `value_type`, `date_from` +- `store_id`, `value_int`, `active`, `category` - целые числа +- `value_type`, `date_from`, `date_to` - строки до 100 символов +- `value_string` - строка до 255 символов + +--- + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех свойств: + +**`getStoreId()` / `setStoreId(int $store_id)`** +- Получение/установка ID магазина + +**`getValueType()` / `setValueType(string $value_type)`** +- Получение/установка типа значения + +**`getValueInt()` / `setValueInt(?int $value_int)`** +- Получение/установка числового значения + +**`getValueString()` / `setValueString(?string $value_string)`** +- Получение/установка строкового значения + +**`getDateFrom()` / `setDateFrom(string $date_from)`** +- Получение/установка даты начала действия + +**`getDateTo()` / `setDateTo(?string $date_to)`** +- Получение/установка даты окончания действия + +**`getActive()` / `setActive(int $active)`** +- Получение/установка флага активности + +**`getCategory()` / `setCategory(?int $category)`** +- Получение/установка категории параметра + +--- + +### `getValue(int $storeId, $dayFrom, $dayTo, int $category = 1)` +**Описание:** Получает значение параметра магазина за указанный период. + +**Параметры:** +- `$storeId` (int) - ID магазина +- `$dayFrom` - дата начала периода +- `$dayTo` - дата окончания периода +- `$category` (int) - категория параметра (по умолчанию 1) + +**Возвращает:** `mixed` - значение параметра (int или string) или null + +**Логика работы:** +1. Ищет запись в таблице CityStore (ошибка в коде - должно быть StoreDynamic) +2. Фильтрует по категории, store_id и диапазону дат +3. Определяет тип значения из поля `value_type` +4. Возвращает соответствующее значение из `value_int` или `value_string` + +**Примечание:** В коде есть ошибка - используется `CityStore::find()` вместо `StoreDynamic::find()`. + +**Пример:** +```php +$value = StoreDynamic::getValue(15, '2024-01-01', '2024-01-31', 1); +if ($value !== null) { + echo "Значение параметра: {$value}"; +} +``` + +--- + +## Примеры использования + +### 1. Создание динамического параметра магазина +```php +$dynamic = new StoreDynamic(); +$dynamic->setStoreId(15); +$dynamic->setValueType('int'); +$dynamic->setValueInt(250); // Например, площадь зала +$dynamic->setDateFrom('2024-01-01'); +$dynamic->setDateTo('2024-12-31'); +$dynamic->setActive(1); +$dynamic->setCategory(1); // Категория "Площадь" + +if ($dynamic->save()) { + echo "Параметр сохранён с ID: {$dynamic->id}"; +} +``` + +### 2. Создание строкового параметра +```php +$dynamic = new StoreDynamic(); +$dynamic->setStoreId(20); +$dynamic->setValueType('string'); +$dynamic->setValueString('Премиум-сегмент'); // Категория магазина +$dynamic->setDateFrom('2024-06-01'); +$dynamic->setActive(1); +$dynamic->setCategory(5); // Категория "Сегмент" + +$dynamic->save(); +``` + +### 3. Получение активных параметров магазина +```php +$storeId = 15; +$currentDate = date('Y-m-d'); + +$activeParams = StoreDynamic::find() + ->where(['store_id' => $storeId, 'active' => 1]) + ->andWhere(['<=', 'date_from', $currentDate]) + ->andWhere(['or', + ['date_to' => null], + ['>=', 'date_to', $currentDate] + ]) + ->all(); + +foreach ($activeParams as $param) { + $value = $param->getValueType() === 'int' + ? $param->getValueInt() + : $param->getValueString(); + + echo "Категория {$param->getCategory()}: {$value}\n"; +} +``` + +### 4. Обновление параметра с версионированием +```php +// Закрываем старое значение +$oldParam = StoreDynamic::findOne([ + 'store_id' => 15, + 'category' => 1, + 'active' => 1 +]); + +if ($oldParam) { + $oldParam->setDateTo(date('Y-m-d')); + $oldParam->setActive(0); + $oldParam->save(); +} + +// Создаём новое значение +$newParam = new StoreDynamic(); +$newParam->setStoreId(15); +$newParam->setValueType('int'); +$newParam->setValueInt(300); // Увеличили площадь +$newParam->setDateFrom(date('Y-m-d')); +$newParam->setActive(1); +$newParam->setCategory(1); +$newParam->save(); +``` + +### 5. История изменений параметра +```php +$storeId = 15; +$category = 1; + +$history = StoreDynamic::find() + ->where(['store_id' => $storeId, 'category' => $category]) + ->orderBy(['date_from' => SORT_ASC]) + ->all(); + +echo "История изменений параметра категории {$category}:\n"; +foreach ($history as $record) { + $value = $record->value_type === 'int' + ? $record->value_int + : $record->value_string; + + $dateTo = $record->date_to ?? 'по настоящее время'; + echo "{$record->date_from} - {$dateTo}: {$value}\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_DYNAMIC ||--|| CITY_STORE : "belongs to" + STORE_DYNAMIC ||--o| CATEGORY_DICT : "has category" + + STORE_DYNAMIC { + int id PK + int store_id FK "ID магазина" + string value_type "Тип значения" + int value_int "Числовое значение" + string value_string "Строковое значение" + string date_from "Дата начала" + string date_to "Дата окончания" + int active "Активность" + int category FK "Категория" + } + + CITY_STORE { + int id PK + string name "Название" + } + + CATEGORY_DICT { + int id PK + string name "Название категории" + } +``` + +--- + +## Особенности реализации + +### Версионирование данных +Модель поддерживает историческое версионирование параметров через поля `date_from`, `date_to` и `active`. Это позволяет: +- Хранить историю изменений параметров +- Получать актуальные значения на любую дату +- Отслеживать, когда и какие изменения произошли + +### Гибкая типизация +Использование двух полей (`value_int` и `value_string`) с указателем типа (`value_type`) позволяет хранить разнородные данные в одной таблице. + +### Категоризация +Поле `category` позволяет группировать параметры по типам (площадь, тип матрицы, сегмент и т.д.). + +--- + +## Бизнес-логика + +### Использование в системе + +1. **История изменений** - отслеживание эволюции характеристик магазинов +2. **Аналитика** - анализ влияния изменений параметров на продажи +3. **Планирование** - учёт исторических данных при прогнозировании +4. **Отчётность** - получение параметров на конкретную дату для отчётов + +--- + +## Примечания + +**Обнаруженные проблемы:** +1. Метод `getValue()` использует `CityStore::find()` вместо `StoreDynamic::find()` +2. Некорректное использование оператора `andWhere` в методе `getValue()` + +**Рекомендации:** +1. Исправить ошибку в методе `getValue()` +2. Добавить константы для категорий +3. Создать методы для автоматического версионирования +4. Добавить валидацию дат (date_to >= date_from) +5. Создать индексы на поля store_id, category, active, date_from diff --git a/erp24/docs/models/StoreGuidBuh.md b/erp24/docs/models/StoreGuidBuh.md new file mode 100644 index 00000000..5a749332 --- /dev/null +++ b/erp24/docs/models/StoreGuidBuh.md @@ -0,0 +1,237 @@ +# Класс: StoreGuidBuh + + +## Mindmap + +```mermaid +mindmap + root((StoreGuidBuh)) + Таблица БД + store_guid_buh + Свойства + id + int + store_id + int + store_guid + string + created_by + int + created_at + string + Связи + Store + 1:1 CityStore + Created + 1:1 Admin + Updated + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель маппинга магазинов на бухгалтерские GUID в ERP24. Связывает внутренний идентификатор магазина с его GUID в бухгалтерской системе 1С для корректной синхронизации данных. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`store_guid_buh` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поведения (Behaviors) + +| Поведение | Описание | +|-----------|----------| +| `TimestampBehavior` | Автоматическое заполнение created_at/updated_at | +| `BlameableBehavior` | Автоматическое заполнение created_by/updated_by | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_id` | int | FK на магазин (CityStore) | +| `store_guid` | varchar(255) | GUID магазина в бухгалтерской системе | +| `created_by` | int | FK на создателя (Admin) | +| `updated_by` | int / null | FK на редактора (Admin) | +| `created_at` | datetime | Дата создания записи | +| `updated_at` | datetime / null | Дата обновления записи | + +## Уникальные индексы + +| Поля | Описание | +|------|----------| +| `store_id` + `store_guid` | Уникальность пары магазин-GUID | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getStore()` | hasOne | CityStore | Магазин | +| `getCreated()` | hasOne | Admin | Создатель записи | +| `getUpdated()` | hasOne | Admin | Редактор записи | + +## Диаграмма связей + +```mermaid +erDiagram + StoreGuidBuh { + int id PK + int store_id FK + varchar store_guid + int created_by FK + int updated_by FK + datetime created_at + datetime updated_at + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + CityStore ||--o{ StoreGuidBuh : "store_id" + Admin ||--o{ StoreGuidBuh : "created_by" + Admin ||--o{ StoreGuidBuh : "updated_by" +``` + +## Диаграмма интеграции с 1С + +```mermaid +flowchart LR + subgraph ERP24 + A[CityStore
    id=5] + B[StoreGuidBuh
    store_id=5
    store_guid=abc-123] + end + + subgraph Бухгалтерия 1С + C[Склад
    GUID=abc-123] + end + + A --> B + B -.->|маппинг| C +``` + +## Примеры использования + +### Создание маппинга +```php +$mapping = new StoreGuidBuh(); +$mapping->store_id = $storeId; +$mapping->store_guid = '12345678-1234-1234-1234-123456789abc'; +$mapping->save(); +``` + +### Получение GUID магазина +```php +$mapping = StoreGuidBuh::find() + ->where(['store_id' => $storeId]) + ->one(); + +if ($mapping) { + $guid = $mapping->store_guid; + echo "GUID в бухгалтерии: {$guid}"; +} +``` + +### Поиск магазина по GUID +```php +$mapping = StoreGuidBuh::find() + ->where(['store_guid' => $guid1c]) + ->one(); + +if ($mapping) { + $store = $mapping->store; + echo "Магазин: {$store->name}"; +} +``` + +### Получение всех маппингов с магазинами +```php +$mappings = StoreGuidBuh::find() + ->with('store') + ->all(); + +foreach ($mappings as $mapping) { + echo "{$mapping->store->name}: {$mapping->store_guid}\n"; +} +``` + +### Синхронизация данных из 1С +```php +$dataFrom1c = [ + ['store_id' => 1, 'guid' => 'guid-1'], + ['store_id' => 2, 'guid' => 'guid-2'], +]; + +foreach ($dataFrom1c as $item) { + $existing = StoreGuidBuh::find() + ->where([ + 'store_id' => $item['store_id'], + 'store_guid' => $item['guid'] + ]) + ->one(); + + if (!$existing) { + $mapping = new StoreGuidBuh(); + $mapping->store_id = $item['store_id']; + $mapping->store_guid = $item['guid']; + $mapping->save(); + } +} +``` + +### Обновление GUID магазина +```php +$mapping = StoreGuidBuh::find() + ->where(['store_id' => $storeId]) + ->one(); + +if ($mapping) { + $mapping->store_guid = $newGuid; + $mapping->save(); +} +``` + +### Получение истории изменений +```php +$mapping = StoreGuidBuh::findOne($id); + +echo "Создан: {$mapping->created->name} ({$mapping->created_at})\n"; + +if ($mapping->updated_by) { + echo "Изменён: {$mapping->updated->name} ({$mapping->updated_at})\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, unique в паре с store_guid | +| `store_guid` | required, string (max 255), unique в паре с store_id | +| `created_at` | safe | +| `updated_at` | safe | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Маппинг магазин-GUID**: Связь внутреннего ID с внешним GUID +2. **TimestampBehavior**: Автоматические временные метки +3. **BlameableBehavior**: Автоматическое отслеживание авторства +4. **Составной уникальный индекс**: store_id + store_guid +5. **Интеграция с 1С**: Бухгалтерские GUID для синхронизации +6. **Аудит изменений**: created_by/updated_by для отслеживания diff --git a/erp24/docs/models/StoreOrderStatus.md b/erp24/docs/models/StoreOrderStatus.md new file mode 100644 index 00000000..2580df34 --- /dev/null +++ b/erp24/docs/models/StoreOrderStatus.md @@ -0,0 +1,475 @@ +# Модель StoreOrderStatus + + +## Mindmap + +```mermaid +mindmap + root((StoreOrderStatus)) + Таблица БД + store_order_status + Свойства + order_id + int + store_id + string + status_id + int + status + int + date + string + admin_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrderStatus` хранит историю изменений статусов заказов магазинов на закупку товаров. Позволяет отслеживать полный жизненный цикл заказа: от создания до выполнения, с фиксацией времени каждого изменения статуса и ответственного сотрудника. + +**Файл модели:** `erp24/records/StoreOrderStatus.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_order_status` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Составной первичный ключ + +| Поле | Тип | Описание | +|------|-----|----------| +| `order_id` | INTEGER | ID заказа магазина (часть PK) (обязательное) | +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) (обязательное) | +| `status_id` | INTEGER | ID статуса (часть PK) (обязательное) | + +### Данные статуса + +| Поле | Тип | Описание | +|------|-----|----------| +| `status` | INTEGER | Код статуса (дублирует status_id для удобства) | +| `date` | TIMESTAMP | Дата и время установки статуса (обязательное) | +| `admin_id` | INTEGER | ID сотрудника, изменившего статус (обязательное) | + +--- + +## Уникальный индекс + +Модель имеет составной уникальный индекс на все поля первичного ключа: + +``` +[order_id, store_id, status_id] +``` + +Это гарантирует, что каждый статус для конкретного заказа и магазина записывается только один раз. + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'order_id', 'store_id', 'status_id', 'date', 'admin_id' +], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `order_id`, `status_id`, `status`, `admin_id` | Целочисленные значения | +| `safe` | `date` | Дата/время без дополнительной валидации | +| `string`, max=36 | `store_id` | GUID магазина | + +--- + +## Типовые статусы заказов магазинов + +Хотя модель не содержит констант статусов, в системе используется следующая типология: + +| status_id | Описание статуса | Комментарий | +|-----------|------------------|-------------| +| 1 | Создан | Заказ создан, но не отправлен | +| 2 | Отправлен поставщику | Заказ отправлен поставщику | +| 3 | Подтвержден поставщиком | Поставщик подтвердил заказ | +| 4 | В пути | Товар в процессе доставки | +| 5 | Получен частично | Получена часть товара | +| 6 | Получен полностью | Весь товар получен магазином | +| 7 | Отменен | Заказ отменен | +| 8 | На проверке закупщика | Закупщик проверяет заказ | + +Примечание: Точные значения статусов могут быть определены в справочной таблице или константах приложения. + +--- + +## Логика работы с историей статусов + +### Принцип работы + +1. При создании заказа создается первая запись с статусом "Создан" +2. При каждом изменении статуса добавляется новая запись +3. Текущий статус заказа - это запись с максимальной датой +4. История полностью сохраняется и не перезаписывается + +### Особенности + +- Каждый переход статуса фиксируется с точным временем +- Сохраняется информация о сотруднике, изменившем статус +- Невозможно установить один и тот же статус дважды для одного заказа (защита уникальным индексом) +- Можно анализировать время нахождения заказа в каждом статусе + +--- + +## Примеры использования + +### Создание первой записи статуса при создании заказа + +```php +$statusRecord = new StoreOrderStatus(); +$statusRecord->order_id = $orderId; +$statusRecord->store_id = $storeId; +$statusRecord->status_id = 1; // Создан +$statusRecord->status = 1; +$statusRecord->date = date('Y-m-d H:i:s'); +$statusRecord->admin_id = Yii::$app->user->id; + +if ($statusRecord->save()) { + echo "Статус заказа установлен"; +} +``` + +### Изменение статуса заказа + +```php +// Проверяем, не установлен ли уже этот статус +$existingStatus = StoreOrderStatus::findOne([ + 'order_id' => $orderId, + 'store_id' => $storeId, + 'status_id' => $newStatusId +]); + +if (!$existingStatus) { + $statusRecord = new StoreOrderStatus(); + $statusRecord->order_id = $orderId; + $statusRecord->store_id = $storeId; + $statusRecord->status_id = $newStatusId; + $statusRecord->status = $newStatusId; + $statusRecord->date = date('Y-m-d H:i:s'); + $statusRecord->admin_id = Yii::$app->user->id; + $statusRecord->save(); +} else { + echo "Статус уже установлен"; +} +``` + +### Получение текущего статуса заказа + +```php +$currentStatus = StoreOrderStatus::find() + ->where([ + 'order_id' => $orderId, + 'store_id' => $storeId + ]) + ->orderBy(['date' => SORT_DESC]) + ->one(); + +if ($currentStatus) { + echo "Текущий статус: {$currentStatus->status_id}"; + echo "Установлен: {$currentStatus->date}"; +} +``` + +### Получение полной истории статусов заказа + +```php +$statusHistory = StoreOrderStatus::find() + ->where([ + 'order_id' => $orderId, + 'store_id' => $storeId + ]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +echo "История статусов заказа №{$orderId}:\n"; +foreach ($statusHistory as $status) { + echo "{$status->date}: Статус {$status->status_id} (сотрудник ID: {$status->admin_id})\n"; +} +``` + +### Расчет времени в каждом статусе + +```php +$statusHistory = StoreOrderStatus::find() + ->where([ + 'order_id' => $orderId, + 'store_id' => $storeId + ]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +for ($i = 0; $i < count($statusHistory) - 1; $i++) { + $current = $statusHistory[$i]; + $next = $statusHistory[$i + 1]; + + $start = new DateTime($current->date); + $end = new DateTime($next->date); + $interval = $start->diff($end); + + echo "Статус {$current->status_id}: "; + echo "{$interval->days} дней, {$interval->h} часов, {$interval->i} минут\n"; +} +``` + +### Получение заказов в определенном статусе + +```php +// Все заказы со статусом "Отправлен поставщику" +$ordersInStatus = StoreOrderStatus::find() + ->select(['order_id', 'store_id', 'MAX(date) as last_date']) + ->where(['status_id' => 2]) + ->groupBy(['order_id', 'store_id']) + ->having(['status_id' => 2]) + ->all(); +``` + +### Получение статусов с информацией о сотруднике + +```php +$statusWithAdmin = StoreOrderStatus::find() + ->select([ + 'store_order_status.*', + 'admin.name as admin_name' + ]) + ->leftJoin('admin', 'admin.id = store_order_status.admin_id') + ->where([ + 'store_order_status.order_id' => $orderId, + 'store_order_status.store_id' => $storeId + ]) + ->orderBy(['store_order_status.date' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($statusWithAdmin as $status) { + echo "{$status['date']}: Статус {$status['status_id']} "; + echo "(изменил: {$status['admin_name']})\n"; +} +``` + +### Проверка прохождения через определенный статус + +```php +$hasPassedStatus = StoreOrderStatus::find() + ->where([ + 'order_id' => $orderId, + 'store_id' => $storeId, + 'status_id' => 3 // Подтвержден поставщиком + ]) + ->exists(); + +if ($hasPassedStatus) { + echo "Заказ прошел через статус 'Подтвержден поставщиком'"; +} +``` + +### Статистика времени обработки заказов + +```php +// Среднее время от создания до получения +$stats = Yii::$app->db->createCommand(" + SELECT + AVG(EXTRACT(EPOCH FROM (received.date - created.date))) / 3600 as avg_hours + FROM store_order_status created + JOIN store_order_status received + ON created.order_id = received.order_id + AND created.store_id = received.store_id + WHERE created.status_id = 1 -- Создан + AND received.status_id = 6 -- Получен полностью + AND created.date >= :start_date +", [':start_date' => $startDate])->queryScalar(); + +echo "Среднее время обработки: " . round($stats, 2) . " часов"; +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **StoreOrders** - заказы магазинов (через `order_id`) +- **CityStore** - магазины (через `store_id`) +- **Admin** - сотрудники (через `admin_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_order_status }o--|| store_orders : "order" + store_order_status }o--|| city_store : "store" + store_order_status }o--|| admin : "changed_by" + + store_order_status { + int order_id PK,FK + string store_id PK,FK + int status_id PK + int status + timestamp date + int admin_id FK + } + + store_orders { + int id PK + string store_id FK + date date + int current_status + } + + city_store { + string id PK + string name + string address + } + + admin { + int id PK + string name + string email + } +``` + +--- + +## Жизненный цикл статусов заказа + +```mermaid +stateDiagram-v2 + [*] --> Создан: Новый заказ + Создан --> ОтправленПоставщику: Отправка + ОтправленПоставщику --> ПодтвержденПоставщиком: Подтверждение + ПодтвержденПоставщиком --> ВПути: Отгрузка + ВПути --> ПолученЧастично: Частичная поставка + ВПути --> ПолученПолностью: Полная поставка + ПолученЧастично --> ПолученПолностью: Дополучение + ПолученПолностью --> [*]: Завершение + + Создан --> Отменен: Отмена + ОтправленПоставщику --> Отменен: Отмена + ПодтвержденПоставщиком --> Отменен: Отмена + Отменен --> [*] + + Создан --> НаПроверкеЗакупщика: Проверка + НаПроверкеЗакупщика --> ОтправленПоставщику: Утверждение + НаПроверкеЗакупщика --> Создан: Возврат на доработку +``` + +--- + +## Временная шкала обработки заказа + +```mermaid +gantt + title Типичная временная шкала заказа магазина + dateFormat YYYY-MM-DD + section Подготовка + Создан :status1, 2025-01-01, 1d + На проверке закупщика :status8, after status1, 1d + section Заказ + Отправлен поставщику :status2, after status8, 1d + Подтвержден поставщиком :status3, after status2, 1d + section Доставка + В пути :status4, after status3, 3d + Получен частично :status5, after status4, 1d + Получен полностью :status6, after status5, 1d +``` + +--- + +## Процесс изменения статуса + +```mermaid +flowchart TD + A[Начало: Изменение статуса] --> B{Статус уже установлен?} + B -->|Да| C[Ошибка: Статус уже существует] + B -->|Нет| D[Создать новую запись StoreOrderStatus] + D --> E[Установить order_id, store_id, status_id] + E --> F[Установить текущую дату] + F --> G[Установить admin_id текущего пользователя] + G --> H[Сохранить запись] + H --> I{Сохранение успешно?} + I -->|Да| J[Логировать изменение] + I -->|Нет| K[Обработать ошибку] + J --> L[Отправить уведомления] + L --> M[Конец] + C --> M + K --> M +``` + +--- + +## Примеры аналитических запросов + +### Количество заказов в каждом статусе + +```php +$statusCounts = Yii::$app->db->createCommand(" + SELECT + status_id, + COUNT(DISTINCT order_id) as order_count + FROM ( + SELECT DISTINCT ON (order_id, store_id) + order_id, + store_id, + status_id + FROM store_order_status + ORDER BY order_id, store_id, date DESC + ) as latest_statuses + GROUP BY status_id + ORDER BY status_id +")->queryAll(); +``` + +### Заказы, долго находящиеся в одном статусе + +```php +$stuckOrders = Yii::$app->db->createCommand(" + SELECT + order_id, + store_id, + status_id, + date, + NOW() - date as time_in_status + FROM ( + SELECT DISTINCT ON (order_id, store_id) + order_id, + store_id, + status_id, + date + FROM store_order_status + ORDER BY order_id, store_id, date DESC + ) as latest_statuses + WHERE NOW() - date > INTERVAL '3 days' + AND status_id NOT IN (6, 7) -- Исключаем завершенные и отмененные + ORDER BY time_in_status DESC +")->queryAll(); +``` + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** - заказы магазинов +- **[StoreOrdersItem](./StoreOrdersItem.md)** - позиции заказов магазинов +- **[CityStore](./CityStore.md)** - магазины +- **[Admin](./Admin.md)** - сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrderStatusLog.md b/erp24/docs/models/StoreOrderStatusLog.md new file mode 100644 index 00000000..f2f69739 --- /dev/null +++ b/erp24/docs/models/StoreOrderStatusLog.md @@ -0,0 +1,173 @@ +# Модель StoreOrderStatusLog + + +## Mindmap + +```mermaid +mindmap + root((StoreOrderStatusLog)) + Таблица БД + store_order_status_log + Свойства + id + int + order_id + string + status + int + date + string + admin_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrderStatusLog` ведёт журнал изменений статусов заказов магазина. Фиксирует все переходы между статусами с указанием времени и автора изменения. Используется для отслеживания жизненного цикла заказа и аудита. + +**Файл модели:** `erp24/records/StoreOrderStatusLog.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_order_status_log` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `order_id` | VARCHAR(36) | GUID заказа магазина | +| `status` | INTEGER | Новый статус заказа | +| `date` | TIMESTAMP | Дата и время изменения статуса | +| `admin_id` | INTEGER | ID сотрудника, изменившего статус | + +--- + +## Типичные статусы заказа + +| ID | Описание | +|----|----------| +| 0 | Черновик | +| 1 | На согласовании | +| 2 | Согласован | +| 3 | В обработке | +| 4 | Отправлен поставщику | +| 5 | Выполнен | +| 6 | Отменён | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_order_status_log }o--|| store_orders : "order" + store_order_status_log }o--|| admin : "author" + + store_order_status_log { + int id PK + string order_id FK + int status + timestamp date + int admin_id FK + } + + store_orders { + string id PK + int status + string store_id + } +``` + +--- + +## Примеры использования + +### Создание записи лога + +```php +$log = new StoreOrderStatusLog(); +$log->order_id = $orderGuid; +$log->status = StoreOrders::STATUS_APPROVED; +$log->date = date('Y-m-d H:i:s'); +$log->admin_id = Yii::$app->user->id; +$log->save(); +``` + +### Получение истории статусов заказа + +```php +$history = StoreOrderStatusLog::find() + ->where(['order_id' => $orderGuid]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +foreach ($history as $log) { + echo "{$log->date}: статус изменён на {$log->status} (admin #{$log->admin_id})\n"; +} +``` + +### Получение времени в каждом статусе + +```php +$statusLogs = StoreOrderStatusLog::find() + ->where(['order_id' => $orderGuid]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$durations = []; +for ($i = 0; $i < count($statusLogs) - 1; $i++) { + $current = new DateTime($statusLogs[$i]->date); + $next = new DateTime($statusLogs[$i + 1]->date); + $diff = $current->diff($next); + $durations[$statusLogs[$i]->status] = $diff->format('%H:%I:%S'); +} +``` + +### Поиск заказов, изменённых сотрудником + +```php +$orderIds = StoreOrderStatusLog::find() + ->select('order_id') + ->where(['admin_id' => $adminId]) + ->distinct() + ->column(); +``` + +### Получение последнего изменения статуса + +```php +$lastChange = StoreOrderStatusLog::find() + ->where(['order_id' => $orderGuid]) + ->orderBy(['date' => SORT_DESC]) + ->one(); + +if ($lastChange) { + echo "Последнее изменение: {$lastChange->date}, статус: {$lastChange->status}"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `order_id`, `status`, `date`, `admin_id` | Обязательные | +| `status`, `admin_id` | Целые числа | +| `order_id` | Строка, макс. 36 символов | + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrders.md b/erp24/docs/models/StoreOrders.md index 0a88c013..867f023d 100644 --- a/erp24/docs/models/StoreOrders.md +++ b/erp24/docs/models/StoreOrders.md @@ -1,5 +1,30 @@ # Class: StoreOrders + +## Mindmap + +```mermaid +mindmap + root((StoreOrders)) + Таблица БД + store_orders + Свойства + id + int + name + string + parent_id + int + date_start + string + date_end + string + delivery_date + string + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель `StoreOrders` управляет заказами товаров для магазинов в системе ERP24. Она отвечает за планирование поставок, учёт сроков доставки, управление поставщиками и логистическими расходами. Модель используется для координации закупок, контроля прихода товара и хранения связанных данных о заказах. diff --git a/erp24/docs/models/StoreOrdersColors.md b/erp24/docs/models/StoreOrdersColors.md new file mode 100644 index 00000000..4c167034 --- /dev/null +++ b/erp24/docs/models/StoreOrdersColors.md @@ -0,0 +1,181 @@ +# Модель StoreOrdersColors + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersColors)) + Таблица БД + store_orders_colors + Свойства + product_id + string + order_id + int + store_id + string + field_id + int + color + string + provider_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersColors` хранит информацию о заказанных цветах (цветочной продукции) в разрезе конкретных позиций заказа магазина. Детализирует заказ по цветам товара, поставщикам и количеству. Используется для формирования заявок на закупку цветочной продукции с учётом цветовой гаммы. + +**Файл модели:** `erp24/records/StoreOrdersColors.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_colors` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `product_id` | VARCHAR(36) | GUID товара (часть PK) | +| `order_id` | INTEGER | ID заказа магазина (часть PK) | +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) | +| `field_id` | INTEGER | ID поля заказа (часть PK) | +| `color` | VARCHAR(45) | Название цвета (часть PK) | +| `provider_id` | INTEGER | ID поставщика (часть PK) | +| `quantity` | INTEGER | Количество единиц товара | +| `date` | DATE | Дата заказа | + +--- + +## Особенности + +- **Составной уникальный ключ** по `product_id`, `order_id`, `store_id`, `field_id`, `color`, `provider_id` +- Позволяет заказывать один товар разных цветов от разных поставщиков +- Связь с товарами через GUID для синхронизации с 1С +- Все поля обязательные + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_colors }o--|| store_orders : "order" + store_orders_colors }o--|| products_1c : "product" + store_orders_colors }o--|| city_store : "store" + store_orders_colors }o--|| store_orders_fields : "field" + store_orders_colors }o--|| providers : "provider" + + store_orders_colors { + string product_id PK,FK + int order_id PK,FK + string store_id PK,FK + int field_id PK,FK + string color PK + int provider_id PK,FK + int quantity + date date + } +``` + +--- + +## Примеры использования + +### Добавление цвета в заказ + +```php +$orderColor = new StoreOrdersColors(); +$orderColor->product_id = $productGuid; +$orderColor->order_id = $orderId; +$orderColor->store_id = $storeGuid; +$orderColor->field_id = $fieldId; +$orderColor->color = 'Красный'; +$orderColor->provider_id = $providerId; +$orderColor->quantity = 50; +$orderColor->date = date('Y-m-d'); +$orderColor->save(); +``` + +### Получение цветов по заказу + +```php +$colors = StoreOrdersColors::find() + ->where(['order_id' => $orderId, 'store_id' => $storeGuid]) + ->orderBy(['color' => SORT_ASC]) + ->all(); + +foreach ($colors as $item) { + echo "{$item->color}: {$item->quantity} шт. от поставщика #{$item->provider_id}\n"; +} +``` + +### Группировка по цветам + +```php +$colorStats = StoreOrdersColors::find() + ->select(['color', 'SUM(quantity) as total']) + ->where(['order_id' => $orderId]) + ->groupBy('color') + ->asArray() + ->all(); +``` + +### Получение заказов по поставщику + +```php +$providerOrders = StoreOrdersColors::find() + ->where([ + 'provider_id' => $providerId, + 'date' => date('Y-m-d') + ]) + ->all(); +``` + +### Обновление количества + +```php +$color = StoreOrdersColors::findOne([ + 'product_id' => $productGuid, + 'order_id' => $orderId, + 'store_id' => $storeGuid, + 'field_id' => $fieldId, + 'color' => 'Красный', + 'provider_id' => $providerId +]); + +if ($color) { + $color->quantity += 10; + $color->save(); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| Все поля | Обязательные | +| `order_id`, `field_id`, `provider_id`, `quantity` | Целые числа | +| `product_id`, `store_id` | Строка, макс. 36 символов | +| `color` | Строка, макс. 45 символов | +| `product_id + order_id + store_id + field_id + color + provider_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов +- **[StoreOrdersFields](./StoreOrdersFields.md)** — поля заказа +- **[Products1c](./Products1c.md)** — товары +- **[CityStore](./CityStore.md)** — магазины + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersFields.md b/erp24/docs/models/StoreOrdersFields.md new file mode 100644 index 00000000..a19d1a43 --- /dev/null +++ b/erp24/docs/models/StoreOrdersFields.md @@ -0,0 +1,525 @@ +# Модель StoreOrdersFields + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersFields)) + Таблица БД + store_orders_fields + Свойства + id + int + name_eng + string + position + int + field_type + string + row_type_sum + string + type + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersFields` представляет справочник полей для расширенных характеристик позиций заказов магазинов. Определяет метаданные полей, их типы, формулы расчета, права доступа и правила отображения для разных контекстов (магазин, склад, статистика). + +**Файл модели:** `erp24/records/StoreOrdersFields.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_fields` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Основные идентификаторы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ | +| `name_eng` | VARCHAR(35) | Системное имя поля на английском (обязательное) | +| `name_eng_sql` | VARCHAR(35) | SQL-имя поля для запросов (обязательное) | +| `name` | VARCHAR(125) | Название поля на русском (обязательное) | +| `name_full` | TEXT | Полное развернутое название столбца (обязательное) | + +### Типы и приоритеты + +| Поле | Тип | Описание | +|------|-----|----------| +| `position` | INTEGER | Приоритет подсчета: 0 - глобальные переменные, 1 - формулы 0-го уровня, 2 - формулы 1-го уровня, 3 - формулы 2-го уровня | +| `field_type` | VARCHAR(25) | Тип переменной: глобальная, статическая, вычисляемая | +| `row_type_sum` | VARCHAR(15) | Тип суммирования столбца: sum, avg, count и т.д. | +| `type` | VARCHAR(35) | Тип поля: number, text, date, select и т.д. | +| `tip` | VARCHAR(35) | Подсказка по типу данных | + +### Настройки отображения + +| Поле | Тип | Описание | +|------|-----|----------| +| `store_save` | INTEGER | Сохраняем и редактируем в магазине (0/1) | +| `store_show` | INTEGER | Показываем и считаем поле в магазинах (0/1) | +| `stores_show` | INTEGER | Считаем и показываем значение в списке магазинов (0/1) | +| `stores_stats` | INTEGER | Статистика по магазинам по этому полю - сохраняем или нет (0/1) | + +### Настройки редактирования + +| Поле | Тип | Описание | +|------|-----|----------| +| `field_edit` | INTEGER | Можно ли в принципе изменять поле руками (0/1) | +| `field_store_edit` | INTEGER | Можно ли менять значение для магазина руками (0/1) | + +### Функции и зависимости + +| Поле | Тип | Описание | +|------|-----|----------| +| `func` | INTEGER | Поле вычисляется функцией (0/1) | +| `func_content` | TEXT | Содержимое функции расчета (код или формула) (обязательное) | +| `dependent_fields` | TEXT | ID зависимых полей через запятую (обязательное) | +| `summ` | INTEGER | Суммируемое поле (0/1) | + +### Настройки ввода + +| Поле | Тип | Описание | +|------|-----|----------| +| `step` | VARCHAR(6) | Шаг для числовых полей (обязательное) | +| `placeholder` | VARCHAR(12) | Подсказка в поле ввода (обязательное) | +| `pattern` | VARCHAR(155) | Регулярное выражение для валидации (обязательное) | + +### Дополнительные настройки + +| Поле | Тип | Описание | +|------|-----|----------| +| `description` | VARCHAR(255) | Описание назначения поля (обязательное) | +| `sql_table_values` | VARCHAR(55) | SQL-таблица для получения значений справочника (обязательное) | +| `colors_save` | INTEGER | Используем поля для сохранения по цветам (0/1) | +| `dostup` | TEXT | Права доступа (JSON или строка) (обязательное) | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'name_eng', 'name', 'name_full', 'description', 'dependent_fields', + 'name_eng_sql', 'sql_table_values', 'func_content', 'step', + 'placeholder', 'pattern', 'dostup' +], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `position`, `store_save`, `store_show`, `stores_show`, `stores_stats`, `field_edit`, `field_store_edit`, `summ`, `func`, `colors_save` | Целочисленные флаги | +| `string` | `name_full`, `dependent_fields`, `func_content`, `dostup` | Текст без ограничения | +| `string`, max=35 | `name_eng`, `type`, `tip`, `name_eng_sql` | Короткие идентификаторы | +| `string`, max=25 | `field_type` | Тип поля | +| `string`, max=15 | `row_type_sum` | Тип суммирования | +| `string`, max=125 | `name` | Название | +| `string`, max=255 | `description` | Описание | +| `string`, max=55 | `sql_table_values` | Имя таблицы | +| `string`, max=6 | `step` | Шаг ввода | +| `string`, max=12 | `placeholder` | Подсказка | +| `string`, max=155 | `pattern` | Регулярное выражение | + +--- + +## Приоритеты подсчета (position) + +Поле `position` определяет порядок вычисления полей с формулами: + +### Уровень 0 - Глобальные переменные +Базовые статистические данные, не зависящие от других вычисляемых полей: +- Остатки на складе +- Количество продаж +- Сумма продаж +- Товар в пути + +### Уровень 1 - Простые формулы +Формулы, использующие только переменные 0-го уровня: +- Средняя цена продажи +- Остаток в днях продаж +- Процент списаний + +### Уровень 2 - Сложные формулы +Формулы, использующие переменные 0-го и 1-го уровня: +- Рекомендуемый заказ с учетом остатка в днях +- Оптимальный запас + +### Уровень 3 - Композитные формулы +Формулы, использующие переменные всех предыдущих уровней: +- Итоговое количество заказа +- Финальная стоимость с учетом всех факторов + +--- + +## Типы полей (field_type) + +| Тип | Описание | Пример | +|-----|----------|--------| +| `global` | Глобальная переменная из базы данных | `sales_cnt`, `quantity_storage` | +| `static` | Статическое значение, вводится вручную | `price`, `provider_name` | +| `calculated` | Вычисляемое по формуле | `sales_day`, `recommended_quantity` | +| `reference` | Значение из справочника | `product_name`, `store_name` | + +--- + +## Типы суммирования (row_type_sum) + +| Тип | Описание | +|-----|----------| +| `sum` | Суммирование значений | +| `avg` | Среднее арифметическое | +| `count` | Подсчет количества | +| `min` | Минимальное значение | +| `max` | Максимальное значение | +| `none` | Не суммируется | + +--- + +## Структура func_content + +Поле `func_content` содержит код или формулу для вычисления значения поля: + +### Пример простой формулы +```javascript +// Расчет остатка в днях продаж +{ + "formula": "quantity_storage / (sales_cnt / 30)", + "dependencies": ["quantity_storage", "sales_cnt"] +} +``` + +### Пример сложной функции +```javascript +// Рекомендуемое количество заказа +{ + "function": "calculateRecommendedQuantity", + "parameters": { + "sales_day": "field:sales_day", + "target_days": 14, + "safety_stock": 5 + }, + "formula": "(target_days - sales_day) * (sales_cnt / 30) + safety_stock" +} +``` + +--- + +## Структура dependent_fields + +Поле содержит ID полей через запятую, от которых зависит текущее поле: + +``` +"12,15,18,22" +``` + +Эти зависимости используются для: +- Определения порядка пересчета полей +- Каскадного обновления при изменении значений +- Валидации циклических зависимостей + +--- + +## Структура dostup (права доступа) + +Поле `dostup` содержит JSON с правами доступа по ролям: + +```json +{ + "view": ["manager", "admin", "purchaser"], + "edit": ["admin", "purchaser"], + "required": ["purchaser"] +} +``` + +**Ключи:** +- `view` - кто может просматривать поле +- `edit` - кто может редактировать +- `required` - для кого поле обязательно + +--- + +## Примеры использования + +### Создание нового поля + +```php +$field = new StoreOrdersFields(); +$field->name_eng = 'recommended_quantity'; +$field->name_eng_sql = 'recommended_quantity'; +$field->name = 'Рекомендуемое количество'; +$field->name_full = 'Рекомендуемое количество для заказа на основе статистики продаж'; +$field->description = 'Автоматически рассчитывается на основе остатков и продаж'; +$field->position = 2; +$field->field_type = 'calculated'; +$field->row_type_sum = 'sum'; +$field->type = 'number'; +$field->tip = 'integer'; +$field->store_save = 0; +$field->store_show = 1; +$field->stores_show = 1; +$field->stores_stats = 1; +$field->field_edit = 0; +$field->field_store_edit = 0; +$field->func = 1; +$field->func_content = json_encode([ + 'formula' => '(14 - sales_day) * (sales_cnt / 30) + 5', + 'dependencies' => ['sales_day', 'sales_cnt'] +]); +$field->dependent_fields = '5,8,12'; +$field->summ = 1; +$field->step = '1'; +$field->placeholder = '0'; +$field->pattern = '^[0-9]+$'; +$field->colors_save = 0; +$field->sql_table_values = ''; +$field->dostup = json_encode([ + 'view' => ['manager', 'admin', 'purchaser'], + 'edit' => ['admin'], + 'required' => [] +]); + +if ($field->save()) { + echo "Поле создано с ID: {$field->id}"; +} +``` + +### Получение всех полей для отображения в магазине + +```php +$storeFields = StoreOrdersFields::find() + ->where(['store_show' => 1]) + ->orderBy(['position' => SORT_ASC]) + ->all(); + +foreach ($storeFields as $field) { + echo "Поле: {$field->name}, Тип: {$field->type}\n"; +} +``` + +### Получение вычисляемых полей в порядке приоритета + +```php +$calculatedFields = StoreOrdersFields::find() + ->where(['func' => 1]) + ->orderBy(['position' => SORT_ASC, 'id' => SORT_ASC]) + ->all(); + +// Вычисляем поля в правильном порядке +foreach ($calculatedFields as $field) { + $funcData = json_decode($field->func_content, true); + // Применить формулу... +} +``` + +### Получение редактируемых полей для магазина + +```php +$editableFields = StoreOrdersFields::find() + ->where([ + 'store_show' => 1, + 'field_store_edit' => 1 + ]) + ->all(); +``` + +### Получение полей для статистики + +```php +$statsFields = StoreOrdersFields::find() + ->where(['stores_stats' => 1]) + ->andWhere(['!=', 'row_type_sum', 'none']) + ->all(); + +foreach ($statsFields as $field) { + echo "Статистика: {$field->name}, "; + echo "Агрегация: {$field->row_type_sum}\n"; +} +``` + +### Проверка зависимостей поля + +```php +$field = StoreOrdersFields::findOne($fieldId); +$dependentIds = array_filter(explode(',', $field->dependent_fields)); + +$dependentFields = StoreOrdersFields::find() + ->where(['id' => $dependentIds]) + ->all(); + +echo "Поле '{$field->name}' зависит от:\n"; +foreach ($dependentFields as $depField) { + echo "- {$depField->name}\n"; +} +``` + +### Получение полей для работы с цветами + +```php +$colorFields = StoreOrdersFields::find() + ->where(['colors_save' => 1]) + ->all(); + +foreach ($colorFields as $field) { + echo "Поле с цветами: {$field->name}\n"; +} +``` + +--- + +## Связь с другими моделями + +Модель логически связана с: + +- **StoreOrdersFieldsData** - фактические значения полей для позиций заказов +- **StoreOrdersItem** - позиции заказов магазинов +- **Admin** - сотрудники (права доступа) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_fields ||--o{ store_orders_fields_data : "defines_structure" + store_orders_fields }o--|| store_orders_fields : "depends_on" + store_orders_item ||--o{ store_orders_fields_data : "has_field_values" + + store_orders_fields { + int id PK + string name_eng UK + int position + string field_type + string row_type_sum + string type + string tip + int store_save + int store_show + int stores_show + int stores_stats + int field_edit + int field_store_edit + string name + text name_full + string description + text dependent_fields + string name_eng_sql + string sql_table_values + text func_content + string step + string placeholder + string pattern + int summ + int func + int colors_save + text dostup + } + + store_orders_fields_data { + int order_id PK,FK + string product_id PK,FK + string store_id PK,FK + int field_id PK,FK + string field_name PK + string color PK + float value + text value_text + } + + store_orders_item { + string product_id PK,FK + string store_id PK,FK + int order_id PK,FK + int quantity + } +``` + +--- + +## Процесс вычисления полей + +```mermaid +flowchart TD + A[Начало] --> B[Загрузить все поля] + B --> C[Сортировать по position] + C --> D{Есть еще поля?} + D -->|Да| E[Взять следующее поле] + E --> F{Поле вычисляемое?} + F -->|Нет| D + F -->|Да| G[Получить зависимые поля] + G --> H{Все зависимости вычислены?} + H -->|Нет| I[Отложить вычисление] + I --> D + H -->|Да| J[Парсить func_content] + J --> K[Применить формулу] + K --> L[Сохранить результат] + L --> D + D -->|Нет| M[Конец] +``` + +--- + +## Пример конфигурации полей + +### Глобальное поле (position = 0) +```php +// Количество продаж +[ + 'name_eng' => 'sales_cnt', + 'position' => 0, + 'field_type' => 'global', + 'type' => 'number', + 'func' => 0, + 'dependent_fields' => '' +] +``` + +### Вычисляемое поле 1-го уровня (position = 1) +```php +// Остаток в днях продаж +[ + 'name_eng' => 'sales_day', + 'position' => 1, + 'field_type' => 'calculated', + 'type' => 'number', + 'func' => 1, + 'func_content' => '{"formula": "quantity_storage / (sales_cnt / 30)"}', + 'dependent_fields' => '1,2' // quantity_storage, sales_cnt +] +``` + +### Вычисляемое поле 2-го уровня (position = 2) +```php +// Рекомендуемый заказ +[ + 'name_eng' => 'recommended_order', + 'position' => 2, + 'field_type' => 'calculated', + 'type' => 'number', + 'func' => 1, + 'func_content' => '{"formula": "(14 - sales_day) * avg_daily_sales"}', + 'dependent_fields' => '5,6' // sales_day, avg_daily_sales +] +``` + +--- + +## Связанные модели + +- **[StoreOrdersFieldsData](./StoreOrdersFieldsData.md)** - данные полей заказов +- **[StoreOrdersItem](./StoreOrdersItem.md)** - позиции заказов магазинов +- **[StoreOrders](./StoreOrders.md)** - заказы магазинов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersFieldsData.md b/erp24/docs/models/StoreOrdersFieldsData.md new file mode 100644 index 00000000..f6111d13 --- /dev/null +++ b/erp24/docs/models/StoreOrdersFieldsData.md @@ -0,0 +1,573 @@ +# Модель StoreOrdersFieldsData + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersFieldsData)) + Таблица БД + store_orders_fields_data + Свойства + order_id + int + product_id + string + store_id + string + field_id + int + field_name + string + color + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersFieldsData` хранит фактические значения расширенных полей для конкретных позиций заказов магазинов. Является хранилищем данных для динамических характеристик товаров в заказах, определенных в справочнике `StoreOrdersFields`. Поддерживает хранение значений как по общей позиции, так и с детализацией по цветам (для кустовых растений). + +**Файл модели:** `erp24/records/StoreOrdersFieldsData.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_fields_data` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Составной первичный ключ + +| Поле | Тип | Описание | +|------|-----|----------| +| `order_id` | INTEGER | ID заказа из store_orders (часть PK) (обязательное) | +| `product_id` | VARCHAR(36) | GUID товара (часть PK) (обязательное) | +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) (обязательное) | +| `field_id` | INTEGER | ID поля из store_orders_fields (часть PK) (обязательное) | +| `field_name` | VARCHAR(45) | Системное имя поля (часть PK) (обязательное) | +| `color` | VARCHAR(60) | Цвет товара для детализации (часть PK) (обязательное) | + +### Значения + +| Поле | Тип | Описание | +|------|-----|----------| +| `value` | FLOAT | Числовое значение поля (обязательное) | +| `value_text` | TEXT | Текстовое значение поля (если это текстовое поле) | + +### Метаданные + +| Поле | Тип | Описание | +|------|-----|----------| +| `title` | TEXT | Подсказка или заголовок для значения (обязательное) | +| `date_update` | TIMESTAMP | Время последнего изменения значения (обязательное) | +| `hand` | INTEGER | Флаг: значение внесено вручную (0 - автоматически, 1 - вручную) | + +--- + +## Уникальный индекс + +Модель имеет составной уникальный индекс на все поля первичного ключа: + +``` +[order_id, product_id, store_id, field_id, field_name, color] +``` + +Это гарантирует, что для каждой комбинации заказа, товара, магазина, поля и цвета существует только одно значение. + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + 'order_id', 'product_id', 'store_id', 'field_id', 'field_name', + 'color', 'value', 'title', 'date_update' +], 'required' +``` + +### Типы данных + +| Правило | Поля | Ограничение | +|---------|------|-------------| +| `integer` | `order_id`, `field_id`, `hand` | Целочисленные значения | +| `number` | `value` | Числовое значение (с плавающей точкой) | +| `string` | `value_text`, `title` | Текст без ограничения длины | +| `safe` | `date_update` | Дата/время без дополнительной валидации | +| `string`, max=36 | `product_id`, `store_id` | GUID идентификаторы | +| `string`, max=45 | `field_name` | Системное имя поля | +| `string`, max=60 | `color` | Название цвета | + +--- + +## Структура хранения данных + +### Общие значения (без цветов) + +Для полей, которые не зависят от цвета, поле `color` устанавливается в значение по умолчанию (например, `"default"` или `"N/A"`): + +```php +[ + 'order_id' => 123, + 'product_id' => 'guid-товара', + 'store_id' => 'guid-магазина', + 'field_id' => 5, + 'field_name' => 'sales_cnt', + 'color' => 'default', + 'value' => 45.0, + 'value_text' => null, + 'title' => 'Количество продаж', + 'hand' => 0 +] +``` + +### Значения с детализацией по цветам + +Для кустовых растений хранятся отдельные значения для каждого цвета: + +```php +// Розовые розы +[ + 'order_id' => 123, + 'product_id' => 'guid-розы', + 'store_id' => 'guid-магазина', + 'field_id' => 8, + 'field_name' => 'quantity', + 'color' => 'Розовый', + 'value' => 25.0, + 'title' => 'Количество розовых', + 'hand' => 0 +] + +// Красные розы +[ + 'order_id' => 123, + 'product_id' => 'guid-розы', + 'store_id' => 'guid-магазина', + 'field_id' => 8, + 'field_name' => 'quantity', + 'color' => 'Красный', + 'value' => 30.0, + 'title' => 'Количество красных', + 'hand' => 0 +] +``` + +--- + +## Логика работы с данными + +### Автоматические значения (hand = 0) + +Значения, рассчитанные системой автоматически: +- На основе формул из `StoreOrdersFields.func_content` +- Из статистики продаж +- Из остатков на складе +- Из данных 1С + +### Ручные значения (hand = 1) + +Значения, введенные пользователем вручную: +- Корректировки менеджера +- Особые условия заказа +- Специальные требования магазина + +**Важно:** Ручные значения имеют приоритет над автоматическими и не перезаписываются при пересчете. + +--- + +## Примеры использования + +### Сохранение числового значения + +```php +$data = new StoreOrdersFieldsData(); +$data->order_id = 123; +$data->product_id = 'e7f8a9b0-1234-5678-90ab-cdef12345678'; +$data->store_id = 'a1b2c3d4-5678-90ef-1234-567890abcdef'; +$data->field_id = 5; +$data->field_name = 'sales_cnt'; +$data->color = 'default'; +$data->value = 45; +$data->value_text = null; +$data->title = 'Количество продаж за период'; +$data->date_update = date('Y-m-d H:i:s'); +$data->hand = 0; // Автоматический расчет + +if ($data->save()) { + echo "Данные сохранены"; +} +``` + +### Сохранение текстового значения + +```php +$data = new StoreOrdersFieldsData(); +$data->order_id = 123; +$data->product_id = 'guid-товара'; +$data->store_id = 'guid-магазина'; +$data->field_id = 15; +$data->field_name = 'comment'; +$data->color = 'default'; +$data->value = 0; // Для текстовых полей value = 0 +$data->value_text = 'Требуется дополнительная упаковка'; +$data->title = 'Комментарий к заказу'; +$data->date_update = date('Y-m-d H:i:s'); +$data->hand = 1; // Введено вручную + +$data->save(); +``` + +### Сохранение значений по цветам + +```php +$colors = [ + 'Розовый' => 25, + 'Красный' => 30, + 'Белый' => 20 +]; + +foreach ($colors as $color => $quantity) { + $data = new StoreOrdersFieldsData(); + $data->order_id = 123; + $data->product_id = 'guid-розы'; + $data->store_id = 'guid-магазина'; + $data->field_id = 8; + $data->field_name = 'quantity'; + $data->color = $color; + $data->value = $quantity; + $data->value_text = null; + $data->title = "Количество {$color}"; + $data->date_update = date('Y-m-d H:i:s'); + $data->hand = 0; + $data->save(); +} +``` + +### Получение всех значений для позиции заказа + +```php +$fieldData = StoreOrdersFieldsData::find() + ->where([ + 'order_id' => $orderId, + 'product_id' => $productId, + 'store_id' => $storeId + ]) + ->all(); + +foreach ($fieldData as $data) { + $valueDisplay = $data->value_text ?? $data->value; + echo "{$data->field_name} ({$data->color}): {$valueDisplay}\n"; +} +``` + +### Получение конкретного значения поля + +```php +$data = StoreOrdersFieldsData::findOne([ + 'order_id' => $orderId, + 'product_id' => $productId, + 'store_id' => $storeId, + 'field_id' => $fieldId, + 'field_name' => 'sales_cnt', + 'color' => 'default' +]); + +if ($data) { + $salesCount = $data->value; +} +``` + +### Обновление значения с сохранением истории + +```php +$data = StoreOrdersFieldsData::findOne([ + 'order_id' => $orderId, + 'product_id' => $productId, + 'store_id' => $storeId, + 'field_id' => $fieldId, + 'field_name' => $fieldName, + 'color' => $color +]); + +if ($data) { + $oldValue = $data->value; + $data->value = $newValue; + $data->date_update = date('Y-m-d H:i:s'); + $data->hand = 1; // Помечаем как ручное изменение + + if ($data->save()) { + // Логируем изменение + Yii::info("Поле {$fieldName} изменено: {$oldValue} -> {$newValue}"); + } +} +``` + +### Получение всех ручных корректировок + +```php +$manualData = StoreOrdersFieldsData::find() + ->where(['hand' => 1]) + ->andWhere(['order_id' => $orderId]) + ->all(); + +foreach ($manualData as $data) { + echo "Ручная корректировка: {$data->field_name} = {$data->value}\n"; +} +``` + +### Суммирование значений по цветам + +```php +$totalByField = StoreOrdersFieldsData::find() + ->select(['field_name', 'SUM(value) as total']) + ->where([ + 'order_id' => $orderId, + 'product_id' => $productId, + 'store_id' => $storeId + ]) + ->groupBy('field_name') + ->asArray() + ->all(); + +foreach ($totalByField as $row) { + echo "{$row['field_name']}: итого = {$row['total']}\n"; +} +``` + +### Получение данных с информацией о поле + +```php +$dataWithFields = StoreOrdersFieldsData::find() + ->select([ + 'store_orders_fields_data.*', + 'store_orders_fields.name', + 'store_orders_fields.type', + 'store_orders_fields.description' + ]) + ->leftJoin('store_orders_fields', 'store_orders_fields.id = store_orders_fields_data.field_id') + ->where([ + 'store_orders_fields_data.order_id' => $orderId, + 'store_orders_fields_data.product_id' => $productId, + 'store_orders_fields_data.store_id' => $storeId + ]) + ->asArray() + ->all(); +``` + +### Пакетное обновление значений + +```php +$fieldsToUpdate = [ + ['field_name' => 'sales_cnt', 'value' => 50], + ['field_name' => 'sales_amount', 'value' => 25000], + ['field_name' => 'sales_day', 'value' => 2.5] +]; + +foreach ($fieldsToUpdate as $fieldData) { + StoreOrdersFieldsData::updateAll( + [ + 'value' => $fieldData['value'], + 'date_update' => date('Y-m-d H:i:s') + ], + [ + 'order_id' => $orderId, + 'product_id' => $productId, + 'store_id' => $storeId, + 'field_name' => $fieldData['field_name'], + 'color' => 'default' + ] + ); +} +``` + +--- + +## Связь с другими моделями + +Модель напрямую связана с: + +- **StoreOrdersFields** - справочник полей (через `field_id`) +- **StoreOrdersItem** - позиции заказов (через `order_id`, `product_id`, `store_id`) +- **StoreOrders** - заказы магазинов (через `order_id`) +- **Products1c** - товары (через `product_id`) +- **CityStore** - магазины (через `store_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_fields_data }o--|| store_orders_fields : "field_definition" + store_orders_fields_data }o--|| store_orders_item : "item_data" + store_orders_fields_data }o--|| store_orders : "order" + store_orders_fields_data }o--|| products_1c : "product" + store_orders_fields_data }o--|| city_store : "store" + + store_orders_fields_data { + int order_id PK,FK + string product_id PK,FK + string store_id PK,FK + int field_id PK,FK + string field_name PK + string color PK + float value + text value_text + text title + timestamp date_update + int hand + } + + store_orders_fields { + int id PK + string name_eng UK + string name + int position + text func_content + } + + store_orders_item { + string product_id PK,FK + string store_id PK,FK + int order_id PK,FK + int quantity + text colors_json + } + + store_orders { + int id PK + string store_id FK + date date + int status + } + + products_1c { + string guid PK + string name + string articule + } + + city_store { + string id PK + string name + } +``` + +--- + +## Жизненный цикл данных поля + +```mermaid +stateDiagram-v2 + [*] --> Создание: Новая позиция заказа + Создание --> АвтоРасчет: Применение формул + АвтоРасчет --> Сохранено: date_update, hand=0 + Сохранено --> РучноеИзменение: Корректировка менеджером + РучноеИзменение --> Сохранено: date_update, hand=1 + Сохранено --> Пересчет: Обновление данных + Пересчет --> Сохранено: Если hand=0 + Пересчет --> Сохранено: Если hand=1 (без изменений) + Сохранено --> [*]: Закрытие заказа +``` + +--- + +## Процесс сохранения данных по цветам + +```mermaid +flowchart TD + A[Начало: Позиция заказа] --> B{Товар имеет цвета?} + B -->|Нет| C[Сохранить с color='default'] + B -->|Да| D[Парсить colors_json] + D --> E[Для каждого цвета] + E --> F[Создать запись StoreOrdersFieldsData] + F --> G[Установить color=название цвета] + G --> H[Рассчитать value для цвета] + H --> I[Сохранить запись] + I --> J{Есть еще цвета?} + J -->|Да| E + J -->|Нет| K[Рассчитать общую сумму] + C --> K + K --> L[Конец] +``` + +--- + +## Пример полного набора данных для позиции + +```php +// Позиция: Роза кустовая, заказ №123, магазин "Центральный" + +// 1. Общие статистические данные +[ + 'field_name' => 'sales_cnt', + 'color' => 'default', + 'value' => 75, + 'title' => 'Всего продано штук' +] + +[ + 'field_name' => 'sales_amount', + 'color' => 'default', + 'value' => 37500, + 'title' => 'Сумма продаж' +] + +// 2. Данные по цветам +[ + 'field_name' => 'quantity', + 'color' => 'Розовый', + 'value' => 25, + 'title' => 'Заказано розовых' +] + +[ + 'field_name' => 'quantity', + 'color' => 'Красный', + 'value' => 30, + 'title' => 'Заказано красных' +] + +[ + 'field_name' => 'quantity', + 'color' => 'Белый', + 'value' => 20, + 'title' => 'Заказано белых' +] + +// 3. Вычисляемые поля +[ + 'field_name' => 'sales_day', + 'color' => 'default', + 'value' => 2.5, + 'title' => 'Остаток в днях продаж' +] + +// 4. Текстовые поля +[ + 'field_name' => 'comment', + 'color' => 'default', + 'value' => 0, + 'value_text' => 'Срочный заказ, нужна доставка в понедельник', + 'title' => 'Комментарий' +] +``` + +--- + +## Связанные модели + +- **[StoreOrdersFields](./StoreOrdersFields.md)** - справочник полей +- **[StoreOrdersItem](./StoreOrdersItem.md)** - позиции заказов магазинов +- **[StoreOrders](./StoreOrders.md)** - заказы магазинов +- **[Products1c](./Products1c.md)** - товары 1С +- **[CityStore](./CityStore.md)** - магазины + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersFieldsDataLogi.md b/erp24/docs/models/StoreOrdersFieldsDataLogi.md new file mode 100644 index 00000000..6c1675d5 --- /dev/null +++ b/erp24/docs/models/StoreOrdersFieldsDataLogi.md @@ -0,0 +1,178 @@ +# Модель StoreOrdersFieldsDataLogi + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersFieldsDataLogi)) + Таблица БД + store_orders_fields_data_logi + Свойства + id + int + order_id + int + field_id + int + product_id + string + store_id + string + color + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersFieldsDataLogi` ведёт журнал изменений данных полей заказа магазина. Сохраняет историю всех изменений значений с указанием старого и нового значения, времени изменения и автора. Используется для аудита и отслеживания корректировок заказов. + +**Файл модели:** `erp24/records/StoreOrdersFieldsDataLogi.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_fields_data_logi` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `order_id` | INTEGER | ID заказа магазина | +| `field_id` | INTEGER | ID поля заказа | +| `product_id` | VARCHAR(36) | GUID товара | +| `store_id` | VARCHAR(36) | GUID магазина | +| `color` | VARCHAR(120) | Цвет товара | +| `value` | VARCHAR(255) | Новое значение поля | +| `value_old` | VARCHAR(120) | Предыдущее значение поля | +| `date_add` | TIMESTAMP | Дата и время изменения | +| `admin_id` | INTEGER | ID сотрудника, внёсшего изменение | + +--- + +## Особенности + +- Хранит полную историю изменений значений полей заказа +- Позволяет восстановить хронологию корректировок +- Фиксирует автора каждого изменения +- Детализация до уровня товар + цвет + поле + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_fields_data_logi }o--|| store_orders : "order" + store_orders_fields_data_logi }o--|| store_orders_fields : "field" + store_orders_fields_data_logi }o--|| products_1c : "product" + store_orders_fields_data_logi }o--|| city_store : "store" + store_orders_fields_data_logi }o--|| admin : "author" + + store_orders_fields_data_logi { + int id PK + int order_id FK + int field_id FK + string product_id FK + string store_id FK + string color + string value + string value_old + timestamp date_add + int admin_id FK + } +``` + +--- + +## Примеры использования + +### Создание записи лога + +```php +$log = new StoreOrdersFieldsDataLogi(); +$log->order_id = $orderId; +$log->field_id = $fieldId; +$log->product_id = $productGuid; +$log->store_id = $storeGuid; +$log->color = 'Красный'; +$log->value = '100'; +$log->value_old = '50'; +$log->date_add = date('Y-m-d H:i:s'); +$log->admin_id = Yii::$app->user->id; +$log->save(); +``` + +### Получение истории изменений поля + +```php +$history = StoreOrdersFieldsDataLogi::find() + ->where([ + 'order_id' => $orderId, + 'field_id' => $fieldId, + 'product_id' => $productGuid + ]) + ->orderBy(['date_add' => SORT_DESC]) + ->all(); + +foreach ($history as $log) { + echo "{$log->date_add}: {$log->value_old} → {$log->value} (admin #{$log->admin_id})\n"; +} +``` + +### Получение всех изменений за период + +```php +$changes = StoreOrdersFieldsDataLogi::find() + ->where(['between', 'date_add', $dateFrom, $dateTo]) + ->andWhere(['store_id' => $storeGuid]) + ->orderBy(['date_add' => SORT_DESC]) + ->all(); +``` + +### Получение изменений конкретного сотрудника + +```php +$adminChanges = StoreOrdersFieldsDataLogi::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['date_add' => SORT_DESC]) + ->limit(100) + ->all(); +``` + +### Подсчёт изменений по заказу + +```php +$changesCount = StoreOrdersFieldsDataLogi::find() + ->where(['order_id' => $orderId]) + ->count(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| Все поля | Обязательные | +| `order_id`, `field_id`, `admin_id` | Целые числа | +| `product_id`, `store_id` | Строка, макс. 36 символов | +| `color`, `value_old` | Строка, макс. 120 символов | +| `value` | Строка, макс. 255 символов | + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов +- **[StoreOrdersFields](./StoreOrdersFields.md)** — поля заказа +- **[StoreOrdersFieldsData](./StoreOrdersFieldsData.md)** — данные полей +- **[Admin](./Admin.md)** — сотрудники + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersFieldsProperty.md b/erp24/docs/models/StoreOrdersFieldsProperty.md new file mode 100644 index 00000000..53382701 --- /dev/null +++ b/erp24/docs/models/StoreOrdersFieldsProperty.md @@ -0,0 +1,255 @@ +# Класс: StoreOrdersFieldsProperty + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersFieldsProperty)) + Таблица БД + store_orders_fields_property + Свойства + id + int + field_id + int + field_name + string + type + string + value + float + style_class + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель настроек визуального отображения полей заказов в ERP24. Определяет правила условного форматирования ячеек таблицы заказов на основе значений полей — аналог условного форматирования в Excel. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`store_orders_fields_property` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `field_id` | int | ID поля для применения правила | +| `field_name` | varchar(55) | Имя поля | +| `type` | varchar(35) | Тип сравнения (>, <, =, >=, <= и т.д.) | +| `value` | float | Пороговое значение для сравнения | +| `style_class` | varchar(120) | CSS-класс для применения при выполнении условия | + +## Диаграмма связей + +```mermaid +erDiagram + StoreOrdersFieldsProperty { + int id PK + int field_id + varchar field_name + varchar type + float value + varchar style_class + } + + StoreOrders { + int id PK + float field_value + } + + StoreOrdersFieldsProperty ||--o{ StoreOrders : "условное форматирование" +``` + +## Диаграмма логики применения + +```mermaid +flowchart TD + A[Поле заказа
    field_id=5, value=150] --> B{Проверка правил} + B --> C[Правило 1:
    type='>' value=100
    style_class='bg-warning'] + B --> D[Правило 2:
    type='>' value=200
    style_class='bg-danger'] + + C -->|150 > 100 = true| E[Применить bg-warning] + D -->|150 > 200 = false| F[Не применять] + + E --> G[Итоговый CSS:
    class='bg-warning'] +``` + +## Типы сравнения + +| type | Описание | Пример | +|------|----------|--------| +| `>` | Больше | value > 100 | +| `<` | Меньше | value < 50 | +| `>=` | Больше или равно | value >= 100 | +| `<=` | Меньше или равно | value <= 50 | +| `=` | Равно | value = 0 | +| `!=` | Не равно | value != 0 | + +## Примеры использования + +### Создание правила форматирования +```php +$property = new StoreOrdersFieldsProperty(); +$property->field_id = 5; +$property->field_name = 'total_sum'; +$property->type = '>'; +$property->value = 10000; +$property->style_class = 'bg-success text-white'; +$property->save(); +``` + +### Получение правил для поля +```php +$rules = StoreOrdersFieldsProperty::find() + ->where(['field_id' => $fieldId]) + ->all(); + +foreach ($rules as $rule) { + echo "{$rule->field_name} {$rule->type} {$rule->value}: {$rule->style_class}\n"; +} +``` + +### Применение правил к значению +```php +function getStyleForValue($fieldId, $value) +{ + $rules = StoreOrdersFieldsProperty::find() + ->where(['field_id' => $fieldId]) + ->all(); + + $classes = []; + foreach ($rules as $rule) { + $match = false; + switch ($rule->type) { + case '>': + $match = $value > $rule->value; + break; + case '<': + $match = $value < $rule->value; + break; + case '>=': + $match = $value >= $rule->value; + break; + case '<=': + $match = $value <= $rule->value; + break; + case '=': + $match = $value == $rule->value; + break; + case '!=': + $match = $value != $rule->value; + break; + } + + if ($match) { + $classes[] = $rule->style_class; + } + } + + return implode(' ', $classes); +} + +$cssClass = getStyleForValue(5, 15000); +// Результат: 'bg-success text-white' +``` + +### Получение всех правил по полям +```php +$allRules = StoreOrdersFieldsProperty::find() + ->orderBy(['field_id' => SORT_ASC, 'value' => SORT_ASC]) + ->all(); + +$grouped = []; +foreach ($allRules as $rule) { + $grouped[$rule->field_name][] = $rule; +} +``` + +### Обновление правила +```php +$rule = StoreOrdersFieldsProperty::findOne($id); +$rule->value = 15000; +$rule->style_class = 'bg-warning'; +$rule->save(); +``` + +### Удаление правил для поля +```php +StoreOrdersFieldsProperty::deleteAll(['field_id' => $fieldId]); +``` + +### Использование в представлении +```php +// В view-файле +foreach ($orders as $order) { + $totalClass = getStyleForValue($totalFieldId, $order->total_sum); + $qtyClass = getStyleForValue($qtyFieldId, $order->quantity); + + echo ""; + echo "{$order->total_sum}"; + echo "{$order->quantity}"; + echo ""; +} +``` + +### Создание набора правил для индикации +```php +// Красный для критических значений +$critical = new StoreOrdersFieldsProperty(); +$critical->field_id = 10; +$critical->field_name = 'delay_days'; +$critical->type = '>'; +$critical->value = 7; +$critical->style_class = 'bg-danger text-white'; +$critical->save(); + +// Жёлтый для предупреждений +$warning = new StoreOrdersFieldsProperty(); +$warning->field_id = 10; +$warning->field_name = 'delay_days'; +$warning->type = '>'; +$warning->value = 3; +$warning->style_class = 'bg-warning'; +$warning->save(); + +// Зелёный для нормальных значений +$ok = new StoreOrdersFieldsProperty(); +$ok->field_id = 10; +$ok->field_name = 'delay_days'; +$ok->type = '<='; +$ok->value = 3; +$ok->style_class = 'bg-success text-white'; +$ok->save(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `field_id` | required, integer | +| `field_name` | required, string (max 55) | +| `type` | required, string (max 35) | +| `value` | required, number | +| `style_class` | required, string (max 120) | + +## Связанные модели + +- [StoreOrders](./StoreOrders.md) — заказы магазинов + +## Особенности реализации + +1. **Условное форматирование**: CSS-классы применяются по условию +2. **Гибкие типы сравнения**: Поддержка >, <, =, >=, <=, != +3. **Множественные правила**: Несколько правил для одного поля +4. **CSS-классы**: Произвольные классы для стилизации +5. **Визуальная индикация**: Подсветка критических значений в таблицах +6. **Настраиваемость**: Правила создаются администратором diff --git a/erp24/docs/models/StoreOrdersItem.md b/erp24/docs/models/StoreOrdersItem.md new file mode 100644 index 00000000..9da6b7fa --- /dev/null +++ b/erp24/docs/models/StoreOrdersItem.md @@ -0,0 +1,431 @@ +# Модель StoreOrdersItem + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersItem)) + Таблица БД + store_orders_item + Свойства + product_id + string + store_id + string + order_id + int + provider_id + int + colors_json + string + quantity + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersItem` представляет позиции заказов магазинов на закупку товаров. Хранит информацию о товарах, запрошенных магазинами для пополнения запасов, включая количество, статистику продаж, списания и распределение кустовыми цветами. + +**Файл модели:** `erp24/records/StoreOrdersItem.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_item` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +### Идентификаторы и связи + +| Поле | Тип | Описание | +|------|-----|----------| +| `product_id` | VARCHAR(36) | GUID товара из Products1c (первичный ключ) | +| `store_id` | VARCHAR(36) | GUID магазина (первичный ключ) | +| `order_id` | INTEGER | ID заказа магазина (первичный ключ) | +| `provider_id` | INTEGER | ID поставщика товара | +| `admin_id` | INTEGER | ID сотрудника, создавшего позицию | + +### Количество и распределение + +| Поле | Тип | Описание | +|------|-----|----------| +| `quantity` | INTEGER | Количество товара в заказе (обязательное) | +| `quantity_purchase` | INTEGER | Количество заказанное кустовыми цветами | +| `quantity_fact` | INTEGER | Фактическое количество полученного товара | +| `quantity_storage` | INTEGER | Количество на складе на момент заказа | +| `division_auto` | INTEGER | Автоматическое деление по формуле | +| `division_hand` | INTEGER | Ручная добавка по делению кустовыми | +| `division_fact` | INTEGER | Фактическое деление кустовыми | +| `division_quantity` | INTEGER | Итоговое количество распределения | + +### Статистика продаж + +| Поле | Тип | Описание | +|------|-----|----------| +| `sales_cnt` | INTEGER | Количество продаж в штуках | +| `sales_amount` | INTEGER | Сумма продаж в рублях | +| `sales_day` | FLOAT | Остаток в количестве дней продаж | +| `write_downs` | INTEGER | Количество списаний в штуках | +| `goods_in_transit` | INTEGER | Товар в пути (количество) | + +### Дополнительные данные + +| Поле | Тип | Описание | +|------|-----|----------| +| `colors_json` | TEXT | JSON-массив с характеристиками по цветам | +| `status` | INTEGER | Статус позиции (1 - создал и отправил) | +| `status_zakup` | INTEGER | Статус закупщика | +| `date` | DATE | Дата внесения позиции | + +--- + +## Уникальные индексы + +Модель имеет два составных уникальных индекса: + +1. `[product_id, store_id, order_id]` - базовый уникальный ключ позиции заказа +2. `[product_id, store_id, order_id, provider_id]` - расширенный ключ с учетом поставщика + +--- + +## Правила валидации + +### Обязательные поля + +Все поля модели являются обязательными: +```php +[ + 'product_id', 'store_id', 'order_id', 'provider_id', 'colors_json', + 'quantity', 'quantity_purchase', 'quantity_fact', 'quantity_storage', + 'admin_id', 'date', 'sales_cnt', 'sales_amount', 'write_downs', + 'goods_in_transit', 'sales_day', 'division_auto', 'division_hand', + 'division_fact', 'division_quantity' +], 'required' +``` + +### Типы данных + +| Правило | Поля | Описание | +|---------|------|----------| +| `integer` | `order_id`, `provider_id`, `quantity`, `quantity_purchase`, `quantity_fact`, `quantity_storage`, `admin_id`, `status`, `status_zakup`, `sales_cnt`, `sales_amount`, `write_downs`, `goods_in_transit`, `division_auto`, `division_hand`, `division_fact`, `division_quantity` | Целочисленные значения | +| `string` | `colors_json` | JSON-данные | +| `safe` | `date` | Дата без дополнительной валидации | +| `number` | `sales_day` | Числовое значение с плавающей точкой | +| `string`, max=36 | `product_id`, `store_id` | GUID идентификаторы | + +--- + +## Структура colors_json + +Поле `colors_json` содержит JSON-массив с характеристиками по цветам кустовых растений: + +```json +[ + { + "color": "Розовый", + "quantity": 25, + "division": 5 + }, + { + "color": "Красный", + "quantity": 30, + "division": 6 + } +] +``` + +**Структура объекта:** +- `color` - название цвета +- `quantity` - количество данного цвета +- `division` - результат деления по цвету + +--- + +## Логика расчета количества + +### Автоматическое распределение + +Поле `division_auto` рассчитывается автоматически по формуле на основе: +- Текущих остатков (`quantity_storage`) +- Статистики продаж (`sales_cnt`, `sales_day`) +- Товара в пути (`goods_in_transit`) + +### Ручная корректировка + +Поле `division_hand` позволяет менеджеру вручную скорректировать количество, добавив или убавив товар. + +### Итоговое количество + +``` +division_quantity = division_auto + division_hand +``` + +Это итоговое количество учитывается при формировании окончательного заказа поставщику. + +--- + +## Статусы позиции + +### status + +| Значение | Описание | +|----------|----------| +| 0 | Черновик, позиция не отправлена | +| 1 | Создана и отправлена поставщику | + +### status_zakup + +Статус обработки закупщиком (значения специфичны для бизнес-логики). + +--- + +## Примеры использования + +### Создание новой позиции заказа + +```php +$item = new StoreOrdersItem(); +$item->product_id = 'e7f8a9b0-1234-5678-90ab-cdef12345678'; +$item->store_id = 'a1b2c3d4-5678-90ef-1234-567890abcdef'; +$item->order_id = 123; +$item->provider_id = 5; +$item->colors_json = json_encode([ + ['color' => 'Розовый', 'quantity' => 25, 'division' => 5], + ['color' => 'Красный', 'quantity' => 30, 'division' => 6] +]); +$item->quantity = 55; +$item->quantity_purchase = 55; +$item->quantity_fact = 0; +$item->quantity_storage = 10; +$item->admin_id = Yii::$app->user->id; +$item->date = date('Y-m-d'); +$item->status = 0; +$item->status_zakup = 0; +$item->sales_cnt = 45; +$item->sales_amount = 22500; +$item->write_downs = 2; +$item->goods_in_transit = 0; +$item->sales_day = 1.2; +$item->division_auto = 50; +$item->division_hand = 5; +$item->division_fact = 0; +$item->division_quantity = 55; + +if ($item->save()) { + echo "Позиция заказа создана"; +} +``` + +### Получение позиций заказа + +```php +// Все позиции конкретного заказа +$items = StoreOrdersItem::find() + ->where(['order_id' => $orderId]) + ->all(); + +// Позиции для конкретного магазина +$storeItems = StoreOrdersItem::find() + ->where(['store_id' => $storeId]) + ->andWhere(['order_id' => $orderId]) + ->all(); +``` + +### Обновление статуса позиции + +```php +$item = StoreOrdersItem::findOne([ + 'product_id' => $productId, + 'store_id' => $storeId, + 'order_id' => $orderId +]); + +if ($item) { + $item->status = 1; // Отправлено + $item->save(); +} +``` + +### Расчет общего количества по заказу + +```php +$totalQuantity = StoreOrdersItem::find() + ->where(['order_id' => $orderId]) + ->sum('quantity'); + +echo "Всего товаров в заказе: {$totalQuantity}"; +``` + +### Анализ продаж по позициям + +```php +$stats = StoreOrdersItem::find() + ->select([ + 'product_id', + 'SUM(sales_cnt) as total_sales', + 'SUM(sales_amount) as total_amount', + 'AVG(sales_day) as avg_sales_day' + ]) + ->where(['store_id' => $storeId]) + ->andWhere(['>=', 'date', $startDate]) + ->groupBy('product_id') + ->asArray() + ->all(); +``` + +### Работа с JSON цветов + +```php +$item = StoreOrdersItem::findOne([...]); +$colors = json_decode($item->colors_json, true); + +foreach ($colors as $colorData) { + echo "Цвет: {$colorData['color']}, "; + echo "Количество: {$colorData['quantity']}, "; + echo "Деление: {$colorData['division']}\n"; +} +``` + +### Корректировка ручного деления + +```php +$item = StoreOrdersItem::findOne([...]); + +// Добавляем 10 штук вручную +$item->division_hand += 10; +$item->division_quantity = $item->division_auto + $item->division_hand; +$item->quantity = $item->division_quantity; + +$item->save(); +``` + +--- + +## Связь с другими моделями + +Хотя модель не содержит явных связей через `relations()`, она логически связана с: + +- **StoreOrders** - основной заказ магазина (через `order_id`) +- **Products1c** - товары из справочника 1С (через `product_id`) +- **CityStore** - магазины (через `store_id`) +- **Admin** - сотрудники (через `admin_id`) +- **StoreOrdersFields** и **StoreOrdersFieldsData** - расширенные характеристики позиций + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_item }o--|| store_orders : "belongs_to" + store_orders_item }o--|| products_1c : "product" + store_orders_item }o--|| city_store : "store" + store_orders_item }o--|| admin : "created_by" + store_orders_item ||--o{ store_orders_fields_data : "has_field_data" + + store_orders_item { + string product_id PK,FK + string store_id PK,FK + int order_id PK,FK + int provider_id FK + text colors_json + int quantity + int quantity_purchase + int quantity_fact + int quantity_storage + int admin_id FK + date date + int status + int status_zakup + int sales_cnt + int sales_amount + int write_downs + int goods_in_transit + float sales_day + int division_auto + int division_hand + int division_fact + int division_quantity + } + + store_orders { + int id PK + string store_id FK + date date + int status + } + + products_1c { + string guid PK + string name + string articule + } + + city_store { + string id PK + string name + } + + admin { + int id PK + string name + } +``` + +--- + +## Бизнес-логика + +### Цикл обработки позиции заказа + +```mermaid +stateDiagram-v2 + [*] --> Черновик: Создание позиции + Черновик --> Расчет: Автоматический расчет + Расчет --> Корректировка: Ручная правка менеджера + Корректировка --> Отправлен: Подтверждение заказа + Отправлен --> ОбработкаЗакупщиком: Закупщик обрабатывает + ОбработкаЗакупщиком --> Получен: Товар поступил + Получен --> [*]: Закрытие позиции + + Расчет --> Черновик: Пересчет + Корректировка --> Расчет: Пересчет +``` + +--- + +## Процесс расчета количества + +```mermaid +flowchart TD + A[Начало расчета] --> B[Получить остаток на складе] + B --> C[Получить статистику продаж] + C --> D[Рассчитать товар в пути] + D --> E[Применить формулу division_auto] + E --> F{Нужна ручная корректировка?} + F -->|Да| G[Добавить division_hand] + F -->|Нет| H[division_hand = 0] + G --> I[Рассчитать division_quantity] + H --> I + I --> J[Установить quantity] + J --> K[Сохранить позицию] + K --> L[Конец] +``` + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** - заказы магазинов +- **[StoreOrdersFields](./StoreOrdersFields.md)** - поля заказов +- **[StoreOrdersFieldsData](./StoreOrdersFieldsData.md)** - данные полей заказов +- **[Products1c](./Products1c.md)** - товары 1С +- **[CityStore](./CityStore.md)** - магазины + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersPrices.md b/erp24/docs/models/StoreOrdersPrices.md new file mode 100644 index 00000000..e40834d7 --- /dev/null +++ b/erp24/docs/models/StoreOrdersPrices.md @@ -0,0 +1,227 @@ +# Модель StoreOrdersPrices + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersPrices)) + Таблица БД + store_orders_prices + Свойства + product_id + string + order_id + int + provider_id + int + purchase_price + float + purchase_summ + float + purchase_price_zakup + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersPrices` хранит информацию о ценах и количествах товаров в заказах магазина на разных этапах закупки. Отслеживает заказанное, закупленное и фактически полученное количество товара с учётом цен и расхождений. Используется для учёта закупок и расчёта себестоимости. + +**Файл модели:** `erp24/records/StoreOrdersPrices.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_prices` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `product_id` | VARCHAR(36) | GUID товара (часть PK) | +| `order_id` | INTEGER | ID заказа магазина (часть PK) | +| `provider_id` | INTEGER | ID поставщика | +| `purchase_price` | FLOAT | Цена закупки (заявленная) | +| `purchase_summ` | FLOAT | Сумма закупки (заявленная) | +| `purchase_price_zakup` | FLOAT | Фактическая цена закупки | +| `price_zakup_summ` | FLOAT | Фактическая сумма закупки | +| `quantity_purchase_summ` | INTEGER | Заказанное количество (от кустовых) | +| `quantity_zakup` | INTEGER | Количество к закупке | +| `quantity_zakup_fact` | INTEGER | Фактически закуплено | +| `comment_zakup` | TEXT | Комментарий закупщика | +| `comment_discrepancy_polnogramm` | TEXT | Комментарий по расхождению полнограмм | +| `quantity_warehouseman_fact` | INTEGER | Принято кладовщиком | +| `quantity_rejection` | INTEGER | Количество брака/отказа | +| `quantity_zakup_info` | INTEGER | Куплено по факту (информация) | +| `cost_price` | FLOAT | Себестоимость единицы | +| `additional_quantity` | INTEGER | Дозакупка в штуках | + +--- + +## Особенности + +- **Составной уникальный ключ** по `product_id`, `order_id` +- Отслеживает полный цикл закупки: заказ → закупка → приёмка +- Хранит расхождения между заявленным и фактическим количеством +- Позволяет учитывать брак и дозакупки + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_prices }o--|| store_orders : "order" + store_orders_prices }o--|| products_1c : "product" + store_orders_prices }o--o| providers : "provider" + + store_orders_prices { + string product_id PK,FK + int order_id PK,FK + int provider_id FK + float purchase_price + float purchase_summ + float purchase_price_zakup + float price_zakup_summ + int quantity_purchase_summ + int quantity_zakup + int quantity_zakup_fact + text comment_zakup + int quantity_warehouseman_fact + int quantity_rejection + float cost_price + int additional_quantity + } +``` + +--- + +## Примеры использования + +### Создание записи цены закупки + +```php +$price = new StoreOrdersPrices(); +$price->product_id = $productGuid; +$price->order_id = $orderId; +$price->provider_id = $providerId; +$price->purchase_price = 150.00; +$price->purchase_summ = 1500.00; +$price->purchase_price_zakup = 150.00; +$price->price_zakup_summ = 1500.00; +$price->quantity_purchase_summ = 10; +$price->quantity_zakup = 10; +$price->quantity_zakup_fact = 0; +$price->comment_zakup = ''; +$price->comment_discrepancy_polnogramm = ''; +$price->quantity_warehouseman_fact = 0; +$price->quantity_rejection = 0; +$price->quantity_zakup_info = 0; +$price->cost_price = 0; +$price->additional_quantity = 0; +$price->save(); +``` + +### Получение цен по заказу + +```php +$prices = StoreOrdersPrices::find() + ->where(['order_id' => $orderId]) + ->all(); + +$totalPurchase = 0; +$totalFact = 0; + +foreach ($prices as $price) { + $totalPurchase += $price->purchase_summ; + $totalFact += $price->price_zakup_summ; + + echo "Товар {$price->product_id}: "; + echo "заказано {$price->quantity_zakup}, "; + echo "закуплено {$price->quantity_zakup_fact}\n"; +} +``` + +### Обновление данных после закупки + +```php +$price = StoreOrdersPrices::findOne([ + 'product_id' => $productGuid, + 'order_id' => $orderId +]); + +if ($price) { + $price->quantity_zakup_fact = 9; + $price->purchase_price_zakup = 155.00; + $price->price_zakup_summ = 1395.00; + $price->comment_zakup = 'Цена выросла на 5 руб.'; + $price->save(); +} +``` + +### Фиксация приёмки кладовщиком + +```php +$price->quantity_warehouseman_fact = 8; +$price->quantity_rejection = 1; +$price->comment_discrepancy_polnogramm = 'Обнаружен 1 брак'; +$price->save(); +``` + +### Расчёт расхождений по заказу + +```php +$prices = StoreOrdersPrices::find() + ->where(['order_id' => $orderId]) + ->all(); + +$discrepancies = []; +foreach ($prices as $price) { + $ordered = $price->quantity_zakup; + $received = $price->quantity_warehouseman_fact; + + if ($ordered != $received) { + $discrepancies[] = [ + 'product' => $price->product_id, + 'ordered' => $ordered, + 'received' => $received, + 'diff' => $received - $ordered + ]; + } +} +``` + +### Расчёт общей себестоимости + +```php +$totalCost = StoreOrdersPrices::find() + ->where(['order_id' => $orderId]) + ->sum('cost_price * quantity_warehouseman_fact'); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| Большинство полей | Обязательные | +| `order_id`, `provider_id`, `quantity_*`, `additional_quantity` | Целые числа | +| `purchase_price`, `purchase_summ`, `*_zakup*`, `cost_price` | Числа с плавающей точкой | +| `product_id` | Строка, макс. 36 символов | +| `comment_zakup`, `comment_discrepancy_polnogramm` | Текст | +| `product_id + order_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов +- **[Products1c](./Products1c.md)** — товары + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersStatuses.md b/erp24/docs/models/StoreOrdersStatuses.md new file mode 100644 index 00000000..072eda25 --- /dev/null +++ b/erp24/docs/models/StoreOrdersStatuses.md @@ -0,0 +1,165 @@ +# Модель StoreOrdersStatuses + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersStatuses)) + Таблица БД + store_orders_statuses + Свойства + id + int + name + string + posit + int + groups + string + description + string + background + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreOrdersStatuses` представляет справочник статусов заказов магазинов. Определяет жизненный цикл заказа: от черновика до выполнения. Содержит настройки визуального отображения (цвета), права доступа и конфигурацию полей для каждого статуса. Используется для управления workflow заказов. + +**Файл модели:** `erp24/records/StoreOrdersStatuses.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_orders_statuses` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Название статуса | +| `posit` | INTEGER | Порядок сортировки | +| `groups` | VARCHAR(255) | Группы должностей, видящих этот статус (JSON/CSV) | +| `description` | TEXT | Описание статуса | +| `background` | VARCHAR(55) | CSS-цвет фона статуса | +| `color` | VARCHAR(25) | CSS-цвет текста | +| `dostup` | TEXT | Права доступа (группы, которые могут видеть) | +| `stores_show` | INTEGER | Флаг показа на списке магазинов | +| `status_edit_dostup` | TEXT | Права на изменение статуса (JSON) | +| `fields_sort` | TEXT | Порядок сортировки полей для статуса (JSON) | +| `fields_hide` | TEXT | ID скрываемых полей на этом статусе (JSON) | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_orders_statuses ||--o{ store_orders : "status" + store_orders_statuses ||--o{ store_order_status_log : "history" + + store_orders_statuses { + int id PK + string name + int posit + string groups + text description + string background + string color + text dostup + int stores_show + text status_edit_dostup + text fields_sort + text fields_hide + } +``` + +--- + +## Примеры использования + +### Получение всех статусов + +```php +$statuses = StoreOrdersStatuses::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statuses as $status) { + echo ""; + echo "{$status->name}"; + echo "\n"; +} +``` + +### Получение статусов, доступных группе + +```php +$groupId = Yii::$app->user->identity->group_id; + +$statuses = StoreOrdersStatuses::find() + ->where(['like', 'groups', $groupId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Проверка прав на изменение статуса + +```php +$status = StoreOrdersStatuses::findOne($statusId); +$editAccess = json_decode($status->status_edit_dostup, true); + +$canEdit = in_array(Yii::$app->user->identity->group_id, $editAccess); +``` + +### Получение настроек полей для статуса + +```php +$status = StoreOrdersStatuses::findOne($statusId); +$fieldsSort = json_decode($status->fields_sort, true); +$fieldsHide = json_decode($status->fields_hide, true); + +// Применение сортировки и скрытия полей +$visibleFields = array_diff($allFields, $fieldsHide); +``` + +### Формирование выпадающего списка + +```php +$statusList = ArrayHelper::map( + StoreOrdersStatuses::find()->orderBy(['posit' => SORT_ASC])->all(), + 'id', + 'name' +); + +echo Html::dropDownList('status', $selected, $statusList); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name`, `posit`, `groups`, `description`, `background`, `color`, `dostup`, `status_edit_dostup`, `fields_sort`, `fields_hide` | Обязательные | +| `posit`, `stores_show` | Целые числа | +| `name`, `groups` | Строка, макс. 255 символов | +| `background` | Строка, макс. 55 символов | +| `color` | Строка, макс. 25 символов | +| `description`, `dostup`, `status_edit_dostup`, `fields_sort`, `fields_hide` | Текст | + +--- + +## Связанные модели + +- **[StoreOrders](./StoreOrders.md)** — заказы магазинов +- **[StoreOrderStatusLog](./StoreOrderStatusLog.md)** — история статусов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreOrdersStatusesSearch.md b/erp24/docs/models/StoreOrdersStatusesSearch.md new file mode 100644 index 00000000..c9e94bb7 --- /dev/null +++ b/erp24/docs/models/StoreOrdersStatusesSearch.md @@ -0,0 +1,192 @@ +# Класс: StoreOrdersStatusesSearch + + +## Mindmap + +```mermaid +mindmap + root((StoreOrdersStatusesSearch)) + Таблица БД + ActiveRecord + Наследование + extends StoreOrdersStatuses +``` + +## Назначение +Search-модель для поиска и фильтрации статусов заказов магазинов в ERP24. Справочник статусов с настройками визуализации (цвет, фон), прав доступа и порядка полей. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`StoreOrdersStatuses` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `posit`, `stores_show` — integer +- `name`, `groups`, `description`, `background`, `color`, `dostup`, `status_edit_dostup`, `fields_sort`, `fields_hide` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска статусов заказов. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос StoreOrdersStatuses::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, posit, stores_show + - like: name, groups, description, background, color, dostup, status_edit_dostup, fields_sort, fields_hide + +## Диаграмма структуры + +```mermaid +erDiagram + StoreOrdersStatuses { + int id PK + varchar name + int posit + varchar groups + varchar description + varchar background + varchar color + int stores_show + varchar dostup + varchar status_edit_dostup + varchar fields_sort + varchar fields_hide + } +``` + +## Диаграмма визуализации статусов + +```mermaid +flowchart LR + A[Статус] --> B[Визуализация] + B --> C[background - фон] + B --> D[color - цвет текста] + + A --> E[Права] + E --> F[dostup - просмотр] + E --> G[status_edit_dostup - редактирование] + + A --> H[Отображение] + H --> I[fields_sort - порядок полей] + H --> J[fields_hide - скрытые поля] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new StoreOrdersStatusesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию статуса +```php +$searchModel = new StoreOrdersStatusesSearch(); +$dataProvider = $searchModel->search([ + 'StoreOrdersStatusesSearch' => [ + 'name' => 'Новый', + ] +]); +``` + +### Поиск видимых в магазинах +```php +$searchModel = new StoreOrdersStatusesSearch(); +$dataProvider = $searchModel->search([ + 'StoreOrdersStatusesSearch' => [ + 'stores_show' => 1, + ] +]); +``` + +### Поиск по группе +```php +$searchModel = new StoreOrdersStatusesSearch(); +$dataProvider = $searchModel->search([ + 'StoreOrdersStatusesSearch' => [ + 'groups' => 'active', + ] +]); +``` + +### Поиск по цвету фона +```php +$searchModel = new StoreOrdersStatusesSearch(); +$dataProvider = $searchModel->search([ + 'StoreOrdersStatusesSearch' => [ + 'background' => '#FF0000', + ] +]); +``` + +### GridView с визуализацией +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'posit', + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + return Html::tag('span', $model->name, [ + 'style' => "background: {$model->background}; color: {$model->color}; padding: 3px 8px;" + ]); + }, + ], + 'groups', + 'description', + [ + 'attribute' => 'stores_show', + 'value' => function($model) { + return $model->stores_show ? 'Да' : 'Нет'; + }, + 'filter' => [0 => 'Нет', 1 => 'Да'], + ], + ], +]) ?> +``` + +## Связанные модели + +- [StoreOrdersStatuses](./StoreOrdersStatuses.md) — базовая модель статусов +- [StoreOrders](./StoreOrders.md) — заказы магазинов + +## Особенности реализации + +1. **Позиционирование**: posit для сортировки в интерфейсе +2. **Визуализация**: background и color для цветовой схемы +3. **Группировка**: groups для логической группировки +4. **Права доступа**: dostup и status_edit_dostup в JSON-формате +5. **Настройка полей**: fields_sort и fields_hide для кастомизации формы +6. **Видимость в магазинах**: stores_show для фильтрации по точкам +7. **like вместо ilike**: Регистрозависимый поиск diff --git a/erp24/docs/models/StorePlan.md b/erp24/docs/models/StorePlan.md new file mode 100644 index 00000000..06a5b365 --- /dev/null +++ b/erp24/docs/models/StorePlan.md @@ -0,0 +1,398 @@ +# Class: StorePlan + + +## Mindmap + +```mermaid +mindmap + root((StorePlan)) + Таблица БД + store_plan + Свойства + store_id + string + month + string + year + int + summ_week + float + summ + float + summ_fact + float + Связи + StoreId + 1:1 ExportImportTable + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StorePlan` представляет плановые показатели продаж магазина в системе ERP24. Она хранит информацию о плановых и фактических суммах продаж по месяцам и годам. Модель используется для планирования выручки, контроля выполнения плана и анализа эффективности работы магазинов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_plan` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `store_id` | string(36) | да | GUID магазина (связь через ExportImportTable) | +| `month` | string(2) | да | Месяц планирования (01-12) | +| `year` | int | да | Год планирования | +| `summ_week` | float | да | Плановая сумма продаж в неделю | +| `summ` | float | да | Плановая сумма продаж за месяц | +| `summ_fact` | float | да | Фактическая сумма продаж за месяц | +| `admin_id` | int | да | ID администратора, установившего план | +| `date_edit` | timestamp | да | Дата и время последнего редактирования плана | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_plan' + +**Пример:** +```php +$tableName = StorePlan::tableName(); // 'store_plan' +``` + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Все поля обязательны для заполнения +- `year` и `admin_id` - целые числа +- `summ_week`, `summ`, `summ_fact` - числа с плавающей точкой (деньги) +- `store_id` - строка длиной 36 символов (GUID) +- `month` - строка длиной 2 символа (01-12) +- Уникальность по комбинации `store_id`, `month`, `year` (один план на месяц для магазина) + +**Пример:** +```php +$plan = new StorePlan(); +$plan->store_id = '550e8400-e29b-41d4-a716-446655440000'; +$plan->month = '03'; +$plan->year = 2024; +$plan->summ = 500000.00; + +if ($plan->validate()) { + echo "План валиден"; +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает человекочитаемые метки для атрибутов модели. + +**Возвращает:** `array` - ассоциативный массив меток на английском + +**Пример:** +```php +$labels = (new StorePlan())->attributeLabels(); +echo $labels['summ']; // 'Summ' +``` + +--- + +### `getStoreId()` +**Описание:** Получает связь с магазином через таблицу экспорта/импорта GUID. + +**Возвращает:** `ActiveQuery` - запрос для получения записи магазина + +**Логика работы:** +- Связывает GUID магазина (`store_id`) с таблицей `export_import_table` +- Фильтрует по условиям: `export_id = 1` и `entity = 'city_store'` +- Через эту связь можно получить реальный ID магазина в таблице `city_store` + +**Пример:** +```php +$plan = StorePlan::findOne(['store_id' => '550e8400-...', 'month' => '03', 'year' => 2024]); +$storeExport = $plan->storeId; + +if ($storeExport) { + $realStoreId = $storeExport->entity_id; // ID магазина в city_store +} +``` + +--- + +## Связи (Relations) + +### `getStoreId()` +**Тип связи:** hasOne + +**Связанная модель:** `ExportImportTable` + +**Условие:** `['export_val' => 'store_id']` с фильтрами `export_id=1, entity='city_store'` + +**Описание:** Связывает GUID магазина с таблицей экспорта для получения реального ID магазина. + +--- + +## Примеры использования + +### 1. Создание плана продаж на месяц +```php +$plan = new StorePlan(); +$plan->store_id = '550e8400-e29b-41d4-a716-446655440000'; // GUID магазина +$plan->month = '04'; // Апрель +$plan->year = 2024; +$plan->summ_week = 125000.00; // План на неделю: 125 000 руб. +$plan->summ = 500000.00; // План на месяц: 500 000 руб. +$plan->summ_fact = 0.00; // Фактические продажи пока 0 +$plan->admin_id = 5; // ID руководителя +$plan->date_edit = date('Y-m-d H:i:s'); + +if ($plan->save()) { + echo "План на апрель 2024 создан"; +} else { + print_r($plan->getErrors()); +} +``` + +### 2. Обновление фактических продаж +```php +$plan = StorePlan::findOne([ + 'store_id' => '550e8400-e29b-41d4-a716-446655440000', + 'month' => '04', + 'year' => 2024 +]); + +if ($plan) { + $plan->summ_fact = 520000.00; // Факт: 520 000 руб. + $plan->date_edit = date('Y-m-d H:i:s'); + $plan->save(); + + $percent = ($plan->summ_fact / $plan->summ) * 100; + echo "Выполнение плана: " . round($percent, 2) . "%"; +} +``` + +### 3. Получение плана магазина на текущий месяц +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; +$currentMonth = date('m'); +$currentYear = date('Y'); + +$plan = StorePlan::findOne([ + 'store_id' => $storeGuid, + 'month' => $currentMonth, + 'year' => $currentYear +]); + +if ($plan) { + echo "План на месяц: {$plan->summ} руб.\n"; + echo "Факт: {$plan->summ_fact} руб.\n"; + echo "Отклонение: " . ($plan->summ_fact - $plan->summ) . " руб.\n"; +} +``` + +### 4. Получение всех планов магазина за год +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; +$year = 2024; + +$plans = StorePlan::find() + ->where(['store_id' => $storeGuid, 'year' => $year]) + ->orderBy(['month' => SORT_ASC]) + ->all(); + +$totalPlan = 0; +$totalFact = 0; + +foreach ($plans as $plan) { + $totalPlan += $plan->summ; + $totalFact += $plan->summ_fact; + echo "Месяц {$plan->month}: план={$plan->summ}, факт={$plan->summ_fact}\n"; +} + +echo "Итого за год: план={$totalPlan}, факт={$totalFact}\n"; +echo "Выполнение: " . round(($totalFact / $totalPlan) * 100, 2) . "%\n"; +``` + +### 5. Поиск магазинов с невыполненным планом +```php +$month = '04'; +$year = 2024; + +$underperformers = StorePlan::find() + ->where(['month' => $month, 'year' => $year]) + ->andWhere('summ_fact < summ') + ->orderBy(['(summ - summ_fact)' => SORT_DESC]) + ->all(); + +echo "Магазины, не выполнившие план:\n"; +foreach ($underperformers as $plan) { + $deficit = $plan->summ - $plan->summ_fact; + $percent = ($plan->summ_fact / $plan->summ) * 100; + echo "Store GUID: {$plan->store_id}, недовыполнение: {$deficit} руб. ({$percent}%)\n"; +} +``` + +### 6. Массовое создание планов на год +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; +$year = 2024; +$adminId = 5; + +$monthlyPlans = [ + '01' => 450000, '02' => 480000, '03' => 520000, + '04' => 500000, '05' => 550000, '06' => 530000, + '07' => 480000, '08' => 460000, '09' => 540000, + '10' => 570000, '11' => 600000, '12' => 750000 // Декабрь - пик продаж +]; + +foreach ($monthlyPlans as $month => $plannedSum) { + $plan = new StorePlan(); + $plan->store_id = $storeGuid; + $plan->month = $month; + $plan->year = $year; + $plan->summ = $plannedSum; + $plan->summ_week = $plannedSum / 4; // Примерно 4 недели в месяце + $plan->summ_fact = 0.00; + $plan->admin_id = $adminId; + $plan->date_edit = date('Y-m-d H:i:s'); + $plan->save(); +} + +echo "Планы на {$year} год созданы"; +``` + +### 7. Расчёт среднего выполнения плана +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; +$year = 2024; + +$plans = StorePlan::find() + ->where(['store_id' => $storeGuid, 'year' => $year]) + ->all(); + +$performanceRates = []; + +foreach ($plans as $plan) { + if ($plan->summ > 0) { + $performanceRates[] = ($plan->summ_fact / $plan->summ) * 100; + } +} + +$avgPerformance = array_sum($performanceRates) / count($performanceRates); +echo "Среднее выполнение плана за год: " . round($avgPerformance, 2) . "%"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_PLAN ||--|| EXPORT_IMPORT_TABLE : "links via GUID" + EXPORT_IMPORT_TABLE ||--|| CITY_STORE : "references" + STORE_PLAN ||--o| ADMIN : "managed by" + + STORE_PLAN { + string store_id PK,FK "GUID магазина" + string month PK "Месяц (01-12)" + int year PK "Год" + float summ_week "План в неделю" + float summ "План за месяц" + float summ_fact "Факт за месяц" + int admin_id FK "ID администратора" + timestamp date_edit "Дата редактирования" + } + + EXPORT_IMPORT_TABLE { + int id PK + string export_val "GUID" + int entity_id FK "ID сущности" + string entity "Тип сущности" + int export_id "ID экспорта" + } + + CITY_STORE { + int id PK + string name "Название" + } + + ADMIN { + int id PK + string name_full "ФИО" + } +``` + +--- + +## Особенности реализации + +### Использование GUID вместо ID +Модель использует GUID (36-символьная строка) для идентификации магазина вместо числового ID. Это связано с интеграцией с внешней системой (1C), где магазины идентифицируются по GUID. Для получения реального ID магазина используется связь через таблицу `export_import_table`. + +### Составной первичный ключ +Уникальность записи обеспечивается комбинацией трёх полей: `store_id + month + year`. Это гарантирует, что для каждого магазина может быть только один план на конкретный месяц. + +### Недельный план +Поле `summ_week` хранит плановую сумму продаж в неделю. Это позволяет более детально контролировать выполнение плана в течение месяца. + +### Контроль изменений +Поле `date_edit` фиксирует дату последнего изменения плана, что важно для аудита и понимания, когда план был скорректирован. + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Планирование** - установка целевых показателей продаж для магазинов +2. **Контроль** - сравнение фактических продаж с планом +3. **Мотивация** - расчёт бонусов и премий на основе выполнения плана +4. **Аналитика** - анализ динамики продаж, сезонности, трендов +5. **Прогнозирование** - планирование закупок и персонала на основе планов + +### Ключевые метрики + +- **Процент выполнения плана** = (summ_fact / summ) * 100% +- **Отклонение от плана** = summ_fact - summ +- **Темп роста** = сравнение с аналогичным периодом прошлого года + +--- + +## Связанные компоненты + +- **Модели:** CityStore, Admin, ExportImportTable +- **Сервисы:** SalesService, PlanningService +- **Контроллеры:** StorePlanController, DashboardController +- **Отчёты:** План-факт анализ, выполнение KPI + +--- + +## Примечания + +**Рекомендации:** + +1. Устанавливать планы заранее (в конце предыдущего месяца) +2. Регулярно обновлять `summ_fact` (ежедневно или еженедельно) +3. Анализировать отклонения и корректировать стратегию +4. Учитывать сезонность при планировании +5. Хранить историю изменений планов для аудита + +**Потенциальные улучшения:** + +1. Добавить связь с Admin для получения информации о планировщике +2. Реализовать автоматический расчёт `summ_week` из `summ` +3. Добавить методы для расчёта процента выполнения +4. Создать scope-методы для часто используемых запросов +5. Добавить валидацию корректности месяца (01-12) +6. Реализовать историю изменений плана diff --git a/erp24/docs/models/StorePlanIncreaseHolidays.md b/erp24/docs/models/StorePlanIncreaseHolidays.md new file mode 100644 index 00000000..a5bc5e7f --- /dev/null +++ b/erp24/docs/models/StorePlanIncreaseHolidays.md @@ -0,0 +1,227 @@ +# Модель StorePlanIncreaseHolidays + + +## Mindmap + +```mermaid +mindmap + root((StorePlanIncreaseHolidays)) + Таблица БД + store_plan_increase_holidays + Свойства + id + int + date + string + year + int + month + int + day + int + type_increase + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StorePlanIncreaseHolidays` хранит коэффициенты увеличения плана продаж для праздничных и особых дней. Определяет, на сколько процентов нужно увеличить план в конкретные даты (8 марта, 14 февраля, 1 сентября и т.д.). Используется при расчёте ежедневных планов магазинов с учётом сезонности. + +**Файл модели:** `erp24/records/StorePlanIncreaseHolidays.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_plan_increase_holidays` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `date` | VARCHAR(100) | Дата в формате YYYY-MM-DD | +| `year` | INTEGER | Год | +| `month` | INTEGER | Месяц (1-12) | +| `day` | INTEGER | День (1-31) | +| `type_increase` | VARCHAR(100) | Тип увеличения (percent, fixed) | +| `value` | FLOAT | Значение увеличения | +| `date_time` | TIMESTAMP | Время создания/обновления | + +--- + +## Типы увеличения (type_increase) + +| Тип | Описание | Пример | +|-----|----------|--------| +| `percent` | Процентное увеличение | `value=150` → план × 1.5 | +| `fixed` | Фиксированная сумма | `value=50000` → план + 50000 | +| `multiply` | Множитель | `value=2` → план × 2 | + +--- + +## Методы модели + +### `getPlanMonthHolidays(int $month, int $year): array` + +Возвращает все праздничные дни для указанного месяца и года. + +```php +$holidays = StorePlanIncreaseHolidays::getPlanMonthHolidays(3, 2025); +// Все праздники марта 2025 +``` + +--- + +## Примеры использования + +### Создание записи праздника + +```php +$holiday = new StorePlanIncreaseHolidays(); +$holiday->setDate('2025-03-08'); +$holiday->setYear(2025); +$holiday->setMonth(3); +$holiday->setDay(8); +$holiday->setTypeIncrease('percent'); +$holiday->setValue(300); // +200% к плану +$holiday->setDateTime(date('Y-m-d H:i:s')); +$holiday->save(); +``` + +### Получение праздников месяца + +```php +$holidays = StorePlanIncreaseHolidays::getPlanMonthHolidays(3, 2025); + +foreach ($holidays as $holiday) { + $increase = $holiday['type_increase'] === 'percent' + ? "+{$holiday['value']}%" + : "+{$holiday['value']} руб."; + + echo "{$holiday['date']}: {$increase}\n"; +} +``` + +### Расчёт плана с учётом праздника + +```php +function calculateDayPlan(int $storeId, string $date): float +{ + $basePlan = $this->getBasePlan($storeId, $date); + + [$year, $month, $day] = explode('-', $date); + + $holiday = StorePlanIncreaseHolidays::findOne([ + 'year' => (int) $year, + 'month' => (int) $month, + 'day' => (int) $day + ]); + + if (!$holiday) { + return $basePlan; + } + + switch ($holiday->type_increase) { + case 'percent': + return $basePlan * ($holiday->value / 100); + + case 'fixed': + return $basePlan + $holiday->value; + + case 'multiply': + return $basePlan * $holiday->value; + + default: + return $basePlan; + } +} +``` + +### Массовое создание праздников на год + +```php +$holidays2025 = [ + ['date' => '2025-02-14', 'name' => 'День влюблённых', 'value' => 200], + ['date' => '2025-02-23', 'name' => '23 февраля', 'value' => 150], + ['date' => '2025-03-08', 'name' => '8 марта', 'value' => 350], + ['date' => '2025-05-01', 'name' => '1 мая', 'value' => 130], + ['date' => '2025-05-09', 'name' => 'День Победы', 'value' => 140], + ['date' => '2025-09-01', 'name' => '1 сентября', 'value' => 200], + ['date' => '2025-12-31', 'name' => 'Новый год', 'value' => 400], +]; + +foreach ($holidays2025 as $data) { + $parts = explode('-', $data['date']); + + $holiday = new StorePlanIncreaseHolidays(); + $holiday->date = $data['date']; + $holiday->year = (int) $parts[0]; + $holiday->month = (int) $parts[1]; + $holiday->day = (int) $parts[2]; + $holiday->type_increase = 'percent'; + $holiday->value = $data['value']; + $holiday->date_time = date('Y-m-d H:i:s'); + $holiday->save(); +} +``` + +### Проверка праздничного дня + +```php +function isHoliday(string $date): bool +{ + $parts = explode('-', $date); + + return StorePlanIncreaseHolidays::find() + ->where([ + 'year' => (int) $parts[0], + 'month' => (int) $parts[1], + 'day' => (int) $parts[2] + ]) + ->exists(); +} +``` + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + store_plan_increase_holidays { + int id PK + string date + int year + int month + int day + string type_increase + float value + timestamp date_time + } +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `date` | Обязательное, строка, макс. 100 символов | +| `year`, `month`, `day` | Обязательные, целые числа | +| `type_increase` | Обязательное, строка, макс. 100 символов | +| `date_time` | Обязательное | +| `value` | Число (float) | +| `day + year + month` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[StorePlan](./StorePlan.md)** — планы магазинов + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StorePlanogram.md b/erp24/docs/models/StorePlanogram.md new file mode 100644 index 00000000..d5e53209 --- /dev/null +++ b/erp24/docs/models/StorePlanogram.md @@ -0,0 +1,306 @@ +# Class: StorePlanogram + + +## Mindmap + +```mermaid +mindmap + root((StorePlanogram)) + Таблица БД + store_planogram + Свойства + store_id + string + product_id + string + quantity + float + quantity_max + int + color + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StorePlanogram` представляет планограмму товаров в магазине - нормативы минимального и максимального количества товаров, которые должны быть в наличии. Она используется для автоматизации заказов, контроля остатков и оптимизации ассортимента. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_planogram` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `store_id` | string(36) | да | GUID магазина (часть составного ключа) | +| `product_id` | string(36) | да | GUID товара (часть составного ключа) | +| `quantity` | float | да | Минимальное количество товара по планограмме | +| `quantity_max` | int | да | Максимальное количество товара по планограмме | +| `color` | string(55) | да | Цвет/категория товара (часть составного ключа) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_planogram' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Правила:** +- Все поля обязательны +- `quantity` - число с плавающей точкой (для товаров с дробным учётом) +- `quantity_max` - целое число +- `store_id`, `product_id` - строки длиной 36 символов (GUID) +- `color` - строка до 55 символов +- Уникальность по комбинации `store_id + product_id + color` + +--- + +### `attributeLabels()` +**Описание:** Возвращает метки атрибутов. + +**Возвращает:** `array` - массив английских меток + +--- + +## Примеры использования + +### 1. Создание планограммы для товара +```php +$planogram = new StorePlanogram(); +$planogram->store_id = '550e8400-e29b-41d4-a716-446655440000'; +$planogram->product_id = 'a1b2c3d4-5678-90ab-cdef-1234567890ab'; +$planogram->quantity = 50.0; // Минимум 50 штук +$planogram->quantity_max = 200; // Максимум 200 штук +$planogram->color = 'red'; // Красные розы + +if ($planogram->save()) { + echo "Планограмма создана"; +} +``` + +### 2. Получение планограммы магазина +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +$planograms = StorePlanogram::find() + ->where(['store_id' => $storeGuid]) + ->all(); + +echo "Планограмма магазина:\n"; +foreach ($planograms as $p) { + echo "Товар {$p->product_id}: мин={$p->quantity}, макс={$p->quantity_max}, цвет={$p->color}\n"; +} +``` + +### 3. Проверка необходимости заказа товара +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; +$productGuid = 'a1b2c3d4-5678-90ab-cdef-1234567890ab'; +$color = 'red'; + +$planogram = StorePlanogram::findOne([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid, + 'color' => $color +]); + +if ($planogram) { + $currentStock = 30; // Текущий остаток + + if ($currentStock < $planogram->quantity) { + $orderQuantity = $planogram->quantity_max - $currentStock; + echo "Необходимо заказать {$orderQuantity} единиц товара"; + } else { + echo "Остаток в норме"; + } +} +``` + +### 4. Обновление нормативов планограммы +```php +$planogram = StorePlanogram::findOne([ + 'store_id' => '550e8400-...', + 'product_id' => 'a1b2c3d4-...', + 'color' => 'red' +]); + +if ($planogram) { + $planogram->quantity = 60.0; // Увеличили минимум + $planogram->quantity_max = 250; // Увеличили максимум + $planogram->save(); +} +``` + +### 5. Получение списка товаров для пополнения +```php +use yii\db\Query; + +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +// Получаем планограммы и сравниваем с текущими остатками +$query = (new Query()) + ->select([ + 'sp.product_id', + 'sp.color', + 'sp.quantity as min_qty', + 'sp.quantity_max as max_qty', + 'COALESCE(stock.current_qty, 0) as current_qty' + ]) + ->from(['sp' => 'store_planogram']) + ->leftJoin(['stock' => 'store_stock'], + 'stock.product_id = sp.product_id AND stock.store_id = sp.store_id') + ->where(['sp.store_id' => $storeGuid]) + ->having('current_qty < sp.quantity') + ->all(); + +foreach ($query as $item) { + $toOrder = $item['max_qty'] - $item['current_qty']; + echo "Товар {$item['product_id']} ({$item['color']}): заказать {$toOrder} шт.\n"; +} +``` + +### 6. Массовая установка планограмм для магазина +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +$products = [ + ['product_id' => 'prod-1-guid', 'color' => 'red', 'min' => 50, 'max' => 200], + ['product_id' => 'prod-2-guid', 'color' => 'white', 'min' => 40, 'max' => 150], + ['product_id' => 'prod-3-guid', 'color' => 'yellow', 'min' => 30, 'max' => 100], +]; + +foreach ($products as $product) { + $planogram = new StorePlanogram(); + $planogram->store_id = $storeGuid; + $planogram->product_id = $product['product_id']; + $planogram->color = $product['color']; + $planogram->quantity = $product['min']; + $planogram->quantity_max = $product['max']; + $planogram->save(); +} + +echo "Планограммы загружены"; +``` + +### 7. Анализ планограммы по цветам +```php +$storeGuid = '550e8400-e29b-41d4-a716-446655440000'; + +$colorStats = StorePlanogram::find() + ->select(['color', 'COUNT(*) as products_count', 'SUM(quantity) as total_min']) + ->where(['store_id' => $storeGuid]) + ->groupBy('color') + ->asArray() + ->all(); + +echo "Распределение по цветам:\n"; +foreach ($colorStats as $stat) { + echo "{$stat['color']}: {$stat['products_count']} товаров, минимум {$stat['total_min']} шт.\n"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_PLANOGRAM ||--|| CITY_STORE : "belongs to (via GUID)" + STORE_PLANOGRAM ||--|| PRODUCTS_1C : "references product (via GUID)" + + STORE_PLANOGRAM { + string store_id PK,FK "GUID магазина" + string product_id PK,FK "GUID товара" + string color PK "Цвет/категория" + float quantity "Минимум" + int quantity_max "Максимум" + } + + CITY_STORE { + int id PK + string guid "GUID" + } + + PRODUCTS_1C { + int id PK + string guid "GUID" + string name "Название" + } +``` + +--- + +## Особенности реализации + +### Составной первичный ключ +Уникальность обеспечивается комбинацией `store_id + product_id + color`. Это позволяет иметь разные нормативы для одного товара в разных цветах. + +### Использование GUID +Идентификаторы магазина и товара хранятся как GUID (36 символов) для интеграции с внешними системами (1C). + +### Дробный учёт минимума +Поле `quantity` имеет тип `float`, что позволяет указывать дробные минимальные количества (например, 0.5 для товаров, продающихся по весу). + +### Цвет как категория +Поле `color` используется не только для обозначения цвета, но и как дополнительная категоризация товара (размер, сорт и т.д.). + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Автоматические заказы** - система формирует заказы при падении остатков ниже минимума +2. **Контроль остатков** - предотвращение дефицита и затоваривания +3. **Оптимизация ассортимента** - планирование ассортиментной матрицы магазина +4. **Логистика** - расчёт оптимальных объёмов поставок +5. **Аналитика** - анализ оборачиваемости и эффективности ассортимента + +### Алгоритм пополнения + +1. Проверить текущий остаток товара +2. Если остаток < `quantity` (минимум), то: + - Рассчитать количество для заказа = `quantity_max` - текущий_остаток + - Создать заявку на пополнение + +--- + +## Связанные компоненты + +- **Модели:** CityStore, Products1c, StoreStock, ExportImportTable +- **Сервисы:** OrderService, StockService, PlanogramService +- **Контроллеры:** PlanogramController, OrderController +- **Задачи:** Автоматическое формирование заказов (cron) + +--- + +## Примечания + +**Рекомендации:** + +1. Регулярно пересматривать нормативы планограммы +2. Учитывать сезонность при установке min/max +3. Настроить автоматические уведомления при низких остатках +4. Анализировать фактическое потребление для корректировки нормативов + +**Потенциальные улучшения:** + +1. Добавить связи с CityStore и Products1c через GUID +2. Создать методы для расчёта количества к заказу +3. Добавить историю изменений планограммы +4. Реализовать сезонные коэффициенты для нормативов +5. Добавить валидацию: quantity <= quantity_max diff --git a/erp24/docs/models/StorePlanogramColorsSort.md b/erp24/docs/models/StorePlanogramColorsSort.md new file mode 100644 index 00000000..d839cbbf --- /dev/null +++ b/erp24/docs/models/StorePlanogramColorsSort.md @@ -0,0 +1,224 @@ +# Модель StorePlanogramColorsSort + + +## Mindmap + +```mermaid +mindmap + root((StorePlanogramColorsSort)) + Таблица БД + store_planogram_colors_sort + Свойства + store_id + string + product_id + string + color + string + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StorePlanogramColorsSort` определяет порядок сортировки цветов (цветочной продукции) в планограмме конкретного магазина. Позволяет настраивать индивидуальную раскладку товаров по цветам для каждой торговой точки. Используется для визуального мерчандайзинга и оптимизации выкладки. + +**Файл модели:** `erp24/records/StorePlanogramColorsSort.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_planogram_colors_sort` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) | +| `product_id` | VARCHAR(36) | GUID товара (часть PK) | +| `color` | VARCHAR(36) | Название цвета (часть PK) | +| `posit` | INTEGER | Позиция/порядок сортировки | + +--- + +## Особенности + +- **Составной уникальный ключ** по `store_id`, `product_id`, `color` +- Использует GUID для связи с магазинами и товарами (синхронизация с 1С) +- Позволяет разный порядок цветов для разных магазинов +- Все поля обязательные + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_planogram_colors_sort }o--|| city_store : "store" + store_planogram_colors_sort }o--|| products_1c : "product" + + store_planogram_colors_sort { + string store_id PK,FK + string product_id PK,FK + string color PK + int posit + } + + city_store { + string id PK + string name + } + + products_1c { + string id PK + string name + } +``` + +--- + +## Примеры использования + +### Добавление цвета в планограмму + +```php +$colorSort = new StorePlanogramColorsSort(); +$colorSort->store_id = $storeGuid; +$colorSort->product_id = $productGuid; +$colorSort->color = 'Красный'; +$colorSort->posit = 1; +$colorSort->save(); +``` + +### Получение порядка цветов для товара в магазине + +```php +$colors = StorePlanogramColorsSort::find() + ->where([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid + ]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($colors as $item) { + echo "{$item->posit}. {$item->color}\n"; +} +``` + +### Изменение позиции цвета + +```php +$colorSort = StorePlanogramColorsSort::findOne([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid, + 'color' => 'Красный' +]); + +if ($colorSort) { + $colorSort->posit = 3; + $colorSort->save(); +} +``` + +### Копирование планограммы между магазинами + +```php +$sourceStoreId = 'source-guid'; +$targetStoreId = 'target-guid'; + +$sourceColors = StorePlanogramColorsSort::find() + ->where(['store_id' => $sourceStoreId]) + ->all(); + +foreach ($sourceColors as $source) { + $exists = StorePlanogramColorsSort::findOne([ + 'store_id' => $targetStoreId, + 'product_id' => $source->product_id, + 'color' => $source->color + ]); + + if (!$exists) { + $new = new StorePlanogramColorsSort(); + $new->store_id = $targetStoreId; + $new->product_id = $source->product_id; + $new->color = $source->color; + $new->posit = $source->posit; + $new->save(); + } +} +``` + +### Удаление цвета из планограммы + +```php +StorePlanogramColorsSort::deleteAll([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid, + 'color' => 'Красный' +]); +``` + +### Переупорядочивание цветов + +```php +$newOrder = ['Белый', 'Красный', 'Розовый', 'Жёлтый']; +$storeId = 'store-guid'; +$productId = 'product-guid'; + +foreach ($newOrder as $position => $color) { + $colorSort = StorePlanogramColorsSort::findOne([ + 'store_id' => $storeId, + 'product_id' => $productId, + 'color' => $color + ]); + + if ($colorSort) { + $colorSort->posit = $position + 1; + $colorSort->save(); + } +} +``` + +### Получение статистики по цветам в магазине + +```php +$stats = StorePlanogramColorsSort::find() + ->select(['color', 'COUNT(*) as count']) + ->where(['store_id' => $storeGuid]) + ->groupBy('color') + ->orderBy(['count' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['color']}: {$stat['count']} товаров\n"; +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `store_id` | Обязательное, строка, макс. 36 символов | +| `product_id` | Обязательное, строка, макс. 36 символов | +| `color` | Обязательное, строка, макс. 36 символов | +| `posit` | Обязательное, целое число | +| `store_id + product_id + color` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[CityStore](./CityStore.md)** — магазины +- **[StorePlanogram](./StorePlanogram.md)** — планограммы +- **[Products1c](./Products1c.md)** — товары + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StorePlanogramLogi.md b/erp24/docs/models/StorePlanogramLogi.md new file mode 100644 index 00000000..88c51db6 --- /dev/null +++ b/erp24/docs/models/StorePlanogramLogi.md @@ -0,0 +1,202 @@ +# Модель StorePlanogramLogi + + +## Mindmap + +```mermaid +mindmap + root((StorePlanogramLogi)) + Таблица БД + store_planogram_logi + Свойства + date_id + int + store_id + string + product_id + string + quantity + float + quantity_max + int + comment_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StorePlanogramLogi` хранит историю изменений планограмм магазина по дням. Сохраняет снимки плановых значений планограммы (минимум и максимум количества товара) для каждой даты. Используется для анализа изменений планограмм во времени и выявления перетарок/недотарок. + +**Файл модели:** `erp24/records/StorePlanogramLogi.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_planogram_logi` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `date_id` | INTEGER | ID даты в формате Ymd (часть PK) | +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) | +| `product_id` | VARCHAR(36) | GUID товара (часть PK) | +| `color` | VARCHAR(55) | Цвет товара (часть PK) | +| `quantity` | FLOAT | Минимальное количество по полнограмме | +| `quantity_max` | INTEGER | Максимальное количество по полнограмме | +| `comment_id` | INTEGER | ID комментария (1 - перетарка, 2 - недотарка) | + +--- + +## Типы комментариев (comment_id) + +| ID | Описание | +|----|----------| +| 1 | Перетарка (избыток товара) | +| 2 | Недотарка (недостаток товара) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `date_id`, `store_id`, `product_id`, `color` +- Формат `date_id`: целое число в формате YYYYMMDD (например, 20251211) +- Хранит исторические данные для ретроспективного анализа +- Детализация до уровня товар + цвет + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_planogram_logi }o--|| products_1c : "product" + store_planogram_logi }o--|| city_store : "store" + + store_planogram_logi { + int date_id PK + string store_id PK,FK + string product_id PK,FK + string color PK + float quantity + int quantity_max + int comment_id + } +``` + +--- + +## Примеры использования + +### Создание записи лога планограммы + +```php +$log = new StorePlanogramLogi(); +$log->date_id = (int) date('Ymd'); +$log->store_id = $storeGuid; +$log->product_id = $productGuid; +$log->color = 'Красный'; +$log->quantity = 10.0; +$log->quantity_max = 15; +$log->comment_id = null; +$log->save(); +``` + +### Получение планограммы магазина на дату + +```php +$dateId = (int) date('Ymd', strtotime('2025-12-11')); + +$planogram = StorePlanogramLogi::find() + ->where([ + 'date_id' => $dateId, + 'store_id' => $storeGuid + ]) + ->orderBy(['product_id' => SORT_ASC, 'color' => SORT_ASC]) + ->all(); + +foreach ($planogram as $item) { + echo "{$item->product_id} ({$item->color}): {$item->quantity}-{$item->quantity_max} шт.\n"; +} +``` + +### Поиск перетарок/недотарок + +```php +// Все перетарки за дату +$overtarget = StorePlanogramLogi::find() + ->where([ + 'date_id' => $dateId, + 'comment_id' => 1 // Перетарка + ]) + ->all(); + +// Все недотарки за дату +$undertarget = StorePlanogramLogi::find() + ->where([ + 'date_id' => $dateId, + 'comment_id' => 2 // Недотарка + ]) + ->all(); +``` + +### Сравнение планограмм за период + +```php +$dateFrom = 20251201; +$dateTo = 20251211; + +$history = StorePlanogramLogi::find() + ->where([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid, + 'color' => 'Красный' + ]) + ->andWhere(['between', 'date_id', $dateFrom, $dateTo]) + ->orderBy(['date_id' => SORT_ASC]) + ->all(); + +foreach ($history as $item) { + echo "{$item->date_id}: {$item->quantity}-{$item->quantity_max}\n"; +} +``` + +### Статистика по комментариям + +```php +$stats = StorePlanogramLogi::find() + ->select(['comment_id', 'COUNT(*) as count']) + ->where(['date_id' => $dateId, 'store_id' => $storeGuid]) + ->andWhere(['is not', 'comment_id', null]) + ->groupBy('comment_id') + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `date_id`, `store_id`, `product_id`, `quantity`, `quantity_max`, `color` | Обязательные | +| `date_id`, `quantity_max`, `comment_id` | Целые числа | +| `quantity` | Число с плавающей точкой | +| `store_id`, `product_id` | Строка, макс. 36 символов | +| `color` | Строка, макс. 55 символов | +| `date_id + store_id + product_id + color` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[StorePlanogram](./StorePlanogram.md)** — текущие планограммы +- **[Products1c](./Products1c.md)** — товары +- **[CityStore](./CityStore.md)** — магазины + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreProductsFact.md b/erp24/docs/models/StoreProductsFact.md new file mode 100644 index 00000000..25437387 --- /dev/null +++ b/erp24/docs/models/StoreProductsFact.md @@ -0,0 +1,215 @@ +# Модель StoreProductsFact + + +## Mindmap + +```mermaid +mindmap + root((StoreProductsFact)) + Таблица БД + store_products_fact + Свойства + date_id + int + product_id + string + color + string + store_id + string + quantity + int + quantity_polnogramm + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `StoreProductsFact` хранит фактические остатки товаров в магазине на конкретную дату с расчётом отклонений от планограммы. Фиксирует расхождения между плановым и фактическим количеством товара, позволяет флористам оставлять комментарии о причинах расхождений. Используется для контроля выкладки и анализа соответствия планограмме. + +**Файл модели:** `erp24/records/StoreProductsFact.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `store_products_fact` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `date_id` | INTEGER | ID даты (часть PK) | +| `product_id` | VARCHAR(36) | GUID товара (часть PK) | +| `color` | VARCHAR(55) | Цвет товара (часть PK) | +| `store_id` | VARCHAR(36) | GUID магазина (часть PK) | +| `quantity` | INTEGER | Фактическое количество товара | +| `quantity_polnogramm` | INTEGER | Плановое количество по полнограмме | +| `difference` | INTEGER | Расхождение в штуках (факт - план) | +| `difference_percent` | INTEGER | Процент расхождения от полнограммы | +| `comment_type_id` | INTEGER | ID типа комментария флориста | +| `comments` | TEXT | Текстовый комментарий флориста | + +--- + +## Типы комментариев (comment_type_id) + +| ID | Описание | +|----|----------| +| 1 | Не хватило товара | +| 2 | Большое списание по товару | +| 3 | Много товара (избыток) | + +--- + +## Особенности + +- **Составной уникальный ключ** по `date_id`, `product_id`, `color`, `store_id` +- Хранит исторические данные о полнограмме для анализа прошлых периодов +- Автоматический расчёт отклонений (difference, difference_percent) +- Детализация до уровня товар + цвет + +--- + +## Диаграмма связей + +```mermaid +erDiagram + store_products_fact }o--|| products_1c : "product" + store_products_fact }o--|| city_store : "store" + store_products_fact }o--|| dates : "date" + + store_products_fact { + int date_id PK,FK + string product_id PK,FK + string color PK + string store_id PK,FK + int quantity + int quantity_polnogramm + int difference + int difference_percent + int comment_type_id + text comments + } +``` + +--- + +## Примеры использования + +### Создание записи фактического остатка + +```php +$fact = new StoreProductsFact(); +$fact->date_id = $dateId; +$fact->product_id = $productGuid; +$fact->color = 'Красный'; +$fact->store_id = $storeGuid; +$fact->quantity = 45; +$fact->quantity_polnogramm = 50; +$fact->difference = -5; +$fact->difference_percent = -10; +$fact->comment_type_id = 1; // Не хватило товара +$fact->comments = 'Поставка задержалась'; +$fact->save(); +``` + +### Получение остатков магазина на дату + +```php +$facts = StoreProductsFact::find() + ->where([ + 'date_id' => $dateId, + 'store_id' => $storeGuid + ]) + ->orderBy(['product_id' => SORT_ASC, 'color' => SORT_ASC]) + ->all(); + +foreach ($facts as $fact) { + $status = $fact->difference >= 0 ? 'OK' : 'НЕДОБОР'; + echo "{$fact->product_id} ({$fact->color}): {$fact->quantity}/{$fact->quantity_polnogramm} [{$status}]\n"; +} +``` + +### Получение товаров с отклонениями + +```php +$deviations = StoreProductsFact::find() + ->where(['date_id' => $dateId, 'store_id' => $storeGuid]) + ->andWhere(['<>', 'difference', 0]) + ->orderBy(['difference_percent' => SORT_ASC]) + ->all(); +``` + +### Анализ причин отклонений + +```php +$commentStats = StoreProductsFact::find() + ->select(['comment_type_id', 'COUNT(*) as count']) + ->where([ + 'date_id' => $dateId, + 'store_id' => $storeGuid + ]) + ->andWhere(['is not', 'comment_type_id', null]) + ->groupBy('comment_type_id') + ->asArray() + ->all(); +``` + +### Расчёт общего отклонения по магазину + +```php +$totalDeviation = StoreProductsFact::find() + ->select('SUM(ABS(difference)) as total_diff') + ->where([ + 'date_id' => $dateId, + 'store_id' => $storeGuid + ]) + ->asArray() + ->one(); + +echo "Общее отклонение: {$totalDeviation['total_diff']} шт."; +``` + +### Динамика отклонений за период + +```php +$dynamics = StoreProductsFact::find() + ->select(['date_id', 'AVG(ABS(difference_percent)) as avg_deviation']) + ->where([ + 'store_id' => $storeGuid, + 'product_id' => $productGuid + ]) + ->andWhere(['between', 'date_id', $dateIdFrom, $dateIdTo]) + ->groupBy('date_id') + ->orderBy(['date_id' => SORT_ASC]) + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `date_id`, `product_id`, `color`, `store_id`, `quantity`, `quantity_polnogramm`, `difference`, `difference_percent` | Обязательные | +| `date_id`, `quantity`, `quantity_polnogramm`, `difference`, `difference_percent`, `comment_type_id` | Целые числа | +| `product_id`, `store_id` | Строка, макс. 36 символов | +| `color` | Строка, макс. 55 символов | +| `comments` | Текст | +| `date_id + product_id + color + store_id` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[Products1c](./Products1c.md)** — товары +- **[CityStore](./CityStore.md)** — магазины +- **[StorePlanogram](./StorePlanogram.md)** — планограммы + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/StoreStaffing.md b/erp24/docs/models/StoreStaffing.md new file mode 100644 index 00000000..ba1db094 --- /dev/null +++ b/erp24/docs/models/StoreStaffing.md @@ -0,0 +1,404 @@ +# Class: StoreStaffing + + +## Mindmap + +```mermaid +mindmap + root((StoreStaffing)) + Таблица БД + store_staffing + Свойства + id + int + store_id + int + employee_position_id + int + count + int + created_at + string + updated_at + string + Связи + Store + 1:1 CityStore + EmployeePosition + 1:1 EmployeePosition + Наследование + extends ActiveRecord +``` + +## Назначение +Модель `StoreStaffing` представляет штатное расписание магазина. Она хранит информацию о количестве штатных единиц по каждой должности для конкретного магазина. Модель используется для планирования персонала, расчёта фонда оплаты труда и контроля укомплектованности магазинов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_staffing` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор записи | +| `store_id` | int | да | ID магазина (связь с CityStore) | +| `employee_position_id` | int | да | ID должности (связь с EmployeePosition) | +| `count` | float | да | Количество штатных единиц (может быть дробным: 0.5, 1.5) | +| `created_at` | timestamp | да | Дата и время создания записи (автозаполнение) | +| `updated_at` | timestamp | да | Дата и время обновления записи (автозаполнение) | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_staffing' + +--- + +### `behaviors()` +**Описание:** Определяет поведение для автоматического управления временными метками. + +**Возвращает:** `array` - конфигурация TimestampBehavior + +**Логика работы:** +- Автоматически заполняет `created_at` и `updated_at` при создании/обновлении +- Использует формат даты 'Y-m-d H:i:s' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Правила валидации:** +- `store_id`, `employee_position_id`, `count` - обязательные поля +- `store_id`, `employee_position_id` - целые числа +- `count` - число с минимальным значением 0.1 (можно дробные ставки) +- Уникальность по комбинации `store_id + employee_position_id` (одна запись на должность в магазине) +- Проверка существования магазина в таблице CityStore +- Проверка существования должности в таблице EmployeePosition + +**Пример:** +```php +$staffing = new StoreStaffing(); +$staffing->store_id = 15; +$staffing->employee_position_id = 3; +$staffing->count = 1.5; // 1.5 ставки + +if ($staffing->validate()) { + echo "Данные валидны"; +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает русские метки для атрибутов. + +**Пример:** +```php +echo (new StoreStaffing())->getAttributeLabel('count'); // 'Количество' +``` + +--- + +### `getStore()` +**Описание:** Получает связь с магазином. + +**Тип связи:** hasOne + +**Связанная модель:** `CityStore` + +**Пример:** +```php +$staffing = StoreStaffing::findOne(1); +echo $staffing->store->name; // Название магазина +``` + +--- + +### `getEmployeePosition()` +**Описание:** Получает связь с должностью. + +**Тип связи:** hasOne + +**Связанная модель:** `EmployeePosition` + +**Пример:** +```php +$staffing = StoreStaffing::findOne(1); +echo $staffing->employeePosition->name; // Название должности +``` + +--- + +### `afterSave($insert, $changedAttributes)` +**Описание:** Логирует все изменения штатного расписания после сохранения записи. + +**Параметры:** +- `$insert` (bool) - признак создания новой записи +- `$changedAttributes` (array) - массив изменённых атрибутов + +**Логика работы:** +1. Создаёт новую запись в таблице `StoreStaffingLog` +2. Сохраняет информацию о действии: создание, обновление +3. Фиксирует старое и новое значение `count` +4. Записывает ID пользователя, внёсшего изменения (`Yii::$app->user->id`) + +**Вызываемые методы:** +- `StoreStaffingLog::__construct()` - создание объекта лога +- `StoreStaffingLog::save()` - сохранение лога + +**Пример записи в лог:** +```php +$staffing = new StoreStaffing(); +$staffing->store_id = 15; +$staffing->employee_position_id = 3; +$staffing->count = 2.0; +$staffing->save(); + +// Автоматически создастся запись в store_staffing_log: +// action = 'create', new_count = 2.0, changed_by = user_id +``` + +--- + +### `afterDelete()` +**Описание:** Логирует удаление записи штатного расписания. + +**Логика работы:** +1. Создаёт запись в `StoreStaffingLog` с действием 'delete' +2. Сохраняет информацию о магазине, должности и количестве ставок +3. Фиксирует ID пользователя, удалившего запись + +**Вызываемые методы:** +- `StoreStaffingLog::__construct()` - создание объекта лога +- `StoreStaffingLog::save()` - сохранение лога + +--- + +## Связи (Relations) + +### `getStore()` +**Тип:** hasOne → `CityStore` + +**Условие:** `['id' => 'store_id']` + +### `getEmployeePosition()` +**Тип:** hasOne → `EmployeePosition` + +**Условие:** `['id' => 'employee_position_id']` + +--- + +## Примеры использования + +### 1. Создание штатного расписания +```php +$staffing = new StoreStaffing(); +$staffing->store_id = 15; +$staffing->employee_position_id = 3; // Флорист +$staffing->count = 2.0; // 2 ставки + +if ($staffing->save()) { + echo "Штатное расписание создано"; + // Автоматически создастся запись в логе +} +``` + +### 2. Получение штатного расписания магазина +```php +$storeId = 15; + +$staffing = StoreStaffing::find() + ->joinWith(['employeePosition']) + ->where(['store_id' => $storeId]) + ->all(); + +echo "Штатное расписание магазина {$storeId}:\n"; +foreach ($staffing as $item) { + echo "{$item->employeePosition->name}: {$item->count} ставок\n"; +} +``` + +### 3. Расчёт общего ФОТ магазина +```php +$storeId = 15; + +$staffing = StoreStaffing::find() + ->joinWith('employeePosition') + ->where(['store_id' => $storeId]) + ->all(); + +$totalSalary = 0; + +foreach ($staffing as $item) { + $positionSalary = $item->employeePosition->base_salary; + $totalSalary += $positionSalary * $item->count; +} + +echo "Общий ФОТ магазина: {$totalSalary} руб."; +``` + +### 4. Обновление количества ставок +```php +$staffing = StoreStaffing::findOne([ + 'store_id' => 15, + 'employee_position_id' => 3 +]); + +if ($staffing) { + $oldCount = $staffing->count; + $staffing->count = 2.5; // Увеличили до 2.5 ставок + $staffing->save(); + + // Автоматически создастся запись в логе: + // action = 'update', old_count = 2.0, new_count = 2.5 +} +``` + +### 5. Проверка комплектации магазина +```php +$storeId = 15; + +$staffing = StoreStaffing::find() + ->select(['employee_position_id', 'count']) + ->where(['store_id' => $storeId]) + ->indexBy('employee_position_id') + ->asArray() + ->all(); + +// Получаем фактическое количество сотрудников +$actualStaff = Admin::find() + ->select(['position_id', 'COUNT(*) as actual_count']) + ->where(['store_id' => $storeId, 'active' => 1]) + ->groupBy('position_id') + ->indexBy('position_id') + ->asArray() + ->all(); + +foreach ($staffing as $positionId => $plan) { + $actual = $actualStaff[$positionId]['actual_count'] ?? 0; + $vacancy = $plan['count'] - $actual; + + if ($vacancy > 0) { + echo "Должность {$positionId}: вакансий {$vacancy}\n"; + } +} +``` + +### 6. Массовое создание штатного расписания +```php +$storeId = 15; + +$positions = [ + ['position_id' => 1, 'count' => 1.0], // Администратор + ['position_id' => 2, 'count' => 0.5], // Бухгалтер (0.5 ставки) + ['position_id' => 3, 'count' => 2.0], // Флорист + ['position_id' => 4, 'count' => 1.5], // Продавец +]; + +foreach ($positions as $pos) { + $staffing = new StoreStaffing(); + $staffing->store_id = $storeId; + $staffing->employee_position_id = $pos['position_id']; + $staffing->count = $pos['count']; + $staffing->save(); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_STAFFING ||--|| CITY_STORE : "belongs to" + STORE_STAFFING ||--|| EMPLOYEE_POSITION : "has position" + STORE_STAFFING ||--o{ STORE_STAFFING_LOG : "has logs" + + STORE_STAFFING { + int id PK + int store_id FK,UK "ID магазина" + int employee_position_id FK,UK "ID должности" + float count "Количество ставок" + timestamp created_at "Дата создания" + timestamp updated_at "Дата обновления" + } + + CITY_STORE { + int id PK + string name "Название" + } + + EMPLOYEE_POSITION { + int id PK + string name "Должность" + float base_salary "Оклад" + } + + STORE_STAFFING_LOG { + int id PK + int store_staffing_id FK "ID записи ШР" + string action "Действие" + float old_count "Старое значение" + float new_count "Новое значение" + int changed_by FK "Кто изменил" + timestamp created_at "Дата" + } +``` + +--- + +## Особенности реализации + +### Дробные ставки +Поле `count` имеет тип `float`, что позволяет указывать неполные ставки (0.5, 0.75, 1.5 и т.д.). Минимальное значение - 0.1. + +### Автоматическое логирование +Все операции создания, обновления и удаления автоматически записываются в таблицу `store_staffing_log` через методы `afterSave()` и `afterDelete()`. + +### Уникальность +Комбинация `store_id + employee_position_id` должна быть уникальной - для каждой должности в магазине может быть только одна запись. + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Планирование персонала** - определение необходимого количества сотрудников +2. **Расчёт ФОТ** - планирование фонда оплаты труда +3. **Контроль вакансий** - выявление незакрытых позиций +4. **HR аналитика** - анализ укомплектованности сети +5. **Бюджетирование** - планирование расходов на персонал + +--- + +## Связанные компоненты + +- **Модели:** CityStore, EmployeePosition, StoreStaffingLog, Admin +- **Сервисы:** StaffingService, HRService +- **Контроллеры:** StoreStaffingController + +--- + +## Примечания + +**Рекомендации:** +1. Регулярно пересматривать штатное расписание +2. Учитывать сезонность при планировании +3. Анализировать эффективность распределения ставок +4. Отслеживать изменения через логи + +**Потенциальные улучшения:** +1. Добавить валидацию максимального значения count +2. Создать методы для расчёта укомплектованности +3. Реализовать автоматические уведомления о вакансиях +4. Добавить поддержку сезонных коэффициентов diff --git a/erp24/docs/models/StoreStaffingLog.md b/erp24/docs/models/StoreStaffingLog.md new file mode 100644 index 00000000..e671cecc --- /dev/null +++ b/erp24/docs/models/StoreStaffingLog.md @@ -0,0 +1,310 @@ +# Класс: StoreStaffingLog + + +## Mindmap + +```mermaid +mindmap + root((StoreStaffingLog)) + Таблица БД + store_staffing_log + Свойства + id + int + store_id + int + employee_position_id + int + action + string + created_at + string + storeStaffing + StoreStaffing + Связи + StoreStaffing + 1:1 StoreStaffing + Store + 1:1 CityStore + EmployeePosition + 1:1 EmployeePosition + ChangedByUser + 1:1 Admin + Наследование + extends ActiveRecord +``` + +## Назначение +Модель логирования изменений штатного расписания магазинов в ERP24. Фиксирует историю создания, обновления и удаления записей штатного расписания с указанием старых и новых значений количества сотрудников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`store_staffing_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы действий + +```php +const ACTION_CREATE = 'create'; // Создание записи +const ACTION_UPDATE = 'update'; // Обновление записи +const ACTION_DELETE = 'delete'; // Удаление записи +``` + +## Массив названий действий + +```php +public static array $actions = [ + 'create' => 'Создание', + 'update' => 'Обновление', + 'delete' => 'Удаление', +]; +``` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_staffing_id` | int / null | FK на запись штатного расписания (NULL для удалений) | +| `store_id` | int | FK на магазин (CityStore) | +| `employee_position_id` | int | FK на должность (EmployeePosition) | +| `action` | varchar(10) | Действие: create, update, delete | +| `old_count` | int / null | Старое количество сотрудников | +| `new_count` | int / null | Новое количество сотрудников | +| `changed_by` | int / null | FK на администратора (Admin) | +| `created_at` | datetime | Дата создания записи лога | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getStoreStaffing()` | hasOne | StoreStaffing | Запись штатного расписания | +| `getStore()` | hasOne | CityStore | Магазин | +| `getEmployeePosition()` | hasOne | EmployeePosition | Должность | +| `getChangedByUser()` | hasOne | Admin | Автор изменения | + +## Методы + +### getActionName() +**Описание:** Возвращает локализованное название действия. + +**Возвращает:** `string` — 'Создание', 'Обновление' или 'Удаление' + +**Пример:** +```php +$log = StoreStaffingLog::findOne($id); +echo $log->getActionName(); // "Обновление" +``` + +## Диаграмма связей + +```mermaid +erDiagram + StoreStaffingLog { + int id PK + int store_staffing_id FK + int store_id FK + int employee_position_id FK + varchar action + int old_count + int new_count + int changed_by FK + datetime created_at + } + + StoreStaffing { + int id PK + int store_id FK + int employee_position_id FK + int count + } + + CityStore { + int id PK + varchar name + } + + EmployeePosition { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + StoreStaffing ||--o{ StoreStaffingLog : "store_staffing_id" + CityStore ||--o{ StoreStaffingLog : "store_id" + EmployeePosition ||--o{ StoreStaffingLog : "employee_position_id" + Admin ||--o{ StoreStaffingLog : "changed_by" +``` + +## Диаграмма жизненного цикла записи + +```mermaid +flowchart TD + A[Создание записи
    StoreStaffing] --> B[Лог: action=create
    old_count=NULL
    new_count=5] + + B --> C[Изменение
    count: 5→8] + C --> D[Лог: action=update
    old_count=5
    new_count=8] + + D --> E[Удаление записи] + E --> F[Лог: action=delete
    old_count=8
    new_count=NULL
    store_staffing_id=NULL] +``` + +## Примеры использования + +### Логирование создания записи +```php +$staffing = new StoreStaffing(); +$staffing->store_id = $storeId; +$staffing->employee_position_id = $positionId; +$staffing->count = 5; +$staffing->save(); + +$log = new StoreStaffingLog(); +$log->store_staffing_id = $staffing->id; +$log->store_id = $staffing->store_id; +$log->employee_position_id = $staffing->employee_position_id; +$log->action = StoreStaffingLog::ACTION_CREATE; +$log->old_count = null; +$log->new_count = $staffing->count; +$log->changed_by = Yii::$app->user->id; +$log->save(); +``` + +### Логирование обновления +```php +$oldCount = $staffing->count; +$staffing->count = $newCount; +$staffing->save(); + +$log = new StoreStaffingLog(); +$log->store_staffing_id = $staffing->id; +$log->store_id = $staffing->store_id; +$log->employee_position_id = $staffing->employee_position_id; +$log->action = StoreStaffingLog::ACTION_UPDATE; +$log->old_count = $oldCount; +$log->new_count = $newCount; +$log->changed_by = Yii::$app->user->id; +$log->save(); +``` + +### Логирование удаления +```php +$log = new StoreStaffingLog(); +$log->store_staffing_id = null; // Запись удалена +$log->store_id = $staffing->store_id; +$log->employee_position_id = $staffing->employee_position_id; +$log->action = StoreStaffingLog::ACTION_DELETE; +$log->old_count = $staffing->count; +$log->new_count = null; +$log->changed_by = Yii::$app->user->id; +$log->save(); + +$staffing->delete(); +``` + +### История изменений магазина +```php +$logs = StoreStaffingLog::find() + ->where(['store_id' => $storeId]) + ->with(['employeePosition', 'changedByUser']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + echo "{$log->created_at}: {$log->getActionName()} "; + echo "должности '{$log->employeePosition->name}' "; + echo "({$log->old_count} → {$log->new_count}) "; + echo "от {$log->changedByUser->name}\n"; +} +``` + +### История изменений должности +```php +$logs = StoreStaffingLog::find() + ->where(['employee_position_id' => $positionId]) + ->with('store') + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + echo "{$log->store->name}: {$log->old_count} → {$log->new_count}\n"; +} +``` + +### Статистика действий +```php +$stats = StoreStaffingLog::find() + ->select(['action', 'COUNT(*) as count']) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->groupBy('action') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $actionName = StoreStaffingLog::$actions[$stat['action']] ?? $stat['action']; + echo "{$actionName}: {$stat['count']}\n"; +} +``` + +### Поиск изменений администратора +```php +$adminLogs = StoreStaffingLog::find() + ->where(['changed_by' => $adminId]) + ->with(['store', 'employeePosition']) + ->orderBy(['created_at' => SORT_DESC]) + ->limit(50) + ->all(); +``` + +### Анализ изменений штатки +```php +// Общее изменение штата за месяц +$changes = StoreStaffingLog::find() + ->select([ + 'SUM(CASE WHEN action = \'create\' THEN new_count ELSE 0 END) as added', + 'SUM(CASE WHEN action = \'delete\' THEN old_count ELSE 0 END) as removed', + 'SUM(CASE WHEN action = \'update\' THEN (new_count - old_count) ELSE 0 END) as changed' + ]) + ->where(['>=', 'created_at', date('Y-m-01')]) + ->asArray() + ->one(); + +$netChange = $changes['added'] - $changes['removed'] + $changes['changed']; +echo "Чистое изменение штата: {$netChange} позиций"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, integer, exists в CityStore | +| `employee_position_id` | required, integer, exists в EmployeePosition | +| `action` | required, string (max 10) | +| `store_staffing_id` | integer, exists в StoreStaffing | +| `old_count` | integer | +| `new_count` | integer | +| `changed_by` | integer, exists в Admin | + +## Связанные модели + +- [StoreStaffing](./StoreStaffing.md) — штатное расписание +- [CityStore](./CityStore.md) — магазины +- [EmployeePosition](./EmployeePosition.md) — должности +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Три типа действий**: create, update, delete с локализованными названиями +2. **NULL для удалений**: store_staffing_id = NULL после удаления записи +3. **История значений**: old_count и new_count для отслеживания изменений +4. **Денормализация**: store_id и employee_position_id дублируются для истории +5. **Аудит**: changed_by для отслеживания авторства изменений +6. **Локализация**: Метод getActionName() для русских названий diff --git a/erp24/docs/models/StoreStaffingSearch.md b/erp24/docs/models/StoreStaffingSearch.md new file mode 100644 index 00000000..90d318b0 --- /dev/null +++ b/erp24/docs/models/StoreStaffingSearch.md @@ -0,0 +1,202 @@ +# Класс: StoreStaffingSearch + + +## Mindmap + +```mermaid +mindmap + root((StoreStaffingSearch)) + Таблица БД + ActiveRecord + Наследование + extends Model +``` + +## Назначение +Search-модель для поиска и фильтрации штатного расписания магазинов в ERP24. Наследуется от Model (не от ActiveRecord), с сортировкой по умолчанию по магазину и должности. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\base\Model` + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$id` | int | ID записи | +| `$store_id` | int | ID магазина | +| `$employee_position_id` | int | ID должности | +| `$count` | int | Количество ставок | +| `$created_at` | string | Дата создания | +| `$updated_at` | string | Дата обновления | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `store_id`, `employee_position_id`, `count` — integer +- `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с сортировкой по магазину и должности. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос StoreStaffing::find() +2. Оборачивает в ActiveDataProvider с сортировкой: + - defaultOrder: store_id ASC, employee_position_id ASC +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, store_id, employee_position_id, count + - like: created_at, updated_at + +## Диаграмма связей + +```mermaid +erDiagram + StoreStaffing { + int id PK + int store_id FK + int employee_position_id FK + int count + datetime created_at + datetime updated_at + } + + CityStore { + int id PK + varchar name + } + + EmployeePosition { + int id PK + varchar name + } + + StoreStaffing }o--|| CityStore : "store_id" + StoreStaffing }o--|| EmployeePosition : "employee_position_id" +``` + +## Диаграмма сортировки + +```mermaid +flowchart TD + A[StoreStaffingSearch] --> B[Сортировка по умолчанию] + B --> C[1. store_id ASC] + C --> D[2. employee_position_id ASC] + + E[Результат] --> F[Группировка по магазинам] + F --> G[Внутри - по должностям] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new StoreStaffingSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по магазину +```php +$searchModel = new StoreStaffingSearch(); +$dataProvider = $searchModel->search([ + 'StoreStaffingSearch' => [ + 'store_id' => 5, + ] +]); +``` + +### Поиск по должности +```php +$searchModel = new StoreStaffingSearch(); +$dataProvider = $searchModel->search([ + 'StoreStaffingSearch' => [ + 'employee_position_id' => 3, // Флорист + ] +]); +``` + +### Поиск по количеству ставок +```php +$searchModel = new StoreStaffingSearch(); +$dataProvider = $searchModel->search([ + 'StoreStaffingSearch' => [ + 'count' => 2, + ] +]); +``` + +### Поиск штатного расписания магазина +```php +$searchModel = new StoreStaffingSearch(); +$dataProvider = $searchModel->search([ + 'StoreStaffingSearch' => [ + 'store_id' => 5, + ] +]); + +// Результат отсортирован по должностям +foreach ($dataProvider->getModels() as $staffing) { + echo $staffing->employeePosition->name . ': ' . $staffing->count . ' ставок'; +} +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'store_id', + 'value' => 'store.name', + ], + [ + 'attribute' => 'employee_position_id', + 'value' => 'employeePosition.name', + ], + 'count', + 'created_at:datetime', + 'updated_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [StoreStaffing](./StoreStaffing.md) — базовая модель штатного расписания +- [CityStore](./CityStore.md) — магазины +- [EmployeePosition](./EmployeePosition.md) — должности сотрудников + +## Особенности реализации + +1. **Наследование от Model**: Не от ActiveRecord, свойства объявлены явно +2. **Двойная сортировка**: store_id ASC, затем employee_position_id ASC +3. **Штатное расписание**: Количество ставок по должностям для магазина +4. **like на датах**: created_at и updated_at ищутся через like (нестандартно) +5. **Точное совпадение count**: Количество ставок фильтруется точно diff --git a/erp24/docs/models/StoreType.md b/erp24/docs/models/StoreType.md new file mode 100644 index 00000000..4a7c834d --- /dev/null +++ b/erp24/docs/models/StoreType.md @@ -0,0 +1,247 @@ +# Class: StoreType + + +## Mindmap + +```mermaid +mindmap + root((StoreType)) + Таблица БД + {{%erp24.store_type}} + Свойства + id + int + name + string + created_by + int + created_at + string + sequence_number + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StoreType` представляет справочник типов магазинов в системе ERP24. Она хранит классификацию торговых точек по различным форматам (павильон, магазин, киоск, флагман и т.д.). Модель используется для категоризации магазинов, анализа эффективности по форматам и планирования развития сети. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`erp24.store_type` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | да | Уникальный идентификатор типа магазина | +| `name` | string(255) | да | Название типа магазина | +| `created_by` | int | да | ID пользователя, создавшего запись (автозаполнение) | +| `created_at` | timestamp | да | Дата и время создания (автозаполнение) | +| `updated_by` | int | нет | ID пользователя, обновившего запись (автозаполнение) | +| `updated_at` | timestamp | нет | Дата и время обновления (автозаполнение) | +| `sequence_number` | int | да | Порядковый номер для сортировки | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы с префиксом схемы. + +**Возвращает:** `string` - '{{%erp24.store_type}}' + +**Пример:** +```php +$tableName = StoreType::tableName(); // '{{%erp24.store_type}}' +``` + +--- + +### `behaviors()` +**Описание:** Определяет поведения модели для автоматического управления временными метками и авторством. + +**Возвращает:** `array` - массив конфигураций поведений + +**Используемые поведения:** +- **TimestampBehavior** - автозаполнение created_at и updated_at +- **BlameableBehavior** - автозаполнение created_by и updated_by + +**Пример:** +```php +$type = new StoreType(); +$type->name = 'Флагманский магазин'; +$type->save(); + +// Автоматически заполнятся: +// $type->created_by = Yii::$app->user->id; +// $type->created_at = '2024-01-15 10:30:00'; +``` + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Правила:** +- `name` - обязательное строковое поле до 255 символов +- `created_by`, `updated_by`, `sequence_number` - целые числа +- `created_at`, `updated_at` - безопасные поля для дат + +--- + +### `attributeLabels()` +**Описание:** Возвращает русские метки для атрибутов модели. + +**Возвращает:** `array` - ассоциативный массив меток + +**Пример:** +```php +$type = new StoreType(); +echo $type->getAttributeLabel('name'); // 'Название типа магазина' +``` + +--- + +## Примеры использования + +### 1. Создание нового типа магазина +```php +$type = new StoreType(); +$type->name = 'Павильон'; +$type->sequence_number = 1; + +if ($type->save()) { + echo "Тип магазина создан с ID: {$type->id}"; +} +``` + +### 2. Получение списка типов для dropdown +```php +use yii\helpers\ArrayHelper; + +$types = StoreType::find() + ->orderBy(['sequence_number' => SORT_ASC]) + ->all(); + +$typesList = ArrayHelper::map($types, 'id', 'name'); + +// Использование в форме +echo Html::dropDownList('store_type', null, $typesList, [ + 'prompt' => 'Выберите тип магазина' +]); +``` + +### 3. Обновление порядка отображения +```php +$type = StoreType::findOne(5); +$type->sequence_number = 10; +$type->save(); +``` + +### 4. Получение информации о создателе +```php +$type = StoreType::find() + ->where(['id' => 1]) + ->one(); + +if ($type) { + echo "Создано: {$type->created_at}\n"; + // Для получения данных создателя нужно добавить связь с Admin +} +``` + +### 5. Поиск типа по названию +```php +$type = StoreType::find() + ->where(['name' => 'Флагманский магазин']) + ->one(); + +if ($type) { + echo "Тип найден, ID: {$type->id}"; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_TYPE ||--o{ CITY_STORE_PARAMS : "used in" + STORE_TYPE ||--|| ADMIN : "created by" + STORE_TYPE ||--o| ADMIN : "updated by" + + STORE_TYPE { + int id PK + string name "Название типа" + int created_by FK "Создал" + timestamp created_at "Дата создания" + int updated_by FK "Обновил" + timestamp updated_at "Дата обновления" + int sequence_number "Порядковый номер" + } + + CITY_STORE_PARAMS { + int id PK + int store_type FK "Тип магазина" + } + + ADMIN { + int id PK + string name_full "ФИО" + } +``` + +--- + +## Особенности реализации + +### Автоматическое управление метаданными +Использование TimestampBehavior и BlameableBehavior обеспечивает автоматическое заполнение полей аудита. + +### Сортировка +Поле `sequence_number` позволяет управлять порядком отображения типов в списках и формах. + +### Схема базы данных +Таблица находится в схеме `erp24`, что видно из имени таблицы `{{%erp24.store_type}}`. + +--- + +## Бизнес-логика + +### Использование в системе +1. **Классификация магазинов** - группировка по форматам +2. **Аналитика** - сравнение эффективности разных типов +3. **Планирование** - разработка стратегии развития по форматам +4. **Отчётность** - сегментация данных по типам магазинов + +### Примеры типов +- Флагманский магазин +- Магазин формата "У дома" +- Павильон +- Киоск +- Корнер +- Pop-up store + +--- + +## Связанные компоненты +- **Модели:** CityStoreParams, CityStore, Admin +- **Контроллеры:** StoreTypeController +- **Формы:** StoreTypeForm + +--- + +## Примечания + +**Рекомендации:** +1. Добавить связи `getCreatedBy()` и `getUpdatedBy()` с моделью Admin +2. Создать scope-методы для сортировки по sequence_number +3. Добавить валидацию уникальности названия +4. Создать метод для получения активных типов diff --git a/erp24/docs/models/StoreVisitors.md b/erp24/docs/models/StoreVisitors.md new file mode 100644 index 00000000..d290349e --- /dev/null +++ b/erp24/docs/models/StoreVisitors.md @@ -0,0 +1,340 @@ +# Class: StoreVisitors + + +## Mindmap + +```mermaid +mindmap + root((StoreVisitors)) + Таблица БД + store_visitors + Свойства + store_id + int + date + string + date_hour + int + counter + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель `StoreVisitors` представляет учёт посетителей магазинов в системе ERP24. Она хранит почасовую статистику посещаемости торговых точек. Модель используется для анализа трафика, планирования нагрузки персонала и оптимизации работы магазинов. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`yii\db\ActiveRecord` + +## Таблица базы данных +`store_visitors` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `store_id` | int | да | ID магазина (часть составного ключа) | +| `date` | date | да | Дата учёта посетителей (часть составного ключа) | +| `date_hour` | int | да | Час дня от 0 до 23 (часть составного ключа) | +| `counter` | int | да | Количество посетителей за час | + +## Методы + +### `tableName()` +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'store_visitors' + +--- + +### `rules()` +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +- Все поля обязательны для заполнения +- `store_id`, `date_hour`, `counter` - целые числа +- `date` - безопасное поле для даты +- Уникальность по комбинации `store_id + date + date_hour` (один счётчик на час) + +**Пример:** +```php +$visitor = new StoreVisitors(); +$visitor->store_id = 15; +$visitor->date = '2024-01-15'; +$visitor->date_hour = 14; +$visitor->counter = 45; + +if ($visitor->validate()) { + echo "Данные валидны"; +} +``` + +--- + +### `attributeLabels()` +**Описание:** Возвращает метки атрибутов. + +**Возвращает:** `array` - массив английских меток + +--- + +## Примеры использования + +### 1. Запись данных счётчика посетителей +```php +$visitor = new StoreVisitors(); +$visitor->store_id = 15; +$visitor->date = date('Y-m-d'); +$visitor->date_hour = (int)date('H'); // Текущий час +$visitor->counter = 45; // 45 посетителей за текущий час + +if ($visitor->save()) { + echo "Данные о посетителях сохранены"; +} else { + print_r($visitor->getErrors()); +} +``` + +### 2. Получение почасовой статистики за день +```php +$storeId = 15; +$date = '2024-01-15'; + +$stats = StoreVisitors::find() + ->where(['store_id' => $storeId, 'date' => $date]) + ->orderBy(['date_hour' => SORT_ASC]) + ->all(); + +echo "Посещаемость магазина {$storeId} за {$date}:\n"; +foreach ($stats as $stat) { + echo "{$stat->date_hour}:00 - {$stat->counter} посетителей\n"; +} +``` + +### 3. Расчёт общего количества посетителей за день +```php +$storeId = 15; +$date = '2024-01-15'; + +$totalVisitors = StoreVisitors::find() + ->where(['store_id' => $storeId, 'date' => $date]) + ->sum('counter'); + +echo "Всего посетителей за {$date}: {$totalVisitors}"; +``` + +### 4. Определение часов пик +```php +$storeId = 15; +$dateFrom = '2024-01-01'; +$dateTo = '2024-01-31'; + +$peakHours = StoreVisitors::find() + ->select(['date_hour', 'AVG(counter) as avg_visitors']) + ->where(['store_id' => $storeId]) + ->andWhere(['between', 'date', $dateFrom, $dateTo]) + ->groupBy('date_hour') + ->orderBy(['avg_visitors' => SORT_DESC]) + ->limit(3) + ->asArray() + ->all(); + +echo "Часы пик (топ-3):\n"; +foreach ($peakHours as $hour) { + echo "{$hour['date_hour']}:00 - " . round($hour['avg_visitors'], 2) . " посетителей в среднем\n"; +} +``` + +### 5. Сравнение посещаемости разных магазинов +```php +$date = '2024-01-15'; + +$storeStats = StoreVisitors::find() + ->select(['store_id', 'SUM(counter) as total_visitors']) + ->where(['date' => $date]) + ->groupBy('store_id') + ->orderBy(['total_visitors' => SORT_DESC]) + ->asArray() + ->all(); + +echo "Посещаемость магазинов за {$date}:\n"; +foreach ($storeStats as $stat) { + echo "Магазин {$stat['store_id']}: {$stat['total_visitors']} посетителей\n"; +} +``` + +### 6. Обновление данных счётчика +```php +$visitor = StoreVisitors::findOne([ + 'store_id' => 15, + 'date' => '2024-01-15', + 'date_hour' => 14 +]); + +if ($visitor) { + $visitor->counter += 10; // Добавили 10 посетителей + $visitor->save(); +} else { + // Создаём новую запись + $visitor = new StoreVisitors(); + $visitor->store_id = 15; + $visitor->date = '2024-01-15'; + $visitor->date_hour = 14; + $visitor->counter = 10; + $visitor->save(); +} +``` + +### 7. Анализ динамики посещаемости по дням недели +```php +use yii\db\Expression; + +$storeId = 15; +$dateFrom = '2024-01-01'; +$dateTo = '2024-01-31'; + +$weekdayStats = StoreVisitors::find() + ->select([ + new Expression('EXTRACT(DOW FROM date) as day_of_week'), + new Expression('AVG(counter) as avg_visitors') + ]) + ->where(['store_id' => $storeId]) + ->andWhere(['between', 'date', $dateFrom, $dateTo]) + ->groupBy('day_of_week') + ->orderBy('day_of_week') + ->asArray() + ->all(); + +$daysNames = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']; + +foreach ($weekdayStats as $stat) { + $dayName = $daysNames[$stat['day_of_week']]; + echo "{$dayName}: " . round($stat['avg_visitors'], 2) . " посетителей в среднем\n"; +} +``` + +### 8. Массовая запись данных за день +```php +$storeId = 15; +$date = '2024-01-15'; + +$hourlyData = [ + 9 => 12, // 9:00 - 12 посетителей + 10 => 25, // 10:00 - 25 посетителей + 11 => 38, + 12 => 52, + 13 => 45, + 14 => 55, + 15 => 60, + 16 => 58, + 17 => 65, + 18 => 70, + 19 => 48, + 20 => 30, +]; + +foreach ($hourlyData as $hour => $count) { + $visitor = new StoreVisitors(); + $visitor->store_id = $storeId; + $visitor->date = $date; + $visitor->date_hour = $hour; + $visitor->counter = $count; + $visitor->save(); +} + +echo "Данные за день загружены"; +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + STORE_VISITORS ||--|| CITY_STORE : "belongs to" + + STORE_VISITORS { + int store_id PK,FK "ID магазина" + date date PK "Дата" + int date_hour PK "Час (0-23)" + int counter "Количество посетителей" + } + + CITY_STORE { + int id PK + string name "Название" + } +``` + +--- + +## Особенности реализации + +### Составной первичный ключ +Уникальность записи обеспечивается комбинацией трёх полей: `store_id + date + date_hour`. Это гарантирует одну запись на каждый час каждого дня для каждого магазина. + +### Почасовой учёт +Поле `date_hour` хранит час дня от 0 до 23, что позволяет: +- Анализировать почасовую динамику посещаемости +- Определять часы пик +- Планировать нагрузку персонала + +### Счётчик посетителей +Поле `counter` может заполняться: +- Автоматически из систем подсчёта посетителей (датчики, камеры) +- Вручную сотрудниками магазина +- Через API интеграции + +--- + +## Бизнес-логика + +### Использование в системе + +1. **Аналитика трафика** - понимание паттернов посещаемости +2. **Планирование персонала** - определение необходимого числа сотрудников +3. **Оптимизация работы** - корректировка режима работы под загрузку +4. **Маркетинг** - оценка эффективности акций и рекламы +5. **KPI** - конверсия посетителей в покупателей + +### Ключевые метрики + +- **Средняя посещаемость в час** - counter / количество дней +- **Часы пик** - часы с максимальным counter +- **Конверсия** - (количество покупок / counter) * 100% +- **Средний чек на посетителя** - выручка / counter + +--- + +## Связанные компоненты + +- **Модели:** CityStore +- **Сервисы:** AnalyticsService, StaffingService +- **Контроллеры:** StoreVisitorsController, AnalyticsController +- **Отчёты:** Отчёт о посещаемости, анализ часов пик + +--- + +## Примечания + +**Рекомендации:** + +1. Настроить автоматический сбор данных из систем подсчёта посетителей +2. Создать индексы на комбинации полей для оптимизации запросов +3. Добавить валидацию диапазона `date_hour` (0-23) +4. Реализовать архивирование старых данных +5. Создать агрегированные таблицы для быстрых отчётов + +**Потенциальные улучшения:** + +1. Добавить связь с CityStore через метод `getStore()` +2. Создать scope-методы для часто используемых запросов +3. Добавить методы для расчёта средних значений +4. Реализовать методы для определения часов пик +5. Добавить поддержку минутного учёта для более детальной аналитики diff --git a/erp24/docs/models/StoresTypeList.md b/erp24/docs/models/StoresTypeList.md new file mode 100644 index 00000000..77956ce9 --- /dev/null +++ b/erp24/docs/models/StoresTypeList.md @@ -0,0 +1,276 @@ +# Класс: StoresTypeList + + +## Mindmap + +```mermaid +mindmap + root((StoresTypeList)) + Таблица БД + stores_type_list + Свойства + id + int + type_name + string + type_alias + string + deleted_by + Admin + active + int + deleted_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов магазинов в ERP24. Классификация точек продаж по формату: флагманский магазин, островок в ТЦ, киоск и т.д. Поддерживает мягкое удаление (soft delete). + +## Пространство имён +`yii_app\records` + +## Таблица БД +`stores_type_list` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Трейты + +| Трейт | Описание | +|-------|----------| +| `SoftDeleteTrait` | Мягкое удаление записей | + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `type_name` | varchar(255) | Наименование типа магазина | +| `type_alias` | varchar(255) | Алиас (краткое имя) типа | +| `type_description` | varchar(255) / null | Описание типа магазина | +| `active` | int | Флаг активности (1 = активен, 0 = удалён) | +| `deleted_by` | int / null | FK на удалившего (Admin) | +| `deleted_at` | datetime / null | Дата мягкого удаления | +| `created_at` | datetime | Дата создания записи | + +## Методы + +### hasSoftDelete() +**Описание:** Указывает, поддерживает ли модель мягкое удаление. + +**Возвращает:** `bool` — всегда `true` + +**Пример:** +```php +if (StoresTypeList::hasSoftDelete()) { + echo "Модель поддерживает soft delete"; +} +``` + +### find() (переопределён) +**Описание:** Возвращает ActiveQuery с автоматической фильтрацией по active=1. + +**Возвращает:** `ActiveQuery` — только активные записи + +**Пример:** +```php +// Автоматически возвращает только active=1 +$types = StoresTypeList::find()->all(); +``` + +## Диаграмма связей + +```mermaid +erDiagram + StoresTypeList { + int id PK + varchar type_name UK + varchar type_alias + varchar type_description + int active + int deleted_by FK + datetime deleted_at + datetime created_at + } + + CityStore { + int id PK + int store_type_id FK + varchar name + } + + Admin { + int id PK + varchar name + } + + StoresTypeList ||--o{ CityStore : "store_type_id" + Admin ||--o{ StoresTypeList : "deleted_by" +``` + +## Диаграмма soft delete + +```mermaid +flowchart TD + A[Тип магазина
    active=1] --> B{Удаление?} + B -->|Soft Delete| C[active=0
    deleted_at=NOW
    deleted_by=user_id] + B -->|Hard Delete| D[DELETE FROM table] + + C --> E[Не виден в find] + E --> F{Восстановление?} + F -->|restore| G[active=1
    deleted_at=NULL] +``` + +## Примеры типов магазинов + +| type_name | type_alias | type_description | +|-----------|------------|------------------| +| Флагманский магазин | flagship | Крупный магазин с полным ассортиментом | +| Островок в ТЦ | island | Торговый островок в торговом центре | +| Киоск | kiosk | Небольшой киоск или павильон | +| Интернет-магазин | online | Только онлайн-продажи | +| Франшиза | franchise | Магазин-партнёр по франшизе | + +## Примеры использования + +### Создание типа магазина +```php +$type = new StoresTypeList(); +$type->type_name = 'Флагманский магазин'; +$type->type_alias = 'flagship'; +$type->type_description = 'Крупный магазин с полным ассортиментом'; +$type->active = 1; +$type->save(); +``` + +### Получение всех активных типов +```php +// Автоматически фильтрует по active=1 +$types = StoresTypeList::find() + ->orderBy(['type_name' => SORT_ASC]) + ->all(); + +foreach ($types as $type) { + echo "{$type->type_name} ({$type->type_alias})\n"; +} +``` + +### Формирование списка для выбора +```php +$typesList = ArrayHelper::map( + StoresTypeList::find()->orderBy(['type_name' => SORT_ASC])->all(), + 'id', + 'type_name' +); + +echo Html::dropDownList('store_type_id', null, $typesList, [ + 'prompt' => 'Выберите тип магазина' +]); +``` + +### Поиск по алиасу +```php +$type = StoresTypeList::find() + ->where(['type_alias' => 'flagship']) + ->one(); + +if ($type) { + echo "Тип: {$type->type_name}"; +} +``` + +### Мягкое удаление +```php +$type = StoresTypeList::findOne($id); + +if ($type) { + $type->softDelete(); // Метод из SoftDeleteTrait +} +``` + +### Получение всех записей включая удалённые +```php +// Обход автоматической фильтрации +$allTypes = StoresTypeList::find() + ->where([]) // Сбрасываем условие + ->all(); + +// Или напрямую через Query +$allTypes = (new \yii\db\Query()) + ->from('stores_type_list') + ->all(); +``` + +### Получение только удалённых +```php +$deletedTypes = (new \yii\db\Query()) + ->from('stores_type_list') + ->where(['active' => 0]) + ->all(); +``` + +### Восстановление удалённого типа +```php +$type = (new \yii\db\Query()) + ->from('stores_type_list') + ->where(['id' => $id, 'active' => 0]) + ->one(); + +if ($type) { + StoresTypeList::updateAll( + ['active' => 1, 'deleted_at' => null, 'deleted_by' => null], + ['id' => $id] + ); +} +``` + +### Проверка уникальности имени +```php +// Валидация уникальности только среди активных +$type = new StoresTypeList(); +$type->type_name = 'Флагманский магазин'; +$type->validate(); // Ошибка, если такое имя уже есть с active=1 +``` + +### Статистика по типам +```php +$stats = CityStore::find() + ->select(['store_type_id', 'COUNT(*) as count']) + ->groupBy('store_type_id') + ->asArray() + ->all(); + +$types = ArrayHelper::index(StoresTypeList::find()->all(), 'id'); + +foreach ($stats as $stat) { + $typeName = $types[$stat['store_type_id']]->type_name ?? 'Неизвестный'; + echo "{$typeName}: {$stat['count']} магазинов\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `type_name` | required, string (max 255), unique среди active=1 | +| `type_alias` | string (max 255) | +| `type_description` | string (max 255) | +| `active` | safe | +| `created_at` | safe | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы (deleted_by) + +## Особенности реализации + +1. **Soft Delete**: Использует SoftDeleteTrait и поле active +2. **Автофильтрация**: find() автоматически фильтрует по active=1 +3. **Уникальность среди активных**: type_name уникально только для active=1 +4. **hasSoftDelete()**: Статический метод для проверки поддержки soft delete +5. **Алиас типа**: type_alias для программного обращения +6. **Аудит удаления**: deleted_by и deleted_at для истории diff --git a/erp24/docs/models/Task.md b/erp24/docs/models/Task.md index b195e77c..a38faec2 100644 --- a/erp24/docs/models/Task.md +++ b/erp24/docs/models/Task.md @@ -1,5 +1,41 @@ # Model: Task + +## Mindmap + +```mermaid +mindmap + root((Task)) + Таблица БД + task + Свойства + id + int + name + string + description + string + group_id + int + children_order_type + string + posit + int + Связи + AttachedFiles + 1:N Files + Proofs + 1:N Files + TaskUsers + 1:N TaskUsers + Users + 1:N Admin + TaskViewers + 1:N TaskViewers + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель задач для управления работой сотрудников. Реализует систему таск-менеджмента с поддержкой иерархии задач, статусов, приоритетов и контроля выполнения. diff --git a/erp24/docs/models/TaskAlertLevel.md b/erp24/docs/models/TaskAlertLevel.md new file mode 100644 index 00000000..81ee7625 --- /dev/null +++ b/erp24/docs/models/TaskAlertLevel.md @@ -0,0 +1,517 @@ +# Model: TaskAlertLevel + + +## Mindmap + +```mermaid +mindmap + root((TaskAlertLevel)) + Таблица БД + task_alert_level + Свойства + id + int + name + string + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника уровней важности (алертов) задач. Определяет степень критичности и срочности задач в системе. Используется для приоритизации уведомлений и определения скорости реагирования на задачи. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_alert_level` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID уровня важности | +| `name` | string | Название уровня важности (макс. 200 символов) | +| `posit` | int | Позиция в списке для сортировки | + +--- + +## Правила валидации + +### Обязательные поля + +Поля `name` и `posit` являются обязательными. + +```php +[['name', 'posit'], 'required'] +``` + +### Типы данных + +```php +// Целочисленное поле +[['posit'], 'integer'] + +// Ограничение длины строки +[['name'], 'string', 'max' => 200] +``` + +--- + +## Связи (Relations) + +### В модели Task + +```php +public function getAlertLevel() { + return $this->hasOne(TaskAlertLevel::class, ['id' => 'alert_level_id']); +} +``` + +### В модели TaskTemplates + +```php +public function getAlertLevel() { + return $this->hasOne(TaskAlertLevel::class, ['id' => 'alert_level_id']); +} +``` + +### В модели TasksType + +```php +public function getAlertLevel() { + return $this->hasOne(TaskAlertLevel::class, ['id' => 'alert_level_id']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_alert_level` + +**Возвращает:** +- `string` - название таблицы `'task_alert_level'` + +```php +public static function tableName() +{ + return 'task_alert_level'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения name и posit +- Проверяет, что posit является целым числом +- Ограничивает максимальную длину name до 200 символов + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'posit'], 'required'], + [['posit'], 'integer'], + [['name'], 'string', 'max' => 200], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'posit' => 'Posit', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task }o--|| TaskAlertLevel : "alertLevel" + TaskTemplates }o--|| TaskAlertLevel : "alertLevel" + TasksType }o--|| TaskAlertLevel : "alertLevel" + + TaskAlertLevel { + int id PK + string name + int posit + } + + Task { + int id PK + int alert_level_id FK + string name + } + + TaskTemplates { + int id PK + int alert_level_id FK + string name + } + + TasksType { + int id PK + int alert_level_id FK + string name + } +``` + +--- + +## Стандартные уровни важности + +| ID | Название | Posit | Описание | Рекомендуемое время реакции | +|----|----------|-------|----------|----------------------------| +| 1 | Низкая | 1 | Обычные задачи, не требующие срочного выполнения | 24-48 часов | +| 2 | Средняя | 2 | Задачи требующие внимания в ближайшее время | 4-8 часов | +| 3 | Высокая | 3 | Важные задачи требующие скорого выполнения | 1-2 часа | +| 4 | Критическая | 4 | Срочные задачи требующие немедленного внимания | До 30 минут | +| 5 | Чрезвычайная | 5 | Задачи блокирующие бизнес-процессы | Немедленно | + +--- + +## Примеры использования + +### Создание нового уровня важности + +```php +$alertLevel = new TaskAlertLevel(); +$alertLevel->name = 'Очень высокая'; +$alertLevel->posit = 4; +$alertLevel->save(); +``` + +### Получение всех уровней важности + +```php +$levels = TaskAlertLevel::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($levels as $level) { + echo "{$level->name} (позиция: {$level->posit})\n"; +} +``` + +### Получение уровня по ID + +```php +$alertLevel = TaskAlertLevel::findOne(3); +echo $alertLevel->name; // "Высокая" +``` + +### Создание задачи с уровнем важности + +```php +$task = new Task(); +$task->name = 'Срочная проверка системы'; +$task->description = 'Обнаружена критическая ошибка'; +$task->alert_level_id = 4; // Критическая +$task->save(); +``` + +### Создание шаблона с уровнем важности + +```php +$template = new TaskTemplates(); +$template->name = 'Утренняя проверка'; +$template->description = 'Проверка работоспособности'; +$template->alert_level_id = 1; // Низкая +// ... остальные поля ... +$template->save(); +``` + +### Получение задач с определённым уровнем важности + +```php +$criticalTasks = Task::find() + ->where(['alert_level_id' => 4]) + ->andWhere(['<>', 'status', Task::STATUS_CLOSED]) + ->all(); + +echo "Найдено критических задач: " . count($criticalTasks); +``` + +### Получение уровней для выпадающего списка + +```php +$alertLevels = TaskAlertLevel::find() + ->select(['id', 'name']) + ->orderBy(['posit' => SORT_ASC]) + ->indexBy('id') + ->column(); + +// Результат: [1 => 'Низкая', 2 => 'Средняя', 3 => 'Высокая', ...] +``` + +### Статистика задач по уровням важности + +```php +$stats = TaskAlertLevel::find() + ->select([ + 'task_alert_level.id', + 'task_alert_level.name', + 'COUNT(task.id) as task_count' + ]) + ->leftJoin('task', 'task.alert_level_id = task_alert_level.id') + ->groupBy('task_alert_level.id') + ->orderBy(['task_alert_level.posit' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['task_count']} задач\n"; +} +``` + +### Обновление уровня важности задачи + +```php +$task = Task::findOne(123); +$task->alert_level_id = 5; // Повысить до чрезвычайной +$task->save(); + +// Отправить уведомление +Yii::$app->mailer->compose() + ->setTo($task->updatedBy->email) + ->setSubject('СРОЧНО: Уровень важности задачи повышен') + ->setTextBody("Задача #{$task->id} теперь имеет чрезвычайный уровень важности") + ->send(); +``` + +### Фильтрация задач по уровню важности + +```php +$highPriorityTasks = Task::find() + ->joinWith('alertLevel') + ->where(['>=', 'task_alert_level.posit', 3]) + ->andWhere(['<>', 'task.status', Task::STATUS_CLOSED]) + ->all(); +``` + +### Изменение порядка уровней важности + +```php +// Переместить уровень на позицию выше +$alertLevel = TaskAlertLevel::findOne(3); +$currentPosition = $alertLevel->posit; + +TaskAlertLevel::updateAll( + ['posit' => new \yii\db\Expression('posit + 1')], + ['and', ['>=', 'posit', $currentPosition - 1], ['<', 'posit', $currentPosition]] +); + +$alertLevel->posit = $currentPosition - 1; +$alertLevel->save(); +``` + +--- + +## Использование в уведомлениях + +### Определение типа уведомления по уровню важности + +```php +class NotificationService +{ + public function sendTaskNotification($task) + { + $alertLevel = $task->alertLevel; + + if (!$alertLevel) { + // Уровень не указан - стандартное уведомление + $this->sendEmailNotification($task); + return; + } + + switch ($alertLevel->posit) { + case 1: // Низкая + $this->sendEmailNotification($task); + break; + + case 2: // Средняя + $this->sendEmailNotification($task); + $this->sendTelegramNotification($task); + break; + + case 3: // Высокая + $this->sendEmailNotification($task); + $this->sendTelegramNotification($task); + $this->sendSMSNotification($task); + break; + + case 4: // Критическая + case 5: // Чрезвычайная + $this->sendEmailNotification($task); + $this->sendTelegramNotification($task); + $this->sendSMSNotification($task); + $this->sendPushNotification($task); + $this->callPhone($task); + break; + } + } +} +``` + +### Расчёт SLA на основе уровня важности + +```php +function calculateSLA($alertLevelId) +{ + $slaMap = [ + 1 => 48 * 3600, // 48 часов + 2 => 8 * 3600, // 8 часов + 3 => 2 * 3600, // 2 часа + 4 => 1800, // 30 минут + 5 => 600, // 10 минут + ]; + + return $slaMap[$alertLevelId] ?? 24 * 3600; // По умолчанию 24 часа +} + +$task = Task::findOne(123); +$slaSeconds = calculateSLA($task->alert_level_id); +$deadline = date('Y-m-d H:i:s', strtotime($task->created_at) + $slaSeconds); + +echo "SLA дедлайн: {$deadline}"; +``` + +--- + +## Использование в представлениях + +### Отображение badge с уровнем важности + +```php +function getAlertLevelBadge($alertLevelId) +{ + $colorMap = [ + 1 => 'success', // Зелёный + 2 => 'info', // Синий + 3 => 'warning', // Жёлтый + 4 => 'danger', // Красный + 5 => 'dark', // Чёрный + ]; + + $alertLevel = TaskAlertLevel::findOne($alertLevelId); + $color = $colorMap[$alertLevelId] ?? 'secondary'; + + return Html::tag('span', $alertLevel->name, [ + 'class' => "badge badge-{$color}" + ]); +} + +$task = Task::findOne(123); +echo getAlertLevelBadge($task->alert_level_id); +``` + +### Иконки для уровней важности + +```php +function getAlertLevelIcon($alertLevelId) +{ + $iconMap = [ + 1 => 'fa-info-circle', + 2 => 'fa-exclamation-circle', + 3 => 'fa-exclamation-triangle', + 4 => 'fa-fire', + 5 => 'fa-bomb', + ]; + + $icon = $iconMap[$alertLevelId] ?? 'fa-question-circle'; + return Html::tag('i', '', ['class' => "fas {$icon}"]); +} +``` + +--- + +## Dashboard и аналитика + +### Получение задач сгруппированных по уровням важности + +```php +$tasksByAlertLevel = Task::find() + ->select([ + 'task_alert_level.name', + 'task_alert_level.posit', + 'COUNT(task.id) as count' + ]) + ->leftJoin('task_alert_level', 'task.alert_level_id = task_alert_level.id') + ->where(['<>', 'task.status', Task::STATUS_CLOSED]) + ->groupBy('task.alert_level_id') + ->orderBy(['task_alert_level.posit' => SORT_DESC]) + ->asArray() + ->all(); +``` + +### Мониторинг критических задач + +```php +$criticalTasks = Task::find() + ->joinWith('alertLevel') + ->where(['>=', 'task_alert_level.posit', 4]) + ->andWhere(['<>', 'task.status', Task::STATUS_CLOSED]) + ->count(); + +if ($criticalTasks > 0) { + // Отправить алерт администраторам + AlertService::sendCriticalTasksAlert($criticalTasks); +} +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач +- [TasksType](TasksType.md) - Типы задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskAlertLevelData.md b/erp24/docs/models/TaskAlertLevelData.md new file mode 100644 index 00000000..e9d4c571 --- /dev/null +++ b/erp24/docs/models/TaskAlertLevelData.md @@ -0,0 +1,204 @@ +# Модель TaskAlertLevelData + + +## Mindmap + +```mermaid +mindmap + root((TaskAlertLevelData)) + Таблица БД + task_alert_level_data + Свойства + id + int + level_id + int + status_id + int + type_id + int + recipient_type_id + int + recipient_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TaskAlertLevelData` содержит детальные настройки уведомлений для каждого уровня эскалации задач. Определяет, кому и какое сообщение отправить при достижении определённого времени просрочки. Используется для построения цепочек эскалации невыполненных задач. + +**Файл модели:** `erp24/records/TaskAlertLevelData.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `task_alert_level_data` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `level_id` | INTEGER | ID уровня (FK → task_alert_level.id) | +| `status_id` | INTEGER | ID статуса задачи (FK → task_status.id) | +| `type_id` | INTEGER | Тип коммуникации (сообщение, звонок) | +| `recipient_type_id` | INTEGER | Тип получателя (исполнитель, автор, контролёр) | +| `recipient_id` | INTEGER | ID конкретного получателя (для типа "конкретный сотрудник") | +| `posit` | INTEGER | Позиция в последовательности | +| `trigger_time` | INTEGER | Время срабатывания (минуты от назначения) | +| `message` | TEXT | Шаблон сообщения | + +--- + +## Типы коммуникации (type_id) + +| ID | Описание | +|----|----------| +| 1 | Сообщение (push/email/telegram) | +| 2 | Задача | +| 3 | Звонок | +| 4 | Звонок коммуникатора | +| 5 | Сообщение руководителю | +| 6 | Звонок руководителю | + +--- + +## Типы получателей (recipient_type_id) + +| ID | alias | Описание | +|----|-------|----------| +| 1 | executor | Исполнитель | +| 2 | author | Создатель задачи | +| 3 | controller | Контролёр | +| 4 | mentor | Ментор | +| 5 | communicator | Коммуникатор | +| 6 | admin | Конкретный сотрудник | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + task_alert_level_data }o--|| task_alert_level : "level" + task_alert_level_data }o--|| task_status : "status" + task_alert_level_data }o--o| admin : "recipient" + + task_alert_level_data { + int id PK + int level_id FK + int status_id FK + int type_id + int recipient_type_id + int recipient_id FK + int posit + int trigger_time + text message + } +``` + +--- + +## Примеры использования + +### Создание настройки уведомления + +```php +$alertData = new TaskAlertLevelData(); +$alertData->level_id = 1; // Уровень 1 +$alertData->status_id = 2; // Статус "В работе" +$alertData->type_id = 1; // Сообщение +$alertData->recipient_type_id = 1; // Исполнителю +$alertData->posit = 1; // Первое в цепочке +$alertData->trigger_time = 60; // Через 60 минут +$alertData->message = 'Задача "{task_name}" требует внимания. Времени осталось: {time_left}'; +$alertData->save(); +``` + +### Получение уведомлений для уровня + +```php +$alerts = TaskAlertLevelData::find() + ->where(['level_id' => $levelId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($alerts as $alert) { + echo "Через {$alert->trigger_time} мин: {$alert->message}\n"; +} +``` + +### Получение уведомлений для статуса + +```php +$alerts = TaskAlertLevelData::find() + ->where([ + 'level_id' => $task->alert_level_id, + 'status_id' => $task->status_id + ]) + ->orderBy(['trigger_time' => SORT_ASC]) + ->all(); +``` + +### Определение получателя уведомления + +```php +function getRecipient(TaskAlertLevelData $alert, Task $task): ?int +{ + switch ($alert->recipient_type_id) { + case 1: // Исполнитель + return $task->executor_id; + case 2: // Автор + return $task->author_id; + case 3: // Контролёр + return $task->controller_id; + case 6: // Конкретный сотрудник + return $alert->recipient_id; + default: + return null; + } +} +``` + +### Подстановка переменных в шаблон + +```php +function renderMessage(TaskAlertLevelData $alert, Task $task): string +{ + $vars = [ + '{task_name}' => $task->name, + '{task_id}' => $task->id, + '{time_left}' => $this->formatTimeLeft($task), + '{deadline}' => $task->deadline, + ]; + + return strtr($alert->message, $vars); +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `level_id`, `status_id`, `type_id`, `recipient_type_id`, `posit`, `trigger_time` | Обязательные, целые числа | +| `message` | Обязательное, строка | +| `recipient_id` | Целое число (опционально) | +| `level_id + status_id + posit` | Уникальная комбинация | + +--- + +## Связанные модели + +- **[TaskAlertLevel](./TaskAlertLevel.md)** — уровни уведомлений +- **[TaskStatus](./TaskStatus.md)** — статусы задач +- **[TaskAlertLog](./TaskAlertLog.md)** — история отправки +- **[Admin](./Admin.md)** — получатели + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TaskAlertLog.md b/erp24/docs/models/TaskAlertLog.md new file mode 100644 index 00000000..182356a5 --- /dev/null +++ b/erp24/docs/models/TaskAlertLog.md @@ -0,0 +1,150 @@ +# Модель TaskAlertLog + + +## Mindmap + +```mermaid +mindmap + root((TaskAlertLog)) + Таблица БД + task_alert_log + Свойства + task_id + int + alert_id + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TaskAlertLog` фиксирует историю отправки уведомлений по задачам. Хранит связь между задачей и сработавшим уведомлением, а также время срабатывания. Используется для предотвращения повторной отправки уведомлений и анализа эффективности эскалации. + +**Файл модели:** `erp24/records/TaskAlertLog.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `task_alert_log` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `task_id` | INTEGER | ID задачи (FK → task.id) | +| `alert_id` | INTEGER | ID уведомления (FK → task_alert_level_data.id) | +| `created_at` | TIMESTAMP | Время срабатывания уведомления | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + task_alert_log }o--|| task : "task" + task_alert_log }o--|| task_alert_level_data : "alert" + + task_alert_log { + int task_id FK + int alert_id FK + timestamp created_at + } + + task { + int id PK + string name + } + + task_alert_level_data { + int id PK + int trigger_time + string message + } +``` + +--- + +## Примеры использования + +### Регистрация отправки уведомления + +```php +$log = new TaskAlertLog(); +$log->task_id = $taskId; +$log->alert_id = $alertId; +$log->created_at = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Проверка, было ли отправлено уведомление + +```php +$alreadySent = TaskAlertLog::find() + ->where([ + 'task_id' => $taskId, + 'alert_id' => $alertId + ]) + ->exists(); + +if (!$alreadySent) { + // Отправляем уведомление + $this->sendAlert($task, $alert); + + // Логируем отправку + $log = new TaskAlertLog(); + $log->task_id = $taskId; + $log->alert_id = $alertId; + $log->created_at = date('Y-m-d H:i:s'); + $log->save(); +} +``` + +### История уведомлений по задаче + +```php +$logs = TaskAlertLog::find() + ->where(['task_id' => $taskId]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +foreach ($logs as $log) { + echo "Уведомление #{$log->alert_id} отправлено: {$log->created_at}\n"; +} +``` + +### Статистика уведомлений за период + +```php +$stats = TaskAlertLog::find() + ->select(['alert_id', 'COUNT(*) as count']) + ->where(['>=', 'created_at', date('Y-m-d', strtotime('-30 days'))]) + ->groupBy('alert_id') + ->asArray() + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `task_id` | Обязательное, целое число | +| `alert_id` | Обязательное, целое число | +| `created_at` | Обязательное | + +--- + +## Связанные модели + +- **[Task](./Task.md)** — задачи +- **[TaskAlertLevelData](./TaskAlertLevelData.md)** — настройки уведомлений +- **[TaskAlertLevel](./TaskAlertLevel.md)** — уровни уведомлений + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TaskEntity.md b/erp24/docs/models/TaskEntity.md new file mode 100644 index 00000000..7e7f6e11 --- /dev/null +++ b/erp24/docs/models/TaskEntity.md @@ -0,0 +1,404 @@ +# Model: TaskEntity + + +## Mindmap + +```mermaid +mindmap + root((TaskEntity)) + Таблица БД + task_entity + Свойства + id + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника типов сущностей задач. Определяет к каким объектам системы могут быть привязаны задачи (магазины, уроки, сотрудники и т.д.). Используется для категоризации задач по связанным с ними бизнес-объектам. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_entity` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID типа сущности | +| `name` | string | Название сущности для отображения пользователю (макс. 100 символов) | +| `alias` | string | Название сущности для внутреннего использования в коде (макс. 100 символов) | + +--- + +## Правила валидации + +### Обязательные поля + +Оба поля `name` и `alias` являются обязательными. + +```php +[['name', 'alias'], 'required'] +``` + +### Типы данных и ограничения + +```php +// Ограничения длины строк +[['name', 'alias'], 'string', 'max' => 100] + +// Уникальность alias +[['alias'], 'unique'] +``` + +--- + +## Связи (Relations) + +### В модели Task + +```php +public function getTaskEntity() { + return $this->hasOne(TaskEntity::class, ['id' => 'entity_type']); +} +``` + +### В модели TaskTemplates + +```php +public function getTaskEntity() { + return $this->hasOne(TaskEntity::class, ['id' => 'entity_type']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_entity` + +**Возвращает:** +- `string` - название таблицы `'task_entity'` + +```php +public static function tableName() +{ + return 'task_entity'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения name и alias +- Ограничивает максимальную длину строковых полей до 100 символов +- Обеспечивает уникальность поля alias для предотвращения конфликтов в коде + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'alias'], 'required'], + [['name', 'alias'], 'string', 'max' => 100], + [['alias'], 'unique'], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'alias' => 'Alias', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task }o--|| TaskEntity : "taskEntity" + TaskTemplates }o--|| TaskEntity : "taskEntity" + + TaskEntity { + int id PK + string name + string alias UK + } + + Task { + int id PK + int entity_type FK + string entity_id + } + + TaskTemplates { + int id PK + int entity_type FK + string entity_id + } +``` + +--- + +## Примеры типов сущностей + +| ID | Name (Название) | Alias (Внутреннее имя) | Описание | +|----|-----------------|------------------------|----------| +| 1 | Магазин | store | Задачи связанные с магазинами | +| 2 | Урок | lesson | Задачи по обучению сотрудников | +| 3 | Сотрудник | employee | Задачи связанные с конкретным сотрудником | +| 4 | Заказ | order | Задачи по обработке заказов | +| 5 | Товар | product | Задачи связанные с товарами | +| 6 | Склад | warehouse | Задачи по складской логистике | +| 7 | Общая | general | Общие задачи без привязки | + +--- + +## Примеры использования + +### Создание нового типа сущности + +```php +$entity = new TaskEntity(); +$entity->name = 'Поставщик'; +$entity->alias = 'supplier'; +$entity->save(); +``` + +### Получение всех типов сущностей + +```php +$entities = TaskEntity::find()->all(); + +foreach ($entities as $entity) { + echo "{$entity->name} ({$entity->alias})\n"; +} +``` + +### Получение типа сущности по ID + +```php +$entity = TaskEntity::findOne(1); +echo $entity->name; // "Магазин" +echo $entity->alias; // "store" +``` + +### Поиск типа сущности по alias + +```php +$entity = TaskEntity::find() + ->where(['alias' => 'lesson']) + ->one(); + +if ($entity) { + echo "Найден тип сущности: {$entity->name}"; +} +``` + +### Получение задач определённого типа сущности + +```php +$entityType = 1; // Магазин +$tasks = Task::find() + ->where(['entity_type' => $entityType]) + ->all(); + +foreach ($tasks as $task) { + echo "Задача: {$task->name}, ID сущности: {$task->entity_id}\n"; +} +``` + +### Создание задачи с привязкой к сущности + +```php +$task = new Task(); +$task->name = 'Проверка витрины'; +$task->entity_type = 1; // Магазин +$task->entity_id = 'uuid-магазина'; // ID конкретного магазина +$task->description = 'Проверить выкладку товаров'; +$task->save(); +``` + +### Получение типов сущностей для выпадающего списка + +```php +$entityList = TaskEntity::find() + ->select(['id', 'name']) + ->indexBy('id') + ->column(); + +// Результат: [1 => 'Магазин', 2 => 'Урок', ...] +``` + +### Статистика задач по типам сущностей + +```php +$stats = TaskEntity::find() + ->select(['task_entity.id', 'task_entity.name', 'COUNT(task.id) as task_count']) + ->leftJoin('task', 'task.entity_type = task_entity.id') + ->groupBy('task_entity.id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['task_count']} задач\n"; +} +``` + +### Проверка существования alias перед созданием + +```php +function isAliasUnique($alias) { + return !TaskEntity::find() + ->where(['alias' => $alias]) + ->exists(); +} + +if (isAliasUnique('new_entity')) { + $entity = new TaskEntity(); + $entity->name = 'Новая сущность'; + $entity->alias = 'new_entity'; + $entity->save(); +} +``` + +### Получение связанной сущности в задаче + +```php +$task = Task::findOne(123); +$entityType = $task->taskEntity; + +switch ($entityType->alias) { + case 'store': + $store = $task->store; // Products1c + echo "Магазин: {$store->name}"; + break; + case 'lesson': + $lesson = $task->lesson; // Lessons + echo "Урок: {$lesson->name}"; + break; + default: + echo "Тип сущности: {$entityType->name}"; +} +``` + +### Обновление названия типа сущности + +```php +$entity = TaskEntity::findOne(1); +$entity->name = 'Торговая точка'; +$entity->save(); +``` + +### Получение задач с определённым типом сущности и фильтрацией + +```php +$tasks = Task::find() + ->joinWith('taskEntity') + ->where(['task_entity.alias' => 'store']) + ->andWhere(['<>', 'task.status', Task::STATUS_CLOSED]) + ->all(); +``` + +--- + +## Использование alias в коде + +Поле `alias` используется для программной логики работы с сущностями: + +```php +class TaskService +{ + public function getEntityObject($task) + { + $entityType = $task->taskEntity; + + switch ($entityType->alias) { + case 'store': + return Products1c::findOne($task->entity_id); + case 'lesson': + return Lessons::findOne($task->entity_id); + case 'employee': + return Admin::findOne($task->entity_id); + case 'order': + return Orders::findOne($task->entity_id); + default: + return null; + } + } +} +``` + +--- + +## Валидация уникальности alias + +```php +$entity = new TaskEntity(); +$entity->name = 'Новая сущность'; +$entity->alias = 'store'; // Уже существует + +if (!$entity->save()) { + print_r($entity->errors); + // Output: ['alias' => ['Alias "store" has already been taken.']] +} +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskLogs.md b/erp24/docs/models/TaskLogs.md new file mode 100644 index 00000000..d184eee5 --- /dev/null +++ b/erp24/docs/models/TaskLogs.md @@ -0,0 +1,418 @@ +# Model: TaskLogs + + +## Mindmap + +```mermaid +mindmap + root((TaskLogs)) + Таблица БД + task_logs + Свойства + id + int + name + string + task_id + int + field_name + string + value_before + string + value_after + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель журнала изменений задач. Хранит историю всех изменений полей задач, позволяя отслеживать аудит и изменения состояния задач в системе. Каждая запись фиксирует изменение одного поля задачи с сохранением предыдущего и нового значения. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_logs` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID записи журнала | +| `name` | string | Название изменения или описание действия (макс. 255 символов) | +| `task_id` | int | ID задачи (внешний ключ на таблицу task) | +| `field_name` | string | Название изменённого поля задачи (макс. 55 символов) | +| `value_before` | string | Значение поля до изменения (TEXT) | +| `value_after` | string | Значение поля после изменения (TEXT) | +| `created_at` | string | Дата и время создания записи лога | + +--- + +## Правила валидации + +### Обязательные поля + +Все поля модели являются обязательными для заполнения. + +```php +[['name', 'task_id', 'field_name', 'value_before', 'value_after', 'created_at'], 'required'] +``` + +### Типы данных + +```php +// Целочисленное значение для ID задачи +[['task_id'], 'integer'] + +// Текстовые поля для значений (могут быть большими) +[['value_before', 'value_after'], 'string'] + +// Дата и время в безопасном формате +[['created_at'], 'safe'] + +// Ограничение длины названия +[['name'], 'string', 'max' => 255] + +// Ограничение длины названия поля +[['field_name'], 'string', 'max' => 55] +``` + +--- + +## Связи (Relations) + +### В модели Task + +```php +public function getLogs() { + return $this->hasMany(TaskLogs::class, ['task_id' => 'id']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_logs` + +**Возвращает:** +- `string` - название таблицы `'task_logs'` + +```php +public static function tableName() +{ + return 'task_logs'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения всех полей +- Проверяет типы данных для каждого поля +- Устанавливает максимальную длину для строковых полей + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'task_id', 'field_name', 'value_before', 'value_after', 'created_at'], 'required'], + [['task_id'], 'integer'], + [['value_before', 'value_after'], 'string'], + [['created_at'], 'safe'], + [['name'], 'string', 'max' => 255], + [['field_name'], 'string', 'max' => 55], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'task_id' => 'Task ID', + 'field_name' => 'Field Name', + 'value_before' => 'Value Before', + 'value_after' => 'Value After', + 'created_at' => 'Created At', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task ||--o{ TaskLogs : "has many" + TaskLogs }o--|| Task : "task_id" + + Task { + int id PK + string name + int status + string description + } + + TaskLogs { + int id PK + string name + int task_id FK + string field_name + string value_before + string value_after + datetime created_at + } +``` + +--- + +## Типичные значения field_name + +| Название поля | Описание изменения | +|---------------|-------------------| +| `status` | Изменение статуса задачи | +| `name` | Изменение названия задачи | +| `description` | Изменение описания задачи | +| `updated_by` | Изменение исполнителя | +| `controller_id` | Изменение проверяющего | +| `deadline` | Изменение дедлайна | +| `prioritet` | Изменение приоритета | +| `result` | Добавление/изменение результата | +| `duration` | Изменение времени выполнения | +| `parent_id` | Изменение родительской задачи | + +--- + +## Примеры использования + +### Создание записи лога при изменении статуса + +```php +$task = Task::findOne(123); +$oldStatus = $task->status; +$task->status = Task::STATUS_IN_WORK; +$task->save(); + +// Создаём запись в логе +$log = new TaskLogs(); +$log->name = 'Изменение статуса задачи'; +$log->task_id = $task->id; +$log->field_name = 'status'; +$log->value_before = (string)$oldStatus; +$log->value_after = (string)$task->status; +$log->created_at = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Логирование изменения исполнителя + +```php +$task = Task::findOne(123); +$oldExecutor = $task->updated_by; +$task->updated_by = 45; +$task->save(); + +$log = new TaskLogs(); +$log->name = 'Назначен новый исполнитель'; +$log->task_id = $task->id; +$log->field_name = 'updated_by'; +$log->value_before = (string)$oldExecutor; +$log->value_after = (string)$task->updated_by; +$log->created_at = date('Y-m-d H:i:s'); +$log->save(); +``` + +### Получение всех изменений задачи + +```php +$taskId = 123; +$logs = TaskLogs::find() + ->where(['task_id' => $taskId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + echo "{$log->created_at}: {$log->name} ({$log->field_name})\n"; + echo "До: {$log->value_before}\n"; + echo "После: {$log->value_after}\n\n"; +} +``` + +### Получение истории изменений конкретного поля + +```php +$taskId = 123; +$fieldName = 'status'; + +$logs = TaskLogs::find() + ->where(['task_id' => $taskId, 'field_name' => $fieldName]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); +``` + +### Проверка, было ли изменено поле + +```php +$wasChanged = TaskLogs::find() + ->where(['task_id' => 123, 'field_name' => 'deadline']) + ->exists(); +``` + +### Получение последнего изменения задачи + +```php +$lastLog = TaskLogs::find() + ->where(['task_id' => 123]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastLog) { + echo "Последнее изменение: {$lastLog->name} в {$lastLog->created_at}"; +} +``` + +### Получение количества изменений задачи + +```php +$changesCount = TaskLogs::find() + ->where(['task_id' => 123]) + ->count(); +``` + +### Получение изменений за определенный период + +```php +$logs = TaskLogs::find() + ->where(['task_id' => 123]) + ->andWhere(['>=', 'created_at', '2025-12-01 00:00:00']) + ->andWhere(['<=', 'created_at', '2025-12-31 23:59:59']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Универсальная функция логирования изменений + +```php +/** + * Создаёт запись лога об изменении поля задачи + * + * @param int $taskId ID задачи + * @param string $fieldName Название поля + * @param mixed $oldValue Старое значение + * @param mixed $newValue Новое значение + * @param string $description Описание изменения + * @return bool Успешность сохранения + */ +function logTaskChange($taskId, $fieldName, $oldValue, $newValue, $description) +{ + $log = new TaskLogs(); + $log->name = $description; + $log->task_id = $taskId; + $log->field_name = $fieldName; + $log->value_before = (string)$oldValue; + $log->value_after = (string)$newValue; + $log->created_at = date('Y-m-d H:i:s'); + + return $log->save(); +} + +// Использование +logTaskChange(123, 'prioritet', 5, 8, 'Повышение приоритета задачи'); +``` + +### Получение статистики изменений по полям + +```php +$taskId = 123; +$stats = TaskLogs::find() + ->select(['field_name', 'COUNT(*) as change_count']) + ->where(['task_id' => $taskId]) + ->groupBy('field_name') + ->asArray() + ->all(); + +// Результат: [ +// ['field_name' => 'status', 'change_count' => 5], +// ['field_name' => 'deadline', 'change_count' => 2], +// ] +``` + +--- + +## Паттерны использования + +### Автоматическое логирование в модели Task + +В модели Task можно переопределить метод `afterSave()` для автоматического логирования: + +```php +class Task extends ActiveRecord +{ + public function afterSave($insert, $changedAttributes) + { + parent::afterSave($insert, $changedAttributes); + + if (!$insert) { + foreach ($changedAttributes as $attribute => $oldValue) { + $log = new TaskLogs(); + $log->name = "Изменение поля {$attribute}"; + $log->task_id = $this->id; + $log->field_name = $attribute; + $log->value_before = (string)$oldValue; + $log->value_after = (string)$this->$attribute; + $log->created_at = date('Y-m-d H:i:s'); + $log->save(); + } + } + } +} +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Модель задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskMotivation.md b/erp24/docs/models/TaskMotivation.md new file mode 100644 index 00000000..82725558 --- /dev/null +++ b/erp24/docs/models/TaskMotivation.md @@ -0,0 +1,546 @@ +# Model: TaskMotivation + + +## Mindmap + +```mermaid +mindmap + root((TaskMotivation)) + Таблица БД + task_motivation + Свойства + id + int + name + string + posit + int + price + int + penalty + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника схем мотивации для задач. Определяет финансовые параметры вознаграждения и штрафов за выполнение задач. Используется в системе мотивации сотрудников для автоматического начисления премий и удержаний на основе результатов выполнения задач. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_motivation` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID схемы мотивации | +| `name` | string | Название схемы мотивации (макс. 250 символов) | +| `posit` | int | Позиция в списке для сортировки | +| `price` | int | Сумма вознаграждения за выполнение задачи (в копейках или минимальных единицах валюты) | +| `penalty` | int | Сумма штрафа за невыполнение или некачественное выполнение (в копейках или минимальных единицах валюты) | + +--- + +## Правила валидации + +### Обязательные поля + +Все поля модели являются обязательными для заполнения. + +```php +[['name', 'posit', 'price', 'penalty'], 'required'] +``` + +### Типы данных + +```php +// Целочисленные поля +[['posit', 'price', 'penalty'], 'integer'] + +// Ограничение длины строки +[['name'], 'string', 'max' => 250] +``` + +--- + +## Связи (Relations) + +### В модели Task + +```php +public function getMotivation() { + return $this->hasOne(TaskMotivation::class, ['id' => 'motivation_id']); +} +``` + +### В модели TaskTemplates + +```php +public function getMotivation() { + return $this->hasOne(TaskMotivation::class, ['id' => 'motivation_id']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_motivation` + +**Возвращает:** +- `string` - название таблицы `'task_motivation'` + +```php +public static function tableName() +{ + return 'task_motivation'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения всех полей +- Проверяет, что posit, price и penalty являются целыми числами +- Ограничивает максимальную длину name до 250 символов + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'posit', 'price', 'penalty'], 'required'], + [['posit', 'price', 'penalty'], 'integer'], + [['name'], 'string', 'max' => 250], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'posit' => 'Posit', + 'price' => 'Price', + 'penalty' => 'Penalty', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task }o--|| TaskMotivation : "motivation" + TaskTemplates }o--|| TaskMotivation : "motivation" + + TaskMotivation { + int id PK + string name + int posit + int price + int penalty + } + + Task { + int id PK + int motivation_id FK + string name + } + + TaskTemplates { + int id PK + int motivation_id FK + string name + } +``` + +--- + +## Примеры схем мотивации + +| ID | Название | Posit | Price (руб) | Penalty (руб) | Описание | +|----|----------|-------|-------------|---------------|----------| +| 0 | Без мотивации | 0 | 0 | 0 | Задачи без финансовой мотивации | +| 1 | Минимальная | 1 | 50 | 0 | Небольшая премия за выполнение | +| 2 | Стандартная | 2 | 100 | 50 | Стандартная схема с премией и штрафом | +| 3 | Повышенная | 3 | 200 | 100 | Для важных задач | +| 4 | Высокая | 4 | 500 | 250 | Для критически важных задач | +| 5 | Только штраф | 5 | 0 | 100 | Обязательные задачи без премии | + +--- + +## Формат хранения сумм + +Суммы хранятся в **копейках** (или минимальных единицах валюты): +- `price = 10000` означает 100.00 рублей +- `penalty = 5000` означает 50.00 рублей + +--- + +## Примеры использования + +### Создание новой схемы мотивации + +```php +$motivation = new TaskMotivation(); +$motivation->name = 'Еженедельная проверка'; +$motivation->posit = 3; +$motivation->price = 15000; // 150 рублей +$motivation->penalty = 7500; // 75 рублей +$motivation->save(); +``` + +### Получение всех схем мотивации + +```php +$motivations = TaskMotivation::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($motivations as $motivation) { + $priceRub = $motivation->price / 100; + $penaltyRub = $motivation->penalty / 100; + echo "{$motivation->name}: +{$priceRub}₽ / -{$penaltyRub}₽\n"; +} +``` + +### Получение схемы мотивации по ID + +```php +$motivation = TaskMotivation::findOne(2); +echo $motivation->name; // "Стандартная" +echo "Премия: " . ($motivation->price / 100) . " руб\n"; +echo "Штраф: " . ($motivation->penalty / 100) . " руб\n"; +``` + +### Создание задачи с мотивацией + +```php +$task = new Task(); +$task->name = 'Проверка витрины'; +$task->description = 'Проверить выкладку товаров'; +$task->motivation_id = 2; // Стандартная схема +$task->save(); +``` + +### Создание шаблона с мотивацией + +```php +$template = new TaskTemplates(); +$template->name = 'Ежедневная инвентаризация'; +$template->motivation_id = 3; // Повышенная схема +// ... остальные поля ... +$template->save(); +``` + +### Получение схем для выпадающего списка + +```php +$motivationList = TaskMotivation::find() + ->select(['id', 'name']) + ->orderBy(['posit' => SORT_ASC]) + ->indexBy('id') + ->column(); + +// Результат: [0 => 'Без мотивации', 1 => 'Минимальная', ...] +``` + +### Расчёт потенциального дохода по задачам + +```php +$tasks = Task::find() + ->where(['updated_by' => $adminId]) + ->andWhere(['status' => Task::STATUS_CLOSED]) + ->with('motivation') + ->all(); + +$totalEarnings = 0; +foreach ($tasks as $task) { + if ($task->motivation) { + $totalEarnings += $task->motivation->price; + } +} + +echo "Потенциальный доход: " . ($totalEarnings / 100) . " руб"; +``` + +### Статистика по схемам мотивации + +```php +$stats = TaskMotivation::find() + ->select([ + 'task_motivation.id', + 'task_motivation.name', + 'COUNT(task.id) as task_count', + 'SUM(task_motivation.price) as total_price' + ]) + ->leftJoin('task', 'task.motivation_id = task_motivation.id') + ->where(['task.status' => Task::STATUS_CLOSED]) + ->groupBy('task_motivation.id') + ->orderBy(['task_motivation.posit' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $totalRub = ($stat['total_price'] ?? 0) / 100; + echo "{$stat['name']}: {$stat['task_count']} задач, {$totalRub}₽\n"; +} +``` + +--- + +## Расчёт мотивации + +### Начисление премии за выполненную задачу + +```php +class MotivationService +{ + /** + * Начисляет премию за выполнение задачи + * + * @param Task $task Выполненная задача + * @return bool Успешность начисления + */ + public function applyReward($task) + { + if ($task->status != Task::STATUS_CLOSED) { + return false; + } + + $motivation = $task->motivation; + if (!$motivation || $motivation->price == 0) { + return false; + } + + // Начисляем премию исполнителю + $bonus = new UsersBonus(); + $bonus->admin_id = $task->updated_by; + $bonus->amount = $motivation->price; + $bonus->type = 'task_reward'; + $bonus->description = "Премия за выполнение задачи: {$task->name}"; + $bonus->task_id = $task->id; + $bonus->created_at = date('Y-m-d H:i:s'); + + return $bonus->save(); + } + + /** + * Начисляет штраф за невыполнение задачи + * + * @param Task $task Проваленная задача + * @return bool Успешность начисления + */ + public function applyPenalty($task) + { + $motivation = $task->motivation; + if (!$motivation || $motivation->penalty == 0) { + return false; + } + + // Начисляем штраф исполнителю + $penalty = new UsersBonus(); + $penalty->admin_id = $task->updated_by; + $penalty->amount = -$motivation->penalty; // Отрицательная сумма + $penalty->type = 'task_penalty'; + $penalty->description = "Штраф за невыполнение задачи: {$task->name}"; + $penalty->task_id = $task->id; + $penalty->created_at = date('Y-m-d H:i:s'); + + return $penalty->save(); + } +} +``` + +### Автоматическое начисление при закрытии задачи + +```php +class Task extends ActiveRecord +{ + public function afterSave($insert, $changedAttributes) + { + parent::afterSave($insert, $changedAttributes); + + // Если задача закрыта + if (!$insert && isset($changedAttributes['status']) && $this->status == self::STATUS_CLOSED) { + $motivationService = new MotivationService(); + + // Проверяем качество выполнения + if ($this->check_status_count == 0) { + // Задача выполнена с первого раза - премия + $motivationService->applyReward($this); + } elseif ($this->check_status_count > 2) { + // Задача возвращалась на доработку больше 2 раз - штраф + $motivationService->applyPenalty($this); + } + } + } +} +``` + +### Расчёт мотивации с учётом дедлайна + +```php +function calculateMotivationWithDeadline($task) +{ + $motivation = $task->motivation; + if (!$motivation) { + return 0; + } + + $basePrice = $motivation->price; + + // Если задача выполнена раньше дедлайна - бонус 50% + if ($task->closed_at && $task->deadline && strtotime($task->closed_at) < strtotime($task->deadline)) { + $basePrice *= 1.5; + } + + // Если задача выполнена после дедлайна - штраф + if ($task->closed_at && $task->deadline && strtotime($task->closed_at) > strtotime($task->deadline)) { + $basePrice = -$motivation->penalty; + } + + return $basePrice; +} +``` + +--- + +## Отчёты и аналитика + +### Отчёт по мотивации сотрудника + +```php +function getEmployeeMotivationReport($adminId, $dateFrom, $dateTo) +{ + $tasks = Task::find() + ->where(['updated_by' => $adminId]) + ->andWhere(['status' => Task::STATUS_CLOSED]) + ->andWhere(['>=', 'closed_at', $dateFrom]) + ->andWhere(['<=', 'closed_at', $dateTo]) + ->with('motivation') + ->all(); + + $totalReward = 0; + $totalPenalty = 0; + $taskCount = 0; + + foreach ($tasks as $task) { + if (!$task->motivation) { + continue; + } + + $taskCount++; + if ($task->check_status_count == 0) { + $totalReward += $task->motivation->price; + } else { + $totalPenalty += $task->motivation->penalty; + } + } + + return [ + 'tasks_completed' => $taskCount, + 'total_reward' => $totalReward / 100, + 'total_penalty' => $totalPenalty / 100, + 'net_amount' => ($totalReward - $totalPenalty) / 100, + ]; +} + +$report = getEmployeeMotivationReport(45, '2025-12-01', '2025-12-31'); +echo "Выполнено задач: {$report['tasks_completed']}\n"; +echo "Премии: {$report['total_reward']}₽\n"; +echo "Штрафы: {$report['total_penalty']}₽\n"; +echo "Итого: {$report['net_amount']}₽\n"; +``` + +--- + +## Использование в представлениях + +### Отображение информации о мотивации + +```php +$task = Task::findOne(123); +$motivation = $task->motivation; + +if ($motivation) { + $priceRub = $motivation->price / 100; + $penaltyRub = $motivation->penalty / 100; + + echo Html::tag('div', "Премия: +{$priceRub}₽", ['class' => 'text-success']); + echo Html::tag('div', "Штраф: -{$penaltyRub}₽", ['class' => 'text-danger']); +} +``` + +### Badge с суммой мотивации + +```php +function getMotivationBadge($motivationId) +{ + $motivation = TaskMotivation::findOne($motivationId); + if (!$motivation || $motivation->price == 0) { + return ''; + } + + $priceRub = $motivation->price / 100; + return Html::tag('span', "+{$priceRub}₽", [ + 'class' => 'badge badge-success' + ]); +} + +echo getMotivationBadge($task->motivation_id); +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач +- [UsersBonus](UsersBonus.md) - Бонусы сотрудников + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskMotivationSearch.md b/erp24/docs/models/TaskMotivationSearch.md new file mode 100644 index 00000000..a0755046 --- /dev/null +++ b/erp24/docs/models/TaskMotivationSearch.md @@ -0,0 +1,179 @@ +# Класс: TaskMotivationSearch + + +## Mindmap + +```mermaid +mindmap + root((TaskMotivationSearch)) + Таблица БД + ActiveRecord + Наследование + extends TaskMotivation +``` + +## Назначение +Search-модель для поиска и фильтрации мотиваций по задачам в ERP24. Справочник типов вознаграждений с позицией сортировки и ценой. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`TaskMotivation` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `posit`, `price` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска типов мотивации. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос TaskMotivation::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, posit, price + - like: name + +## Диаграмма структуры + +```mermaid +erDiagram + TaskMotivation { + int id PK + varchar name + int posit + int price + } + + Task { + int id PK + int motivation_id FK + varchar name + } + + Task }o--|| TaskMotivation : "motivation_id" +``` + +## Диаграмма типов мотивации + +```mermaid +flowchart TD + A[TaskMotivation] --> B[Типы] + + B --> C[Денежная премия] + C --> C1[price = 500] + + B --> D[Бонусные баллы] + D --> D1[price = 100] + + B --> E[Выходной день] + E --> E1[price = 0] + + B --> F[Благодарность] + F --> F1[price = 0] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new TaskMotivationSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new TaskMotivationSearch(); +$dataProvider = $searchModel->search([ + 'TaskMotivationSearch' => [ + 'name' => 'премия', + ] +]); +``` + +### Поиск по цене +```php +$searchModel = new TaskMotivationSearch(); +$dataProvider = $searchModel->search([ + 'TaskMotivationSearch' => [ + 'price' => 500, + ] +]); +``` + +### Поиск бесплатных мотиваций +```php +$searchModel = new TaskMotivationSearch(); +$dataProvider = $searchModel->search([ + 'TaskMotivationSearch' => [ + 'price' => 0, + ] +]); +``` + +### Поиск по позиции +```php +$searchModel = new TaskMotivationSearch(); +$dataProvider = $searchModel->search([ + 'TaskMotivationSearch' => [ + 'posit' => 1, // Первая в списке + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'posit', + 'name', + [ + 'attribute' => 'price', + 'format' => 'currency', + ], + ], +]) ?> +``` + +## Связанные модели + +- [TaskMotivation](./TaskMotivation.md) — базовая модель мотиваций +- [Task](./Task.md) — задачи + +## Особенности реализации + +1. **Справочник мотиваций**: Типы вознаграждений за задачи +2. **Позиционирование**: posit для сортировки в интерфейсе +3. **Цена**: price для денежных вознаграждений +4. **Простая структура**: 4 поля, минимальная логика +5. **like вместо ilike**: Регистрозависимый поиск по названию diff --git a/erp24/docs/models/TaskReceiverType.md b/erp24/docs/models/TaskReceiverType.md new file mode 100644 index 00000000..9f0207af --- /dev/null +++ b/erp24/docs/models/TaskReceiverType.md @@ -0,0 +1,457 @@ +# Model: TaskReceiverType + + +## Mindmap + +```mermaid +mindmap + root((TaskReceiverType)) + Таблица БД + task_receiver_type + Свойства + id + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника типов получателей задач. Определяет категории получателей (исполнителей, создателей, проверяющих) в шаблонах задач. Используется для динамического определения ролей в задачах на основе должностей, функций или других критериев вместо жёсткого назначения конкретных пользователей. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_receiver_type` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID типа получателя | +| `name` | string | Название типа получателя для отображения пользователю (макс. 100 символов) | +| `alias` | string | Название типа для внутреннего использования в коде (макс. 100 символов) | + +--- + +## Правила валидации + +### Обязательные поля + +Оба поля `name` и `alias` являются обязательными. + +```php +[['name', 'alias'], 'required'] +``` + +### Типы данных и ограничения + +```php +// Ограничения длины строк +[['name', 'alias'], 'string', 'max' => 100] + +// Уникальность alias +[['alias'], 'unique'] +``` + +--- + +## Связи (Relations) + +### В модели TaskTemplates + +```php +// Для создателя задачи +public function getCreatorEntity() { + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'creator_entity']); +} + +// Для получателя (исполнителя) задачи +public function getRecipientEntity() { + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'recipient_entity']); +} + +// Для проверяющего задачу +public function getControllerEntity() { + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'controller_entity']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_receiver_type` + +**Возвращает:** +- `string` - название таблицы `'task_receiver_type'` + +```php +public static function tableName() +{ + return 'task_receiver_type'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения name и alias +- Ограничивает максимальную длину строковых полей до 100 символов +- Обеспечивает уникальность поля alias для предотвращения конфликтов в коде + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'alias'], 'required'], + [['name', 'alias'], 'string', 'max' => 100], + [['alias'], 'unique'], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'alias' => 'Alias', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + TaskTemplates }o--|| TaskReceiverType : "creatorEntity" + TaskTemplates }o--|| TaskReceiverType : "recipientEntity" + TaskTemplates }o--|| TaskReceiverType : "controllerEntity" + + TaskReceiverType { + int id PK + string name + string alias UK + } + + TaskTemplates { + int id PK + int creator_entity FK + int creator_id + int recipient_entity FK + int recipient_id + int controller_entity FK + int controller_id + } +``` + +--- + +## Примеры типов получателей + +| ID | Name (Название) | Alias (Внутреннее имя) | Описание | +|----|-----------------|------------------------|----------| +| -1 | Конкретный пользователь | specific_user | Задача назначена конкретному сотруднику | +| 1 | Руководитель магазина | store_manager | Руководитель магазина связанного с задачей | +| 2 | Менеджер склада | warehouse_manager | Менеджер склада | +| 3 | Сотрудник на смене | shift_employee | Сотрудник работающий в смену | +| 4 | Наставник | mentor | Наставник сотрудника | +| 5 | Региональный менеджер | regional_manager | Менеджер региона | +| 6 | HR-специалист | hr_specialist | Специалист отдела кадров | +| 7 | Создатель задачи | task_creator | Тот, кто создал задачу | + +--- + +## Использование в шаблонах задач + +### Схема работы + +1. В шаблоне задачи указывается `recipient_entity` (тип получателя) +2. Если `recipient_entity = -1`, используется конкретный пользователь из `recipient_id` +3. Если `recipient_entity > 0`, система динамически определяет исполнителя на основе контекста задачи + +--- + +## Примеры использования + +### Создание нового типа получателя + +```php +$receiverType = new TaskReceiverType(); +$receiverType->name = 'Администратор сети'; +$receiverType->alias = 'network_admin'; +$receiverType->save(); +``` + +### Получение всех типов получателей + +```php +$receiverTypes = TaskReceiverType::find()->all(); + +foreach ($receiverTypes as $type) { + echo "{$type->name} ({$type->alias})\n"; +} +``` + +### Поиск типа получателя по alias + +```php +$receiverType = TaskReceiverType::find() + ->where(['alias' => 'store_manager']) + ->one(); + +if ($receiverType) { + echo "Найден тип: {$receiverType->name}"; +} +``` + +### Создание шаблона с динамическим получателем + +```php +$template = new TaskTemplates(); +$template->name = 'Проверка витрины'; +$template->description = 'Проверить выкладку товаров'; + +// Создатель - конкретный пользователь (руководитель) +$template->creator_entity = -1; +$template->creator_id = 10; + +// Получатель - руководитель магазина (определяется динамически) +$template->recipient_entity = 1; // store_manager +$template->recipient_id = 0; + +// Проверяющий - также создатель +$template->controller_entity = 7; // task_creator +$template->controller_id = 0; + +$template->save(); +``` + +### Получение шаблонов с определённым типом получателя + +```php +$templates = TaskTemplates::find() + ->where(['recipient_entity' => 1]) // store_manager + ->all(); + +foreach ($templates as $template) { + echo "Шаблон: {$template->name}\n"; +} +``` + +### Получение типов для выпадающего списка + +```php +$receiverList = TaskReceiverType::find() + ->select(['id', 'name']) + ->indexBy('id') + ->column(); + +// Добавляем специальную опцию для конкретного пользователя +$receiverList[-1] = 'Конкретный пользователь'; + +// Результат: [-1 => 'Конкретный пользователь', 1 => 'Руководитель магазина', ...] +``` + +### Логика определения получателя при создании задачи + +```php +class TaskService +{ + /** + * Определяет ID исполнителя на основе типа получателя + * + * @param TaskTemplates $template Шаблон задачи + * @param string|null $entityId ID связанной сущности + * @return int|null ID исполнителя + */ + public function resolveRecipient($template, $entityId = null) + { + // Если указан конкретный пользователь + if ($template->recipient_entity == -1) { + return $template->recipient_id; + } + + $receiverType = $template->recipientEntity; + + switch ($receiverType->alias) { + case 'store_manager': + // Находим руководителя магазина + $store = Products1c::findOne($entityId); + return $store ? $store->manager_id : null; + + case 'warehouse_manager': + // Находим менеджера склада + $warehouse = Warehouse::findOne($entityId); + return $warehouse ? $warehouse->manager_id : null; + + case 'shift_employee': + // Находим сотрудника на смене + return $this->getCurrentShiftEmployee($entityId); + + case 'mentor': + // Находим наставника + $employee = Admin::findOne($entityId); + return $employee ? $employee->mentor_id : null; + + case 'task_creator': + // Создатель задачи + return Yii::$app->user->id; + + default: + return null; + } + } +} +``` + +### Статистика использования типов получателей + +```php +$stats = TaskReceiverType::find() + ->select([ + 'task_receiver_type.id', + 'task_receiver_type.name', + 'COUNT(task_templates.id) as template_count' + ]) + ->leftJoin('task_templates', 'task_templates.recipient_entity = task_receiver_type.id') + ->groupBy('task_receiver_type.id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['template_count']} шаблонов\n"; +} +``` + +### Проверка уникальности alias + +```php +function isAliasUnique($alias) { + return !TaskReceiverType::find() + ->where(['alias' => $alias]) + ->exists(); +} + +if (isAliasUnique('new_type')) { + $type = new TaskReceiverType(); + $type->name = 'Новый тип'; + $type->alias = 'new_type'; + $type->save(); +} +``` + +### Обновление названия типа + +```php +$receiverType = TaskReceiverType::findOne(1); +$receiverType->name = 'Управляющий магазином'; +$receiverType->save(); +``` + +--- + +## Преимущества использования типов получателей + +### Гибкость + +Позволяет создавать универсальные шаблоны задач, которые автоматически адаптируются к контексту: + +```php +// Один шаблон для всех магазинов +$template = new TaskTemplates(); +$template->name = 'Утренняя проверка'; +$template->recipient_entity = 1; // store_manager + +// При создании задачи для магазина A - получит руководитель A +// При создании задачи для магазина B - получит руководитель B +``` + +### Масштабируемость + +Легко добавлять новые типы получателей без изменения существующих шаблонов: + +```php +$newType = new TaskReceiverType(); +$newType->name = 'Старший смены'; +$newType->alias = 'shift_supervisor'; +$newType->save(); +``` + +### Централизованное управление + +Изменение логики определения получателя в одном месте (TaskService) влияет на все шаблоны: + +```php +// Изменили логику поиска руководителя магазина +// Все задачи с типом 'store_manager' автоматически используют новую логику +``` + +--- + +## Валидация уникальности alias + +```php +$receiverType = new TaskReceiverType(); +$receiverType->name = 'Новый тип'; +$receiverType->alias = 'store_manager'; // Уже существует + +if (!$receiverType->save()) { + print_r($receiverType->errors); + // Output: ['alias' => ['Alias "store_manager" has already been taken.']] +} +``` + +--- + +## Связанные модели + +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач +- [Admin](Admin.md) - Пользователи системы + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskSearch.md b/erp24/docs/models/TaskSearch.md new file mode 100644 index 00000000..e15d9c5b --- /dev/null +++ b/erp24/docs/models/TaskSearch.md @@ -0,0 +1,228 @@ +# Класс: TaskSearch + + +## Mindmap + +```mermaid +mindmap + root((TaskSearch)) + Таблица БД + ActiveRecord + Наследование + extends Task +``` + +## Назначение +Search-модель для поиска и фильтрации задач в ERP24. Модель с пагинацией 30 записей и двойной сортировкой по умолчанию (дата создания DESC, статус ASC). + +## Пространство имён +`yii_app\records` + +## Родительский класс +`Task` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `name`, `description`, `id` — string (нестандартно для id) +- `status`, `company_function_id` — integer +- `created_by` — safe + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с пагинацией и двойной сортировкой. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос Task::find() +2. Оборачивает в ActiveDataProvider: + - pageSize: 30 + - defaultOrder: created_at DESC, status ASC +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: task_type_id, created_by, status, company_function_id, id + - like: name, description + +**Использование алиаса:** +- Все поля в фильтрах используют префикс `task.` для избежания конфликтов при JOIN + +## Диаграмма связей + +```mermaid +erDiagram + Task { + int id PK + varchar name + text description + int status + int task_type_id FK + int created_by FK + int company_function_id FK + datetime created_at + } + + TasksType { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + CompanyFunction { + int id PK + varchar name + } + + Task }o--|| TasksType : "task_type_id" + Task }o--|| Admin : "created_by" + Task }o--|| CompanyFunction : "company_function_id" +``` + +## Диаграмма сортировки + +```mermaid +flowchart TD + A[TaskSearch] --> B[Сортировка по умолчанию] + B --> C[1. created_at DESC] + C --> D[Новые задачи сверху] + + B --> E[2. status ASC] + E --> F[Открытые задачи приоритетнее] + + G[Пагинация] --> H[pageSize = 30] +``` + +## Диаграмма статусов задач + +```mermaid +flowchart LR + A[status] --> B{Значение} + + B --> C[0 - Новая] + B --> D[1 - В работе] + B --> E[2 - На проверке] + B --> F[3 - Завершена] + B --> G[4 - Отменена] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new TaskSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new TaskSearch(); +$dataProvider = $searchModel->search([ + 'TaskSearch' => [ + 'name' => 'Инвентаризация', + ] +]); +``` + +### Поиск по статусу +```php +$searchModel = new TaskSearch(); +$dataProvider = $searchModel->search([ + 'TaskSearch' => [ + 'status' => 1, // В работе + ] +]); +``` + +### Поиск задач сотрудника +```php +$searchModel = new TaskSearch(); +$dataProvider = $searchModel->search([ + 'TaskSearch' => [ + 'created_by' => Yii::$app->user->id, + ] +]); +``` + +### Поиск по функции компании +```php +$searchModel = new TaskSearch(); +$dataProvider = $searchModel->search([ + 'TaskSearch' => [ + 'company_function_id' => 5, // Логистика + ] +]); +``` + +### Поиск в описании +```php +$searchModel = new TaskSearch(); +$dataProvider = $searchModel->search([ + 'TaskSearch' => [ + 'description' => 'срочно', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'attribute' => 'status', + 'value' => function($model) { + return $model->getStatusLabel(); + }, + 'filter' => Task::getStatusList(), + ], + [ + 'attribute' => 'created_by', + 'value' => 'creator.name', + ], + [ + 'attribute' => 'company_function_id', + 'value' => 'companyFunction.name', + ], + 'created_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [Task](./Task.md) — базовая модель задач +- [TasksType](./TasksType.md) — типы задач +- [Admin](./Admin.md) — создатель задачи +- [CompanyFunction](./CompanyFunction.md) — функция компании +- [TaskMotivation](./TaskMotivation.md) — мотивация за задачу + +## Особенности реализации + +1. **Двойная сортировка**: created_at DESC + status ASC +2. **Пагинация**: 30 записей на странице +3. **Алиас таблицы**: task.* для всех полей в фильтрах +4. **id как string**: Нестандартное правило валидации +5. **like вместо ilike**: Регистрозависимый поиск +6. **Нет scenarios()**: Не переопределяет сценарии (в отличие от других Search-моделей) diff --git a/erp24/docs/models/TaskStatus.md b/erp24/docs/models/TaskStatus.md new file mode 100644 index 00000000..c633df94 --- /dev/null +++ b/erp24/docs/models/TaskStatus.md @@ -0,0 +1,482 @@ +# Model: TaskStatus + + +## Mindmap + +```mermaid +mindmap + root((TaskStatus)) + Таблица БД + task_status + Свойства + id + int + name + string + posit + int + config + string + bgcolor + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника статусов задач. Определяет возможные состояния задач в системе с визуальным представлением (цвет фона) и конфигурацией. Используется для отслеживания жизненного цикла задачи от создания до закрытия. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_status` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID статуса | +| `name` | string | Название статуса (макс. 200 символов) | +| `posit` | int | Позиция в списке для сортировки | +| `config` | string | JSON конфигурация с дополнительными параметрами статуса (TEXT) | +| `bgcolor` | string | Цвет фона для визуального отображения в HEX формате (макс. 20 символов) | + +--- + +## Правила валидации + +### Обязательные поля + +Поля `name`, `config` и `bgcolor` являются обязательными. + +```php +[['name', 'config', 'bgcolor'], 'required'] +``` + +### Типы данных + +```php +// Целочисленное поле +[['posit'], 'integer'] + +// Текстовое поле для конфигурации +[['config'], 'string'] + +// Ограничения длины строк +[['name'], 'string', 'max' => 200] +[['bgcolor'], 'string', 'max' => 20] +``` + +--- + +## Связи (Relations) + +### В модели Task + +```php +public function getStatusEntity() { + return $this->hasOne(TaskStatus::class, ['id' => 'status']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_status` + +**Возвращает:** +- `string` - название таблицы `'task_status'` + +```php +public static function tableName() +{ + return 'task_status'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения name, config и bgcolor +- Проверяет тип данных для posit (integer) +- Валидирует формат текстового поля config +- Устанавливает ограничения длины для строковых полей + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'config', 'bgcolor'], 'required'], + [['posit'], 'integer'], + [['config'], 'string'], + [['name'], 'string', 'max' => 200], + [['bgcolor'], 'string', 'max' => 20], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'posit' => 'Posit', + 'config' => 'Config', + 'bgcolor' => 'Bgcolor', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task }o--|| TaskStatus : "statusEntity" + + TaskStatus { + int id PK + string name + int posit + string config + string bgcolor + } + + Task { + int id PK + int status FK + string name + } +``` + +--- + +## Стандартные статусы + +Соответствие ID статусов из таблицы и констант в модели Task: + +| ID | Константа в Task | Название | Цвет | Описание | +|----|------------------|----------|------|----------| +| -1 | STATUS_DRAFT | Черновик | #e0e0e0 | Задача в черновике, не опубликована | +| 1 | STATUS_NEW | Новая | #bbdefb | Задача создана и назначена | +| 2 | STATUS_READ | Прочитана | #90caf9 | Исполнитель просмотрел задачу | +| 3 | STATUS_TAKEN | Взята в работу | #64b5f6 | Исполнитель принял задачу | +| 4 | STATUS_IN_WORK | В процессе | #42a5f5 | Задача выполняется | +| 5 | STATUS_CHECK_PROOFS | На проверке | #ffb74d | Результат отправлен на проверку | +| 6 | STATUS_CLOSED | Закрыта | #81c784 | Задача выполнена и закрыта | + +--- + +## Формат config + +Поле `config` может содержать дополнительную конфигурацию для статуса: + +```json +{ + "is_final": false, + "allows_edit": true, + "requires_comment": false, + "next_statuses": [2, 3], + "notification_type": "email", + "auto_close_subtasks": false, + "time_tracking": true +} +``` + +### Параметры конфигурации + +| Параметр | Тип | Описание | +|----------|-----|----------| +| `is_final` | boolean | Является ли статус финальным (закрывающим) | +| `allows_edit` | boolean | Можно ли редактировать задачу в этом статусе | +| `requires_comment` | boolean | Требуется ли комментарий при переходе в статус | +| `next_statuses` | array | Список ID доступных следующих статусов | +| `notification_type` | string | Тип уведомления при переходе в статус | +| `auto_close_subtasks` | boolean | Автоматически закрывать подзадачи | +| `time_tracking` | boolean | Учитывать время в этом статусе | + +--- + +## Примеры использования + +### Создание нового статуса + +```php +$status = new TaskStatus(); +$status->name = 'Приостановлена'; +$status->posit = 7; +$status->bgcolor = '#ff9800'; +$status->config = json_encode([ + 'is_final' => false, + 'allows_edit' => true, + 'requires_comment' => true, + 'next_statuses' => [4, 6], + 'notification_type' => 'telegram' +]); +$status->save(); +``` + +### Получение всех статусов + +```php +$statuses = TaskStatus::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statuses as $status) { + echo "{$status->name} ({$status->bgcolor})\n"; +} +``` + +### Получение статуса по ID + +```php +$status = TaskStatus::findOne(4); +echo $status->name; // "В процессе" +``` + +### Получение конфигурации статуса + +```php +$status = TaskStatus::findOne(5); +$config = json_decode($status->config, true); + +if ($config['requires_comment']) { + echo "При переходе в статус '{$status->name}' требуется комментарий"; +} +``` + +### Проверка возможных переходов статуса + +```php +$currentStatus = TaskStatus::findOne(3); +$config = json_decode($currentStatus->config, true); + +$nextStatuses = TaskStatus::find() + ->where(['id' => $config['next_statuses']]) + ->all(); + +echo "Доступные статусы для перехода:\n"; +foreach ($nextStatuses as $status) { + echo "- {$status->name}\n"; +} +``` + +### Получение задач в определённом статусе + +```php +$statusId = 4; +$tasksInWork = Task::find() + ->where(['status' => $statusId]) + ->all(); +``` + +### Получение статусов для выпадающего списка + +```php +$statusList = TaskStatus::find() + ->select(['id', 'name']) + ->orderBy(['posit' => SORT_ASC]) + ->indexBy('id') + ->column(); + +// Результат: [1 => 'Новая', 2 => 'Прочитана', ...] +``` + +### Статистика задач по статусам + +```php +$stats = TaskStatus::find() + ->select(['task_status.id', 'task_status.name', 'COUNT(task.id) as task_count']) + ->leftJoin('task', 'task.status = task_status.id') + ->groupBy('task_status.id') + ->orderBy(['task_status.posit' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['task_count']} задач\n"; +} +``` + +### Обновление конфигурации статуса + +```php +$status = TaskStatus::findOne(5); +$config = json_decode($status->config, true); +$config['notification_type'] = 'push'; +$config['auto_close_subtasks'] = true; +$status->config = json_encode($config); +$status->save(); +``` + +### Проверка финального статуса + +```php +function isFinalStatus($statusId) { + $status = TaskStatus::findOne($statusId); + if (!$status) { + return false; + } + + $config = json_decode($status->config, true); + return isset($config['is_final']) && $config['is_final']; +} + +if (isFinalStatus(6)) { + echo "Статус является финальным"; +} +``` + +### Изменение порядка статусов + +```php +// Переместить статус на позицию выше +$status = TaskStatus::findOne(5); +$currentPosition = $status->posit; + +TaskStatus::updateAll( + ['posit' => new \yii\db\Expression('posit + 1')], + ['and', ['>=', 'posit', $currentPosition - 1], ['<', 'posit', $currentPosition]] +); + +$status->posit = $currentPosition - 1; +$status->save(); +``` + +--- + +## Использование в представлениях + +### Отображение статуса с цветом + +```php +$task = Task::findOne(123); +$status = $task->statusEntity; + +echo Html::tag('span', $status->name, [ + 'class' => 'badge', + 'style' => "background-color: {$status->bgcolor}; color: white;" +]); +``` + +### Индикатор прогресса на основе статуса + +```php +function getStatusProgress($statusId) { + $progressMap = [ + -1 => 0, // Черновик + 1 => 10, // Новая + 2 => 25, // Прочитана + 3 => 40, // Взята + 4 => 60, // В работе + 5 => 85, // На проверке + 6 => 100, // Закрыта + ]; + + return $progressMap[$statusId] ?? 0; +} + +$task = Task::findOne(123); +$progress = getStatusProgress($task->status); + +echo Html::tag('div', '', [ + 'class' => 'progress-bar', + 'style' => "width: {$progress}%" +]); +``` + +### Dropdown для смены статуса + +```php +$task = Task::findOne(123); +$currentStatus = $task->statusEntity; +$config = json_decode($currentStatus->config, true); + +$availableStatuses = TaskStatus::find() + ->where(['id' => $config['next_statuses']]) + ->all(); + +echo Html::beginForm(['task/change-status', 'id' => $task->id]); +echo Html::dropDownList('new_status', null, + ArrayHelper::map($availableStatuses, 'id', 'name'), + ['class' => 'form-control'] +); +echo Html::submitButton('Изменить статус', ['class' => 'btn btn-primary']); +echo Html::endForm(); +``` + +--- + +## Жизненный цикл задачи + +```mermaid +stateDiagram-v2 + [*] --> Draft: Создание + Draft --> New: Публикация + New --> Read: Просмотр + Read --> Taken: Принятие + Taken --> InWork: Начало работы + InWork --> CheckProofs: Отправка на проверку + CheckProofs --> InWork: Доработка + CheckProofs --> Closed: Принятие + InWork --> Closed: Закрытие без проверки + Closed --> [*] + + state Draft as "Черновик (-1)" + state New as "Новая (1)" + state Read as "Прочитана (2)" + state Taken as "Взята (3)" + state InWork as "В работе (4)" + state CheckProofs as "На проверке (5)" + state Closed as "Закрыта (6)" +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskTemplates.md b/erp24/docs/models/TaskTemplates.md new file mode 100644 index 00000000..56a7aba0 --- /dev/null +++ b/erp24/docs/models/TaskTemplates.md @@ -0,0 +1,883 @@ +# Model: TaskTemplates + + +## Mindmap + +```mermaid +mindmap + root((TaskTemplates)) + Таблица БД + task_templates + Свойства + id + int + name + string + description + string + task_type_id + int + children_order_type + string + posit + int + Связи + TimeTriggers + 1:N TaskTriggerTimeConditions + EventTriggers + 1:N TaskTriggerConditions + TaskType + 1:1 TasksType + CreatorEntity + 1:1 TemplatesReceiverType + Creator + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель шаблонов задач. Реализует систему шаблонирования для автоматического создания задач по заданным правилам. Шаблоны определяют структуру задачи, её параметры, исполнителей, проверяющих и условия создания. Поддерживает иерархию шаблонов и автоматическое создание задач по триггерам. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_templates` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID шаблона задачи | +| `name` | string | Название задачи (макс. 255 символов) | +| `description` | string | Описание задачи (TEXT) | +| `task_type_id` | int | ID типа задачи из таблицы tasks_type | +| `parent_id` | int\|null | ID родительского шаблона для создания иерархии | +| `children_order_type` | string | Порядок прикреплённых шаблонов: последовательный/параллельный (ENUM/TEXT) | +| `posit` | int | Позиция шаблона в последовательном порядке | +| `creator_entity` | int | ID сущности создателя задачи | +| `creator_id` | int | ID конкретного создателя (если creator_entity = -1) | +| `recipient_entity` | int | ID сущности получателя (исполнителя) | +| `recipient_id` | int | ID конкретного получателя (если recipient_entity = -1) | +| `controller_entity` | int | ID сущности проверяющего | +| `controller_id` | int | ID конкретного проверяющего (если controller_entity = -1) | +| `company_function_id` | int | ID функции компании из таблицы company_functions | +| `entity_type` | int | Тип сущности к которой привязана задача | +| `entity_id` | string\|null | ID сущности (GUID, макс. 36 символов) | +| `duration` | string | Время на выполнение задачи в формате interval (TEXT) | +| `expected_time` | string | Чистое время выполнения задачи в теории (TEXT) | +| `deadline_count` | string | Дедлайн от времени создания задачи (TEXT) | +| `deadline_permission` | int | Разрешён ли перенос крайнего срока задачи (0/1) | +| `alert_level_id` | int\|null | ID уровня важности из таблицы task_alert_level | +| `motivation_id` | int | ID мотивации из таблицы task_motivation | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + [ + 'name', 'description', 'task_type_id', 'creator_entity', + 'recipient_entity', 'controller_entity', 'company_function_id', + 'entity_type', 'duration', 'deadline_count', 'children_order_type' + ], + 'required' +] +``` + +### Типы данных + +```php +// Текстовые поля +[['description', 'duration', 'expected_time', 'deadline_count', 'children_order_type'], 'string'] + +// Целочисленные поля +[ + [ + 'task_type_id', 'creator_entity', 'creator_id', 'recipient_entity', + 'recipient_id', 'controller_entity', 'controller_id', + 'company_function_id', 'entity_type', 'deadline_permission', + 'alert_level_id', 'motivation_id', 'posit' + ], + 'integer' +] + +// Ограничения длины строк +[['name'], 'string', 'max' => 255] +[['entity_id'], 'string', 'max' => 36] + +// Дополнительные поля +['parent_id', 'safe'] +``` + +--- + +## Связи (Relations) + +### hasMany + +| Метод | Связанная модель | Описание | +|-------|------------------|----------| +| `getTimeTriggers()` | TaskTriggerTimeConditions | Временные триггеры создания задач | +| `getEventTriggers()` | TaskTriggerConditions | Событийные триггеры с условием block=1 | +| `getChildren()` | TaskTemplates | Дочерние шаблоны, отсортированные по posit | +| `getOpenTasks()` | Task | Открытые задачи созданные по шаблону (status < 6 и > 0) | + +### hasOne + +| Метод | Связанная модель | Описание | +|-------|------------------|----------| +| `getTaskType()` | TasksType | Тип задачи | +| `getCreatorEntity()` | TemplatesReceiverType | Тип сущности создателя | +| `getCreator()` | Admin | Конкретный создатель | +| `getRecipientEntity()` | TemplatesReceiverType | Тип сущности получателя | +| `getRecipient()` | Admin | Конкретный получатель | +| `getControllerEntity()` | TemplatesReceiverType | Тип сущности проверяющего | +| `getController()` | Admin | Конкретный проверяющий | +| `getCompanyFunction()` | CompanyFunctions | Функция компании | +| `getTaskEntity()` | TaskEntity | Тип сущности задачи | +| `getStore()` | Products1c | Связанный магазин (если entity_type указывает на магазин) | +| `getLesson()` | Lessons | Связанный урок (если entity_type указывает на урок) | +| `getAlertLevel()` | TaskAlertLevel | Уровень важности | +| `getMotivation()` | TaskMotivation | Мотивация по задаче | +| `getParent()` | TaskTemplates | Родительский шаблон | + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_templates` + +**Возвращает:** +- `string` - название таблицы `'task_templates'` + +```php +public static function tableName() +{ + return 'task_templates'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения ключевых полей шаблона +- Проверяет типы данных для всех атрибутов +- Устанавливает ограничения длины для строковых полей +- Добавляет безопасное правило для parent_id + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'description', 'task_type_id', 'creator_entity', 'recipient_entity', 'controller_entity', 'company_function_id', 'entity_type', 'duration', 'deadline_count', 'children_order_type'], 'required'], + [['description', 'duration', 'expected_time', 'deadline_count', 'children_order_type'], 'string'], + [['task_type_id', 'creator_entity', 'creator_id', 'recipient_entity', 'recipient_id', 'controller_entity', 'controller_id', 'company_function_id', 'entity_type', 'deadline_permission', 'alert_level_id', 'motivation_id', 'posit'], 'integer'], + [['name'], 'string', 'max' => 255], + [['entity_id'], 'string', 'max' => 36], + ['parent_id', 'safe'], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов на русском и английском языках +- Используется в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID шаблона задачи', + 'name' => 'Название задачи', + 'description' => 'Описание задачи', + 'task_type_id' => 'Тип задачи', + 'parent_id' => 'Parent ID', + 'children_order_type' => 'Children Order Type', + 'posit' => 'Posit', + 'creator_entity' => 'Creator Entity', + 'creator_id' => 'Creator Id', + 'recipient_entity' => 'Recipient Entity', + 'recipient_id' => 'Recipient Id', + 'controller_entity' => 'Controller Entity', + 'controller_id' => 'Controller Id', + 'company_function_id' => 'Company Function ID', + 'entity_id' => 'Entity ID', + 'entity_type' => 'Entity Type', + 'duration' => 'Duration', + 'expected_time' => 'Expected Time', + 'deadline_count' => 'Deadline Count', + 'deadline_permission' => 'Deadline Permission', + 'alert_level_id' => 'Alert Level ID', + 'motivation_id' => 'Motivation ID', + ]; +} +``` + +### getTimeTriggers(): ActiveQuery + +Получает все временные триггеры для шаблона. + +**Логика:** +- Создаёт связь hasMany с моделью TaskTriggerTimeConditions +- Связывает по полю task_template_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения временных триггеров + +**Вызовы сторонних методов:** +- `hasMany()` - метод Yii2 для создания связи один-ко-многим + +```php +public function getTimeTriggers() +{ + return $this->hasMany(TaskTriggerTimeConditions::class, ['task_template_id' => 'id']); +} +``` + +### getEventTriggers(): ActiveQuery + +Получает все событийные триггеры для шаблона с условием block=1. + +**Логика:** +- Создаёт связь hasMany с моделью TaskTriggerConditions +- Связывает по полю task_template_id +- Добавляет фильтр andWhere для выборки только блоков (block = 1) + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения событийных триггеров-блоков + +**Вызовы сторонних методов:** +- `hasMany()` - метод Yii2 для создания связи один-ко-многим +- `andWhere()` - метод Yii2 для добавления условия WHERE + +```php +public function getEventTriggers() +{ + return $this->hasMany(TaskTriggerConditions::class, ['task_template_id' => 'id']) + ->andWhere(['block' => '1']); +} +``` + +### getTaskType(): ActiveQuery + +Получает тип задачи для шаблона. + +**Логика:** +- Создаёт связь hasOne с моделью TasksType +- Связывает по полю task_type_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения типа задачи + +**Вызовы сторонних методов:** +- `hasOne()` - метод Yii2 для создания связи один-к-одному + +```php +public function getTaskType() +{ + return $this->hasOne(TasksType::class, ['id' => 'task_type_id']); +} +``` + +### getCreatorEntity(): ActiveQuery + +Получает тип сущности создателя. + +**Логика:** +- Создаёт связь hasOne с моделью TemplatesReceiverType +- Связывает по полю creator_entity + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения типа сущности создателя + +```php +public function getCreatorEntity() +{ + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'creator_entity']); +} +``` + +### getCreator(): ActiveQuery + +Получает конкретного создателя (пользователя). + +**Логика:** +- Создаёт связь hasOne с моделью Admin +- Используется когда creator_entity = -1 (конкретный пользователь) +- Связывает по полю creator_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения создателя + +```php +public function getCreator() +{ + return $this->hasOne(Admin::class, ['id' => 'creator_id']); +} +``` + +### getRecipientEntity(): ActiveQuery + +Получает тип сущности получателя (исполнителя). + +**Логика:** +- Создаёт связь hasOne с моделью TemplatesReceiverType +- Связывает по полю recipient_entity + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения типа сущности получателя + +```php +public function getRecipientEntity() +{ + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'recipient_entity']); +} +``` + +### getRecipient(): ActiveQuery + +Получает конкретного получателя (исполнителя). + +**Логика:** +- Создаёт связь hasOne с моделью Admin +- Используется когда recipient_entity = -1 (конкретный пользователь) +- Связывает по полю recipient_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения получателя + +```php +public function getRecipient() +{ + return $this->hasOne(Admin::class, ['id' => 'recipient_id']); +} +``` + +### getControllerEntity(): ActiveQuery + +Получает тип сущности проверяющего. + +**Логика:** +- Создаёт связь hasOne с моделью TemplatesReceiverType +- Связывает по полю controller_entity + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения типа сущности проверяющего + +```php +public function getControllerEntity() +{ + return $this->hasOne(TemplatesReceiverType::class, ['id' => 'controller_entity']); +} +``` + +### getController(): ActiveQuery + +Получает конкретного проверяющего. + +**Логика:** +- Создаёт связь hasOne с моделью Admin +- Используется когда controller_entity = -1 (конкретный пользователь) +- Связывает по полю controller_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения проверяющего + +```php +public function getController() +{ + return $this->hasOne(Admin::class, ['id' => 'controller_id']); +} +``` + +### getCompanyFunction(): ActiveQuery + +Получает функцию компании для шаблона. + +**Логика:** +- Создаёт связь hasOne с моделью CompanyFunctions +- Связывает по полю company_function_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения функции компании + +```php +public function getCompanyFunction() +{ + return $this->hasOne(CompanyFunctions::class, ['id' => 'company_function_id']); +} +``` + +### getTaskEntity(): ActiveQuery + +Получает тип сущности задачи. + +**Логика:** +- Создаёт связь hasOne с моделью TaskEntity +- Связывает по полю entity_type + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения типа сущности + +```php +public function getTaskEntity() +{ + return $this->hasOne(TaskEntity::class, ['id' => 'entity_type']); +} +``` + +### getStore(): ActiveQuery + +Получает связанный магазин. + +**Логика:** +- Создаёт связь hasOne с моделью Products1c +- Используется когда entity_type указывает на магазин +- Связывает по полю entity_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения магазина + +```php +public function getStore() +{ + return $this->hasOne(Products1c::class, ['id' => 'entity_id']); +} +``` + +### getLesson(): ActiveQuery + +Получает связанный урок. + +**Логика:** +- Создаёт связь hasOne с моделью Lessons +- Используется когда entity_type указывает на урок +- Связывает по полю entity_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения урока + +```php +public function getLesson() +{ + return $this->hasOne(Lessons::class, ['id' => 'entity_id']); +} +``` + +### getDurationField(): TimeDiffValue + +Получает объект времени выполнения задачи. + +**Логика:** +- Создаёт экземпляр класса TimeDiffValue из строки duration +- Позволяет работать с временем выполнения как с объектом + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `TimeDiffValue` - объект временного интервала + +```php +public function getDurationField() +{ + return new TimeDiffValue($this->duration); +} +``` + +### getExpectedTimeField(): TimeDiffValue + +Получает объект ожидаемого времени выполнения задачи. + +**Логика:** +- Создаёт экземпляр класса TimeDiffValue из строки expected_time +- Позволяет работать с ожидаемым временем как с объектом + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `TimeDiffValue` - объект временного интервала + +```php +public function getExpectedTimeField() +{ + return new TimeDiffValue($this->expected_time); +} +``` + +### getAlertLevel(): ActiveQuery + +Получает уровень важности задачи. + +**Логика:** +- Создаёт связь hasOne с моделью TaskAlertLevel +- Связывает по полю alert_level_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения уровня важности + +```php +public function getAlertLevel() +{ + return $this->hasOne(TaskAlertLevel::class, ['id' => 'alert_level_id']); +} +``` + +### getMotivation(): ActiveQuery + +Получает мотивацию по задаче. + +**Логика:** +- Создаёт связь hasOne с моделью TaskMotivation +- Связывает по полю motivation_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения мотивации + +```php +public function getMotivation() +{ + return $this->hasOne(TaskMotivation::class, ['id' => 'motivation_id']); +} +``` + +### getParent(): ActiveQuery + +Получает родительский шаблон. + +**Логика:** +- Создаёт связь hasOne с моделью TaskTemplates +- Позволяет построить иерархию шаблонов +- Связывает по полю parent_id + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения родительского шаблона + +```php +public function getParent() +{ + return $this->hasOne(TaskTemplates::class, ['id' => 'parent_id']); +} +``` + +### getChildren(): ActiveQuery + +Получает дочерние шаблоны. + +**Логика:** +- Создаёт связь hasMany с моделью TaskTemplates +- Получает все дочерние шаблоны текущего шаблона +- Сортирует результат по полю posit в порядке возрастания +- Используется для построения иерархии шаблонов + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения дочерних шаблонов + +**Вызовы сторонних методов:** +- `hasMany()` - метод Yii2 для создания связи один-ко-многим +- `orderBy()` - метод Yii2 для сортировки результатов + +```php +public function getChildren() +{ + return $this->hasMany(TaskTemplates::class, ['parent_id' => 'id']) + ->orderBy(['posit' => SORT_ASC]); +} +``` + +### getOpenTasks(): ActiveQuery + +Получает открытые задачи, созданные по данному шаблону. + +**Логика:** +- Создаёт связь hasMany с моделью Task +- Связывает по полю task_template_id +- Фильтрует задачи со статусом меньше 6 (закрыта) и больше 0 +- Возвращает только активные (не закрытые) задачи + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения открытых задач + +**Вызовы сторонних методов:** +- `hasMany()` - метод Yii2 для создания связи один-ко-многим +- `andWhere()` - метод Yii2 для добавления условий WHERE + +```php +public function getOpenTasks() +{ + return $this->hasMany(Task::class, ['task_template_id' => 'id']) + ->andWhere(['and', ['<', 'status', 6], ['>', 'status', 0]]); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + TaskTemplates ||--o{ TaskTriggerTimeConditions : "timeTriggers" + TaskTemplates ||--o{ TaskTriggerConditions : "eventTriggers" + TaskTemplates ||--o{ Task : "openTasks" + TaskTemplates ||--o{ TaskTemplates : "children" + TaskTemplates }o--|| TaskTemplates : "parent" + TaskTemplates }o--|| TasksType : "taskType" + TaskTemplates }o--|| TemplatesReceiverType : "creatorEntity" + TaskTemplates }o--|| Admin : "creator" + TaskTemplates }o--|| TemplatesReceiverType : "recipientEntity" + TaskTemplates }o--|| Admin : "recipient" + TaskTemplates }o--|| TemplatesReceiverType : "controllerEntity" + TaskTemplates }o--|| Admin : "controller" + TaskTemplates }o--|| CompanyFunctions : "companyFunction" + TaskTemplates }o--|| TaskEntity : "taskEntity" + TaskTemplates }o--|| TaskAlertLevel : "alertLevel" + TaskTemplates }o--|| TaskMotivation : "motivation" + + TaskTemplates { + int id PK + string name + int task_type_id FK + int parent_id FK + int recipient_entity FK + int controller_entity FK + } + + Task { + int id PK + int task_template_id FK + int status + } + + TaskTriggerConditions { + int id PK + int task_template_id FK + int block + } +``` + +--- + +## Примеры использования + +### Создание простого шаблона задачи + +```php +$template = new TaskTemplates(); +$template->name = 'Проверка склада'; +$template->description = 'Ежедневная проверка остатков на складе'; +$template->task_type_id = 1; +$template->creator_entity = -1; +$template->creator_id = 10; // ID руководителя +$template->recipient_entity = 5; // Тип: менеджер склада +$template->controller_entity = -1; +$template->controller_id = 10; +$template->company_function_id = 3; +$template->entity_type = 1; +$template->duration = '02:00:00'; // 2 часа +$template->expected_time = '01:30:00'; // 1.5 часа +$template->deadline_count = '1 day'; // Дедлайн через 1 день +$template->deadline_permission = 0; +$template->children_order_type = 'sequential'; +$template->posit = 0; +$template->motivation_id = 1; +$template->save(); +``` + +### Создание иерархии шаблонов + +```php +// Родительский шаблон +$parentTemplate = new TaskTemplates(); +$parentTemplate->name = 'Инвентаризация'; +// ... заполнение полей ... +$parentTemplate->save(); + +// Дочерний шаблон 1 +$childTemplate1 = new TaskTemplates(); +$childTemplate1->name = 'Проверка склада А'; +$childTemplate1->parent_id = $parentTemplate->id; +$childTemplate1->posit = 1; +$childTemplate1->children_order_type = 'parallel'; +// ... заполнение полей ... +$childTemplate1->save(); + +// Дочерний шаблон 2 +$childTemplate2 = new TaskTemplates(); +$childTemplate2->name = 'Проверка склада Б'; +$childTemplate2->parent_id = $parentTemplate->id; +$childTemplate2->posit = 2; +// ... заполнение полей ... +$childTemplate2->save(); +``` + +### Получение дочерних шаблонов + +```php +$template = TaskTemplates::findOne(10); +$children = $template->children; // Отсортировано по posit +``` + +### Получение активных задач по шаблону + +```php +$template = TaskTemplates::findOne(10); +$openTasks = $template->openTasks; // Только незакрытые задачи +``` + +### Получение временных триггеров + +```php +$template = TaskTemplates::findOne(10); +$timeTriggers = $template->timeTriggers; + +foreach ($timeTriggers as $trigger) { + echo "Триггер: создание задачи каждые {$trigger->interval}\n"; +} +``` + +### Получение событийных триггеров + +```php +$template = TaskTemplates::findOne(10); +$eventTriggers = $template->eventTriggers; + +foreach ($eventTriggers as $trigger) { + echo "Условие: {$trigger->name} {$trigger->type} {$trigger->value}\n"; +} +``` + +### Поиск шаблонов по типу задачи + +```php +$templates = TaskTemplates::find() + ->where(['task_type_id' => 5]) + ->all(); +``` + +### Получение шаблонов с определённым уровнем важности + +```php +$urgentTemplates = TaskTemplates::find() + ->where(['alert_level_id' => 3]) + ->all(); +``` + +### Клонирование шаблона + +```php +$original = TaskTemplates::findOne(10); + +$clone = new TaskTemplates(); +$clone->attributes = $original->attributes; +$clone->id = null; // Сброс ID +$clone->name = $original->name . ' (копия)'; +$clone->save(); +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи, созданные по шаблону +- [TasksType](TasksType.md) - Типы задач +- [TaskEntity](TaskEntity.md) - Типы сущностей задач +- [TaskAlertLevel](TaskAlertLevel.md) - Уровни важности +- [TaskMotivation](TaskMotivation.md) - Мотивация +- [TaskTriggerConditions](TaskTriggerConditions.md) - Условия триггеров +- [Admin](Admin.md) - Пользователи системы + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskTemplatesSearch.md b/erp24/docs/models/TaskTemplatesSearch.md new file mode 100644 index 00000000..33d06f21 --- /dev/null +++ b/erp24/docs/models/TaskTemplatesSearch.md @@ -0,0 +1,196 @@ +# Класс: TaskTemplatesSearch + + +## Mindmap + +```mermaid +mindmap + root((TaskTemplatesSearch)) + Таблица БД + ActiveRecord + Наследование + extends TaskTemplates +``` + +## Назначение +Search-модель для поиска и фильтрации шаблонов задач в ERP24. Шаблоны содержат предопределённые настройки задач с указанием создателя, исполнителя и контролёра по типам сущностей. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`TaskTemplates` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `task_type_id`, `creator_entity`, `recipient_entity`, `controller_entity`, `company_function_id`, `entity_id` — integer +- `name`, `description` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска шаблонов задач. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос TaskTemplates::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, task_type_id, creator_entity, recipient_entity, controller_entity, company_function_id, entity_id + - like: name, description + +## Диаграмма структуры + +```mermaid +erDiagram + TaskTemplates { + int id PK + varchar name + text description + int task_type_id FK + int creator_entity + int recipient_entity + int controller_entity + int company_function_id FK + int entity_id + } + + TasksType { + int id PK + varchar name + } + + CompanyFunction { + int id PK + varchar name + } + + TaskTemplates }o--|| TasksType : "task_type_id" + TaskTemplates }o--|| CompanyFunction : "company_function_id" +``` + +## Диаграмма ролей в шаблоне + +```mermaid +flowchart TD + A[TaskTemplate] --> B[Роли] + + B --> C[creator_entity] + C --> C1[Тип создателя задачи] + + B --> D[recipient_entity] + D --> D1[Тип исполнителя] + + B --> E[controller_entity] + E --> E1[Тип контролёра] + + F[entity_id] --> G[Конкретная сущность] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new TaskTemplatesSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new TaskTemplatesSearch(); +$dataProvider = $searchModel->search([ + 'TaskTemplatesSearch' => [ + 'name' => 'Инвентаризация', + ] +]); +``` + +### Поиск по типу задачи +```php +$searchModel = new TaskTemplatesSearch(); +$dataProvider = $searchModel->search([ + 'TaskTemplatesSearch' => [ + 'task_type_id' => 3, + ] +]); +``` + +### Поиск по функции компании +```php +$searchModel = new TaskTemplatesSearch(); +$dataProvider = $searchModel->search([ + 'TaskTemplatesSearch' => [ + 'company_function_id' => 5, // Логистика + ] +]); +``` + +### Поиск шаблонов для конкретной сущности +```php +$searchModel = new TaskTemplatesSearch(); +$dataProvider = $searchModel->search([ + 'TaskTemplatesSearch' => [ + 'entity_id' => 10, // Магазин №10 + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'attribute' => 'task_type_id', + 'value' => 'taskType.name', + ], + [ + 'attribute' => 'company_function_id', + 'value' => 'companyFunction.name', + ], + 'creator_entity', + 'recipient_entity', + 'controller_entity', + ], +]) ?> +``` + +## Связанные модели + +- [TaskTemplates](./TaskTemplates.md) — базовая модель шаблонов +- [Task](./Task.md) — задачи +- [TasksType](./TasksType.md) — типы задач +- [CompanyFunction](./CompanyFunction.md) — функции компании + +## Особенности реализации + +1. **Типы сущностей**: creator_entity, recipient_entity, controller_entity — типы ролей +2. **Конкретная сущность**: entity_id для привязки к объекту (магазин, отдел) +3. **Предопределённые задачи**: Шаблоны для быстрого создания типовых задач +4. **like вместо ilike**: Регистрозависимый поиск +5. **Связь с функциями**: company_function_id для маршрутизации задач diff --git a/erp24/docs/models/TaskTriggerConditions.md b/erp24/docs/models/TaskTriggerConditions.md new file mode 100644 index 00000000..ad164027 --- /dev/null +++ b/erp24/docs/models/TaskTriggerConditions.md @@ -0,0 +1,612 @@ +# Model: TaskTriggerConditions + + +## Mindmap + +```mermaid +mindmap + root((TaskTriggerConditions)) + Таблица БД + task_trigger_conditions + Свойства + id + int + block + int + task_template_id + int + name + string + name_table + string + setting_json + string + Связи + SubTriggers + 1:N TaskTriggerConditions + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель условий триггеров для автоматического создания задач. Реализует систему событийных триггеров, которые определяют когда и при каких условиях должны создаваться задачи из шаблонов. Поддерживает сложные логические условия с группировкой (блоками) и вложенностью. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_trigger_conditions` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID условия триггера | +| `block` | int | Тип записи: 1 - блок условий, 0 - отдельное условие | +| `task_template_id` | int | ID шаблона задачи из таблицы task_templates | +| `name` | string | Название переменной для проверки (макс. 150 символов) | +| `name_table` | string | Название таблицы откуда берутся данные (макс. 200 символов) | +| `setting_json` | string | JSON с настройками и условиями запроса для сбора данных (TEXT) | +| `type` | string | Тип логического условия: less, more, same и др. (макс. 12 символов) | +| `value` | string | Значение для сравнения с результатом запроса (макс. 30 символов) | +| `block_id` | int | ID родительского блока для вложенных условий | +| `rule_condition` | string | Логическое условие объединения: OR или AND (TEXT) | + +--- + +## Правила валидации + +### Обязательные поля + +```php +[ + [ + 'task_template_id', 'name', 'name_table', + 'setting_json', 'value', 'block_id' + ], + 'required' +] +``` + +### Типы данных + +```php +// Целочисленные поля +[['block', 'task_template_id', 'block_id'], 'integer'] + +// Текстовые поля +[['setting_json', 'rule_condition'], 'string'] + +// Ограничения длины строк +[['name'], 'string', 'max' => 150] +[['name_table'], 'string', 'max' => 200] +[['type'], 'string', 'max' => 12] +[['value'], 'string', 'max' => 30] +``` + +--- + +## Связи (Relations) + +### hasMany + +| Метод | Связанная модель | Описание | +|-------|------------------|----------| +| `getSubTriggers()` | TaskTriggerConditions | Вложенные условия триггера | + +### В модели TaskTemplates + +```php +public function getEventTriggers() { + return $this->hasMany(TaskTriggerConditions::class, ['task_template_id' => 'id']) + ->andWhere(['block' => '1']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_trigger_conditions` + +**Возвращает:** +- `string` - название таблицы `'task_trigger_conditions'` + +```php +public static function tableName() +{ + return 'task_trigger_conditions'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения ключевых полей +- Проверяет типы данных для всех атрибутов +- Устанавливает ограничения длины для строковых полей +- Валидирует формат JSON для setting_json + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['block', 'task_template_id', 'block_id'], 'integer'], + [['task_template_id', 'name', 'name_table', 'setting_json', 'value', 'block_id'], 'required'], + [['setting_json', 'rule_condition'], 'string'], + [['name'], 'string', 'max' => 150], + [['name_table'], 'string', 'max' => 200], + [['type'], 'string', 'max' => 12], + [['value'], 'string', 'max' => 30], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'block' => 'Block', + 'task_template_id' => 'Task Template ID', + 'name' => 'Name', + 'name_table' => 'Name Table', + 'setting_json' => 'Setting Json', + 'type' => 'Type', + 'value' => 'Value', + 'block_id' => 'Block ID', + 'rule_condition' => 'Rule Condition', + ]; +} +``` + +### getSubTriggers(): ActiveQuery + +Получает вложенные условия триггера. + +**Логика:** +- Создаёт связь hasMany с моделью TaskTriggerConditions +- Связывает по полю block_id с id текущего блока +- Позволяет создавать иерархию условий + +**Параметры:** +- Нет параметров + +**Возвращает:** +- `ActiveQuery` - запрос для получения вложенных условий + +**Вызовы сторонних методов:** +- `hasMany()` - метод Yii2 для создания связи один-ко-многим + +```php +public function getSubTriggers() +{ + return $this->hasMany(TaskTriggerConditions::class, ['block_id' => 'id']); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + TaskTemplates ||--o{ TaskTriggerConditions : "eventTriggers" + TaskTriggerConditions ||--o{ TaskTriggerConditions : "subTriggers" + + TaskTemplates { + int id PK + string name + } + + TaskTriggerConditions { + int id PK + int block + int task_template_id FK + string name + string name_table + string setting_json + string type + string value + int block_id FK + string rule_condition + } +``` + +--- + +## Типы условий (type) + +| Тип | Описание | Пример | +|-----|----------|--------| +| `less` | Меньше (<) | Количество товаров < 10 | +| `more` | Больше (>) | Сумма заказов > 1000 | +| `same` | Равно (=) | Статус = 'новый' | +| `not_same` | Не равно (!=) | Роль != 'admin' | +| `less_equal` | Меньше или равно (<=) | Остаток <= 5 | +| `more_equal` | Больше или равно (>=) | Рейтинг >= 4 | +| `contains` | Содержит | Название содержит 'срочно' | +| `not_contains` | Не содержит | Комментарий не содержит 'отменено' | + +--- + +## Формат setting_json + +Поле `setting_json` содержит конфигурацию запроса для получения данных: + +```json +{ + "table": "products", + "field": "quantity", + "conditions": [ + {"field": "store_id", "operator": "=", "value": "{{entity_id}}"}, + {"field": "category", "operator": "=", "value": "продукты"} + ], + "aggregation": "sum", + "joins": [ + { + "table": "stores", + "on": "products.store_id = stores.id" + } + ] +} +``` + +### Параметры конфигурации + +| Параметр | Тип | Описание | +|----------|-----|----------| +| `table` | string | Таблица для запроса данных | +| `field` | string | Поле для выборки значения | +| `conditions` | array | Массив условий WHERE | +| `aggregation` | string | Агрегирующая функция (sum, count, avg, min, max) | +| `joins` | array | Массив JOIN условий | + +--- + +## Структура блоков + +### Блок (block = 1) +Группа условий, объединённых логическим оператором (AND/OR). + +### Условие (block = 0) +Отдельное условие проверки значения. + +### Вложенность +Условия могут быть вложены в блоки через `block_id`. + +--- + +## Примеры использования + +### Создание простого условия + +```php +$condition = new TaskTriggerConditions(); +$condition->block = 0; // Отдельное условие +$condition->task_template_id = 10; +$condition->name = 'stock_quantity'; +$condition->name_table = 'products'; +$condition->setting_json = json_encode([ + 'table' => 'products', + 'field' => 'quantity', + 'conditions' => [ + ['field' => 'store_id', 'operator' => '=', 'value' => '{{entity_id}}'] + ], + 'aggregation' => 'sum' +]); +$condition->type = 'less'; +$condition->value = '10'; +$condition->block_id = 0; +$condition->rule_condition = 'AND'; +$condition->save(); +``` + +### Создание блока условий + +```php +// Создаём блок +$block = new TaskTriggerConditions(); +$block->block = 1; // Блок +$block->task_template_id = 10; +$block->name = 'Проверка остатков'; +$block->name_table = 'products'; +$block->setting_json = '{}'; +$block->type = ''; +$block->value = ''; +$block->block_id = 0; +$block->rule_condition = 'AND'; // Условия внутри блока объединяются через AND +$block->save(); + +// Создаём условия внутри блока +$condition1 = new TaskTriggerConditions(); +$condition1->block = 0; +$condition1->task_template_id = 10; +$condition1->name = 'low_stock'; +$condition1->name_table = 'products'; +$condition1->setting_json = json_encode([ + 'table' => 'products', + 'field' => 'quantity', + 'aggregation' => 'count', + 'conditions' => [ + ['field' => 'quantity', 'operator' => '<', 'value' => '5'] + ] +]); +$condition1->type = 'more'; +$condition1->value = '0'; +$condition1->block_id = $block->id; // Привязка к блоку +$condition1->rule_condition = 'AND'; +$condition1->save(); +``` + +### Получение условий шаблона + +```php +$template = TaskTemplates::findOne(10); +$eventTriggers = $template->eventTriggers; // Только блоки (block = 1) + +foreach ($eventTriggers as $trigger) { + echo "Блок: {$trigger->name}\n"; + + $subTriggers = $trigger->subTriggers; + foreach ($subTriggers as $subTrigger) { + echo " Условие: {$subTrigger->name} {$subTrigger->type} {$subTrigger->value}\n"; + } +} +``` + +### Проверка выполнения условия + +```php +class TriggerService +{ + /** + * Проверяет выполнение условия триггера + * + * @param TaskTriggerConditions $condition Условие + * @param array $context Контекст выполнения + * @return bool Результат проверки + */ + public function checkCondition($condition, $context = []) + { + // Получаем данные из БД + $value = $this->executeQuery($condition, $context); + + // Сравниваем с ожидаемым значением + switch ($condition->type) { + case 'less': + return $value < $condition->value; + case 'more': + return $value > $condition->value; + case 'same': + return $value == $condition->value; + case 'not_same': + return $value != $condition->value; + case 'less_equal': + return $value <= $condition->value; + case 'more_equal': + return $value >= $condition->value; + case 'contains': + return strpos($value, $condition->value) !== false; + case 'not_contains': + return strpos($value, $condition->value) === false; + default: + return false; + } + } + + /** + * Выполняет запрос из конфигурации условия + * + * @param TaskTriggerConditions $condition Условие + * @param array $context Контекст выполнения + * @return mixed Результат запроса + */ + protected function executeQuery($condition, $context) + { + $settings = json_decode($condition->setting_json, true); + + // Формируем запрос + $query = new \yii\db\Query(); + $query->from($settings['table']); + + // Применяем условия с заменой переменных + if (isset($settings['conditions'])) { + foreach ($settings['conditions'] as $cond) { + $value = $this->replaceVariables($cond['value'], $context); + $query->andWhere([$cond['operator'], $cond['field'], $value]); + } + } + + // Применяем агрегацию + if (isset($settings['aggregation'])) { + $field = $settings['field']; + switch ($settings['aggregation']) { + case 'sum': + return $query->sum($field); + case 'count': + return $query->count($field); + case 'avg': + return $query->average($field); + case 'min': + return $query->min($field); + case 'max': + return $query->max($field); + } + } + + return $query->scalar(); + } + + /** + * Заменяет переменные в значениях + * + * @param string $value Значение с переменными + * @param array $context Контекст выполнения + * @return string Значение с замененными переменными + */ + protected function replaceVariables($value, $context) + { + // Замена переменных типа {{entity_id}} + foreach ($context as $key => $val) { + $value = str_replace("{{{$key}}}", $val, $value); + } + return $value; + } +} +``` + +### Проверка блока условий + +```php +/** + * Проверяет выполнение блока условий + * + * @param TaskTriggerConditions $block Блок условий + * @param array $context Контекст выполнения + * @return bool Результат проверки + */ +public function checkBlock($block, $context = []) +{ + $subTriggers = $block->subTriggers; + $ruleCondition = $block->rule_condition; + + $results = []; + foreach ($subTriggers as $trigger) { + if ($trigger->block == 1) { + // Вложенный блок + $results[] = $this->checkBlock($trigger, $context); + } else { + // Отдельное условие + $results[] = $this->checkCondition($trigger, $context); + } + } + + // Объединяем результаты + if ($ruleCondition === 'OR') { + return in_array(true, $results); + } else { // AND + return !in_array(false, $results); + } +} +``` + +### Автоматическое создание задачи при выполнении условий + +```php +/** + * Проверяет триггеры и создаёт задачи + * + * @param int $templateId ID шаблона + * @param array $context Контекст выполнения + */ +public function processTriggers($templateId, $context) +{ + $template = TaskTemplates::findOne($templateId); + $triggers = $template->eventTriggers; + + $triggerService = new TriggerService(); + $shouldCreateTask = false; + + foreach ($triggers as $trigger) { + if ($triggerService->checkBlock($trigger, $context)) { + $shouldCreateTask = true; + break; + } + } + + if ($shouldCreateTask) { + $this->createTaskFromTemplate($template, $context); + } +} +``` + +--- + +## Примеры условий + +### Проверка остатков на складе + +```php +// Создать задачу если остаток товаров меньше 10 +$condition = [ + 'name' => 'stock_check', + 'name_table' => 'products', + 'setting_json' => json_encode([ + 'table' => 'products', + 'field' => 'quantity', + 'conditions' => [ + ['field' => 'store_id', 'operator' => '=', 'value' => '{{store_id}}'] + ], + 'aggregation' => 'sum' + ]), + 'type' => 'less', + 'value' => '10' +]; +``` + +### Проверка просроченных заказов + +```php +// Создать задачу если есть заказы старше 24 часов +$condition = [ + 'name' => 'overdue_orders', + 'name_table' => 'orders', + 'setting_json' => json_encode([ + 'table' => 'orders', + 'field' => 'id', + 'conditions' => [ + ['field' => 'status', 'operator' => '=', 'value' => 'pending'], + ['field' => 'created_at', 'operator' => '<', 'value' => 'DATE_SUB(NOW(), INTERVAL 24 HOUR)'] + ], + 'aggregation' => 'count' + ]), + 'type' => 'more', + 'value' => '0' +]; +``` + +--- + +## Связанные модели + +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskTriggerTimeConditions.md b/erp24/docs/models/TaskTriggerTimeConditions.md new file mode 100644 index 00000000..6a7c5066 --- /dev/null +++ b/erp24/docs/models/TaskTriggerTimeConditions.md @@ -0,0 +1,238 @@ +# Модель TaskTriggerTimeConditions + + +## Mindmap + +```mermaid +mindmap + root((TaskTriggerTimeConditions)) + Таблица БД + task_trigger_time_conditions + Свойства + id + int + task_template_id + int + minutes + string + hours + string + days + string + months + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TaskTriggerTimeConditions` хранит временны́е условия автоматического создания задач по шаблонам. Определяет расписание в формате, аналогичном cron: минуты, часы, дни месяца, месяцы и дни недели. Используется для автоматизации создания периодических задач. + +**Файл модели:** `erp24/records/TaskTriggerTimeConditions.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `task_trigger_time_conditions` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `task_template_id` | INTEGER | ID шаблона задачи (FK → task_templates.id) | +| `minutes` | VARCHAR(255) | Минуты (0-59, * — любая, список через запятую) | +| `hours` | VARCHAR(255) | Часы (0-23, * — любой, список через запятую) | +| `days` | VARCHAR(255) | Дни месяца (1-31, * — любой, список через запятую) | +| `months` | VARCHAR(255) | Месяцы (1-12, * — любой, список через запятую) | +| `weekday` | VARCHAR(266) | Дни недели (0-6, 0=воскресенье, * — любой) | + +--- + +## Формат значений (аналогично cron) + +| Символ | Описание | Пример | +|--------|----------|--------| +| `*` | Любое значение | `*` — каждую минуту | +| `число` | Конкретное значение | `30` — в 30 минут | +| `a,b,c` | Список значений | `0,30` — в 0 и 30 минут | +| `a-b` | Диапазон | `9-17` — с 9 до 17 часов | +| `*/n` | Каждые n | `*/15` — каждые 15 минут | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + task_trigger_time_conditions }o--|| task_templates : "belongs_to" + + task_trigger_time_conditions { + int id PK + int task_template_id FK + string minutes + string hours + string days + string months + string weekday + } + + task_templates { + int id PK + string name + int active + } +``` + +--- + +## Примеры использования + +### Создание расписания: каждый день в 9:00 + +```php +$schedule = new TaskTriggerTimeConditions(); +$schedule->task_template_id = $templateId; +$schedule->minutes = '0'; +$schedule->hours = '9'; +$schedule->days = '*'; +$schedule->months = '*'; +$schedule->weekday = '*'; +$schedule->save(); +``` + +### Каждый понедельник в 10:00 + +```php +$schedule = new TaskTriggerTimeConditions(); +$schedule->task_template_id = $templateId; +$schedule->minutes = '0'; +$schedule->hours = '10'; +$schedule->days = '*'; +$schedule->months = '*'; +$schedule->weekday = '1'; // Понедельник +$schedule->save(); +``` + +### Первого числа каждого месяца в 8:00 + +```php +$schedule = new TaskTriggerTimeConditions(); +$schedule->task_template_id = $templateId; +$schedule->minutes = '0'; +$schedule->hours = '8'; +$schedule->days = '1'; +$schedule->months = '*'; +$schedule->weekday = '*'; +$schedule->save(); +``` + +### Каждые 30 минут в рабочее время + +```php +$schedule = new TaskTriggerTimeConditions(); +$schedule->task_template_id = $templateId; +$schedule->minutes = '0,30'; +$schedule->hours = '9,10,11,12,13,14,15,16,17,18'; +$schedule->days = '*'; +$schedule->months = '*'; +$schedule->weekday = '1,2,3,4,5'; // Пн-Пт +$schedule->save(); +``` + +### Проверка, нужно ли запускать сейчас + +```php +function shouldTriggerNow(TaskTriggerTimeConditions $schedule): bool +{ + $now = new DateTime(); + + // Проверяем минуты + if (!matchesCronField($schedule->minutes, (int)$now->format('i'))) { + return false; + } + + // Проверяем часы + if (!matchesCronField($schedule->hours, (int)$now->format('G'))) { + return false; + } + + // Проверяем день месяца + if (!matchesCronField($schedule->days, (int)$now->format('j'))) { + return false; + } + + // Проверяем месяц + if (!matchesCronField($schedule->months, (int)$now->format('n'))) { + return false; + } + + // Проверяем день недели + if (!matchesCronField($schedule->weekday, (int)$now->format('w'))) { + return false; + } + + return true; +} + +function matchesCronField(string $field, int $value): bool +{ + if ($field === '*') { + return true; + } + + $parts = explode(',', $field); + foreach ($parts as $part) { + if (strpos($part, '-') !== false) { + [$start, $end] = explode('-', $part); + if ($value >= $start && $value <= $end) { + return true; + } + } elseif ((int)$part === $value) { + return true; + } + } + + return false; +} +``` + +### Получение шаблонов для запуска + +```php +$schedules = TaskTriggerTimeConditions::find() + ->alias('ttc') + ->innerJoin('task_templates tt', 'tt.id = ttc.task_template_id') + ->where(['tt.active' => 1]) + ->all(); + +foreach ($schedules as $schedule) { + if (shouldTriggerNow($schedule)) { + $template = TaskTemplates::findOne($schedule->task_template_id); + $this->createTaskFromTemplate($template); + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `task_template_id` | Обязательное, целое число | +| `minutes`, `hours`, `days`, `months` | Обязательные, строка, макс. 255 символов | +| `weekday` | Обязательное, строка, макс. 266 символов | + +--- + +## Связанные модели + +- **[TaskTemplates](./TaskTemplates.md)** — шаблоны задач +- **[TaskTriggerConditions](./TaskTriggerConditions.md)** — условия срабатывания + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TaskUsers.md b/erp24/docs/models/TaskUsers.md new file mode 100644 index 00000000..13460220 --- /dev/null +++ b/erp24/docs/models/TaskUsers.md @@ -0,0 +1,286 @@ +# Model: TaskUsers + + +## Mindmap + +```mermaid +mindmap + root((TaskUsers)) + Таблица БД + task_users + Свойства + task_id + int + admin_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель связи задач с исполнителями. Реализует отношение многие-ко-многим между таблицами `task` и `admin`, позволяя назначать нескольких исполнителей на одну задачу. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_users` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `task_id` | int | ID задачи (внешний ключ на таблицу task) | +| `admin_id` | int | ID исполнителя (внешний ключ на таблицу admin) | + +--- + +## Правила валидации + +### Обязательные поля + +Оба поля `task_id` и `admin_id` являются обязательными для заполнения. + +```php +[['task_id', 'admin_id'], 'required'] +``` + +### Типы данных + +Оба поля должны быть целыми числами (integer). + +```php +[['task_id', 'admin_id'], 'integer'] +``` + +### Уникальность + +Комбинация `task_id` и `admin_id` должна быть уникальной, что предотвращает назначение одного и того же исполнителя на задачу несколько раз. + +```php +[['task_id', 'admin_id'], 'unique', 'targetAttribute' => ['task_id', 'admin_id']] +``` + +--- + +## Связи (Relations) + +Модель не содержит явно определенных связей, но используется как промежуточная таблица в следующих связях: + +### В модели Task + +```php +// Получение записей связи +public function getTaskUsers() { + return $this->hasMany(TaskUsers::class, ['task_id' => 'id']); +} + +// Получение исполнителей через связь +public function getUsers() { + return $this->hasMany(Admin::class, ['id' => 'admin_id']) + ->via('taskUsers'); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_users` + +**Возвращает:** +- `string` - название таблицы `'task_users'` + +```php +public static function tableName() +{ + return 'task_users'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения обоих полей +- Проверяет, что значения являются целыми числами +- Обеспечивает уникальность комбинации task_id + admin_id + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['task_id', 'admin_id'], 'required'], + [['task_id', 'admin_id'], 'integer'], + [['task_id', 'admin_id'], 'unique', 'targetAttribute' => ['task_id', 'admin_id']], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'task_id' => 'Task ID', + 'admin_id' => 'Admin ID', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task ||--o{ TaskUsers : "has many" + Admin ||--o{ TaskUsers : "has many" + TaskUsers }o--|| Task : "task_id" + TaskUsers }o--|| Admin : "admin_id" + + Task { + int id PK + string name + int status + } + + TaskUsers { + int task_id PK,FK + int admin_id PK,FK + } + + Admin { + int id PK + string name + string email + } +``` + +--- + +## Примеры использования + +### Назначение исполнителя на задачу + +```php +$taskUser = new TaskUsers(); +$taskUser->task_id = 123; +$taskUser->admin_id = 45; + +if ($taskUser->save()) { + // Исполнитель успешно назначен на задачу +} +``` + +### Назначение нескольких исполнителей + +```php +$taskId = 123; +$adminIds = [45, 67, 89]; + +foreach ($adminIds as $adminId) { + $taskUser = new TaskUsers(); + $taskUser->task_id = $taskId; + $taskUser->admin_id = $adminId; + $taskUser->save(); +} +``` + +### Получение всех исполнителей задачи + +```php +$task = Task::findOne(123); +$executors = $task->users; // Массив объектов Admin +``` + +### Проверка назначения исполнителя + +```php +$isAssigned = TaskUsers::find() + ->where(['task_id' => 123, 'admin_id' => 45]) + ->exists(); +``` + +### Удаление исполнителя из задачи + +```php +TaskUsers::deleteAll(['task_id' => 123, 'admin_id' => 45]); +``` + +### Получение всех задач исполнителя через связь + +```php +$adminId = 45; +$taskIds = TaskUsers::find() + ->select('task_id') + ->where(['admin_id' => $adminId]) + ->column(); + +$tasks = Task::find() + ->where(['id' => $taskIds]) + ->all(); +``` + +### Замена исполнителей задачи + +```php +$taskId = 123; +$newAdminIds = [45, 67]; + +// Удаляем текущих исполнителей +TaskUsers::deleteAll(['task_id' => $taskId]); + +// Назначаем новых +foreach ($newAdminIds as $adminId) { + $taskUser = new TaskUsers(); + $taskUser->task_id = $taskId; + $taskUser->admin_id = $adminId; + $taskUser->save(); +} +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Модель задач +- [Admin](Admin.md) - Модель сотрудников +- [TaskViewers](TaskViewers.md) - Наблюдатели задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TaskViewers.md b/erp24/docs/models/TaskViewers.md new file mode 100644 index 00000000..bff4e7e9 --- /dev/null +++ b/erp24/docs/models/TaskViewers.md @@ -0,0 +1,325 @@ +# Model: TaskViewers + + +## Mindmap + +```mermaid +mindmap + root((TaskViewers)) + Таблица БД + task_viewers + Свойства + task_id + int + admin_id + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель связи задач с наблюдателями. Реализует отношение многие-ко-многим между таблицами `task` и `admin`, позволяя назначать нескольких наблюдателей на одну задачу. Наблюдатели получают уведомления об изменениях в задаче, но не являются её исполнителями. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`task_viewers` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `task_id` | int | ID задачи (внешний ключ на таблицу task) | +| `admin_id` | int | ID наблюдателя (внешний ключ на таблицу admin) | + +--- + +## Правила валидации + +### Обязательные поля + +Оба поля `task_id` и `admin_id` являются обязательными для заполнения. + +```php +[['task_id', 'admin_id'], 'required'] +``` + +### Типы данных + +Оба поля должны быть целыми числами (integer). + +```php +[['task_id', 'admin_id'], 'integer'] +``` + +### Уникальность + +Комбинация `task_id` и `admin_id` должна быть уникальной, что предотвращает добавление одного и того же наблюдателя на задачу несколько раз. + +```php +[['task_id', 'admin_id'], 'unique', 'targetAttribute' => ['task_id', 'admin_id']] +``` + +--- + +## Связи (Relations) + +Модель не содержит явно определенных связей, но используется как промежуточная таблица в следующих связях: + +### В модели Task + +```php +// Получение записей связи +public function getTaskViewers() { + return $this->hasMany(TaskViewers::class, ['task_id' => 'id']); +} + +// Получение наблюдателей через связь +public function getViewers() { + return $this->hasMany(Admin::class, ['id' => 'admin_id']) + ->via('taskViewers'); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `task_viewers` + +**Возвращает:** +- `string` - название таблицы `'task_viewers'` + +```php +public static function tableName() +{ + return 'task_viewers'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения обоих полей +- Проверяет, что значения являются целыми числами +- Обеспечивает уникальность комбинации task_id + admin_id + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['task_id', 'admin_id'], 'required'], + [['task_id', 'admin_id'], 'integer'], + [['task_id', 'admin_id'], 'unique', 'targetAttribute' => ['task_id', 'admin_id']], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'task_id' => 'Task ID', + 'admin_id' => 'Admin ID', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task ||--o{ TaskViewers : "has many" + Admin ||--o{ TaskViewers : "has many" + TaskViewers }o--|| Task : "task_id" + TaskViewers }o--|| Admin : "admin_id" + + Task { + int id PK + string name + int status + } + + TaskViewers { + int task_id PK,FK + int admin_id PK,FK + } + + Admin { + int id PK + string name + string email + } +``` + +--- + +## Отличия от TaskUsers + +| Характеристика | TaskUsers | TaskViewers | +|----------------|-----------|-------------| +| Роль | Исполнители задачи | Наблюдатели задачи | +| Ответственность | Выполняют задачу | Получают уведомления | +| Доступ к редактированию | Могут изменять статус | Только просмотр | +| Поле в Task | `updated_by` | - | + +--- + +## Примеры использования + +### Добавление наблюдателя к задаче + +```php +$taskViewer = new TaskViewers(); +$taskViewer->task_id = 123; +$taskViewer->admin_id = 45; + +if ($taskViewer->save()) { + // Наблюдатель успешно добавлен к задаче +} +``` + +### Добавление нескольких наблюдателей + +```php +$taskId = 123; +$viewerIds = [45, 67, 89]; + +foreach ($viewerIds as $viewerId) { + $taskViewer = new TaskViewers(); + $taskViewer->task_id = $taskId; + $taskViewer->admin_id = $viewerId; + $taskViewer->save(); +} +``` + +### Получение всех наблюдателей задачи + +```php +$task = Task::findOne(123); +$viewers = $task->viewers; // Массив объектов Admin +``` + +### Проверка, является ли пользователь наблюдателем задачи + +```php +$isViewer = TaskViewers::find() + ->where(['task_id' => 123, 'admin_id' => 45]) + ->exists(); +``` + +### Удаление наблюдателя из задачи + +```php +TaskViewers::deleteAll(['task_id' => 123, 'admin_id' => 45]); +``` + +### Получение всех задач, которые наблюдает пользователь + +```php +$adminId = 45; +$taskIds = TaskViewers::find() + ->select('task_id') + ->where(['admin_id' => $adminId]) + ->column(); + +$tasks = Task::find() + ->where(['id' => $taskIds]) + ->all(); +``` + +### Замена списка наблюдателей задачи + +```php +$taskId = 123; +$newViewerIds = [45, 67]; + +// Удаляем текущих наблюдателей +TaskViewers::deleteAll(['task_id' => $taskId]); + +// Добавляем новых +foreach ($newViewerIds as $viewerId) { + $taskViewer = new TaskViewers(); + $taskViewer->task_id = $taskId; + $taskViewer->admin_id = $viewerId; + $taskViewer->save(); +} +``` + +### Получение задач с количеством наблюдателей + +```php +$tasks = Task::find() + ->select(['task.*', 'COUNT(task_viewers.admin_id) as viewers_count']) + ->leftJoin('task_viewers', 'task_viewers.task_id = task.id') + ->groupBy('task.id') + ->all(); +``` + +### Копирование наблюдателей из одной задачи в другую + +```php +$sourceTaskId = 100; +$targetTaskId = 200; + +$viewers = TaskViewers::find() + ->where(['task_id' => $sourceTaskId]) + ->all(); + +foreach ($viewers as $viewer) { + $newViewer = new TaskViewers(); + $newViewer->task_id = $targetTaskId; + $newViewer->admin_id = $viewer->admin_id; + $newViewer->save(); +} +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Модель задач +- [Admin](Admin.md) - Модель сотрудников +- [TaskUsers](TaskUsers.md) - Исполнители задач + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TasksType.md b/erp24/docs/models/TasksType.md new file mode 100644 index 00000000..e2454a56 --- /dev/null +++ b/erp24/docs/models/TasksType.md @@ -0,0 +1,424 @@ +# Model: TasksType + + +## Mindmap + +```mermaid +mindmap + root((TasksType)) + Таблица БД + tasks_type + Свойства + id + int + name + string + icon + string + posit + int + color + string + config_json + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель справочника типов задач. Определяет категории задач в системе с визуальным представлением (иконка, цвет) и конфигурацией. Используется для классификации задач и установки стандартных параметров для задач определённого типа. + +## Пространство имён + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица БД + +`tasks_type` + +--- + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `id` | int | ID типа задачи | +| `name` | string | Название типа задачи (макс. 255 символов) | +| `icon` | string | Иконка для визуального отображения (макс. 200 символов) | +| `posit` | int | Позиция в списке для сортировки | +| `color` | string | Цвет для визуального отображения в HEX формате (макс. 15 символов) | +| `config_json` | string | JSON конфигурация с дополнительными параметрами типа (TEXT) | +| `alert_level_id` | int\|null | ID уровня важности из таблицы task_alert_level | + +--- + +## Правила валидации + +### Обязательные поля + +Все поля кроме `alert_level_id` являются обязательными. + +```php +[['name', 'icon', 'posit', 'color', 'config_json'], 'required'] +``` + +### Типы данных + +```php +// Целочисленные поля +[['posit', 'alert_level_id'], 'integer'] + +// Текстовые поля +[['config_json'], 'string'] + +// Ограничения длины строк +[['name'], 'string', 'max' => 255] +[['icon'], 'string', 'max' => 200] +[['color'], 'string', 'max' => 15] +``` + +--- + +## Связи (Relations) + +### В других моделях + +#### Task +```php +public function getTaskType() { + return $this->hasOne(TasksType::class, ['id' => 'task_type_id']); +} +``` + +#### TaskTemplates +```php +public function getTaskType() { + return $this->hasOne(TasksType::class, ['id' => 'task_type_id']); +} +``` + +--- + +## Методы + +### tableName(): string + +Возвращает имя таблицы базы данных. + +**Логика:** +- Статический метод, определяющий связь модели с таблицей `tasks_type` + +**Возвращает:** +- `string` - название таблицы `'tasks_type'` + +```php +public static function tableName() +{ + return 'tasks_type'; +} +``` + +### rules(): array + +Определяет правила валидации для атрибутов модели. + +**Логика:** +- Устанавливает требование обязательного заполнения основных полей +- Проверяет типы данных для каждого атрибута +- Устанавливает ограничения длины для строковых полей +- Валидирует формат JSON для config_json + +**Возвращает:** +- `array` - массив правил валидации + +```php +public function rules() +{ + return [ + [['name', 'icon', 'posit', 'color', 'config_json'], 'required'], + [['posit', 'alert_level_id'], 'integer'], + [['config_json'], 'string'], + [['name'], 'string', 'max' => 255], + [['icon'], 'string', 'max' => 200], + [['color'], 'string', 'max' => 15], + ]; +} +``` + +### attributeLabels(): array + +Возвращает метки для атрибутов модели. + +**Логика:** +- Определяет человекочитаемые названия атрибутов для использования в формах и сообщениях об ошибках + +**Возвращает:** +- `array` - ассоциативный массив меток атрибутов + +```php +public function attributeLabels() +{ + return [ + 'id' => 'ID', + 'name' => 'Name', + 'icon' => 'Icon', + 'posit' => 'Posit', + 'color' => 'Color', + 'config_json' => 'Config Json', + 'alert_level_id' => 'Alert Level Id', + ]; +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + Task }o--|| TasksType : "taskType" + TaskTemplates }o--|| TasksType : "taskType" + TasksType }o--|| TaskAlertLevel : "alertLevel" + + TasksType { + int id PK + string name + string icon + int posit + string color + string config_json + int alert_level_id FK + } + + Task { + int id PK + int task_type_id FK + string name + } + + TaskTemplates { + int id PK + int task_type_id FK + string name + } + + TaskAlertLevel { + int id PK + string name + } +``` + +--- + +## Формат config_json + +Поле `config_json` может содержать дополнительную конфигурацию для типа задачи: + +```json +{ + "default_duration": "02:00:00", + "default_expected_time": "01:30:00", + "requires_proof": true, + "auto_assign": false, + "notification_enabled": true, + "allow_subtasks": true, + "max_priority": 10, + "description_template": "Шаблон описания задачи" +} +``` + +--- + +## Примеры типов задач + +| ID | Название | Иконка | Цвет | Описание | +|----|----------|--------|------|----------| +| 1 | Проверка | fa-check-circle | #4CAF50 | Задачи на проверку документов/работы | +| 2 | Обучение | fa-graduation-cap | #2196F3 | Обучающие задачи | +| 3 | Срочная | fa-exclamation-triangle | #F44336 | Срочные задачи требующие немедленного внимания | +| 4 | Плановая | fa-calendar | #FF9800 | Регулярные плановые задачи | +| 5 | Анализ | fa-chart-bar | #9C27B0 | Задачи по анализу данных | + +--- + +## Примеры использования + +### Создание нового типа задачи + +```php +$taskType = new TasksType(); +$taskType->name = 'Инвентаризация'; +$taskType->icon = 'fa-boxes'; +$taskType->posit = 10; +$taskType->color = '#00BCD4'; +$taskType->config_json = json_encode([ + 'default_duration' => '04:00:00', + 'requires_proof' => true, + 'notification_enabled' => true +]); +$taskType->alert_level_id = 2; +$taskType->save(); +``` + +### Получение всех типов задач + +```php +$taskTypes = TasksType::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Получение типа задачи по ID + +```php +$taskType = TasksType::findOne(5); +echo $taskType->name; // "Анализ" +echo $taskType->color; // "#9C27B0" +``` + +### Получение конфигурации типа задачи + +```php +$taskType = TasksType::findOne(1); +$config = json_decode($taskType->config_json, true); + +if ($config['requires_proof']) { + echo "Тип задачи требует доказательства выполнения"; +} +``` + +### Получение задач определённого типа + +```php +$taskTypeId = 3; +$tasks = Task::find() + ->where(['task_type_id' => $taskTypeId]) + ->all(); +``` + +### Получение типов с определённым уровнем важности + +```php +$urgentTypes = TasksType::find() + ->where(['alert_level_id' => 3]) + ->all(); +``` + +### Обновление конфигурации типа + +```php +$taskType = TasksType::findOne(1); +$config = json_decode($taskType->config_json, true); +$config['auto_assign'] = true; +$config['max_executors'] = 5; +$taskType->config_json = json_encode($config); +$taskType->save(); +``` + +### Получение типов для выпадающего списка + +```php +$typesList = TasksType::find() + ->select(['id', 'name']) + ->orderBy(['posit' => SORT_ASC]) + ->indexBy('id') + ->column(); + +// Результат: [1 => 'Проверка', 2 => 'Обучение', ...] +``` + +### Подсчёт задач по типам + +```php +$stats = TasksType::find() + ->select(['tasks_type.id', 'tasks_type.name', 'COUNT(task.id) as task_count']) + ->leftJoin('task', 'task.task_type_id = tasks_type.id') + ->groupBy('tasks_type.id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['task_count']} задач\n"; +} +``` + +### Клонирование типа задачи + +```php +$original = TasksType::findOne(1); + +$clone = new TasksType(); +$clone->attributes = $original->attributes; +$clone->id = null; +$clone->name = $original->name . ' (копия)'; +$clone->posit = TasksType::find()->max('posit') + 1; +$clone->save(); +``` + +### Изменение порядка отображения типов + +```php +// Переместить тип на позицию выше +$taskType = TasksType::findOne(5); +$currentPosition = $taskType->posit; + +// Сдвигаем все типы с позицией меньше текущей +TasksType::updateAll( + ['posit' => new \yii\db\Expression('posit + 1')], + ['and', ['>=', 'posit', $currentPosition - 1], ['<', 'posit', $currentPosition]] +); + +$taskType->posit = $currentPosition - 1; +$taskType->save(); +``` + +--- + +## Использование в представлениях + +### Отображение иконки и цвета типа + +```php +$task = Task::findOne(123); +$taskType = $task->taskType; + +echo Html::tag('i', '', [ + 'class' => $taskType->icon, + 'style' => "color: {$taskType->color}" +]); +echo " {$taskType->name}"; +``` + +### Создание badge с типом задачи + +```php +$taskType = TasksType::findOne(1); + +echo Html::tag('span', $taskType->name, [ + 'class' => 'badge', + 'style' => "background-color: {$taskType->color}; color: white;" +]); +``` + +--- + +## Связанные модели + +- [Task](Task.md) - Задачи +- [TaskTemplates](TaskTemplates.md) - Шаблоны задач +- [TaskAlertLevel](TaskAlertLevel.md) - Уровни важности + +--- + +## Связанные документы + +- [TaskService](../services/TaskService.md) - Сервис работы с задачами + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TasksTypeSearch.md b/erp24/docs/models/TasksTypeSearch.md new file mode 100644 index 00000000..7f787057 --- /dev/null +++ b/erp24/docs/models/TasksTypeSearch.md @@ -0,0 +1,193 @@ +# Класс: TasksTypeSearch + + +## Mindmap + +```mermaid +mindmap + root((TasksTypeSearch)) + Таблица БД + ActiveRecord + Наследование + extends TasksType +``` + +## Назначение +Search-модель для поиска и фильтрации типов задач в ERP24. Справочник типов с настройками визуализации (иконка, цвет) и конфигурацией в JSON. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`TasksType` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `posit` — integer +- `name`, `icon`, `color`, `config_json` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска типов задач. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос TasksType::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, posit + - like: name, icon, color, config_json + +## Диаграмма структуры + +```mermaid +erDiagram + TasksType { + int id PK + varchar name + int posit + varchar icon + varchar color + json config_json + } + + Task { + int id PK + int task_type_id FK + varchar name + } + + Task }o--|| TasksType : "task_type_id" +``` + +## Диаграмма визуализации типов + +```mermaid +flowchart TD + A[TasksType] --> B[Визуализация] + + B --> C[icon] + C --> C1[fa-tasks] + C --> C2[fa-bug] + C --> C3[fa-clipboard] + + B --> D[color] + D --> D1[#FF0000 - Срочные] + D --> D2[#00FF00 - Обычные] + D --> D3[#0000FF - Плановые] + + E[config_json] --> F[Настройки типа] + F --> G[Поля формы] + F --> H[Валидация] + F --> I[Права] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new TasksTypeSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new TasksTypeSearch(); +$dataProvider = $searchModel->search([ + 'TasksTypeSearch' => [ + 'name' => 'Инвентаризация', + ] +]); +``` + +### Поиск по иконке +```php +$searchModel = new TasksTypeSearch(); +$dataProvider = $searchModel->search([ + 'TasksTypeSearch' => [ + 'icon' => 'fa-tasks', + ] +]); +``` + +### Поиск по цвету +```php +$searchModel = new TasksTypeSearch(); +$dataProvider = $searchModel->search([ + 'TasksTypeSearch' => [ + 'color' => '#FF0000', + ] +]); +``` + +### Поиск в конфигурации +```php +$searchModel = new TasksTypeSearch(); +$dataProvider = $searchModel->search([ + 'TasksTypeSearch' => [ + 'config_json' => 'deadline', // Найти типы с настройкой deadline + ] +]); +``` + +### GridView с визуализацией +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'posit', + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + return Html::tag('span', + Html::tag('i', '', ['class' => $model->icon]) . ' ' . $model->name, + ['style' => "color: {$model->color}"] + ); + }, + ], + 'icon', + 'color', + ], +]) ?> +``` + +## Связанные модели + +- [TasksType](./TasksType.md) — базовая модель типов +- [Task](./Task.md) — задачи +- [TaskTemplates](./TaskTemplates.md) — шаблоны задач + +## Особенности реализации + +1. **Позиционирование**: posit для сортировки в интерфейсе +2. **Визуализация**: icon и color для отображения в UI +3. **JSON конфигурация**: config_json для гибких настроек типа +4. **FontAwesome иконки**: icon содержит класс иконки +5. **like вместо ilike**: Регистрозависимый поиск +6. **Поиск в JSON**: like по config_json для поиска в конфигурации diff --git a/erp24/docs/models/TeambonusSettings.md b/erp24/docs/models/TeambonusSettings.md new file mode 100644 index 00000000..cfac9648 --- /dev/null +++ b/erp24/docs/models/TeambonusSettings.md @@ -0,0 +1,292 @@ +# Класс: TeambonusSettings + + +## Mindmap + +```mermaid +mindmap + root((TeambonusSettings)) + Таблица БД + teambonus_settings + Свойства + id + int + created_by + int + created_at + string + store_id + int + year + int + month + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель настроек командного бонуса (тимбонуса) в ERP24. Хранит помесячные проценты начисления тимбонуса по магазинам для расчёта коллективных премий сотрудников. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`teambonus_settings` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_id` | int | FK на магазин (CityStore) | +| `year` | int | Год начисления процента | +| `month` | int | Месяц начисления процента (1-12) | +| `procent` | float | Процент тимбонуса | +| `created_by` | int | FK на создателя (Admin) | +| `created_at` | datetime | Дата создания записи | + +## Диаграмма связей + +```mermaid +erDiagram + TeambonusSettings { + int id PK + int store_id FK + int year + int month + float procent + int created_by FK + datetime created_at + } + + CityStore { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + CityStore ||--o{ TeambonusSettings : "store_id" + Admin ||--o{ TeambonusSettings : "created_by" +``` + +## Диаграмма расчёта тимбонуса + +```mermaid +flowchart TD + A[Продажи магазина
    за месяц] --> B[Получить процент
    из TeambonusSettings] + B --> C{Процент найден?} + C -->|Да| D[Тимбонус =
    Продажи × Процент / 100] + C -->|Нет| E[Тимбонус = 0] + + D --> F[Распределить между
    сотрудниками] + F --> G[По часам работы] + F --> H[По KPI] + F --> I[Поровну] +``` + +## Примеры использования + +### Создание настройки +```php +$setting = new TeambonusSettings(); +$setting->store_id = $storeId; +$setting->year = 2024; +$setting->month = 12; +$setting->procent = 2.5; // 2.5% от продаж +$setting->created_by = Yii::$app->user->id; +$setting->created_at = date('Y-m-d H:i:s'); +$setting->save(); +``` + +### Получение процента для магазина на месяц +```php +$setting = TeambonusSettings::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->one(); + +if ($setting) { + echo "Процент тимбонуса: {$setting->procent}%"; +} +``` + +### Расчёт тимбонуса +```php +function calculateTeambonus($storeId, $year, $month, $salesSum) +{ + $setting = TeambonusSettings::find() + ->where([ + 'store_id' => $storeId, + 'year' => $year, + 'month' => $month + ]) + ->one(); + + if ($setting && $setting->procent > 0) { + return $salesSum * $setting->procent / 100; + } + + return 0; +} + +$teambonus = calculateTeambonus($storeId, 2024, 12, 1000000); +echo "Тимбонус: {$teambonus} руб."; // 25000 руб. при 2.5% +``` + +### Получение настроек за год +```php +$yearSettings = TeambonusSettings::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024 + ]) + ->orderBy(['month' => SORT_ASC]) + ->all(); + +foreach ($yearSettings as $setting) { + echo "Месяц {$setting->month}: {$setting->procent}%\n"; +} +``` + +### Массовое создание настроек на год +```php +$basePercent = 2.0; + +for ($month = 1; $month <= 12; $month++) { + $existing = TeambonusSettings::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => $month + ]) + ->exists(); + + if (!$existing) { + $setting = new TeambonusSettings(); + $setting->store_id = $storeId; + $setting->year = 2024; + $setting->month = $month; + $setting->procent = $basePercent; + $setting->created_by = Yii::$app->user->id; + $setting->created_at = date('Y-m-d H:i:s'); + $setting->save(); + } +} +``` + +### Сводка процентов по магазинам +```php +$summary = TeambonusSettings::find() + ->select([ + 'store_id', + 'AVG(procent) as avg_procent', + 'MIN(procent) as min_procent', + 'MAX(procent) as max_procent' + ]) + ->where(['year' => 2024]) + ->groupBy('store_id') + ->asArray() + ->all(); + +foreach ($summary as $row) { + echo "Магазин {$row['store_id']}: "; + echo "средний {$row['avg_procent']}%, "; + echo "мин {$row['min_procent']}%, "; + echo "макс {$row['max_procent']}%\n"; +} +``` + +### Обновление процента +```php +$setting = TeambonusSettings::find() + ->where([ + 'store_id' => $storeId, + 'year' => 2024, + 'month' => 12 + ]) + ->one(); + +if ($setting) { + $setting->procent = 3.0; + $setting->save(); +} else { + // Создать новую запись + $setting = new TeambonusSettings(); + $setting->store_id = $storeId; + $setting->year = 2024; + $setting->month = 12; + $setting->procent = 3.0; + $setting->created_by = Yii::$app->user->id; + $setting->created_at = date('Y-m-d H:i:s'); + $setting->save(); +} +``` + +### Копирование настроек на следующий год +```php +$previousYearSettings = TeambonusSettings::find() + ->where(['year' => 2024]) + ->all(); + +foreach ($previousYearSettings as $oldSetting) { + $newSetting = new TeambonusSettings(); + $newSetting->store_id = $oldSetting->store_id; + $newSetting->year = 2025; + $newSetting->month = $oldSetting->month; + $newSetting->procent = $oldSetting->procent; + $newSetting->created_by = Yii::$app->user->id; + $newSetting->created_at = date('Y-m-d H:i:s'); + $newSetting->save(); +} +``` + +### Сравнение процентов по месяцам +```php +$comparison = TeambonusSettings::find() + ->select(['month', 'AVG(procent) as avg_procent']) + ->where(['year' => 2024]) + ->groupBy('month') + ->orderBy(['month' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($comparison as $row) { + echo "Месяц {$row['month']}: средний процент {$row['avg_procent']}%\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, integer | +| `year` | integer | +| `month` | integer | +| `procent` | number | +| `created_by` | required, integer | +| `created_at` | required, safe | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Помесячное планирование**: Процент задаётся на каждый месяц отдельно +2. **Per-store настройки**: Процент может различаться по магазинам +3. **Гибкий процент**: Может изменяться от месяца к месяцу +4. **Аудит создания**: created_by для отслеживания авторства +5. **Расчёт бонуса**: Процент применяется к сумме продаж +6. **Год + месяц**: Составной ключ периода (year + month + store_id) diff --git a/erp24/docs/models/TechnicalRequestType.md b/erp24/docs/models/TechnicalRequestType.md new file mode 100644 index 00000000..627ba564 --- /dev/null +++ b/erp24/docs/models/TechnicalRequestType.md @@ -0,0 +1,260 @@ +# Класс: TechnicalRequestType + + +## Mindmap + +```mermaid +mindmap + root((TechnicalRequestType)) + Таблица БД + technical_request_type + Свойства + id + int + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник типов технических заявок в ERP24. Классификация заявок на техническое обслуживание, ремонт и IT-поддержку с шаблонами названий создаваемых задач и привязкой к функциональным подразделениям. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`technical_request_type` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(250) | Название типа заявки | +| `name_template` | varchar(250) / null | Шаблон названия задачи | +| `posit` | int / null | Позиция для сортировки | +| `company_function_id` | int / null | FK на функциональное подразделение | + +## Диаграмма связей + +```mermaid +erDiagram + TechnicalRequestType { + int id PK + varchar name + varchar name_template + int posit + int company_function_id FK + } + + TechnicalRequest { + int id PK + int type_id FK + varchar title + text description + } + + CompanyFunction { + int id PK + varchar name + } + + Task { + int id PK + varchar title + } + + TechnicalRequestType ||--o{ TechnicalRequest : "type_id" + CompanyFunction ||--o{ TechnicalRequestType : "company_function_id" + TechnicalRequest ||--o| Task : "создаёт" +``` + +## Диаграмма workflow заявки + +```mermaid +flowchart TD + A[Пользователь
    создаёт заявку] --> B[Выбор типа
    TechnicalRequestType] + B --> C[Автоматическое
    название задачи] + + C --> D{name_template?} + D -->|Есть| E[Применить шаблон
    'Ремонт: {описание}'] + D -->|Нет| F[Использовать
    описание заявки] + + E --> G[Создать Task] + F --> G + + G --> H[Назначить на
    company_function_id] +``` + +## Примеры типов заявок + +| name | name_template | company_function_id | +|------|---------------|---------------------| +| Ремонт оборудования | Ремонт: {description} | 5 (АХО) | +| IT-поддержка | IT: {description} | 3 (IT) | +| Электрика | Электрика: {description} | 5 (АХО) | +| Сантехника | Сантехника: {description} | 5 (АХО) | +| Кондиционирование | Климат: {description} | 5 (АХО) | +| Охрана | Охрана: {description} | 6 (СБ) | + +## Примеры использования + +### Создание типа заявки +```php +$type = new TechnicalRequestType(); +$type->name = 'Ремонт оборудования'; +$type->name_template = 'Ремонт: {description}'; +$type->posit = 10; +$type->company_function_id = $ahoFunctionId; +$type->save(); +``` + +### Получение всех типов +```php +$types = TechnicalRequestType::find() + ->orderBy(['posit' => SORT_ASC, 'name' => SORT_ASC]) + ->all(); + +foreach ($types as $type) { + echo "{$type->name}\n"; +} +``` + +### Формирование списка для выбора +```php +$typesList = ArrayHelper::map( + TechnicalRequestType::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(), + 'id', + 'name' +); + +echo Html::dropDownList('type_id', null, $typesList, [ + 'prompt' => 'Выберите тип заявки' +]); +``` + +### Генерация названия задачи по шаблону +```php +function generateTaskTitle($typeId, $description) +{ + $type = TechnicalRequestType::findOne($typeId); + + if ($type && $type->name_template) { + return str_replace('{description}', $description, $type->name_template); + } + + return $description; +} + +$title = generateTaskTitle($typeId, 'не работает кондиционер'); +// Результат: "Климат: не работает кондиционер" +``` + +### Получение типов по подразделению +```php +$itTypes = TechnicalRequestType::find() + ->where(['company_function_id' => $itFunctionId]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Создание заявки с автоматическим названием +```php +$request = new TechnicalRequest(); +$request->type_id = $typeId; +$request->description = 'Не печатает принтер HP LaserJet'; +$request->save(); + +// Создание задачи +$type = TechnicalRequestType::findOne($typeId); +$task = new Task(); +$task->title = generateTaskTitle($typeId, $request->description); +$task->company_function_id = $type->company_function_id; +$task->save(); +``` + +### Группировка по подразделениям +```php +$types = TechnicalRequestType::find() + ->with('companyFunction') + ->orderBy(['company_function_id' => SORT_ASC, 'posit' => SORT_ASC]) + ->all(); + +$grouped = []; +foreach ($types as $type) { + $functionName = $type->companyFunction->name ?? 'Без подразделения'; + $grouped[$functionName][] = $type; +} + +foreach ($grouped as $functionName => $typesList) { + echo "{$functionName}:\n"; + foreach ($typesList as $type) { + echo " - {$type->name}\n"; + } +} +``` + +### Обновление порядка сортировки +```php +$order = [5, 3, 1, 2, 4]; // Новый порядок ID + +foreach ($order as $position => $typeId) { + TechnicalRequestType::updateAll( + ['posit' => $position], + ['id' => $typeId] + ); +} +``` + +### Статистика заявок по типам +```php +$stats = TechnicalRequest::find() + ->select(['type_id', 'COUNT(*) as count']) + ->groupBy('type_id') + ->asArray() + ->all(); + +$types = ArrayHelper::index(TechnicalRequestType::find()->all(), 'id'); + +foreach ($stats as $stat) { + $typeName = $types[$stat['type_id']]->name ?? 'Неизвестный'; + echo "{$typeName}: {$stat['count']} заявок\n"; +} +``` + +### Поиск типа по названию +```php +$type = TechnicalRequestType::find() + ->where(['like', 'name', 'IT']) + ->one(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 250) | +| `name_template` | string (max 250) | +| `posit` | integer | +| `company_function_id` | integer | + +## Связанные модели + +- TechnicalRequest — технические заявки +- CompanyFunction — функциональные подразделения +- [Task](./Task.md) — задачи + +## Особенности реализации + +1. **Шаблон названия**: name_template для автогенерации названий задач +2. **Привязка к подразделению**: company_function_id для маршрутизации заявок +3. **Сортировка**: posit для управления порядком в списках +4. **Плейсхолдеры**: {description} в шаблоне заменяется на описание +5. **Классификация**: Разделение заявок по типам работ +6. **Автоматизация**: Автоматическое создание задач по заявкам diff --git a/erp24/docs/models/TechnicalRequestTypeSearch.md b/erp24/docs/models/TechnicalRequestTypeSearch.md new file mode 100644 index 00000000..f4371d4b --- /dev/null +++ b/erp24/docs/models/TechnicalRequestTypeSearch.md @@ -0,0 +1,178 @@ +# Класс: TechnicalRequestTypeSearch + + +## Mindmap + +```mermaid +mindmap + root((TechnicalRequestTypeSearch)) + Таблица БД + ActiveRecord + Наследование + extends TechnicalRequestType +``` + +## Назначение +Search-модель для поиска и фильтрации типов технических заявок в ERP24. Справочник типов заявок с привязкой к функции компании и шаблоном названия. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`TechnicalRequestType` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `posit`, `company_function_id` — integer +- `name`, `name_template` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска типов заявок. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос TechnicalRequestType::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, posit, company_function_id + - like: name, name_template + +## Диаграмма структуры + +```mermaid +erDiagram + TechnicalRequestType { + int id PK + varchar name + int posit + int company_function_id FK + varchar name_template + } + + CompanyFunction { + int id PK + varchar name + } + + TechnicalRequest { + int id PK + int type_id FK + varchar name + } + + TechnicalRequestType }o--|| CompanyFunction : "company_function_id" + TechnicalRequest }o--|| TechnicalRequestType : "type_id" +``` + +## Диаграмма типов заявок + +```mermaid +flowchart TD + A[TechnicalRequestType] --> B[Типы заявок] + + B --> C[IT-поддержка] + C --> C1[company_function_id = 1] + + B --> D[АХО] + D --> D1[company_function_id = 2] + + B --> E[Логистика] + E --> E1[company_function_id = 3] + + F[name_template] --> G[Шаблон названия] + G --> H[Заявка #{id} от {date}] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new TechnicalRequestTypeSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new TechnicalRequestTypeSearch(); +$dataProvider = $searchModel->search([ + 'TechnicalRequestTypeSearch' => [ + 'name' => 'Ремонт', + ] +]); +``` + +### Поиск по функции компании +```php +$searchModel = new TechnicalRequestTypeSearch(); +$dataProvider = $searchModel->search([ + 'TechnicalRequestTypeSearch' => [ + 'company_function_id' => 1, // IT + ] +]); +``` + +### Поиск по шаблону названия +```php +$searchModel = new TechnicalRequestTypeSearch(); +$dataProvider = $searchModel->search([ + 'TechnicalRequestTypeSearch' => [ + 'name_template' => 'Заявка', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'posit', + 'name', + [ + 'attribute' => 'company_function_id', + 'value' => 'companyFunction.name', + ], + 'name_template', + ], +]) ?> +``` + +## Связанные модели + +- [TechnicalRequestType](./TechnicalRequestType.md) — базовая модель типов +- [TechnicalRequest](./TechnicalRequest.md) — технические заявки +- [CompanyFunction](./CompanyFunction.md) — функции компании + +## Особенности реализации + +1. **Позиционирование**: posit для сортировки в интерфейсе +2. **Маршрутизация**: company_function_id определяет ответственный отдел +3. **Шаблон названия**: name_template для автогенерации названий заявок +4. **like вместо ilike**: Регистрозависимый поиск +5. **Привязка к функции**: Заявки автоматически направляются в нужный отдел diff --git a/erp24/docs/models/TemplatesReceiverType.md b/erp24/docs/models/TemplatesReceiverType.md new file mode 100644 index 00000000..91f95f70 --- /dev/null +++ b/erp24/docs/models/TemplatesReceiverType.md @@ -0,0 +1,135 @@ +# Модель TemplatesReceiverType + + +## Mindmap + +```mermaid +mindmap + root((TemplatesReceiverType)) + Таблица БД + templates_receiver_type + Свойства + id + int + name + string + alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TemplatesReceiverType` представляет справочник типов получателей для шаблонов (уведомлений, задач и т.д.). Определяет, кому могут направляться автоматические уведомления и сообщения по шаблонам. Используется для настройки маршрутизации коммуникаций в шаблонах. + +**Файл модели:** `erp24/records/TemplatesReceiverType.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `templates_receiver_type` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(100) | Отображаемое название типа получателя | +| `alias` | VARCHAR(100) | Системный алиас (уникальный) | + +--- + +## Типичные типы получателей + +| ID | alias | name | Описание | +|----|-------|------|----------| +| 1 | executor | Исполнитель | Исполнитель задачи | +| 2 | author | Автор | Создатель задачи | +| 3 | controller | Контролёр | Контролирующий сотрудник | +| 4 | mentor | Ментор | Наставник сотрудника | +| 5 | communicator | Коммуникатор | Ответственный за коммуникацию | +| 6 | store_director | Директор магазина | Руководитель торговой точки | +| 7 | cluster_manager | Кластер-менеджер | Руководитель кластера | +| 8 | specific_admin | Конкретный сотрудник | Выбранный сотрудник | + +--- + +## Примеры использования + +### Создание типа получателя + +```php +$type = new TemplatesReceiverType(); +$type->name = 'HR-менеджер'; +$type->alias = 'hr_manager'; +$type->save(); +``` + +### Получение всех типов + +```php +$types = TemplatesReceiverType::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +foreach ($types as $type) { + echo "{$type->alias}: {$type->name}\n"; +} +``` + +### Получение по алиасу + +```php +$type = TemplatesReceiverType::findOne(['alias' => 'executor']); + +if ($type) { + echo "ID типа 'executor': {$type->id}"; +} +``` + +### Формирование выпадающего списка + +```php +$types = TemplatesReceiverType::find() + ->select(['id', 'name']) + ->orderBy(['name' => SORT_ASC]) + ->all(); + +$dropdown = ArrayHelper::map($types, 'id', 'name'); +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + templates_receiver_type ||--o{ task_alert_level_data : "used_in" + + templates_receiver_type { + int id PK + string name + string alias UK + } +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, строка, макс. 100 символов | +| `alias` | Обязательное, уникальное, строка, макс. 100 символов | + +--- + +## Связанные модели + +- **[TaskAlertLevelData](./TaskAlertLevelData.md)** — настройки уведомлений + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/Terminals.md b/erp24/docs/models/Terminals.md new file mode 100644 index 00000000..94ff9844 --- /dev/null +++ b/erp24/docs/models/Terminals.md @@ -0,0 +1,229 @@ +# Класс: Terminals + + +## Mindmap + +```mermaid +mindmap + root((Terminals)) + Таблица БД + terminals + Свойства + id + string + name + string + store_id + string + code + string + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Справочник терминалов оплаты в ERP24. Хранит информацию о платёжных терминалах (POS-терминалах), синхронизированных из 1С, с привязкой к магазинам. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`terminals` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | varchar(36) | GUID терминала из 1С (PK) | +| `name` | varchar(255) | Название терминала | +| `store_id` | varchar(36) | GUID магазина из Products1c | +| `code` | varchar(35) | Код терминала в 1С | +| `posit` | int | Позиция для сортировки | +| `date` | datetime / null | Дата синхронизации | + +## Диаграмма связей + +```mermaid +erDiagram + Terminals { + varchar id PK + varchar name + varchar store_id FK + varchar code + int posit + datetime date + } + + Products1c { + varchar id PK + varchar name + varchar tip + } + + Sales { + int id PK + varchar terminal_id FK + } + + Products1c ||--o{ Terminals : "store_id" + Terminals ||--o{ Sales : "terminal_id" +``` + +## Диаграмма инфраструктуры + +```mermaid +flowchart TD + subgraph Магазин + A[POS-терминал 1
    Terminals] + B[POS-терминал 2
    Terminals] + C[Касса
    KKM] + end + + subgraph Банк + D[Процессинг] + end + + subgraph 1С + E[Справочник терминалов] + end + + A --> D + B --> D + A --> C + B --> C + E -.->|синхронизация| A + E -.->|синхронизация| B +``` + +## Примеры использования + +### Получение терминала по GUID +```php +$terminal = Terminals::findOne($guid); + +if ($terminal) { + echo "Терминал: {$terminal->name}"; + echo "Код: {$terminal->code}"; +} +``` + +### Получение всех терминалов +```php +$terminals = Terminals::find() + ->orderBy(['posit' => SORT_ASC, 'name' => SORT_ASC]) + ->all(); + +foreach ($terminals as $terminal) { + echo "{$terminal->name} ({$terminal->code})\n"; +} +``` + +### Терминалы магазина +```php +$storeTerminals = Terminals::find() + ->where(['store_id' => $storeGuid]) + ->orderBy(['posit' => SORT_ASC]) + ->all(); +``` + +### Формирование списка для выбора +```php +$terminalsList = ArrayHelper::map( + Terminals::find() + ->orderBy(['posit' => SORT_ASC]) + ->all(), + 'id', + 'name' +); + +echo Html::dropDownList('terminal_id', null, $terminalsList, [ + 'prompt' => 'Выберите терминал' +]); +``` + +### Поиск по коду +```php +$terminal = Terminals::find() + ->where(['code' => $code1c]) + ->one(); +``` + +### Импорт/синхронизация из 1С +```php +$terminalsFrom1c = [ + ['id' => 'guid1', 'name' => 'Терминал Сбер', 'store_id' => 'store-guid', 'code' => '001'], + ['id' => 'guid2', 'name' => 'Терминал Альфа', 'store_id' => 'store-guid', 'code' => '002'], +]; + +foreach ($terminalsFrom1c as $index => $data) { + $existing = Terminals::findOne($data['id']); + + if ($existing) { + $existing->name = $data['name']; + $existing->store_id = $data['store_id']; + $existing->code = $data['code']; + $existing->date = date('Y-m-d H:i:s'); + $existing->save(); + } else { + $terminal = new Terminals(); + $terminal->id = $data['id']; + $terminal->name = $data['name']; + $terminal->store_id = $data['store_id']; + $terminal->code = $data['code']; + $terminal->posit = $index; + $terminal->date = date('Y-m-d H:i:s'); + $terminal->save(); + } +} +``` + +### Статистика по магазинам +```php +$stats = Terminals::find() + ->select(['store_id', 'COUNT(*) as count']) + ->groupBy('store_id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "Магазин {$stat['store_id']}: {$stat['count']} терминалов\n"; +} +``` + +### Поиск терминала по названию +```php +$terminals = Terminals::find() + ->where(['like', 'name', 'Сбер']) + ->all(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `id` | required, string (max 36), unique | +| `name` | required, string (max 255) | +| `store_id` | required, string (max 36) | +| `code` | required, string (max 35) | +| `posit` | required, integer | +| `date` | safe | + +## Связанные модели + +- [Products1c](./Products1c.md) — магазины (store_id через tip='store') +- [Sales](./Sales.md) — продажи +- [CityStore](./CityStore.md) — магазины + +## Особенности реализации + +1. **GUID первичный ключ**: varchar(36) вместо auto-increment для синхронизации с 1С +2. **Привязка к магазину**: store_id содержит GUID магазина из Products1c +3. **Код 1С**: code для идентификации в бухгалтерской системе +4. **Сортировка**: posit для управления порядком в списках +5. **Синхронизация**: date фиксирует время последнего обновления из 1С +6. **Платёжная инфраструктура**: Связь с кассами и процессингом diff --git a/erp24/docs/models/TgSubscription.md b/erp24/docs/models/TgSubscription.md new file mode 100644 index 00000000..f4c09071 --- /dev/null +++ b/erp24/docs/models/TgSubscription.md @@ -0,0 +1,362 @@ +# Model: TgSubscription + + +## Mindmap + +```mermaid +mindmap + root((TgSubscription)) + Таблица БД + tg_subscription + Свойства + id + int + name + string + chat_id + int + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель подписок сотрудников на уведомления в Telegram. Хранит информацию о сотрудниках (администраторах), которые подписались на получение служебных уведомлений через Telegram. Используется для рассылки важных системных событий, оповещений о заказах, проблемах и других служебных сообщений персоналу. + +В отличие от клиентских подписок (UsersTelegram), эта таблица предназначена для внутренних пользователей системы. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`tg_subscription` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `name` | string(36) | **Имя пользователя** (имя сотрудника в Telegram) | +| `chat_id` | bigint | **Chat ID пользователя** в Telegram (уникальный идентификатор чата) | +| `active` | int | **Статус активности**: 0 - не активен, 1 - активен | + +--- + +## Правила валидации + +### Обязательные поля +```php +['name', 'chat_id'] +``` + +### Целочисленные поля с значением по умолчанию null +```php +['chat_id', 'active'] // integer, default: null +``` + +### Строковые поля +```php +['name'] // max:36 +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'name' => 'Name', + 'chat_id' => 'Chat ID', + 'active' => 'Active', +] +``` + +--- + +## Примеры использования + +### Добавление подписки сотрудника + +```php +use yii_app\records\TgSubscription; + +$subscription = new TgSubscription(); +$subscription->name = 'Иван Иванов'; +$subscription->chat_id = 123456789; +$subscription->active = 1; + +if ($subscription->save()) { + echo "Подписка создана"; +} +``` + +### Получение всех активных подписчиков + +```php +$activeSubscribers = TgSubscription::find() + ->where(['active' => 1]) + ->all(); + +foreach ($activeSubscribers as $subscriber) { + echo "Имя: {$subscriber->name}, Chat ID: {$subscriber->chat_id}\n"; +} +``` + +### Отправка уведомления всем активным подписчикам + +```php +$message = "⚠️ Важное системное уведомление: обнаружена проблема с синхронизацией"; + +$subscribers = TgSubscription::find() + ->where(['active' => 1]) + ->all(); + +foreach ($subscribers as $subscriber) { + // Отправка через Telegram Bot API + TelegramService::sendMessage($subscriber->chat_id, $message); +} +``` + +### Деактивация подписки + +```php +$chatId = 123456789; + +$subscription = TgSubscription::find() + ->where(['chat_id' => $chatId]) + ->one(); + +if ($subscription) { + $subscription->active = 0; + $subscription->save(); + echo "Подписка деактивирована"; +} +``` + +### Проверка наличия подписки + +```php +$chatId = 123456789; + +$exists = TgSubscription::find() + ->where(['chat_id' => $chatId, 'active' => 1]) + ->exists(); + +if ($exists) { + echo "Подписка активна"; +} else { + echo "Подписка отсутствует или неактивна"; +} +``` + +### Получение списка Chat ID для рассылки + +```php +$chatIds = TgSubscription::find() + ->select('chat_id') + ->where(['active' => 1]) + ->column(); + +// Массовая рассылка +foreach ($chatIds as $chatId) { + TelegramService::sendMessage($chatId, $broadcastMessage); +} +``` + +### Обновление имени подписчика + +```php +$chatId = 123456789; + +$subscription = TgSubscription::find() + ->where(['chat_id' => $chatId]) + ->one(); + +if ($subscription) { + $subscription->name = 'Иван Петров'; // Изменилось имя + $subscription->save(); +} +``` + +### Статистика подписчиков + +```php +$activeCount = TgSubscription::find() + ->where(['active' => 1]) + ->count(); + +$inactiveCount = TgSubscription::find() + ->where(['active' => 0]) + ->count(); + +echo "Активных подписок: {$activeCount}\n"; +echo "Неактивных подписок: {$inactiveCount}\n"; +``` + +--- + +## Бизнес-логика + +### Типы уведомлений + +Подписчики получают различные типы служебных уведомлений: + +1. **Критические ошибки** - сбои в работе системы +2. **Новые заказы** - уведомления о поступлении заказов +3. **Проблемы с интеграцией** - ошибки синхронизации с 1С +4. **Превышение лимитов** - достижение критических значений +5. **Системные события** - запуск/остановка служб +6. **Аналитические отчеты** - ежедневные/еженедельные сводки + +### Регистрация подписки + +Процесс регистрации: +1. Сотрудник отправляет команду `/subscribe` в служебный бот +2. Бот получает Chat ID из обновления +3. Проверяется, есть ли уже подписка с этим Chat ID +4. Если нет - создается новая запись с `active = 1` +5. Если есть - активируется существующая + +### Деактивация vs Удаление + +- **Деактивация** (`active = 0`) - временное отключение уведомлений +- **Удаление** - полное удаление записи (редко используется) + +Рекомендуется использовать деактивацию для сохранения истории подписок. + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + TgSubscription { + int id PK + string name + bigint chat_id UK + int active + } + + Admin ||--o| TgSubscription : "may have" + + Admin { + int id PK + string name + string email + } +``` + +--- + +## Диаграмма процесса рассылки уведомлений + +```mermaid +flowchart TD + A[Системное событие] --> B{Критическое?} + B -->|Да| C[Немедленная отправка] + B -->|Нет| D[Добавить в очередь] + + C --> E[Получить активных подписчиков] + D --> E + + E --> F[TgSubscription.find where active=1] + F --> G[Массив chat_id] + G --> H{Для каждого chat_id} + + H --> I[Отправка через Telegram Bot API] + I --> J{Успешно?} + J -->|Да| K[Логирование успеха] + J -->|Нет| L[Логирование ошибки] + + K --> M[Следующий подписчик] + L --> M + + style C fill:#FF6B6B + style E fill:#90EE90 + style I fill:#87CEEB +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE tg_subscription ADD PRIMARY KEY (id); + +-- Уникальный индекс на chat_id +CREATE UNIQUE INDEX idx_tg_subscription_chat_id ON tg_subscription(chat_id); + +-- Индекс для поиска активных подписок +CREATE INDEX idx_tg_subscription_active ON tg_subscription(active); + +-- Составной индекс для частых запросов +CREATE INDEX idx_tg_subscription_active_chat +ON tg_subscription(active, chat_id); +``` + +--- + +## Связанные модели + +- [Admin](Admin.md) - Сотрудники (потенциальная связь через имя или доп. поле) + +--- + +## Связанные сервисы + +- **TelegramNotificationService** - Отправка служебных уведомлений +- **AlertService** - Система оповещений +- **MonitoringService** - Мониторинг системы + +--- + +## API Endpoints + +- `POST /api/telegram/subscribe` - Подписка на уведомления +- `POST /api/telegram/unsubscribe` - Отписка от уведомлений +- `POST /api/telegram/send-notification` - Отправка уведомления +- `GET /api/telegram/subscribers` - Список подписчиков + +--- + +## Замечания + +1. **Chat ID уникален** - один чат может быть связан только с одной подпиской. + +2. **Имя ограничено** - max 36 символов, может содержать Telegram username или реальное имя. + +3. **Активность** - рекомендуется использовать деактивацию вместо удаления. + +4. **Служебные уведомления** - только для персонала, не для клиентов. + +5. **Безопасность** - необходимо валидировать, что подписывается авторизованный сотрудник. + +6. **Кэширование** - список активных chat_id можно кэшировать для быстрой рассылки. + +7. **Обработка ошибок** - при отправке могут возникать ошибки (пользователь заблокировал бота). + +8. **Отсутствие связи с Admin** - прямая связь не реализована, возможна через дополнительное поле admin_id. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/TimeDiffValue.md b/erp24/docs/models/TimeDiffValue.md new file mode 100644 index 00000000..0b87a5e2 --- /dev/null +++ b/erp24/docs/models/TimeDiffValue.md @@ -0,0 +1,188 @@ +# Класс: TimeDiffValue + + +## Mindmap + +```mermaid +mindmap + root((TimeDiffValue)) + Таблица БД + ActiveRecord + Наследование + extends ActiveRecord +``` + +## Назначение +Вспомогательный класс для форматированного отображения временных интервалов в ERP24. Конвертирует строковое представление интервала (например, "5:h") в читаемый текст ("5 часов"). + +## Пространство имён +`yii_app\records` + +## Родительский класс +Нет (обычный PHP-класс, не ActiveRecord) + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$field` | string | Исходная строка формата "значение:единица" | + +## Формат входных данных + +Строка формата `{число}:{единица}`, где единица: +- `m` — минуты +- `h` — часы +- `d` — дни + +## Методы + +### __construct($field) +**Описание:** Конструктор класса. + +**Параметры:** +- `$field` (string) — строка формата "значение:единица" + +**Пример:** +```php +$time = new TimeDiffValue("30:m"); +$time = new TimeDiffValue("5:h"); +$time = new TimeDiffValue("7:d"); +``` + +### __toString(): string +**Описание:** Преобразует объект в читаемую строку при приведении к string. + +**Возвращает:** `string` — форматированная строка вида "5 часов" + +**Логика работы:** +1. Разбивает строку по символу ":" +2. Определяет единицу измерения по второму элементу +3. Формирует строку с локализованным названием единицы + +**Пример:** +```php +$time = new TimeDiffValue("30:m"); +echo $time; // "30 минут" + +$time = new TimeDiffValue("5:h"); +echo $time; // "5 часов" + +$time = new TimeDiffValue("7:d"); +echo $time; // "7 дней" +``` + +## Диаграмма работы класса + +```mermaid +flowchart TD + A[Входная строка
    '5:h'] --> B[Конструктор
    __construct] + B --> C[Сохранение в $field] + + C --> D{Приведение к string} + D --> E[explode по ':'] + E --> F{Определение единицы} + + F -->|m| G[' минут'] + F -->|h| H[' часов'] + F -->|d| I[' дней'] + + G --> J[Результат: '5 минут'] + H --> K[Результат: '5 часов'] + I --> L[Результат: '7 дней'] +``` + +## Таблица единиц измерения + +| Код | Название | Пример входа | Пример выхода | +|-----|----------|--------------|---------------| +| `m` | минуты | "30:m" | "30 минут" | +| `h` | часы | "5:h" | "5 часов" | +| `d` | дни | "7:d" | "7 дней" | + +## Примеры использования + +### Базовое использование +```php +$interval = new TimeDiffValue("120:m"); +echo "Интервал: {$interval}"; // "Интервал: 120 минут" +``` + +### В массиве настроек +```php +$settings = [ + 'notification_delay' => new TimeDiffValue("30:m"), + 'session_timeout' => new TimeDiffValue("2:h"), + 'data_retention' => new TimeDiffValue("30:d"), +]; + +foreach ($settings as $name => $value) { + echo "{$name}: {$value}\n"; +} +// notification_delay: 30 минут +// session_timeout: 2 часов +// data_retention: 30 дней +``` + +### В шаблонах представлений +```php +// Данные из БД или конфигурации +$reminderTime = "15:m"; + +// Отображение пользователю +$formatted = new TimeDiffValue($reminderTime); +echo "Напоминание придёт через {$formatted}"; +// "Напоминание придёт через 15 минут" +``` + +### В формах настроек +```php +$intervals = [ + '5:m' => new TimeDiffValue('5:m'), + '15:m' => new TimeDiffValue('15:m'), + '30:m' => new TimeDiffValue('30:m'), + '1:h' => new TimeDiffValue('1:h'), + '2:h' => new TimeDiffValue('2:h'), +]; + +$options = []; +foreach ($intervals as $value => $label) { + $options[$value] = (string) $label; +} + +echo Html::dropDownList('interval', null, $options); +``` + +### Конвертация для отчётов +```php +function formatTimeInterval($value, $unit) +{ + return new TimeDiffValue("{$value}:{$unit}"); +} + +$reportData = [ + ['name' => 'Среднее время обработки', 'value' => 45, 'unit' => 'm'], + ['name' => 'Время доставки', 'value' => 3, 'unit' => 'h'], +]; + +foreach ($reportData as $row) { + $formatted = formatTimeInterval($row['value'], $row['unit']); + echo "{$row['name']}: {$formatted}\n"; +} +``` + +## Связанные модели + +Класс используется для форматирования временных интервалов в различных местах системы: +- Настройки уведомлений +- Таймауты сессий +- Интервалы синхронизации +- Отчёты по времени + +## Особенности реализации + +1. **Value Object**: Неизменяемый объект-значение для временных интервалов +2. **Магический метод**: __toString() для автоматического преобразования в строку +3. **Локализация**: Русские названия единиц измерения (минут, часов, дней) +4. **Простой формат**: Компактное хранение "значение:единица" +5. **Не ActiveRecord**: Обычный PHP-класс, не связан с БД +6. **Примечание**: В коде двойной пробел перед единицей измерения (особенность реализации) diff --git a/erp24/docs/models/TimetableFact.md b/erp24/docs/models/TimetableFact.md new file mode 100644 index 00000000..eaa537a2 --- /dev/null +++ b/erp24/docs/models/TimetableFact.md @@ -0,0 +1,577 @@ +# Class: TimetableFact + + +## Mindmap + +```mermaid +mindmap + root((TimetableFact)) + Таблица БД + ActiveRecord + Свойства + plan_id + int + Связи + Plan + 1:1 TimetablePlan + Наследование + extends Timetable +``` + +## Назначение + +Модель `TimetableFact` представляет фактическую отработку сотрудников в системе ERP24. Это подкласс модели `Timetable`, использующий паттерн Single Table Inheritance (STI) для работы с записями типа `TABLE_FACT` (tabel = 1). Модель создается на основе плановых смен и отметок времени сотрудников, используется для расчета заработной платы, контроля дисциплины и формирования отчетов. Фактические записи не поддерживают мягкое удаление и имеют строгую привязку к плановым сменам. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii_app\records\Timetable` + +## Таблица базы данных + +`timetable` (наследуется от родителя) + +## Свойства + +Наследует все свойства от `Timetable` плюс: + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `plan_id` | int | да | ID связанной плановой смены | +| `plan` | TimetablePlan | - | Связанная плановая смена (read-only) | +| `late` | \DateInterval | - | Интервал опоздания (read-only) | +| `early` | \DateInterval | - | Интервал раннего ухода (read-only) | + +## Методы + +### `rules()` + +**Описание:** Расширяет правила валидации родительского класса специфичными проверками для фактических смен. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** + +Метод объединяет три набора правил: + +1. **Установка типа табеля и привязка к плану**: + - `plan_id` обязателен и должен существовать в таблице TimetablePlan + - `tabel` по умолчанию устанавливается в `TABLE_FACT` (1) + - `tabel` обязательно должен быть равен `TABLE_FACT` + +2. **Родительские правила**: Наследует все правила валидации от `Timetable` + +3. **Валидация времени для фактических смен**: + - `datetime_start` не может быть в будущем (смена еще не началась) + - `datetime_end` не может быть в будущем (смена еще не закончилась) + - Обе проверки выполняются одним валидатором + +**Вызовы сторонних методов:** +- `parent::rules()` - получение правил валидации от Timetable +- `array_merge()` - объединение массивов правил +- `TimetablePlan::find()` - проверка существования плана +- `new \DateTime()` - получение текущего времени +- `$this->addError()` - добавление ошибки валидации + +**Пример:** +```php +$fact = new TimetableFact(); +$fact->plan_id = 1; +$fact->datetime_start = '2025-12-01 09:00:00'; // будущая дата +$fact->datetime_end = '2025-12-01 18:00:00'; + +if (!$fact->validate()) { + // Ошибка: "Смена ещё не началась" +} +``` + +--- + +### `hasSoftDelete()` + +**Описание:** Указывает, что модель НЕ использует мягкое удаление. + +**Возвращает:** `bool` - false + +**Логика работы:** +Переопределяет родительский метод, возвращая false. Это означает, что фактические записи удаляются физически из базы данных, а не помечаются как неактивные. Фактические данные требуют постоянного хранения для бухгалтерии и аудита, поэтому их нельзя удалять обычным способом. + +**Пример:** +```php +$fact = TimetableFact::findOne(1); +$fact->delete(); // Физическое удаление из БД (не рекомендуется) +``` + +--- + +### `find()` + +**Описание:** Переопределенный метод поиска с автоматической фильтрацией только фактических записей. + +**Возвращает:** `ActiveQuery` - запрос с условием `tabel = TABLE_FACT` + +**Логика работы:** +1. Вызывает родительский метод `Timetable::find()` +2. Добавляет условие `tabel = TABLE_FACT` (1) +3. НЕ фильтрует по полю `active`, так как `hasSoftDelete()` возвращает false + +**Вызовы сторонних методов:** +- `parent::find()` - создание базового запроса +- `$query->andWhere()` - добавление условия WHERE + +**Пример:** +```php +// Автоматически выбираются только фактические смены +$facts = TimetableFact::find() + ->where(['admin_id' => 10]) + ->andWhere(['>=', 'date', '2025-11-01']) + ->all(); +``` + +--- + +### `isAbsent()` + +**Описание:** Проверяет, отсутствовал ли сотрудник на смене (неявка). + +**Возвращает:** `bool` - true если сотрудник не явился + +**Логика работы:** +Проверяет равенство времени начала и окончания смены. Если `datetime_start === datetime_end`, это означает, что сотрудник не явился на работу (создан факт с нулевым рабочим временем методом `TimetablePlan::makeFact()` без отметок). + +**Пример:** +```php +$fact = TimetableFact::findOne(1); + +if ($fact->isAbsent()) { + echo "Сотрудник отсутствовал на смене"; + // Применить штраф или отметить прогул +} else { + echo "Отработано: {$fact->work_time} часов"; +} +``` + +--- + +### `getPlan()` + +**Описание:** Определяет связь с плановой сменой. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `TimetablePlan` через поле `plan_id`. Позволяет получить доступ к плановой смене, на основе которой был создан факт. Через план можно получить информацию об отклонениях (опоздания, ранние уходы). + +**Пример:** +```php +$fact = TimetableFact::findOne(1); +$plan = $fact->plan; + +echo "План: {$plan->datetime_start} - {$plan->datetime_end}\n"; +echo "Факт: {$fact->datetime_start} - {$fact->datetime_end}\n"; +echo "Отклонение: {$fact->getSkippedHours()} часов"; +``` + +--- + +### `getSkippedHours()` + +**Описание:** Вычисляет количество недоработанных часов относительно плана. + +**Возвращает:** `float` - количество часов (положительное если недоработал, отрицательное если переработал) + +**Логика работы:** +1. Получает плановое рабочее время из связанного плана (`$this->plan->work_time`) +2. Вычитает фактическое рабочее время (`$this->work_time`) +3. Возвращает разницу + +**Формула:** +``` +skipped_hours = plan.work_time - fact.work_time +``` + +**Вызовы сторонних методов:** +- `$this->plan->work_time` - получение планового времени через связь + +**Пример:** +```php +$fact = TimetableFact::findOne(1); +$skipped = $fact->getSkippedHours(); + +if ($skipped > 0) { + echo "Недоработано: {$skipped} часов"; +} elseif ($skipped < 0) { + echo "Переработка: " . abs($skipped) . " часов"; +} else { + echo "Отработано точно по плану"; +} + +// Использование для расчета штрафов +if ($skipped > 1) { + $penalty = $skipped * $hourlyRate * 0.5; // 50% от ставки + echo "Штраф: {$penalty} руб."; +} +``` + +--- + +### `getLate()` + +**Описание:** Вычисляет интервал опоздания на смену. + +**Возвращает:** `\DateInterval` - интервал между фактическим и плановым началом + +**Логика работы:** +1. Получает фактическое время начала работы (`$this->getDateTimeStart()`) +2. Получает плановое время начала из связанного плана (`$this->plan->getDateTimeStart()`) +3. Вычисляет разницу через метод `diff()` +4. Возвращает DateInterval объект + +**Вызовы сторонних методов:** +- `$this->getDateTimeStart()` - фактическое время начала +- `$this->plan->getDateTimeStart()` - плановое время начала +- `$dateTime->diff()` - вычисление интервала + +**Примечание:** Интервал всегда положительный. Для определения направления (раньше/позже) используйте свойство `invert`: +- `invert === 0` - пришел раньше или вовремя +- `invert === 1` - опоздал + +**Пример:** +```php +$fact = TimetableFact::findOne(1); +$late = $fact->getLate(); + +if ($late->invert === 1) { + echo "Опоздание: {$late->h} часов {$late->i} минут"; +} else { + echo "Пришел вовремя или раньше на {$late->h}ч {$late->i}м"; +} +``` + +--- + +### `getEarly()` + +**Описание:** Вычисляет интервал раннего ухода со смены. + +**Возвращает:** `\DateInterval` - интервал между фактическим и плановым окончанием + +**Логика работы:** +1. Получает фактическое время окончания работы (`$this->getDateTimeEnd()`) +2. Получает плановое время окончания из связанного плана (`$this->plan->getDateTimeEnd()`) +3. Вычисляет разницу через метод `diff()` +4. Возвращает DateInterval объект + +**Вызовы сторонних методов:** +- `$this->getDateTimeEnd()` - фактическое время окончания +- `$this->plan->getDateTimeEnd()` - плановое время окончания +- `$dateTime->diff()` - вычисление интервала + +**Примечание:** Проверяйте `invert` для определения направления: +- `invert === 0` - ушел раньше +- `invert === 1` - работал дольше (переработка) + +**Пример:** +```php +$fact = TimetableFact::findOne(1); +$early = $fact->getEarly(); + +if ($early->invert === 0) { + echo "Ушел раньше на: {$early->h} часов {$early->i} минут"; + // Вычесть из зарплаты +} else { + echo "Переработка: {$early->h} часов {$early->i} минут"; + // Добавить к зарплате +} +``` + +--- + +## Примеры использования + +### 1. Создание фактической записи вручную + +```php +$fact = new TimetableFact(); +$fact->tabel = Timetable::TABLE_FACT; +$fact->plan_id = 1; // ID плановой смены +$fact->admin_id = 10; +$fact->store_id = 5; +$fact->shift_id = 1; +$fact->d_id = 3; +$fact->date = '2025-11-27'; +$fact->datetime_start = '2025-11-27 09:15:00'; // опоздал на 15 минут +$fact->datetime_end = '2025-11-27 17:45:00'; // ушел раньше на 15 минут +$fact->slot_type_id = Timetable::TIMESLOT_WORK; + +if ($fact->save()) { + $skipped = $fact->getSkippedHours(); + echo "Факт создан. Недоработано: {$skipped} часов"; +} +``` + +### 2. Анализ отклонений от плана + +```php +$fact = TimetableFact::findOne(1); +$plan = $fact->plan; + +echo "Плановая смена:\n"; +echo " Начало: {$plan->datetime_start}\n"; +echo " Окончание: {$plan->datetime_end}\n"; +echo " Рабочее время: {$plan->work_time}ч\n\n"; + +echo "Фактическая смена:\n"; +echo " Начало: {$fact->datetime_start}\n"; +echo " Окончание: {$fact->datetime_end}\n"; +echo " Рабочее время: {$fact->work_time}ч\n\n"; + +$late = $fact->getLate(); +if ($late->invert === 1) { + echo "Опоздание: {$late->h}ч {$late->i}м\n"; +} + +$early = $fact->getEarly(); +if ($early->invert === 0) { + echo "Ранний уход: {$early->h}ч {$early->i}м\n"; +} + +$skipped = $fact->getSkippedHours(); +echo "Недоработано: {$skipped} часов"; +``` + +### 3. Отчет по отсутствующим сотрудникам + +```php +$date = '2025-11-27'; +$absentFacts = TimetableFact::find() + ->where(['date' => $date]) + ->all(); + +foreach ($absentFacts as $fact) { + if ($fact->isAbsent()) { + echo "{$fact->admin->name_full} - отсутствовал в {$fact->store->name}\n"; + } +} +``` + +### 4. Расчет зарплаты с учетом недоработок + +```php +$adminId = 10; +$month = '2025-11'; +$hourlyRate = 250; // руб/час + +$facts = TimetableFact::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['like', 'date', $month]) + ->all(); + +$totalWorked = 0; +$totalPlanned = 0; +$penalties = 0; + +foreach ($facts as $fact) { + if ($fact->isAbsent()) { + // Полный штраф за смену + $penalties += $fact->plan->work_time * $hourlyRate; + continue; + } + + $totalWorked += $fact->work_time; + $totalPlanned += $fact->plan->work_time; + + $skipped = $fact->getSkippedHours(); + if ($skipped > 0) { + // Штраф 50% от недоработанных часов + $penalties += $skipped * $hourlyRate * 0.5; + } +} + +$salary = $totalWorked * $hourlyRate - $penalties; + +echo "Отработано: {$totalWorked} / {$totalPlanned} часов\n"; +echo "Штрафы: {$penalties} руб\n"; +echo "Зарплата: {$salary} руб"; +``` + +### 5. Статистика опозданий за период + +```php +$adminId = 10; +$dateFrom = '2025-11-01'; +$dateTo = '2025-11-30'; + +$facts = TimetableFact::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['>=', 'date', $dateFrom]) + ->andWhere(['<=', 'date', $dateTo]) + ->all(); + +$lateCount = 0; +$totalLateMinutes = 0; + +foreach ($facts as $fact) { + if ($fact->isAbsent()) continue; + + $late = $fact->getLate(); + if ($late->invert === 1 && ($late->h > 0 || $late->i > 0)) { + $lateCount++; + $totalLateMinutes += $late->h * 60 + $late->i; + } +} + +echo "Количество опозданий: {$lateCount}\n"; +echo "Общее время опозданий: " . floor($totalLateMinutes / 60) . "ч " . ($totalLateMinutes % 60) . "м"; +``` + +### 6. Проверка переработок + +```php +$facts = TimetableFact::find() + ->where(['admin_id' => 10]) + ->andWhere(['like', 'date', '2025-11']) + ->all(); + +$overtimeHours = 0; + +foreach ($facts as $fact) { + if ($fact->isAbsent()) continue; + + $skipped = $fact->getSkippedHours(); + if ($skipped < 0) { // переработка + $overtimeHours += abs($skipped); + } +} + +if ($overtimeHours > 0) { + $overtimePay = $overtimeHours * $hourlyRate * 1.5; // 150% за переработку + echo "Переработка: {$overtimeHours} часов\n"; + echo "Доплата: {$overtimePay} руб"; +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + TIMETABLE_FACT ||--|| TIMETABLE_PLAN : "based on" + TIMETABLE_FACT ||--|| ADMIN : "worked by" + TIMETABLE_FACT ||--|| CITY_STORE : "in store" + TIMETABLE_FACT ||--|| SHIFT : "shift type" + + TIMETABLE_FACT { + int id PK + int plan_id FK "обязательная связь" + int tabel "always 1" + int admin_id FK + datetime datetime_start "фактическое начало" + datetime datetime_end "фактическое окончание" + float work_time "фактические часы" + string comment + } + + TIMETABLE_PLAN { + int id PK + int tabel "always 0" + datetime datetime_start "плановое начало" + datetime datetime_end "плановое окончание" + float work_time "плановые часы" + } +``` + +## Диаграмма расчета отклонений + +```mermaid +flowchart TD + A[TimetableFact] --> B{isAbsent?} + B -->|Да| C[Полный прогул] + B -->|Нет| D[getLate] + D --> E{Опоздание?} + E -->|Да| F[Рассчитать штраф за опоздание] + E -->|Нет| G[getEarly] + G --> H{Ранний уход?} + H -->|Да| I[Рассчитать штраф за ранний уход] + H -->|Нет| J[getSkippedHours] + J --> K{Недоработка?} + K -->|Да| L[Вычесть из зарплаты] + K -->|Нет| M{Переработка?} + M -->|Да| N[Добавить доплату] + M -->|Нет| O[Полная отработка] +``` + +## Особенности реализации + +### 1. Отсутствие мягкого удаления + +TimetableFact переопределяет `hasSoftDelete()`: +- Возвращает false +- Записи удаляются физически +- Требуется особая осторожность при удалении +- Рекомендуется вообще не удалять фактические данные + +### 2. Строгая привязка к плану + +Каждый факт обязательно связан с планом: +- `plan_id` обязателен для сохранения +- Валидация проверяет существование плана +- Через план вычисляются все отклонения + +### 3. Валидация времени + +Факт нельзя создать для будущих смен: +- `datetime_start` должен быть <= now +- `datetime_end` должен быть <= now +- Защита от ошибочного ввода + +### 4. Вычисление отклонений + +Все методы отклонений используют связь с планом: +- `getLate()` - через `plan->getDateTimeStart()` +- `getEarly()` - через `plan->getDateTimeEnd()` +- `getSkippedHours()` - через `plan->work_time` + +## Связанные компоненты + +### Модели +- `Timetable` - родительский класс +- `TimetablePlan` - плановая смена +- `Admin` - сотрудники +- `CityStore` - магазины +- `Shift` - справочник смен + +### Сервисы +- `SalaryService` - расчет заработной платы +- `ReportService` - отчеты по отработке +- `PenaltyService` - расчет штрафов + +### Контроллеры +- `TimetableController` - управление табелем +- `SalaryController` - расчет зарплаты + +## Примечания + +### Важные особенности + +1. **Неизменяемость**: После создания факта он не должен изменяться (для бухгалтерии) + +2. **Признак отсутствия**: `datetime_start === datetime_end` означает неявку + +3. **Интервалы с направлением**: DateInterval имеет свойство `invert` для определения направления + +4. **Обязательный план**: Факт не может существовать без плана + +### Рекомендации + +1. **Не удаляйте факты**: Храните все фактические данные для аудита + +2. **Проверяйте invert**: При работе с `getLate()` и `getEarly()` всегда проверяйте направление + +3. **Используйте getSkippedHours()**: Универсальный метод для расчета отклонений + +4. **Создавайте через makeFact()**: Используйте метод TimetablePlan для автоматического создания + +5. **Проверяйте isAbsent()**: Перед расчетом зарплаты проверяйте явку сотрудника diff --git a/erp24/docs/models/TimetableFactModel.md b/erp24/docs/models/TimetableFactModel.md new file mode 100644 index 00000000..2037e6ae --- /dev/null +++ b/erp24/docs/models/TimetableFactModel.md @@ -0,0 +1,370 @@ +# Класс: TimetableFactModel + + +## Mindmap + +```mermaid +mindmap + root((TimetableFactModel)) + Таблица БД + timetable_fact + Свойства + id + int + admin_id + int + store_id + int + admin_group_id + int + d_id + int + is_opening + bool + Связи + CheckIns + 1:N AdminCheckin + CheckInCount + 1:N AdminCheckin + Admin + 1:1 Admin + CheckinStart + 1:1 AdminCheckin + CheckinEnd + 1:1 AdminCheckin + Наследование + extends ActiveRecord +``` + +## Назначение +Модель фактического табеля (учёта рабочего времени) в ERP24. Фиксирует реальные отработанные смены сотрудников на основе чекинов — открытия и закрытия смен через мобильное приложение или терминал. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`timetable_fact` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы + +```php +const WORK_HOURS_TIME = 12; // Максимальное количество рабочих часов в смене +``` + +## Поля таблицы + +### Идентификация +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `admin_id` | int | FK на сотрудника (Admin) | +| `store_id` | int | FK на магазин (CityStore) | +| `admin_group_id` | int | FK на должность сотрудника (AdminGroup) | +| `d_id` | int | FK на должность, в которой работал на смене | + +### Время смены +| Поле | Тип | Описание | +|------|-----|----------| +| `date` | date | Дата смены | +| `date_start` | date | Дата открытия смены | +| `date_end` | date | Дата закрытия смены | +| `time_start` | time | Время начала работы по графику | +| `time_end` | time | Время окончания работы по графику | +| `work_time` | float | Отработано часов | + +### Статус смены +| Поле | Тип | Описание | +|------|-----|----------| +| `is_opening` | bool | Флаг открытия смены | +| `is_close` | bool | Флаг закрытия смены | +| `autoclosed` | bool | Автоматически закрыта (0 = нет, 1 = да) | +| `status` | int | Статус смены (TYPE_START/TYPE_END) | +| `tabel` | int | Табель (1 = по плану, 0 = вне плана) | + +### Оплата +| Поле | Тип | Описание | +|------|-----|----------| +| `price_hour` | float | Часовая ставка | +| `salary_shift` | float | Оплата за смену | +| `shift_id` | int | FK на тип смены (Shift) | +| `slot_type_id` | float | Тип занятости | + +### Связи с планом +| Поле | Тип | Описание | +|------|-----|----------| +| `plan_id` | int | FK на плановый табель (Timetable) | +| `admin_id_add` | int | FK на поставившего смену (Admin) | +| `date_add` | datetime | Дата постановки смены | +| `comment` | text | Комментарий | + +### Чекины +| Поле | Тип | Описание | +|------|-----|----------| +| `checkin_start_id` | int | FK на отметку о начале работы (AdminCheckin) | +| `checkin_end_id` | int | FK на отметку об окончании (AdminCheckin) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getAdmin()` | hasOne | Admin | Сотрудник | +| `getStore()` | hasOne | CityStore | Магазин | +| `getShift()` | hasOne | Shift | Тип смены | +| `getPlan()` | hasOne | Timetable | Плановая смена | +| `getAdminAdd()` | hasOne | Admin | Поставивший смену | +| `getD()` | hasOne | AdminGroup | Должность на смене | +| `getAdminGroup()` | hasOne | AdminGroup | Должность сотрудника | +| `getCheckinStart()` | hasOne | AdminCheckin | Чекин открытия | +| `getCheckinEnd()` | hasOne | AdminCheckin | Чекин закрытия | +| `getCheckIns()` | hasMany | AdminCheckin | Все чекины за смену | + +## Статические методы + +### setValues($adminCheckin, $is_start = true) +**Описание:** Создаёт или обновляет фактическую смену на основе чекина сотрудника. + +**Параметры:** +- `$adminCheckin` (AdminCheckin) — объект чекина +- `$is_start` (bool) — флаг открытия смены (true = открытие, false = закрытие) + +**Логика:** +1. Ищет существующую открытую смену за текущий/предыдущий день +2. Если найдена — закрывает её (заполняет date_end, time_end, work_time) +3. Если не найдена — создаёт новую смену с данными из чекина и планового графика + +**Пример:** +```php +TimetableFactModel::setValues($checkin, true); // Открытие смены +TimetableFactModel::setValues($checkin, false); // Закрытие смены +``` + +### getLast($adminId, $date, $opening = true) +**Описание:** Получает последнюю открытую/закрытую фактическую смену сотрудника. + +**Параметры:** +- `$adminId` (int) — ID сотрудника +- `$date` (string) — дата смены +- `$opening` (bool) — искать открытую (true) или закрытую (false) смену + +**Возвращает:** `TimetableFactModel|null` + +**Пример:** +```php +$openShift = TimetableFactModel::getLast($adminId, '2024-12-15', true); +``` + +### getClosedShiftData($admin_id, $date_start, $date_end) +**Описание:** Получает все закрытые смены сотрудника за период. + +**Параметры:** +- `$admin_id` (int) — ID сотрудника +- `$date_start` (string) — начало периода +- `$date_end` (string) — конец периода + +**Возвращает:** `array` — массив TimetableFactModel + +**Пример:** +```php +$shifts = TimetableFactModel::getClosedShiftData($adminId, '2024-12-01', '2024-12-31'); +``` + +## Методы экземпляра + +### getCheckInCount() +**Описание:** Возвращает количество чекинов за время смены. + +**Возвращает:** `int` + +### isWorkSlot() +**Описание:** Проверяет, является ли запись рабочей сменой. + +**Возвращает:** `bool` — всегда `true` + +## Диаграмма связей + +```mermaid +erDiagram + TimetableFactModel { + int id PK + int admin_id FK + int store_id FK + int admin_group_id FK + int d_id FK + date date + datetime date_start + datetime date_end + time time_start + time time_end + float work_time + bool is_opening + bool is_close + int status + float salary_shift + float price_hour + int plan_id FK + int checkin_start_id FK + int checkin_end_id FK + } + + Admin { + int id PK + varchar name + } + + CityStore { + int id PK + varchar name + } + + Timetable { + int id PK + } + + AdminCheckin { + int id PK + int type + } + + Shift { + int id PK + varchar name + } + + Admin ||--o{ TimetableFactModel : "admin_id" + CityStore ||--o{ TimetableFactModel : "store_id" + Timetable ||--o| TimetableFactModel : "plan_id" + AdminCheckin ||--o| TimetableFactModel : "checkin_start_id" + AdminCheckin ||--o| TimetableFactModel : "checkin_end_id" + Shift ||--o{ TimetableFactModel : "shift_id" +``` + +## Диаграмма жизненного цикла смены + +```mermaid +flowchart TD + A[Сотрудник
    начинает работу] --> B[Чекин TYPE_START] + B --> C[setValues: is_start=true] + C --> D[Создание TimetableFactModel
    status=TYPE_START
    is_opening=true] + + D --> E[Работа в течение дня] + E --> F[Возможные промежуточные
    чекины] + + F --> G[Сотрудник
    заканчивает работу] + G --> H[Чекин TYPE_END] + H --> I[setValues: is_start=false] + I --> J[Обновление TimetableFactModel
    status=TYPE_END
    is_close=true
    work_time=расчёт] +``` + +## Примеры использования + +### Получение открытых смен +```php +$openShifts = TimetableFactModel::find() + ->where(['is_opening' => true, 'is_close' => false]) + ->with(['admin', 'store']) + ->all(); + +foreach ($openShifts as $shift) { + echo "{$shift->admin->name} работает в {$shift->store->name}\n"; +} +``` + +### Расчёт зарплаты за период +```php +$shifts = TimetableFactModel::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['status' => AdminCheckin::TYPE_END]) + ->andWhere(['BETWEEN', 'date', $dateFrom, $dateTo]) + ->all(); + +$totalSalary = 0; +$totalHours = 0; + +foreach ($shifts as $shift) { + $totalHours += $shift->work_time; + $totalSalary += $shift->salary_shift ?? ($shift->work_time * $shift->price_hour); +} + +echo "Отработано: {$totalHours} часов\n"; +echo "К выплате: {$totalSalary} руб.\n"; +``` + +### Статистика по магазину +```php +$stats = TimetableFactModel::find() + ->select([ + 'store_id', + 'SUM(work_time) as total_hours', + 'COUNT(*) as shifts_count' + ]) + ->where(['status' => AdminCheckin::TYPE_END]) + ->andWhere(['>=', 'date', date('Y-m-01')]) + ->groupBy('store_id') + ->asArray() + ->all(); +``` + +### Поиск автозакрытых смен +```php +$autoClosedShifts = TimetableFactModel::find() + ->where(['autoclosed' => 1]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-7 days'))]) + ->with('admin') + ->all(); + +foreach ($autoClosedShifts as $shift) { + echo "Автозакрыта смена {$shift->admin->name} от {$shift->date}\n"; +} +``` + +## API-представление (fields/extraFields) + +```php +// fields() — основные поля +[ + 'id', 'admin_id', 'store_id', 'shift_id', 'salary_shift', + 'tabel', 'date', 'date_start', 'date_end', 'time_start', + 'time_end', 'work_time', 'status', 'checkInCount', 'can_open' +] + +// extraFields() — дополнительные поля +[ + 'admin' => ['id', 'name', 'guid', 'group'], + 'store' => ['id', 'name', 'name_full'], + 'checkIns' +] +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `time_start` | required | +| `shift_id` | required | +| `date` | date (format: yyyy-M-d) | +| `admin_id` | exists в Admin | +| `store_id` | exists в CityStore | +| `d_id` | exists в AdminGroup | +| `comment` | string, default null | + +## Связанные модели + +- [Admin](./Admin.md) — сотрудники +- [CityStore](./CityStore.md) — магазины +- [Timetable](./Timetable.md) — плановый табель +- [AdminCheckin](./AdminCheckin.md) — чекины +- [Shift](./Shift.md) — типы смен +- [AdminGroup](./AdminGroup.md) — должности + +## Особенности реализации + +1. **Связь план-факт**: plan_id связывает фактическую смену с плановой +2. **Чекины**: checkin_start_id и checkin_end_id для привязки к отметкам +3. **Автозакрытие**: autoclosed=1 для смен, закрытых системой автоматически +4. **Расчёт часов**: work_time вычисляется с ограничением WORK_HOURS_TIME (12 часов) +5. **Часовая ставка**: price_hour рассчитывается из salary_shift с учётом должности +6. **Работа вне графика**: tabel=0 для смен без планового табеля +7. **API-интеграция**: fields() и extraFields() для REST API diff --git a/erp24/docs/models/TimetableFactV3.md b/erp24/docs/models/TimetableFactV3.md new file mode 100644 index 00000000..7b4d2d72 --- /dev/null +++ b/erp24/docs/models/TimetableFactV3.md @@ -0,0 +1,256 @@ +# Модель TimetableFactV3 + + +## Mindmap + +```mermaid +mindmap + root((TimetableFactV3)) + Таблица БД + ActiveRecord + Свойства + plan_id + int + Связи + Plan + 1:1 TimetablePlan + Наследование + extends TimetableV3 +``` + +## Назначение + +Модель `TimetableFactV3` представляет фактически отработанные смены сотрудников (табель). Наследует базовую модель `TimetableV3` и добавляет специфичную логику для учёта фактического времени работы: связь с плановой сменой, расчёт опозданий и ранних уходов. Используется для формирования табеля учёта рабочего времени. + +**Файл модели:** `erp24/records/TimetableFactV3.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `timetable` (с фильтром `tabel = 1`) +**Родительский класс:** `TimetableV3` + +--- + +## Поля таблицы + +Наследует все поля от [TimetableV3](./TimetableV3.md), дополнительно: + +| Поле | Тип | Описание | +|------|-----|----------| +| `plan_id` | INTEGER | ID плановой смены (FK → timetable.id) | + +--- + +## Константы + +```php +const TABLE_FACT = 1; // Тип записи: факт (табель) +``` + +--- + +## Методы модели + +### `rules(): array` + +Расширяет правила валидации родителя: +- Проверяет существование `plan_id` в таблице плановых смен +- Устанавливает значение по умолчанию `tabel = TABLE_FACT` +- Валидирует, что смена уже началась/закончилась (не в будущем) + +### `find(): ActiveQuery` + +Переопределяет запрос, добавляя фильтр `tabel = TABLE_FACT`. + +```php +public static function find() +{ + return parent::find()->andWhere(['tabel' => self::TABLE_FACT]); +} +``` + +### `isAbsent(): bool` + +Проверяет, был ли сотрудник на смене (отсутствие = время начала равно времени окончания). + +```php +public function isAbsent() +{ + return $this->datetime_start === $this->datetime_end; +} +``` + +### `getPlan(): ActiveQuery` + +Связь с плановой сменой. + +```php +public function getPlan() +{ + return $this->hasOne(TimetablePlan::class, ['id' => 'plan_id']); +} +``` + +### `getSkippedHours(): float` + +Возвращает количество пропущенных часов (разница между планом и фактом). + +```php +public function getSkippedHours() +{ + return $this->plan->work_time - $this->work_time; +} +``` + +### `getLate(): DateInterval` + +Возвращает опоздание (разница между фактическим и плановым временем начала). + +```php +public function getLate() +{ + return $this->getDateTimeStart()->diff($this->plan->getDateTimeStart()); +} +``` + +### `getEarly(): DateInterval` + +Возвращает ранний уход (разница между плановым и фактическим временем окончания). + +```php +public function getEarly() +{ + return $this->getDateTimeEnd()->diff($this->plan->getDateTimeEnd()); +} +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + timetable_fact }o--|| timetable_plan : "plan" + timetable_fact }o--|| admin : "employee" + timetable_fact }o--|| city_store : "store" + timetable_fact }o--|| shift : "shift" + + timetable_fact { + int id PK + int tabel "1=fact" + int plan_id FK + int admin_id FK + int store_id FK + int shift_id FK + string date + string datetime_start + string datetime_end + float work_time + int slot_type_id + int status + } +``` + +--- + +## Примеры использования + +### Получение факта по плану + +```php +$fact = TimetableFactV3::findOne(['plan_id' => $planId]); + +if ($fact) { + echo "Отработано: {$fact->work_time} часов\n"; + echo "Пропущено: {$fact->getSkippedHours()} часов\n"; +} +``` + +### Получение всех фактов сотрудника за месяц + +```php +$facts = TimetableFactV3::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['between', 'date', $monthStart, $monthEnd]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$totalHours = 0; +foreach ($facts as $fact) { + $totalHours += $fact->work_time; +} +echo "Всего отработано: {$totalHours} часов"; +``` + +### Проверка опозданий + +```php +$facts = TimetableFactV3::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['between', 'date', $monthStart, $monthEnd]) + ->with('plan') + ->all(); + +foreach ($facts as $fact) { + if (!$fact->isAbsent()) { + $late = $fact->getLate(); + if ($late->i > 0 || $late->h > 0) { + echo "{$fact->date}: опоздание на {$late->format('%H:%I')}\n"; + } + } +} +``` + +### Расчёт прогулов + +```php +$absences = TimetableFactV3::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['between', 'date', $monthStart, $monthEnd]) + ->all(); + +$absenceCount = 0; +foreach ($absences as $fact) { + if ($fact->isAbsent()) { + $absenceCount++; + } +} +echo "Прогулов за месяц: {$absenceCount}"; +``` + +### Сравнение плана и факта + +```php +$fact = TimetableFactV3::findOne($factId); + +echo "План: {$fact->plan->datetime_start} - {$fact->plan->datetime_end}\n"; +echo "Факт: {$fact->datetime_start} - {$fact->datetime_end}\n"; +echo "Плановое время: {$fact->plan->work_time} ч.\n"; +echo "Фактическое время: {$fact->work_time} ч.\n"; +``` + +--- + +## Валидация + +Наследует валидацию от `TimetableV3`, дополнительно: + +| Поле | Правило | +|------|---------| +| `plan_id` | Должен существовать в `TimetablePlan` | +| `tabel` | Обязательно равен `TABLE_FACT` (1) | +| `datetime_start` | Не может быть в будущем | +| `datetime_end` | Не может быть в будущем | + +--- + +## Связанные модели + +- **[TimetableV3](./TimetableV3.md)** — базовая модель табеля +- **[TimetablePlanV3](./TimetablePlanV3.md)** — плановые смены +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[Shift](./Shift.md)** — типы смен + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TimetablePlan.md b/erp24/docs/models/TimetablePlan.md new file mode 100644 index 00000000..490b15b5 --- /dev/null +++ b/erp24/docs/models/TimetablePlan.md @@ -0,0 +1,661 @@ +# Class: TimetablePlan + + +## Mindmap + +```mermaid +mindmap + root((TimetablePlan)) + Таблица БД + ActiveRecord + Связи + Fact + 1:1 TimetableFact + Checkins + 1:N AdminCheckin + Наследование + extends Timetable +``` + +## Назначение + +Модель `TimetablePlan` представляет плановый график работы сотрудников в системе ERP24. Это подкласс модели `Timetable`, использующий паттерн Single Table Inheritance (STI) для работы с записями типа `TABLE_PLAN` (tabel = 0). Модель отвечает за создание и управление плановыми сменами, контролирует минимальную длительность смен, связана с явками сотрудников (checkins) и может генерировать фактические записи на основе отметок времени. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii_app\records\Timetable` + +## Таблица базы данных + +`timetable` (наследуется от родителя) + +## Свойства + +Наследует все свойства от `Timetable` плюс: + +| Имя | Тип | Описание | +|-----|-----|----------| +| `fact` | TimetableFact | Связанная фактическая запись (read-only) | +| `checkins` | AdminCheckin[] | Массив отметок времени сотрудника (read-only) | + +## Методы + +### `rules()` + +**Описание:** Расширяет правила валидации родительского класса специфичными проверками для плановых смен. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** + +Метод объединяет три набора правил: + +1. **Установка типа табеля**: + - `tabel` по умолчанию устанавливается в `TABLE_PLAN` (0) + - `tabel` обязательно должен быть равен `TABLE_PLAN` + +2. **Родительские правила**: Наследует все правила валидации от `Timetable` + +3. **Специфичные правила для плана**: + - **Минимальная длительность**: Проверяет, что смена длится не менее 1 часа + - **Минимальное рабочее время**: `work_time` должно быть от 1 до 24 часов + - **Запрет редактирования с фактом**: Если для плановой смены уже создан факт, редактирование запрещено + +**Вызовы сторонних методов:** +- `parent::rules()` - получение правил валидации от Timetable +- `array_merge()` - объединение массивов правил +- `TimetableFact::find()` - проверка существования факта +- `new \DateTime()` - работа с датами +- `$start->diff($end)` - вычисление разницы времени + +**Пример:** +```php +$plan = new TimetablePlan(); +$plan->datetime_start = '2025-11-27 10:00:00'; +$plan->datetime_end = '2025-11-27 10:30:00'; // только 0.5 часа +if (!$plan->validate()) { + // Ошибка: "Смена длится меньше часа" +} +``` + +--- + +### `find()` + +**Описание:** Переопределенный метод поиска с автоматической фильтрацией только плановых записей. + +**Возвращает:** `ActiveQuery` - запрос с условием `tabel = TABLE_PLAN` + +**Логика работы:** +1. Вызывает родительский метод `Timetable::find()`, который уже фильтрует активные записи +2. Добавляет дополнительное условие `tabel = TABLE_PLAN` (0) +3. Возвращает запрос, который автоматически выбирает только плановые смены + +**Вызовы сторонних методов:** +- `parent::find()` - создание базового запроса с фильтром active = 1 +- `$query->andWhere()` - добавление условия WHERE + +**Пример:** +```php +// Автоматически выбираются только плановые смены +$plans = TimetablePlan::find() + ->where(['admin_id' => 10]) + ->andWhere(['>=', 'date', '2025-11-01']) + ->all(); +``` + +--- + +### `makeFact()` + +**Описание:** Создает фактическую запись на основе отметок времени сотрудника (checkins). + +**Возвращает:** `TimetableFact` - новый объект фактической записи (не сохраненный) + +**Логика работы:** + +Метод реализует два сценария создания факта: + +**Сценарий 1: Нет отметок времени (сотрудник не явился)** +1. Проверяет количество отметок `count($checkins) === 0` +2. Создает запись с нулевым рабочим временем +3. Устанавливает `datetime_start = datetime_end` (отсутствие) +4. Добавляет комментарий "fakt checkins === 0" + +**Сценарий 2: Есть отметки времени (сотрудник отметился)** +1. Берет первую отметку (`reset($checkins)`) как время прихода +2. Берет последнюю отметку (`end($checkins)`) как время ухода +3. Ограничивает время рамками плановой смены через метод `bound()` +4. Создает факт с фактическим временем работы +5. Добавляет комментарий "fakt checkins != 0" + +**Вызовы сторонних методов:** +- `$this->checkins` - получение связанных отметок +- `count()` - подсчет количества отметок +- `reset()` - получение первого элемента массива +- `end()` - получение последнего элемента массива +- `$checkin->dateTime()` - преобразование отметки в DateTime +- `$this->bound()` - ограничение времени рамками смены +- `$this->toArray()` - преобразование модели в массив +- `$_SESSION['admin_id']` - получение ID текущего пользователя + +**Пример:** +```php +$plan = TimetablePlan::findOne(1); + +// Случай 1: Сотрудник не явился +$fact = $plan->makeFact(); // work_time = 0 + +// Случай 2: Сотрудник отметился в 08:55 и 17:30 +// План был с 09:00 до 18:00 +$fact = $plan->makeFact(); +// datetime_start = '2025-11-27 09:00:00' (ограничено рамками) +// datetime_end = '2025-11-27 17:30:00' + +$fact->save(); // Сохранение созданного факта +``` + +--- + +### `bound(\DateTime $date)` + +**Описание:** Ограничивает переданное время рамками плановой смены. + +**Параметры:** +- `$date` (\DateTime) - время для проверки и ограничения + +**Возвращает:** `\DateTime` - время, ограниченное рамками смены + +**Логика работы:** +1. Если время позже окончания смены → возвращает `datetime_end` +2. Если время раньше начала смены → возвращает `datetime_start` +3. Если время в рамках смены → возвращает само время + +Это предотвращает учет времени, когда сотрудник отметился слишком рано или слишком поздно. + +**Вызовы сторонних методов:** +- `$this->getDateTimeEnd()` - получение времени окончания смены +- `$this->getDateTimeStart()` - получение времени начала смены + +**Пример:** +```php +$plan->datetime_start = '2025-11-27 09:00:00'; +$plan->datetime_end = '2025-11-27 18:00:00'; + +$early = new DateTime('2025-11-27 08:30:00'); +$bounded = $plan->bound($early); // '2025-11-27 09:00:00' + +$late = new DateTime('2025-11-27 19:00:00'); +$bounded = $plan->bound($late); // '2025-11-27 18:00:00' + +$normal = new DateTime('2025-11-27 14:00:00'); +$bounded = $plan->bound($normal); // '2025-11-27 14:00:00' +``` + +--- + +### `getFact()` + +**Описание:** Определяет связь с фактической записью. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `TimetableFact` через поле `plan_id`. Позволяет получить доступ к фактической отработке, связанной с этим планом. + +**Пример:** +```php +$plan = TimetablePlan::findOne(1); +$fact = $plan->fact; +if ($fact) { + echo "Фактическое время: {$fact->work_time} часов"; +} +``` + +--- + +### `getCheckins()` + +**Описание:** Определяет связь с отметками времени сотрудника. + +**Возвращает:** `ActiveQuery` - запрос связи hasMany с сортировкой по времени + +**Логика работы:** +Создает связь один-ко-многим с таблицей `AdminCheckin` через поле `plan_id`. Отметки автоматически сортируются по времени (SORT_ASC). Это отметки, когда сотрудник нажимал кнопку "Отметиться" в течение смены. + +**Пример:** +```php +$plan = TimetablePlan::findOne(1); +foreach ($plan->checkins as $checkin) { + echo $checkin->time . " - " . $checkin->type . "\n"; + // 08:55:32 - приход + // 17:30:15 - уход +} +``` + +--- + +### `isStillAbsent()` + +**Описание:** Проверяет, не явился ли сотрудник на плановую смену, которая уже началась. + +**Возвращает:** `bool` - true если смена началась, но нет отметок + +**Логика работы:** +1. Получает текущее время `new \DateTime()` +2. Проверяет два условия: + - Нет отметок времени (`!$this->checkins`) + - Текущее время больше времени начала смены (`$now > $this->getDateTimeStart()`) +3. Если оба условия true → сотрудник не явился + +**Вызовы сторонних методов:** +- `new \DateTime()` - получение текущего времени +- `$this->checkins` - получение отметок +- `$this->getDateTimeStart()` - получение времени начала + +**Пример:** +```php +// Сегодня 27.11.2025 10:30 +$plan->datetime_start = '2025-11-27 09:00:00'; + +if ($plan->isStillAbsent()) { + // Отправить уведомление руководителю + NotificationService::send("Сотрудник не явился на смену"); +} +``` + +--- + +### `getLate()` + +**Описание:** Вычисляет опоздание сотрудника. + +**Возвращает:** `\DateInterval` - интервал опоздания + +**Логика работы:** +1. Получает фактическое время начала из связанного факта +2. Получает плановое время начала +3. Вычисляет разницу между фактом и планом +4. Возвращает интервал (может быть отрицательным, если пришел раньше) + +**Вызовы сторонних методов:** +- `$this->fact->getDateTimeStart()` - фактическое время прихода +- `$this->getDateTimeStart()` - плановое время начала +- `$factStart->diff($planStart)` - вычисление разницы + +**Пример:** +```php +$late = $plan->getLate(); +if ($late->invert === 0) { // не опоздал + echo "Пришел вовремя или раньше"; +} else { + echo "Опоздал на {$late->h} часов {$late->i} минут"; +} +``` + +--- + +### `getEarly()` + +**Описание:** Вычисляет, насколько рано сотрудник ушел. + +**Возвращает:** `\DateInterval` - интервал раннего ухода + +**Логика работы:** +1. Получает плановое время окончания +2. Получает фактическое время окончания из связанного факта +3. Вычисляет разницу +4. Возвращает интервал (может быть отрицательным, если работал дольше) + +**Вызовы сторонних методов:** +- `$this->getDateTimeEnd()` - плановое время окончания +- `$this->fact->getDateTimeEnd()` - фактическое время ухода +- `$planEnd->diff($factEnd)` - вычисление разницы + +**Пример:** +```php +$early = $plan->getEarly(); +if ($early->invert === 1) { // ушел позже плана + echo "Переработал на {$early->h} часов {$early->i} минут"; +} else { + echo "Ушел раньше на {$early->h} часов {$early->i} минут"; +} +``` + +--- + +### `isLate()` + +**Описание:** Проверяет, опоздал ли сотрудник на смену. + +**Возвращает:** `bool` - true если опоздал + +**Логика работы:** + +Метод проверяет три сценария: + +1. **Смена еще не началась**: + - `$now < $this->getDateTimeStart()` + - Возвращает false (нельзя опоздать на будущую смену) + +2. **Смена началась, нет отметок**: + - `!$this->checkins` + - Возвращает true (считается опозданием/прогулом) + +3. **Есть отметки**: + - Сравнивает фактическое время прихода с плановым + - `$this->fact->getDateTimeStart() > $this->getDateTimeStart()` + - Возвращает true если фактическое время позже планового + +**Вызовы сторонних методов:** +- `new \DateTime()` - получение текущего времени +- `$this->getDateTimeStart()` - плановое время начала +- `$this->checkins` - получение отметок +- `$this->fact->getDateTimeStart()` - фактическое время прихода + +**Пример:** +```php +if ($plan->isLate()) { + $late = $plan->getLate(); + echo "Опоздание: {$late->format('%H:%I')}"; + // Применить штраф или отметить в отчете +} +``` + +--- + +### `isEarly()` + +**Описание:** Проверяет, ушел ли сотрудник раньше окончания смены. + +**Возвращает:** `bool` - true если ушел раньше + +**Логика работы:** + +Метод проверяет несколько сценариев: + +1. **Смена еще не началась**: + - `$now < $this->getDateTimeStart()` + - Возвращает false + +2. **Смена идет сейчас**: + - `$now < $this->getDateTimeEnd()` + - Проверяет количество отметок: `count($this->checkins) > 1` + - Если больше 1 (пришел и ушел) → true + +3. **Смена уже закончилась, нет отметок**: + - Возвращает true (не отработал) + +4. **Смена закончилась, есть отметки**: + - Сравнивает фактическое время ухода с плановым + - `$this->fact->getDateTimeEnd() < $this->getDateTimeEnd()` + +**Вызовы сторонних методов:** +- `new \DateTime()` - текущее время +- `$this->getDateTimeStart()`, `$this->getDateTimeEnd()` - плановые границы +- `count($this->checkins)` - количество отметок +- `$this->fact->getDateTimeEnd()` - фактическое время ухода + +**Пример:** +```php +if ($plan->isEarly()) { + $early = $plan->getEarly(); + echo "Ушел раньше на: {$early->format('%H:%I')}"; + // Вычесть из рабочего времени +} +``` + +--- + +### `softDelete($deleted_by = null)` + +**Описание:** Выполняет мягкое удаление плановой смены с проверкой наличия факта. + +**Параметры:** +- `$deleted_by` (int|null) - ID пользователя, выполняющего удаление + +**Возвращает:** `bool` - true если удаление выполнено, false если запрещено + +**Логика работы:** +1. Проверяет существование связанной фактической записи через `TimetableFactModel::findOne()` +2. Если факт существует: + - Устанавливает flash-сообщение об ошибке + - Возвращает false (удаление запрещено) +3. Если факта нет: + - Вызывает родительский метод `softDelete()` + - Устанавливает `active = 0` и `deleted_at` + - Возвращает результат удаления + +**Вызовы сторонних методов:** +- `TimetableFactModel::findOne()` - поиск факта +- `Yii::$app->session->setFlash()` - установка сообщения +- `parent::softDelete()` - выполнение мягкого удаления + +**Пример:** +```php +$plan = TimetablePlan::findOne(1); + +if ($plan->softDelete()) { + echo "Плановая смена удалена"; +} else { + // Выведет flash: "Невозможно удалить запись плановой смены + // для которой существует факт смены." +} +``` + +--- + +## Примеры использования + +### 1. Создание плановой смены + +```php +$plan = new TimetablePlan(); +$plan->admin_id = 10; +$plan->store_id = 5; +$plan->shift_id = 1; +$plan->d_id = 3; +$plan->date = '2025-11-27'; +$plan->datetime_start = '2025-11-27 09:00:00'; +$plan->datetime_end = '2025-11-27 18:00:00'; +$plan->slot_type_id = Timetable::TIMESLOT_WORK; + +if ($plan->save()) { + echo "План создан, ID: {$plan->id}"; +} +``` + +### 2. Генерация факта из отметок + +```php +// После окончания смены +$plan = TimetablePlan::findOne(1); +$fact = $plan->makeFact(); + +if ($fact->save()) { + echo "Факт создан. Отработано: {$fact->work_time} часов"; + + if ($plan->isLate()) { + $late = $plan->getLate(); + echo "Опоздание: {$late->h}ч {$late->i}м"; + } + + if ($plan->isEarly()) { + $early = $plan->getEarly(); + echo "Ранний уход: {$early->h}ч {$early->i}м"; + } +} +``` + +### 3. Проверка явки сотрудников + +```php +$today = date('Y-m-d'); +$plans = TimetablePlan::find() + ->where(['date' => $today]) + ->andWhere(['<', 'datetime_start', date('Y-m-d H:i:s')]) + ->all(); + +foreach ($plans as $plan) { + if ($plan->isStillAbsent()) { + echo "Не явился: {$plan->admin->name_full}\n"; + // Отправить уведомление + } +} +``` + +### 4. Отчет по опозданиям за месяц + +```php +$month = '2025-11'; +$plans = TimetablePlan::find() + ->joinWith('fact') + ->where(['like', 'date', $month]) + ->andWhere(['not', ['timetable_fact.id' => null]]) + ->all(); + +foreach ($plans as $plan) { + if ($plan->isLate()) { + $late = $plan->getLate(); + echo sprintf( + "%s - %s: опоздал на %dч %dм\n", + $plan->date, + $plan->admin->name_full, + $late->h, + $late->i + ); + } +} +``` + +### 5. Безопасное удаление плана + +```php +$plan = TimetablePlan::findOne($id); + +if ($plan->fact) { + echo "Удаление невозможно - есть факт"; +} else { + $plan->softDelete($_SESSION['admin_id']); + echo "План удален"; +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + TIMETABLE_PLAN ||--o| TIMETABLE_FACT : "generates" + TIMETABLE_PLAN ||--o{ ADMIN_CHECKIN : "has" + TIMETABLE_PLAN ||--|| ADMIN : "assigned to" + TIMETABLE_PLAN ||--|| SHIFT : "has type" + TIMETABLE_PLAN ||--|| CITY_STORE : "in store" + + TIMETABLE_PLAN { + int id PK + int tabel "always 0" + int admin_id FK + datetime datetime_start + datetime datetime_end + float work_time + string comment + } + + TIMETABLE_FACT { + int id PK + int plan_id FK + int tabel "always 1" + datetime datetime_start + datetime datetime_end + float work_time + } + + ADMIN_CHECKIN { + int id PK + int plan_id FK + datetime time + string type + } +``` + +## Диаграмма процесса создания факта + +```mermaid +flowchart TD + A[Окончание смены] --> B{Есть отметки?} + B -->|Нет| C[Создать факт с work_time=0] + B -->|Да| D[Получить первую и последнюю отметку] + D --> E[Ограничить время методом bound] + E --> F[Создать факт с фактическим временем] + C --> G[fact->save] + F --> G + G --> H{isLate?} + H -->|Да| I[Отметить опоздание] + H -->|Нет| J{isEarly?} + J -->|Да| K[Отметить ранний уход] + J -->|Нет| L[Нормальная отработка] +``` + +## Особенности реализации + +### 1. Паттерн STI (Single Table Inheritance) + +TimetablePlan использует паттерн STI: +- Хранится в той же таблице, что и TimetableFact +- Различается значением поля `tabel = 0` +- Автоматически создается через `Timetable::instantiate()` + +### 2. Интеграция с системой отметок + +Модель тесно интегрирована с AdminCheckin: +- Отметки связаны через `plan_id` +- Используются для автоматического создания факта +- Время ограничивается рамками плана методом `bound()` + +### 3. Защита от удаления + +Реализована защита от случайного удаления: +- Проверка наличия связанного факта +- Flash-сообщение при попытке удалить +- Возврат false вместо исключения + +### 4. Минимальная длительность + +Для планов установлено ограничение: +- Минимум 1 час (в отличие от Timetable) +- Проверяется в rules() через пользовательский валидатор + +## Связанные компоненты + +### Модели +- `Timetable` - родительский класс +- `TimetableFact` - фактическая отработка +- `AdminCheckin` - отметки времени +- `Admin` - сотрудники + +### Контроллеры +- `TimetableController` - управление табелем +- `CheckinController` - отметки времени + +## Примечания + +### Важные особенности + +1. **Автоматическое создание факта**: Метод `makeFact()` НЕ сохраняет запись автоматически, требуется вызов `save()` + +2. **Ограничение времени**: Метод `bound()` предотвращает учет времени вне смены + +3. **Проверка явки**: Метод `isStillAbsent()` работает только для начавшихся смен + +4. **Запрет редактирования**: После создания факта план нельзя редактировать + +### Рекомендации + +1. Всегда проверяйте наличие факта перед удалением плана +2. Используйте `makeFact()` для автоматической генерации факта +3. Проверяйте опоздания и ранние уходы для контроля дисциплины +4. Учитывайте, что `bound()` может изменить фактическое время отметок diff --git a/erp24/docs/models/TimetablePlanV3.md b/erp24/docs/models/TimetablePlanV3.md new file mode 100644 index 00000000..ab5b4b0a --- /dev/null +++ b/erp24/docs/models/TimetablePlanV3.md @@ -0,0 +1,294 @@ +# Модель TimetablePlanV3 + + +## Mindmap + +```mermaid +mindmap + root((TimetablePlanV3)) + Таблица БД + ActiveRecord + Связи + Fact + 1:1 TimetableFact + Checkins + 1:N AdminCheckin + Наследование + extends TimetableV3 +``` + +## Назначение + +Модель `TimetablePlanV3` представляет плановые смены сотрудников (график работы). Наследует базовую модель `TimetableV3` и добавляет логику планирования: создание фактических записей на основе отметок (check-in), расчёт опозданий и ранних уходов. Используется для формирования графика работы магазинов. + +**Файл модели:** `erp24/records/TimetablePlanV3.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `timetable` (с фильтром `tabel = 0`) +**Родительский класс:** `TimetableV3` + +--- + +## Поля таблицы + +Наследует все поля от [TimetableV3](./TimetableV3.md). + +--- + +## Константы + +```php +const TABLE_PLAN = 0; // Тип записи: план (график) +``` + +--- + +## Связи (Relations) + +| Связь | Тип | Модель | Описание | +|-------|-----|--------|----------| +| `fact` | hasOne | TimetableFact | Фактическая смена | +| `checkins` | hasMany | AdminCheckin | Отметки прихода/ухода | + +--- + +## Методы модели + +### `rules(): array` + +Расширяет правила валидации родителя: +- Устанавливает значение по умолчанию `tabel = TABLE_PLAN` +- Валидирует минимальную длительность смены (не менее 1 часа) +- Запрещает редактирование, если уже создан факт + +### `find(): ActiveQuery` + +Переопределяет запрос, добавляя фильтр `tabel = TABLE_PLAN`. + +```php +public static function find() +{ + return parent::find()->andWhere(['tabel' => self::TABLE_PLAN]); +} +``` + +### `makeFact(): TimetableFact` + +Создаёт фактическую запись на основе отметок check-in. Если отметок нет — создаёт запись прогула (start = end). + +**Логика работы:** +1. Если нет отметок — создаёт факт с нулевым рабочим временем (прогул) +2. Берёт первую и последнюю отметку +3. Ограничивает время границами плановой смены (метод `bound`) +4. Рассчитывает фактическое рабочее время + +```php +public function makeFact() +{ + $checkins = $this->checkins; + if (count($checkins) === 0) { + return new TimetableFact([ + 'plan_id' => $this->id, + 'datetime_start' => $this->datetime_end, + 'datetime_end' => $this->datetime_end, + 'work_time' => 0, + 'comment' => 'fakt checkins === 0', + ] + $this->toArray()); + } + // ... расчёт по отметкам +} +``` + +### `bound(DateTime $date): DateTime` + +Ограничивает время рамками плановой смены. + +```php +public function bound(\DateTime $date): \DateTime +{ + if ($date > $this->getDateTimeEnd()) { + return $this->getDateTimeEnd(); + } + if ($date < $this->getDateTimeStart()) { + return $this->getDateTimeStart(); + } + return $date; +} +``` + +### `getFact(): ActiveQuery` + +Связь с фактической сменой. + +### `getCheckins(): ActiveQuery` + +Связь с отметками прихода/ухода, отсортированными по времени. + +### `isStillAbsent(): bool` + +Проверяет, отсутствует ли сотрудник (смена началась, но отметок нет). + +```php +public function isStillAbsent() +{ + $now = new \DateTime(); + return !$this->checkins && $now > $this->getDateTimeStart(); +} +``` + +### `getLate(): DateInterval` + +Возвращает опоздание (на основе факта). + +### `getEarly(): DateInterval` + +Возвращает ранний уход (на основе факта). + +### `isLate(): bool` + +Проверяет, опоздал ли сотрудник. + +### `isEarly(): bool` + +Проверяет, ушёл ли сотрудник раньше времени. + +--- + +## Диаграмма связей + +```mermaid +erDiagram + timetable_plan ||--o| timetable_fact : "fact" + timetable_plan ||--o{ admin_checkin : "checkins" + timetable_plan }o--|| admin : "employee" + timetable_plan }o--|| city_store : "store" + timetable_plan }o--|| shift : "shift" + + timetable_plan { + int id PK + int tabel "0=plan" + int admin_id FK + int store_id FK + int shift_id FK + string date + string datetime_start + string datetime_end + float work_time + int slot_type_id + int status + } + + admin_checkin { + int id PK + int plan_id FK + int admin_id FK + datetime time + } +``` + +--- + +## Примеры использования + +### Создание плановой смены + +```php +$plan = new TimetablePlanV3(); +$plan->admin_id = $adminId; +$plan->store_id = $storeId; +$plan->shift_id = $shiftId; +$plan->date = '2025-12-15'; +$plan->datetime_start = '2025-12-15 09:00:00'; +$plan->datetime_end = '2025-12-15 18:00:00'; +$plan->slot_type_id = TimetableV3::TIMESLOT_WORK; +$plan->admin_id_add = Yii::$app->user->id; +$plan->save(); +``` + +### Получение графика сотрудника на неделю + +```php +$plans = TimetablePlanV3::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['between', 'date', $weekStart, $weekEnd]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +foreach ($plans as $plan) { + echo "{$plan->date}: {$plan->time_start} - {$plan->time_end}\n"; +} +``` + +### Создание факта из плана + +```php +$plan = TimetablePlanV3::findOne($planId); +$fact = $plan->makeFact(); + +if ($fact->save()) { + echo "Факт создан, отработано: {$fact->work_time} часов"; +} +``` + +### Проверка опозданий за день + +```php +$plans = TimetablePlanV3::find() + ->where(['date' => date('Y-m-d')]) + ->with(['checkins', 'admin']) + ->all(); + +foreach ($plans as $plan) { + if ($plan->isStillAbsent()) { + echo "ПРОГУЛ: {$plan->admin->name_full}\n"; + } elseif ($plan->isLate()) { + $late = $plan->getLate(); + echo "ОПОЗДАНИЕ: {$plan->admin->name_full} на {$late->format('%H:%I')}\n"; + } +} +``` + +### Получение графика магазина + +```php +$plans = TimetablePlanV3::find() + ->where([ + 'store_id' => $storeId, + 'date' => date('Y-m-d') + ]) + ->with(['admin', 'shift']) + ->orderBy(['datetime_start' => SORT_ASC]) + ->all(); + +foreach ($plans as $plan) { + echo "{$plan->admin->name_full}: {$plan->shift->name} ({$plan->time_start}-{$plan->time_end})\n"; +} +``` + +--- + +## Валидация + +Наследует валидацию от `TimetableV3`, дополнительно: + +| Поле | Правило | +|------|---------| +| `tabel` | Обязательно равен `TABLE_PLAN` (0) | +| `work_time` | От 1 до 24 часов | +| `time_end` | Смена должна длиться минимум 1 час | +| `time_end` | Нельзя редактировать, если уже создан факт | + +--- + +## Связанные модели + +- **[TimetableV3](./TimetableV3.md)** — базовая модель табеля +- **[TimetableFactV3](./TimetableFactV3.md)** — фактические смены +- **[AdminCheckin](./AdminCheckin.md)** — отметки прихода/ухода +- **[Admin](./Admin.md)** — сотрудники +- **[CityStore](./CityStore.md)** — магазины +- **[Shift](./Shift.md)** — типы смен + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TimetableShift.md b/erp24/docs/models/TimetableShift.md new file mode 100644 index 00000000..5fe7f418 --- /dev/null +++ b/erp24/docs/models/TimetableShift.md @@ -0,0 +1,213 @@ +# Модель TimetableShift + + +## Mindmap + +```mermaid +mindmap + root((TimetableShift)) + Таблица БД + timetable_shift + Свойства + id + int + name + string + short_name + string + start_time + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TimetableShift` представляет справочник рабочих смен. Определяет типы смен с их названиями, временем начала и продолжительностью. Используется для планирования графика работы сотрудников и связи с табелем учёта рабочего времени. + +**Файл модели:** `erp24/records/TimetableShift.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `timetable_shift` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(255) | Полное название смены | +| `short_name` | VARCHAR(255) | Сокращённое название смены | +| `start_time` | TIME | Время начала смены | +| `duration` | FLOAT | Длительность смены в часах | +| `work_time` | FLOAT | Рабочее время (за вычетом перерывов) | +| `end_time` | TIME | Время окончания смены | + +--- + +## Описание полей + +### `name` — Название смены + +Полное название смены для отображения в интерфейсе и отчётах. + +**Примеры значений:** +- "Дневная смена 10:00-22:00" +- "Ночная смена 22:00-10:00" +- "Утренняя смена 08:00-16:00" + +### `short_name` — Сокращённое название + +Короткое название для компактного отображения в расписании и календаре. + +**Примеры значений:** +- "Д" (дневная) +- "Н" (ночная) +- "У" (утренняя) + +### `start_time` — Время начала + +Время начала смены в формате `HH:MM:SS`. + +### `duration` — Длительность + +Общая продолжительность смены в часах (включая перерывы). + +**Примеры:** +- 12.0 — 12-часовая смена +- 8.0 — 8-часовая смена + +### `work_time` — Рабочее время + +Фактическое рабочее время в часах (без учёта перерывов). Используется для расчёта заработной платы. + +**Пример:** +- Смена 12 часов с обедом 1 час → `work_time = 11.0` + +### `end_time` — Время окончания + +Время окончания смены в формате `HH:MM:SS`. Вычисляется как `start_time + duration`. + +--- + +## Примеры типов смен + +| ID | Название | Короткое | Начало | Длительность | Рабочее время | +|----|----------|----------|--------|--------------|---------------| +| 1 | Дневная 10-22 | Д | 10:00 | 12.0 | 11.0 | +| 2 | Ночная 22-10 | Н | 22:00 | 12.0 | 11.0 | +| 3 | Утренняя 08-16 | У | 08:00 | 8.0 | 7.5 | +| 4 | Вечерняя 14-22 | В | 14:00 | 8.0 | 7.5 | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + timetable_shift ||--o{ admin_group_shift : "assigned_to" + timetable_shift ||--o{ timetable : "used_in" + admin_group_shift }o--|| admin_group : "group" + + timetable_shift { + int id PK + string name + string short_name + time start_time + float duration + float work_time + time end_time + } + + admin_group_shift { + int id PK + int admin_group_id FK + int shift_id FK + } + + timetable { + int id PK + int admin_id FK + int shift_id FK + date work_date + } +``` + +--- + +## Примеры использования + +### Получение всех смен + +```php +$shifts = TimetableShift::find() + ->orderBy(['start_time' => SORT_ASC]) + ->all(); +``` + +### Получение смен для выпадающего списка + +```php +$shifts = TimetableShift::find() + ->select(['name', 'id']) + ->indexBy('id') + ->column(); +// [1 => 'Дневная 10-22', 2 => 'Ночная 22-10', ...] +``` + +### Использование в табеле + +```php +use yii_app\records\Timetable; + +$timetableEntry = new Timetable(); +$timetableEntry->admin_id = $adminId; +$timetableEntry->work_date = '2025-12-11'; +$timetableEntry->shift_id = TimetableShift::findOne(['short_name' => 'Д'])->id; +$timetableEntry->save(); +``` + +### Расчёт времени окончания смены + +```php +$shift = TimetableShift::findOne($shiftId); +$startDateTime = new DateTime('2025-12-11 ' . $shift->start_time); +$endDateTime = clone $startDateTime; +$endDateTime->modify('+' . $shift->duration . ' hours'); +echo $endDateTime->format('Y-m-d H:i:s'); +``` + +### Получение смен для группы должностей + +```php +$group = AdminGroup::findOne(AdminGroup::GROUP_FLORIST_DAY); +$availableShifts = $group->shift; // TimetableShift[] через AdminGroupShift +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `name` | Обязательное, макс. 255 символов | +| `short_name` | Обязательное, макс. 255 символов | +| `start_time` | Обязательное | +| `duration` | Число (float) | +| `work_time` | Число (float) | +| `end_time` | Безопасный атрибут | + +--- + +## Связанные модели + +- **[AdminGroup](./AdminGroup.md)** — должности, для которых доступна смена +- **AdminGroupShift** — связь должности и смены +- **[Timetable](./Timetable.md)** — табель учёта рабочего времени +- **EmployeeOnShift** — сотрудники на смене + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/TimetableV3.md b/erp24/docs/models/TimetableV3.md new file mode 100644 index 00000000..945e3a51 --- /dev/null +++ b/erp24/docs/models/TimetableV3.md @@ -0,0 +1,945 @@ +# Class: TimetableV3 + + +## Mindmap + +```mermaid +mindmap + root((TimetableV3)) + Таблица БД + {{%timetable}} + Свойства + id + int + admin_group_id + int + tabel + int + plan_id + int + shift_id + int + store_id + int + Связи + Admin + 1:1 Admin + AdminGroup + 1:1 AdminGroup + Position + 1:1 AdminGroup + Store + 1:1 CityStore + AddedBy + 1:1 Admin + Наследование + extends ActiveRecord +``` + +## Назначение + +Модель `TimetableV3` представляет третью версию табеля учета рабочего времени сотрудников в системе ERP24. Это упрощенная версия основной модели `Timetable`, которая работает с той же таблицей БД, но имеет менее строгую валидацию и предназначена для использования в специфических сценариях. Модель управляет как плановыми графиками, так и фактической отработкой сотрудников, поддерживает различные типы занятости (работа, отпуск, больничный) и автоматически рассчитывает рабочее время с учетом перерывов. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Используемые трейты + +- `yii_app\traits\SoftDeleteTrait` - мягкое удаление записей + +## Таблица базы данных + +`timetable` (использует ту же таблицу, что и `Timetable`) + +## Константы + +### Статусы верификации + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `STATUS_PENDING` | 0 | Смена не проверена | +| `STATUS_VERIFIED` | 1 | Смена проверена | + +### Типы табеля + +| Константа | Значение | Описание | +|-----------|----------|----------| +| `TABLE_PLAN` | 0 | График сотрудников (план) | +| `TABLE_FACT` | 1 | Табель факт (фактическая отработка) | + +### Типы занятости + +| Константа | Значение | Буква | CSS класс | Описание | +|-----------|----------|-------|-----------|----------| +| `TIMESLOT_WORK` | 1 | Р | bg-success | Рабочая смена | +| `TIMESLOT_VACATION` | 2 | О | bg-info | Оплачиваемый отпуск | +| `TIMESLOT_ADMINISTRATIVE` | 3 | А | bg-danger | Административный отпуск | +| `TIMESLOT_SICK_LEAVE` | 4 | Б | bg-danger | Больничный | +| `TIMESLOT_INTERNSHIP` | 5 | С | bg-warning | Стажировка (неоплачиваемая) | +| `TIMESLOT_WEEKEND` | 6 | В | bg-danger | Выходной | +| `TIMESLOT_FREELANCE` | 7 | П | - | Подработчик | + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | - | Уникальный идентификатор записи | +| `admin_group_id` | int | да | ID группы администратора (должность) | +| `tabel` | int | да | Тип табеля (0=план, 1=факт) | +| `plan_id` | int | нет | ID связанной записи плана (для факта) | +| `shift_id` | int | да | ID смены из справочника | +| `store_id` | int | да | ID магазина | +| `date` | string(date) | да | Дата смены (Y-m-d) | +| `admin_id` | int | да | ID работника | +| `d_id` | int | да | ID исполняемой должности | +| `admin_id_add` | int | да | ID добавившего смену | +| `time_start` | string(time) | да | Время начала смены (H:i:s) | +| `time_end` | string(time) | да | Время окончания смены (H:i:s) | +| `work_time` | float | - | Рабочее время в часах (0-24) | +| `slot_type_id` | int | да | Тип занятости (1-7) | +| `comment` | string | нет | Комментарий к смене | +| `date_add` | int | - | Дата добавления (timestamp) | +| `status` | int | - | Статус проверки (0=не проверено, 1=проверено) | +| `datetime_start` | string(datetime) | да | Полная дата и время начала смены | +| `datetime_end` | string(datetime) | да | Полная дата и время окончания смены | +| `active` | int | - | Активность записи (SoftDeleteTrait) | +| `deleted_at` | string | - | Время удаления (SoftDeleteTrait) | + +## Связанные свойства (Relations) + +| Имя | Тип | Модель | Описание | +|-----|-----|--------|----------| +| `shift` | Shift | Shift | Смена (утро, день, вечер) | +| `store` | CityStore | CityStore | Место работы (магазин) | +| `position` | AdminGroup | AdminGroup | Должность работника | +| `admin` | Admin | Admin | Работник | +| `addedBy` | Admin | Admin | Добавивший пользователь | +| `adminGroup` | AdminGroup | AdminGroup | Группа администратора | + +## Методы + +### `tableName()` + +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - '{{%timetable}}' + +**Логика работы:** +Метод возвращает имя таблицы с использованием префикса Yii2. Префикс `{{%}}` автоматически заменяется на настроенный префикс таблиц из конфигурации БД. + +--- + +### `getDateTimeStart()` + +**Описание:** Возвращает объект DateTime начала смены для работы с датами. + +**Возвращает:** `\DateTime` - объект даты и времени начала смены + +**Логика работы:** +Создает новый объект DateTime из строки `datetime_start`, что позволяет выполнять операции с датами (сравнение, форматирование, вычисление разниц). + +**Пример:** +```php +$timetable = TimetableV3::findOne(1); +$startDateTime = $timetable->getDateTimeStart(); +$hour = $startDateTime->format('H'); // '09' +$date = $startDateTime->format('d.m.Y'); // '27.11.2025' +``` + +--- + +### `getDateTimeEnd()` + +**Описание:** Возвращает объект DateTime окончания смены для работы с датами. + +**Возвращает:** `\DateTime` - объект даты и времени окончания смены + +**Логика работы:** +Создает новый объект DateTime из строки `datetime_end`, аналогично методу `getDateTimeStart()`. + +**Пример:** +```php +$endDateTime = $timetable->getDateTimeEnd(); +$duration = $startDateTime->diff($endDateTime); +echo $duration->h; // количество часов +``` + +--- + +### `attributeLabels()` + +**Описание:** Возвращает человекочитаемые метки для атрибутов модели, используемые в формах и сообщениях об ошибках. + +**Возвращает:** `array` - ассоциативный массив ['атрибут' => 'Метка'] + +**Логика работы:** +Определяет русские названия для всех атрибутов модели. Используется автоматически в формах Yii2 и при выводе сообщений валидации. + +--- + +### `tabelType()` + +**Описание:** Возвращает справочник типов табеля для отображения и валидации. + +**Возвращает:** `array` - [0 => 'График сотрудников', 1 => 'Табель факт'] + +**Логика работы:** +Статический метод возвращает константный массив с двумя типами табеля. Используется в формах выбора и фильтрах. + +**Пример:** +```php +$types = TimetableV3::tabelType(); +echo $types[TimetableV3::TABLE_PLAN]; // 'График сотрудников' +``` + +--- + +### `statuses()` + +**Описание:** Возвращает справочник статусов проверки смены. + +**Возвращает:** `array` - [0 => 'Не проверено', 1 => 'Проверено'] + +**Логика работы:** +Статический метод возвращает два статуса: непроверенная и проверенная смена. После проверки руководителем смена получает статус "Проверено". + +**Пример:** +```php +$statuses = TimetableV3::statuses(); +echo $statuses[$timetable->status]; // 'Проверено' +``` + +--- + +### `isWorkSlot()` + +**Описание:** Проверяет, является ли слот рабочим (работа или стажировка). + +**Возвращает:** `bool` - true если рабочий слот + +**Логика работы:** +Метод проверяет, что `slot_type_id` равен либо `TIMESLOT_WORK` (работа), либо `TIMESLOT_INTERNSHIP` (стажировка). Оба эти типа считаются рабочими и учитываются при расчете отработанного времени. + +**Пример:** +```php +if ($timetable->isWorkSlot()) { + // Рассчитать зарплату или учесть в статистике + $totalHours += $timetable->work_time; +} +``` + +--- + +### `slotTypeName()` + +**Описание:** Возвращает справочник названий типов занятости. + +**Возвращает:** `array` - массив названий типов занятости + +**Логика работы:** +Статический метод возвращает полные русские названия для всех типов занятости. Используется для отображения в интерфейсе и отчетах. + +**Пример:** +```php +$types = TimetableV3::slotTypeName(); +echo $types[TimetableV3::TIMESLOT_WORK]; // 'работа' +echo $types[TimetableV3::TIMESLOT_VACATION]; // 'оплачиваемый отпуск' +``` + +--- + +### `slotTypeLetter()` + +**Описание:** Возвращает буквенные обозначения типов занятости для компактного отображения. + +**Возвращает:** `array` - массив букв + +**Логика работы:** +Статический метод возвращает однобуквенные обозначения для каждого типа занятости. Используется в календарях и таблицах для экономии места. + +**Пример:** +```php +$letters = TimetableV3::slotTypeLetter(); +echo $letters[TimetableV3::TIMESLOT_WORK]; // 'Р' +echo $letters[TimetableV3::TIMESLOT_VACATION]; // 'О' +``` + +--- + +### `slotTypeCssClass()` + +**Описание:** Возвращает CSS классы Bootstrap для визуального оформления типов занятости. + +**Возвращает:** `array` - массив CSS классов + +**Логика работы:** +Статический метод связывает каждый тип занятости с CSS классом Bootstrap (bg-success, bg-info, bg-danger, bg-warning). Используется для цветовой индикации в интерфейсе: +- Работа: зеленый (bg-success) +- Отпуск: голубой (bg-info) +- Больничный/Выходной/Админ: красный (bg-danger) +- Стажировка: желтый (bg-warning) + +**Пример:** +```php +$classes = TimetableV3::slotTypeCssClass(); +$class = $classes[$timetable->slot_type_id]; +echo "{$letter}"; +``` + +--- + +### `rules()` + +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +Метод определяет комплексный набор правил валидации: + +1. **Обязательные поля**: `datetime_start`, `datetime_end` обязательны +2. **Целочисленные поля**: `tabel`, `shift_id`, `store_id`, `admin_id`, `d_id`, `admin_group_id` +3. **Форматы дат**: + - `date` - формат Y-m-d + - `time_start`, `time_end` - формат H:i:s + - `date_add`, `datetime_start`, `datetime_end` - формат Y-m-d H:i:s +4. **Числовые ограничения**: `work_time` от 0 до 24 часов +5. **Справочные проверки**: + - `shift_id` должен существовать в `Shift::all()` + - `store_id` должен существовать в таблице `CityStore` + - `admin_id`, `admin_id_add` должны существовать в таблице `Admin` + - `d_id`, `admin_group_id` должны существовать в таблице `AdminGroup` + - `slot_type_id` должен быть в списке доступных типов +6. **Пользовательская валидация**: + - Проверка доступа сотрудника к магазину + - Проверка корректности времени (конец позже начала) + - Проверка длительности смены (не более 12 часов) + - Проверка дублирования смен на дату + - Проверка пересечения смен по времени +7. **Значения по умолчанию**: `comment` по умолчанию пустая строка + +**Отличия от Timetable:** +В TimetableV3 отсутствует валидация `salary_shift` (ставки), что делает модель более гибкой для специфических сценариев. + +**Вызовы сторонних методов:** +- `Shift::all()` - получение списка всех смен +- `CityStore::find()` - проверка существования магазина +- `Admin::find()` - проверка существования сотрудника, получение доступных магазинов +- `AdminGroup::find()` - проверка существования должности +- `$admin->getStoreIds()` - получение списка магазинов, доступных сотруднику + +**Пример:** +```php +$timetable = new TimetableV3(); +$timetable->attributes = $_POST['TimetableV3']; +if (!$timetable->validate()) { + foreach ($timetable->errors as $field => $errors) { + echo "$field: " . implode(', ', $errors) . "\n"; + } +} +``` + +--- + +### `beforeSave($insert)` + +**Описание:** Выполняется перед сохранением записи в БД. Автоматически рассчитывает рабочее время и устанавливает метаданные. + +**Параметры:** +- `$insert` (bool) - true если создается новая запись, false если обновляется существующая + +**Возвращает:** `bool` - результат выполнения родительского метода + +**Логика работы:** +1. Вызывает `calculateWorkTime()` для автоматического расчета рабочего времени с учетом перерывов +2. Устанавливает `date_add` как текущую дату и время, если она еще не установлена (оператор `??=`) +3. НЕ устанавливает `admin_id_add` и `admin_group_id` (закомментировано) - это отличие от основной модели Timetable +4. Вызывает родительский `beforeSave()` для продолжения цепочки сохранения + +**Вызовы сторонних методов:** +- `$this->calculateWorkTime()` - расчет рабочего времени +- `date()` - получение текущей даты +- `parent::beforeSave()` - вызов родительского метода + +**Отличия от Timetable:** +В TimetableV3 закомментированы строки установки `admin_id_add` и `admin_group_id`, что требует их ручной установки перед сохранением. + +**Пример:** +```php +$timetable = new TimetableV3(); +$timetable->datetime_start = '2025-11-27 09:00:00'; +$timetable->datetime_end = '2025-11-27 18:00:00'; +$timetable->shift_id = 1; +// work_time будет рассчитано автоматически при save() +$timetable->save(); +echo $timetable->work_time; // например, 8.0 +``` + +--- + +### `calculateWorkTime()` + +**Описание:** Рассчитывает чистое рабочее время с учетом перерывов, указанных в справочнике смен. + +**Возвращает:** `float` - рабочее время в часах (0.0 если отрицательное) + +**Логика работы:** +1. Создает объекты DateTime для начала и конца смены +2. Вычисляет разницу между началом и концом (`$diff`) +3. Получает длительность перерыва из связанной смены: `break_time = duration - work_time` +4. Рассчитывает чистое рабочее время: `work_time = (часы + минуты/60) - break_time` +5. Если результат отрицательный (смена короче перерыва), возвращает 0 + +**Формула:** +``` +work_time = (datetime_end - datetime_start) - (shift.duration - shift.work_time) +``` + +**Вызовы сторонних методов:** +- `new \DateTime()` - создание объектов дат +- `$start->diff($end)` - вычисление разницы между датами +- `$this->shift->duration` - получение общей длительности смены +- `$this->shift->work_time` - получение рабочего времени без перерывов + +**Пример:** +```php +// Смена с 09:00 до 18:00 (9 часов) +// Справочник: duration = 9.0, work_time = 8.0 (перерыв 1 час) +$timetable->datetime_start = '2025-11-27 09:00:00'; +$timetable->datetime_end = '2025-11-27 18:00:00'; +$workTime = $timetable->calculateWorkTime(); // 8.0 часов + +// Короткая смена с 10:00 до 10:30 (0.5 часа) +// Перерыв 1 час, result < 0 +$timetable->datetime_start = '2025-11-27 10:00:00'; +$timetable->datetime_end = '2025-11-27 10:30:00'; +$workTime = $timetable->calculateWorkTime(); // 0 часов +``` + +--- + +### `getAdmin()` + +**Описание:** Определяет связь с работником, который работает в смену. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `Admin` через поле `admin_id`. Позволяет получить доступ к данным сотрудника (ФИО, GUID, должность, контакты). + +**Пример:** +```php +$employee = $timetable->admin; +echo $employee->name_full; // 'Иванов Иван Иванович' +echo $employee->guid; // 'abc-123-def' +echo $employee->phone; // '+79001234567' +``` + +--- + +### `getAdminGroup()` + +**Описание:** Определяет связь с должностью работающего сотрудника (основная должность). + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `AdminGroup` через поле `admin_group_id`. Возвращает основную должность сотрудника на момент создания записи. + +**Пример:** +```php +$group = $timetable->adminGroup; +echo $group->name; // 'Продавец-флорист' +``` + +--- + +### `getPosition()` + +**Описание:** Определяет связь с исполняемой должностью в конкретной смене. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `AdminGroup` через поле `d_id`. Исполняемая должность может отличаться от основной (например, флорист может работать администратором). + +**Пример:** +```php +$position = $timetable->position; +echo $position->name; // 'Администратор' (хотя основная должность - флорист) +``` + +--- + +### `getStore()` + +**Описание:** Определяет связь с рабочим местом (магазином). + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `CityStore` через поле `store_id`. Позволяет получить информацию о магазине. + +**Пример:** +```php +$store = $timetable->store; +echo $store->name; // 'Магазин на Ленина' +echo $store->address; // 'ул. Ленина, д. 10' +``` + +--- + +### `getAddedBy()` + +**Описание:** Определяет связь с пользователем, создавшим запись смены. + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `Admin` через поле `admin_id_add`. Позволяет отслеживать, кто создал запись для аудита. + +**Пример:** +```php +$creator = $timetable->addedBy; +echo "Смену добавил: {$creator->name_full}"; +``` + +--- + +### `getShift()` + +**Описание:** Определяет связь со справочником смен (утро/день/вечер/ночь). + +**Возвращает:** `ActiveQuery` - запрос связи hasOne + +**Логика работы:** +Создает связь один-к-одному с таблицей `Shift` через поле `shift_id`. Возвращает тип смены с параметрами (название, время начала, длительность, рабочее время). + +**Пример:** +```php +$shift = $timetable->shift; +echo $shift->name; // 'Утренняя смена' +echo $shift->short_name; // 'Утро' +echo $shift->start_time; // '09:00:00' +echo $shift->duration; // 9.0 +echo $shift->work_time; // 8.0 +``` + +--- + +### `hasSoftDelete()` + +**Описание:** Указывает, что модель использует мягкое удаление записей. + +**Возвращает:** `bool` - true + +**Логика работы:** +Статический метод возвращает константное значение true, сигнализируя, что модель использует трейт `SoftDeleteTrait`. При удалении записи не удаляются физически, а помечаются как неактивные (`active = 0`). + +--- + +### `find()` + +**Описание:** Переопределенный метод поиска с автоматической фильтрацией удаленных записей. + +**Возвращает:** `ActiveQuery` - запрос с условием `active = 1` + +**Логика работы:** +1. Вызывает родительский метод `find()` для создания базового запроса +2. Проверяет через `hasSoftDelete()`, использует ли модель мягкое удаление +3. Если да, добавляет условие `timetable.active = 1` для фильтрации удаленных записей +4. Возвращает модифицированный запрос + +**Вызовы сторонних методов:** +- `parent::find()` - создание базового запроса +- `static::hasSoftDelete()` - проверка использования мягкого удаления +- `$query->andWhere()` - добавление условия WHERE + +**Примечание:** Имя таблицы в условии жестко указано как `timetable`, что может вызвать проблемы при использовании префиксов. + +**Пример:** +```php +// Автоматически выбираются только активные записи +$activeTimetables = TimetableV3::find()->all(); + +// Для получения всех записей (включая удаленные) +$allTimetables = TimetableV3::find()->andWhere(['or', ['active' => 1], ['active' => 0]])->all(); +``` + +--- + +## Примеры использования + +### 1. Создание новой записи плана + +```php +$timetable = new TimetableV3(); +$timetable->tabel = TimetableV3::TABLE_PLAN; +$timetable->shift_id = 1; // утренняя смена +$timetable->store_id = 5; +$timetable->date = '2025-11-27'; +$timetable->admin_id = 10; +$timetable->d_id = 3; // флорист +$timetable->admin_id_add = 1; // HR менеджер +$timetable->admin_group_id = 3; // группа флористов +$timetable->datetime_start = '2025-11-27 09:00:00'; +$timetable->datetime_end = '2025-11-27 18:00:00'; +$timetable->slot_type_id = TimetableV3::TIMESLOT_WORK; +$timetable->comment = 'Основная смена'; + +if ($timetable->save()) { + echo "Смена создана. Рабочее время: {$timetable->work_time} часов"; +} else { + print_r($timetable->errors); +} +``` + +### 2. Получение графика сотрудника на месяц + +```php +$adminId = 10; +$month = '2025-11'; + +$schedule = TimetableV3::find() + ->where(['admin_id' => $adminId, 'tabel' => TimetableV3::TABLE_PLAN]) + ->andWhere(['like', 'date', $month]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$letters = TimetableV3::slotTypeLetter(); +foreach ($schedule as $shift) { + $type = $letters[$shift->slot_type_id]; + echo "{$shift->date}: {$type} - {$shift->store->name} ({$shift->work_time}ч)\n"; +} +``` + +### 3. Создание записи отпуска на период + +```php +$startDate = '2025-12-01'; +$endDate = '2025-12-14'; +$adminId = 10; + +$period = new DatePeriod( + new DateTime($startDate), + new DateInterval('P1D'), + (new DateTime($endDate))->modify('+1 day') +); + +foreach ($period as $date) { + $vacation = new TimetableV3(); + $vacation->tabel = TimetableV3::TABLE_PLAN; + $vacation->admin_id = $adminId; + $vacation->admin_id_add = $_SESSION['admin_id']; + $vacation->admin_group_id = 3; + $vacation->date = $date->format('Y-m-d'); + $vacation->slot_type_id = TimetableV3::TIMESLOT_VACATION; + $vacation->datetime_start = $date->format('Y-m-d 00:00:00'); + $vacation->datetime_end = $date->format('Y-m-d 23:59:59'); + $vacation->store_id = 1; + $vacation->shift_id = 1; + $vacation->d_id = 3; + $vacation->save(); +} +``` + +### 4. Расчет отработанных часов за период + +```php +$adminId = 10; +$dateFrom = '2025-11-01'; +$dateTo = '2025-11-30'; + +$shifts = TimetableV3::find() + ->where([ + 'admin_id' => $adminId, + 'tabel' => TimetableV3::TABLE_FACT, + 'slot_type_id' => TimetableV3::TIMESLOT_WORK + ]) + ->andWhere(['>=', 'date', $dateFrom]) + ->andWhere(['<=', 'date', $dateTo]) + ->all(); + +$totalHours = 0; +foreach ($shifts as $shift) { + if ($shift->isWorkSlot()) { + $totalHours += $shift->work_time; + } +} + +echo "Отработано часов: {$totalHours}"; +``` + +### 5. Визуализация календаря с цветовой кодировкой + +```php +$adminId = 10; +$month = '2025-11'; + +$shifts = TimetableV3::find() + ->where(['admin_id' => $adminId]) + ->andWhere(['like', 'date', $month]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +$cssClasses = TimetableV3::slotTypeCssClass(); +$letters = TimetableV3::slotTypeLetter(); + +echo "
    "; +foreach ($shifts as $shift) { + $cssClass = $cssClasses[$shift->slot_type_id] ?? ''; + $letter = $letters[$shift->slot_type_id] ?? ''; + $day = date('d', strtotime($shift->date)); + + echo "
    "; + echo "{$day}"; + echo "{$letter}"; + echo "{$shift->work_time}ч"; + echo "
    "; +} +echo "
    "; +``` + +### 6. Проверка пересечений смен перед сохранением + +```php +$newShift = new TimetableV3(); +$newShift->admin_id = 10; +$newShift->datetime_start = '2025-11-27 14:00:00'; +$newShift->datetime_end = '2025-11-27 22:00:00'; +$newShift->tabel = TimetableV3::TABLE_PLAN; +$newShift->store_id = 5; +$newShift->shift_id = 2; +$newShift->d_id = 3; + +if (!$newShift->validate()) { + echo "Ошибки валидации:\n"; + foreach ($newShift->errors as $field => $errors) { + echo "$field: " . implode(', ', $errors) . "\n"; + } + // Может вывести: "datetime_start: Смены пересекаются для сотрудника..." +} +``` + +### 7. Получение списка сотрудников на смене + +```php +$date = '2025-11-27'; +$storeId = 5; + +$employees = TimetableV3::find() + ->joinWith(['admin', 'position']) + ->where([ + 'timetable.date' => $date, + 'timetable.store_id' => $storeId, + 'timetable.tabel' => TimetableV3::TABLE_PLAN, + 'timetable.slot_type_id' => TimetableV3::TIMESLOT_WORK + ]) + ->all(); + +foreach ($employees as $shift) { + echo sprintf( + "%s - %s (%s - %s, %s часов)\n", + $shift->admin->name_full, + $shift->position->name, + $shift->time_start, + $shift->time_end, + $shift->work_time + ); +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + TIMETABLE_V3 ||--|| ADMIN : "has employee" + TIMETABLE_V3 ||--|| ADMIN_GROUP : "has admin_group" + TIMETABLE_V3 ||--|| ADMIN_GROUP : "has position" + TIMETABLE_V3 ||--|| CITY_STORE : "works in" + TIMETABLE_V3 ||--|| SHIFT : "has shift type" + TIMETABLE_V3 ||--o| ADMIN : "added by" + + TIMETABLE_V3 { + int id PK + int tabel "0=план 1=факт" + int plan_id FK "связь с планом" + int admin_id FK + int admin_group_id FK + int d_id FK "должность" + int store_id FK + int shift_id FK + int admin_id_add FK + date date "дата смены" + datetime datetime_start + datetime datetime_end + time time_start + time time_end + float work_time + int slot_type_id "1-7" + string comment + int status "0|1" + int active "soft delete" + datetime deleted_at + } + + ADMIN { + int id PK + string name_full + string guid + int group_id FK + } + + ADMIN_GROUP { + int id PK + string name + } + + CITY_STORE { + int id PK + string name + } + + SHIFT { + int id PK + string name + float duration + float work_time + } +``` + +## Диаграмма процесса работы с табелем + +```mermaid +flowchart TD + A[Создание записи TimetableV3] --> B{Проверка валидации} + B -->|Ошибка| C[Вывод ошибок] + B -->|Успех| D[beforeSave] + D --> E[calculateWorkTime] + E --> F[Установка date_add] + F --> G[Сохранение в БД] + G --> H{Сохранено?} + H -->|Да| I[Возврат success] + H -->|Нет| C + + I --> J{Тип записи?} + J -->|План| K[Создание графика] + J -->|Факт| L[Учет отработки] + + K --> M[Отображение в календаре] + L --> N[Расчет зарплаты] +``` + +## Особенности реализации + +### 1. Отличия от основной модели Timetable + +TimetableV3 имеет несколько ключевых отличий: + +**Упрощенная валидация:** +- Отсутствует проверка `salary_shift` (ставки за смену) +- Более простая структура правил + +**Отсутствие STI (Single Table Inheritance):** +- Не использует метод `instantiate()` для разделения на подклассы +- Не создает автоматически TimetablePlan или TimetableFact + +**Закомментированная автоматика:** +- В `beforeSave()` закомментированы строки установки `admin_id_add` и `admin_group_id` +- Требует ручной установки этих полей перед сохранением + +**Добавлено поле:** +- `plan_id` - для связи фактических записей с плановыми + +### 2. Мягкое удаление + +Модель использует `SoftDeleteTrait` для безопасного удаления данных: +- Записи не удаляются физически из БД +- При удалении устанавливается `active = 0` и `deleted_at = timestamp` +- Метод `find()` автоматически фильтрует удаленные записи +- Для получения всех записей нужно явно указать условие + +### 3. Автоматический расчет времени + +Метод `calculateWorkTime()` автоматически вызывается в `beforeSave()`: +- Учитывает перерывы из справочника смен +- Предотвращает отрицательные значения +- Пересчитывается при каждом изменении времени смены + +### 4. Комплексная валидация + +Модель выполняет многоуровневую валидацию: +- **Доступ к магазину**: проверяет через `$admin->getStoreIds()` +- **Корректность времени**: конец не раньше начала +- **Длительность**: не более 12 часов +- **Дублирование**: одна смена на дату +- **Пересечения**: проверка начала и конца других смен + +### 5. Использование справочников + +Модель активно использует статические методы-справочники: +- `tabelType()` - типы табеля +- `statuses()` - статусы проверки +- `slotTypeName()` - названия типов занятости +- `slotTypeLetter()` - буквенные обозначения +- `slotTypeCssClass()` - CSS классы для визуализации + +## Связанные компоненты + +### Модели +- `Timetable` - основная модель табеля (родительская концепция) +- `TimetablePlan` - плановый график +- `TimetableFact` - фактическая отработка +- `Admin` - сотрудники +- `AdminGroup` - должности +- `CityStore` - магазины +- `Shift` - справочник смен + +### Сервисы +- Сервисы расчета зарплаты (SalaryService) +- Сервисы отчетности (ReportService) + +### Контроллеры +- TimetableController - управление табелем +- ScheduleController - работа с графиком + +## Примечания + +### Обнаруженные особенности + +1. **Закомментированный код**: В методе `beforeSave()` закомментированы строки 270-271, что требует ручной установки `admin_id_add` и `admin_group_id` + +2. **Жесткое указание таблицы**: В методе `find()` имя таблицы указано как 'timetable', а не через `self::tableName()`, что может вызвать проблемы при использовании префиксов + +3. **Дублирование логики**: Методы валидации пересечений дублируются в основной модели Timetable + +4. **Отсутствие проверки plan_id**: Нет валидации, что для типа TABLE_FACT должен быть указан plan_id + +### Рекомендации по использованию + +1. **Выбор между Timetable и TimetableV3**: + - Используйте `Timetable` для стандартных операций с табелем + - Используйте `TimetableV3` для специфических сценариев, где нужна упрощенная валидация + +2. **Обязательная установка полей**: + ```php + $timetable->admin_id_add = $_SESSION['admin_id'] ?? Yii::$app->user->id; + $timetable->admin_group_id = $timetable->admin->group_id; + ``` + +3. **Проверка доступов**: + - Всегда проверяйте, что сотрудник имеет доступ к магазину + - Используйте `$admin->getStoreIds()` для валидации + +4. **Работа с датами**: + - Используйте методы `getDateTimeStart()` и `getDateTimeEnd()` для операций с датами + - Не забывайте про часовые пояса при работе с datetime + +5. **Визуализация**: + - Используйте `slotTypeCssClass()` для цветовой кодировки + - Используйте `slotTypeLetter()` для компактного отображения в календарях diff --git a/erp24/docs/models/TimetableWorkbot.md b/erp24/docs/models/TimetableWorkbot.md new file mode 100644 index 00000000..0a7a4552 --- /dev/null +++ b/erp24/docs/models/TimetableWorkbot.md @@ -0,0 +1,286 @@ +# Class: TimetableWorkbot + + +## Mindmap + +```mermaid +mindmap + root((TimetableWorkbot)) + Таблица БД + timetable_workbot + Свойства + id + int + remove_id + int + tabel + int + shift_id + int + store_id + int + date + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `TimetableWorkbot` представляет архив удаленных записей табеля в системе ERP24. Это специальная таблица для хранения истории удалений смен, которая служит для аудита, возможности восстановления данных и отслеживания изменений в расписании. Каждая запись содержит полную копию удаленной смены, включая информацию о том, кто и когда выполнил удаление. + +## Пространство имен + +`yii_app\records` + +## Родительский класс + +`yii\db\ActiveRecord` + +## Таблица базы данных + +`timetable_workbot` + +## Свойства + +| Имя | Тип | Обязательное | Описание | +|-----|-----|--------------|----------| +| `id` | int | - | Уникальный идентификатор записи архива | +| `remove_id` | int | да | ID удаляемой смены из таблицы timetable | +| `removed_by` | int | нет | ID сотрудника, удалившего запись | +| `removed_at` | string(datetime) | нет | Дата и время удаления записи | +| `admin_group_id` | int | нет | Группа сотрудника на момент добавления | +| `tabel` | int | - | Табель: 0-график, 1-факт | +| `plan_id` | int | нет | ID связанной записи плана | +| `shift_id` | int | да | Тип смены | +| `store_id` | int | - | ID магазина | +| `date` | string(date) | да | Дата смены | +| `admin_id` | int | да | ID сотрудника | +| `d_id` | int | да | Должность по которой сотрудник отработал | +| `admin_id_add` | int | да | ID кто поставил в смену | +| `datetime_start` | string(datetime) | да | Дата и время начала | +| `datetime_end` | string(datetime) | да | Дата и время окончания | +| `time_start` | string(time) | да | Время начала | +| `time_end` | string(time) | да | Время окончания | +| `work_time` | float | - | Рабочее время в часах | +| `price_hour` | float | нет | Плата за час | +| `slot_type_id` | int | нет | Тип занятости | +| `comment` | string | да | Комментарий | +| `date_add` | string(datetime) | да | Дата создания оригинальной записи | +| `status` | int | - | Статус смены | + +## Методы + +### `tableName()` + +**Описание:** Возвращает имя таблицы базы данных. + +**Возвращает:** `string` - 'timetable_workbot' + +--- + +### `rules()` + +**Описание:** Определяет правила валидации для атрибутов модели. + +**Возвращает:** `array` - массив правил валидации + +**Логика работы:** +Устанавливает базовые правила валидации: +- Все поля оригинальной смены обязательны +- Целочисленные поля проверяются на тип +- Даты и время проверяются как безопасные (safe) +- Числовые поля `work_time` и `price_hour` проверяются на число +- Комментарий может быть строкой любой длины + +**Пример:** +```php +$workbot = new TimetableWorkbot(); +$workbot->attributes = $deletedTimetable->attributes; +$workbot->remove_id = $deletedTimetable->id; +$workbot->removed_by = $_SESSION['admin_id']; +$workbot->removed_at = date('Y-m-d H:i:s'); +$workbot->save(); +``` + +--- + +### `attributeLabels()` + +**Описание:** Возвращает метки для атрибутов модели (на английском). + +**Возвращает:** `array` - ассоциативный массив меток + +**Примечание:** Метки не переведены на русский язык, используются технические названия. + +--- + +## Примеры использования + +### 1. Сохранение удаляемой смены в архив + +```php +// При удалении смены +$timetable = Timetable::findOne($id); + +// Создаем копию в архив +$workbot = new TimetableWorkbot(); +$workbot->attributes = $timetable->attributes; +$workbot->remove_id = $timetable->id; +$workbot->removed_by = $_SESSION['admin_id']; +$workbot->removed_at = date('Y-m-d H:i:s'); + +if ($workbot->save()) { + // Теперь можно удалить оригинал + $timetable->delete(); +} +``` + +### 2. Просмотр истории удалений сотрудника + +```php +$adminId = 10; +$deletedShifts = TimetableWorkbot::find() + ->where(['admin_id' => $adminId]) + ->orderBy(['removed_at' => SORT_DESC]) + ->all(); + +foreach ($deletedShifts as $shift) { + echo "Удалено: {$shift->removed_at}\n"; + echo "Смена: {$shift->date} ({$shift->time_start} - {$shift->time_end})\n"; + echo "Удалил: ID {$shift->removed_by}\n\n"; +} +``` + +### 3. Восстановление удаленной смены + +```php +$workbot = TimetableWorkbot::findOne(['remove_id' => $originalId]); + +if ($workbot) { + $restored = new Timetable(); + $restored->attributes = $workbot->attributes; + unset($restored->id); // Новый ID будет создан + unset($restored->remove_id); + unset($restored->removed_by); + unset($restored->removed_at); + + if ($restored->save()) { + echo "Смена восстановлена"; + // Опционально: удалить из архива + $workbot->delete(); + } +} +``` + +### 4. Отчет по удалениям за период + +```php +$dateFrom = '2025-11-01'; +$dateTo = '2025-11-30'; + +$deletions = TimetableWorkbot::find() + ->where(['>=', 'removed_at', $dateFrom]) + ->andWhere(['<=', 'removed_at', $dateTo]) + ->all(); + +$byUser = []; +foreach ($deletions as $deletion) { + $userId = $deletion->removed_by; + if (!isset($byUser[$userId])) { + $byUser[$userId] = 0; + } + $byUser[$userId]++; +} + +foreach ($byUser as $userId => $count) { + echo "User ID {$userId}: удалил {$count} смен\n"; +} +``` + +### 5. Аудит удалений конкретной смены + +```php +$shiftId = 123; +$history = TimetableWorkbot::find() + ->where(['remove_id' => $shiftId]) + ->orderBy(['removed_at' => SORT_DESC]) + ->one(); + +if ($history) { + echo "Оригинальная смена ID: {$history->remove_id}\n"; + echo "Сотрудник: {$history->admin_id}\n"; + echo "Дата смены: {$history->date}\n"; + echo "Время: {$history->time_start} - {$history->time_end}\n"; + echo "Удалено: {$history->removed_at}\n"; + echo "Удалил: {$history->removed_by}\n"; +} +``` + +## Диаграмма связей + +```mermaid +erDiagram + TIMETABLE_WORKBOT { + int id PK + int remove_id "ID удаленной смены" + int removed_by "кто удалил" + datetime removed_at "когда удалили" + int admin_id "сотрудник" + int store_id "магазин" + date date "дата смены" + datetime datetime_start + datetime datetime_end + float work_time + string comment + } +``` + +## Особенности реализации + +### 1. Архивная таблица + +TimetableWorkbot - это архивная таблица: +- Хранит полные копии удаленных записей +- Позволяет отследить историю изменений +- Используется для аудита и восстановления +- Не связана внешними ключами с другими таблицами + +### 2. Дополнительные метаданные + +Модель добавляет информацию об удалении: +- `remove_id` - ID оригинальной записи +- `removed_by` - кто выполнил удаление +- `removed_at` - когда было выполнено + +### 3. Независимость + +Записи в workbot не зависят от текущих данных: +- Можно удалить сотрудника, смена останется в архиве +- Можно удалить магазин, история сохранится +- Полная автономность данных + +## Связанные компоненты + +### Модели +- `Timetable` - основная модель табеля +- `TimetablePlan` - плановые смены +- `TimetableFact` - фактические смены + +### Сервисы +- Сервисы аудита (AuditService) +- Сервисы восстановления данных + +## Примечания + +### Рекомендации + +1. **Всегда сохраняйте в архив**: Перед удалением смены создавайте запись в workbot + +2. **Автоматизируйте процесс**: Используйте beforeDelete() в Timetable для автоматического архивирования + +3. **Ограничивайте восстановление**: Добавьте проверки перед восстановлением (не восстанавливать старые смены) + +4. **Очистка архива**: Периодически удаляйте очень старые записи (например, старше 2 лет) + +5. **Индексы**: Создайте индексы на `remove_id`, `removed_by`, `removed_at` для быстрого поиска diff --git a/erp24/docs/models/TrackEvent.md b/erp24/docs/models/TrackEvent.md new file mode 100644 index 00000000..a2186e2d --- /dev/null +++ b/erp24/docs/models/TrackEvent.md @@ -0,0 +1,308 @@ +# Класс: TrackEvent + + +## Mindmap + +```mermaid +mindmap + root((TrackEvent)) + Таблица БД + track_event + Свойства + id + int + tag + string + state + int + created_at + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель отслеживания событий в ERP24. Система трекинга бизнес-событий с жизненным циклом: создано → реализовано/не реализовано. Используется для отслеживания конверсий, выполнения задач и аналитики. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`track_event` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы состояний + +```php +const STATE_CREATED = 1; // Событие создано +const STATE_REALISED = 2; // Событие реализовано (успех) +const STATE_NOT_REALISED = 3; // Событие не реализовано (неудача) +``` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `tag` | varchar(1000) | Тег для фильтрации событий | +| `user_id` | int / null | FK на пользователя, которому направлено событие | +| `state` | int | Состояние: 1=создано, 2=реализовано, 3=не реализовано | +| `details` | text / null | Детали события в формате JSON | +| `created_at` | datetime | Время создания события | +| `updated_at` | datetime / null | Время обновления события | + +## Виртуальные свойства (для агрегации) + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$total` | int | Общее количество событий | +| `$created` | int | Количество созданных событий | +| `$realised` | int | Количество реализованных событий | +| `$not_realised` | int | Количество нереализованных событий | + +## Диаграмма связей + +```mermaid +erDiagram + TrackEvent { + int id PK + varchar tag + int user_id FK + int state + text details + datetime created_at + datetime updated_at + } + + Users { + int id PK + varchar name + } + + Users ||--o{ TrackEvent : "user_id" +``` + +## Диаграмма жизненного цикла события + +```mermaid +stateDiagram-v2 + [*] --> Created: Создание события + Created --> Realised: Событие реализовано + Created --> NotRealised: Событие не реализовано + Realised --> [*] + NotRealised --> [*] + + Created: STATE_CREATED = 1 + Realised: STATE_REALISED = 2 + NotRealised: STATE_NOT_REALISED = 3 +``` + +## Диаграмма воронки событий + +```mermaid +flowchart TD + A[Создано
    100 событий] --> B{Результат?} + B -->|Успех| C[Реализовано
    70 событий] + B -->|Неудача| D[Не реализовано
    20 событий] + B -->|Ожидание| E[В процессе
    10 событий] + + C --> F[Конверсия: 70%] +``` + +## Примеры использования + +### Создание события +```php +$event = new TrackEvent(); +$event->tag = 'order_callback'; +$event->user_id = $userId; +$event->state = TrackEvent::STATE_CREATED; +$event->details = json_encode([ + 'order_id' => 12345, + 'phone' => '+79001234567', + 'source' => 'website' +]); +$event->created_at = date('Y-m-d H:i:s'); +$event->save(); +``` + +### Обновление состояния события +```php +$event = TrackEvent::findOne($eventId); + +if ($event) { + $event->state = TrackEvent::STATE_REALISED; + $event->updated_at = date('Y-m-d H:i:s'); + $event->save(); +} +``` + +### Получение событий по тегу +```php +$events = TrackEvent::find() + ->where(['tag' => 'order_callback']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Статистика по состояниям +```php +$stats = TrackEvent::find() + ->select([ + 'COUNT(*) as total', + 'SUM(CASE WHEN state = 1 THEN 1 ELSE 0 END) as created', + 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END) as realised', + 'SUM(CASE WHEN state = 3 THEN 1 ELSE 0 END) as not_realised' + ]) + ->where(['tag' => 'order_callback']) + ->asArray() + ->one(); + +$conversionRate = $stats['total'] > 0 + ? round($stats['realised'] / $stats['total'] * 100, 2) + : 0; + +echo "Конверсия: {$conversionRate}%"; +``` + +### Статистика по тегам +```php +$tagStats = TrackEvent::find() + ->select([ + 'tag', + 'COUNT(*) as total', + 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END) as realised' + ]) + ->groupBy('tag') + ->asArray() + ->all(); + +foreach ($tagStats as $stat) { + $rate = $stat['total'] > 0 ? round($stat['realised'] / $stat['total'] * 100, 1) : 0; + echo "{$stat['tag']}: {$stat['total']} событий, конверсия {$rate}%\n"; +} +``` + +### Нереализованные события пользователя +```php +$pendingEvents = TrackEvent::find() + ->where([ + 'user_id' => $userId, + 'state' => TrackEvent::STATE_CREATED + ]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); + +foreach ($pendingEvents as $event) { + $details = json_decode($event->details, true); + echo "Событие {$event->tag}: " . ($details['order_id'] ?? 'N/A') . "\n"; +} +``` + +### Массовое обновление просроченных событий +```php +$expiredDate = date('Y-m-d H:i:s', strtotime('-7 days')); + +TrackEvent::updateAll( + [ + 'state' => TrackEvent::STATE_NOT_REALISED, + 'updated_at' => date('Y-m-d H:i:s') + ], + [ + 'and', + ['state' => TrackEvent::STATE_CREATED], + ['<', 'created_at', $expiredDate] + ] +); +``` + +### Поиск событий с фильтрацией по details +```php +// События с определённым order_id в details +$events = TrackEvent::find() + ->where(['like', 'details', '"order_id":12345']) + ->all(); +``` + +### Статистика за период +```php +$dateFrom = '2024-12-01'; +$dateTo = '2024-12-31'; + +$periodStats = TrackEvent::find() + ->select([ + 'DATE(created_at) as date', + 'COUNT(*) as total', + 'SUM(CASE WHEN state = 2 THEN 1 ELSE 0 END) as realised' + ]) + ->where(['BETWEEN', 'created_at', $dateFrom, $dateTo]) + ->groupBy('DATE(created_at)') + ->orderBy(['date' => SORT_ASC]) + ->asArray() + ->all(); +``` + +### Создание события с callback +```php +function trackEvent($tag, $userId = null, $details = []) +{ + $event = new TrackEvent(); + $event->tag = $tag; + $event->user_id = $userId; + $event->state = TrackEvent::STATE_CREATED; + $event->details = !empty($details) ? json_encode($details) : null; + $event->created_at = date('Y-m-d H:i:s'); + return $event->save() ? $event->id : null; +} + +function realiseEvent($eventId) +{ + return TrackEvent::updateAll( + ['state' => TrackEvent::STATE_REALISED, 'updated_at' => date('Y-m-d H:i:s')], + ['id' => $eventId] + ); +} + +// Использование +$eventId = trackEvent('newsletter_open', $userId, ['campaign_id' => 'winter_sale']); +// ... позже +realiseEvent($eventId); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `tag` | required, string (max 1000) | +| `created_at` | required, safe | +| `user_id` | integer, default null | +| `state` | integer, default null | +| `details` | string | +| `updated_at` | safe | + +## Связанные модели + +- [Users](./Users.md) — пользователи + +## Примеры тегов событий + +| tag | Описание | +|-----|----------| +| `order_callback` | Запрос обратного звонка по заказу | +| `newsletter_open` | Открытие email-рассылки | +| `cart_abandon` | Брошенная корзина | +| `promo_click` | Клик по промо-акции | +| `review_request` | Запрос отзыва | +| `delivery_confirm` | Подтверждение доставки | + +## Особенности реализации + +1. **Три состояния**: Создано → Реализовано / Не реализовано +2. **Гибкий тег**: tag (до 1000 символов) для категоризации событий +3. **JSON details**: Произвольные детали события в формате JSON +4. **Привязка к пользователю**: user_id для персонализированного трекинга +5. **Виртуальные свойства**: total, created, realised, not_realised для агрегации +6. **Аналитика конверсий**: Расчёт соотношения реализованных к созданным +7. **Временные метки**: created_at и updated_at для истории изменений diff --git a/erp24/docs/models/UniversalCatalog.md b/erp24/docs/models/UniversalCatalog.md new file mode 100644 index 00000000..43558904 --- /dev/null +++ b/erp24/docs/models/UniversalCatalog.md @@ -0,0 +1,262 @@ +# Класс: UniversalCatalog + + +## Mindmap + +```mermaid +mindmap + root((UniversalCatalog)) + Таблица БД + universal_catalog + Свойства + id + int + name + string + alias + string + catalog_alias + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель универсального справочника в ERP24. Мета-справочник для динамического получения различных списков данных из системы: статусы заказов, типы задач, должности, товары по категориям и другие справочные данные. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`universal_catalog` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `name` | varchar(250) | Название каталога | +| `alias` | varchar(30) | Уникальный псевдоним записи | +| `catalog_alias` | varchar(30) | Псевдоним каталога (группы) | +| `bgcolor` | varchar(20) / null | Цвет фона для отображения | + +## Статические методы + +### getStatusList($alias) +**Описание:** Возвращает список статусов по алиасу. + +**Параметры:** +- `$alias` (string) — алиас списка + +**Возвращает:** `array` — массив [id => name] + +**Поддерживаемые алиасы:** +- `orderStatuses` — статусы заказов из pipeline_id=4021495 + +**Пример:** +```php +$statuses = UniversalCatalog::getStatusList('orderStatuses'); +// [142355 => 'Новый', 142356 => 'В работе', ...] +``` + +### getDynamicList($alias) +**Описание:** Возвращает динамический список данных по алиасу. + +**Параметры:** +- `$alias` (string) — алиас списка + +**Возвращает:** `array` — массив [id => name] + +**Поддерживаемые алиасы:** + +| alias | Источник | Описание | +|-------|----------|----------| +| `dashboard` | Dashboard | Дашборды | +| `firm` | Firms (group_id=-1) | Фирмы | +| `productsMatrix` | Products1c (tip=matrix) | Матричные товары | +| `taskTypes` | TasksType | Типы задач | +| `employeePositions` | EmployeePosition | Должности | +| `cashes1c` | Products1c (tip=cashes) | Кассы из 1С | +| `terminals1c` | Products1c (tip=terminals) | Терминалы из 1С | +| `paymentTypes1c` | Products1c (tip=payment_types) | Типы оплаты из 1С | +| `kkms1c` | Products1c (tip=kkms) | ККМ из 1С | +| `counteragents1c` | Products1c (tip=counteragents) | Контрагенты из 1С | +| `employee1c` | Products1c (tip=admin) | Сотрудники из 1С | +| `productsWrap` | Products1c (tip=wrap) | Упаковка | +| `productsRelated` | Products1c (tip=related) | Сопутствующие товары | +| `productsService` | Products1c (tip=services) | Услуги | +| `productsPotted` | Products1c (tip=potted) | Горшечные растения | +| `employeeAll` | Admin (group_id>0) | Все сотрудники | +| `catalog1c` | Products1c (tip=products_group) | Каталог товаров 1С | +| `cameras` | Внешний API | Камеры видеонаблюдения | + +**Пример:** +```php +$taskTypes = UniversalCatalog::getDynamicList('taskTypes'); +// [1 => 'Звонок', 2 => 'Встреча', 3 => 'Задача', ...] +``` + +### getCameras() +**Описание:** Получает список камер видеонаблюдения из внешнего API (vs.domru.ru). + +**Возвращает:** `array` — массив [CameraID => Name] + +**Особенности:** +- Кэшируется (cache key: "camera_name_by_ids") +- Использует curl для обращения к API +- Требует credentials из Yii::$app->params['CAMERAS'] + +**Пример:** +```php +$cameras = UniversalCatalog::getCameras(); +// ['cam123' => 'Камера зал 1', 'cam456' => 'Камера склад', ...] +``` + +## Диаграмма связей + +```mermaid +erDiagram + UniversalCatalog { + int id PK + varchar name + varchar alias UK + varchar catalog_alias + varchar bgcolor + } + + UniversalCatalogItem { + int id PK + varchar catalog_alias FK + varchar name + } + + UniversalCatalog ||--o{ UniversalCatalogItem : "catalog_alias" +``` + +## Диаграмма получения динамических списков + +```mermaid +flowchart TD + A[Запрос списка
    getDynamicList] --> B{alias?} + + B -->|dashboard| C[Dashboard::find] + B -->|taskTypes| D[TasksType::find] + B -->|employeePositions| E[EmployeePosition::find] + B -->|productsMatrix| F[Products1c::find
    tip=matrix] + B -->|cameras| G[getCameras
    External API] + B -->|...| H[Другие источники] + + C --> I[ArrayHelper::map] + D --> I + E --> I + F --> I + G --> J[Кэш] + H --> I + + I --> K[Результат:
    array id => name] + J --> K +``` + +## Примеры использования + +### Получение списка для выпадающего меню +```php +$taskTypes = UniversalCatalog::getDynamicList('taskTypes'); + +echo Html::dropDownList('task_type_id', null, $taskTypes, [ + 'prompt' => 'Выберите тип задачи' +]); +``` + +### Получение статусов заказов +```php +$orderStatuses = UniversalCatalog::getStatusList('orderStatuses'); + +foreach ($orderStatuses as $statusId => $statusName) { + echo "{$statusId}: {$statusName}\n"; +} +``` + +### Получение списка сотрудников +```php +$employees = UniversalCatalog::getDynamicList('employeeAll'); + +// Использование в GridView filter +'filter' => $employees, +``` + +### Работа с записями каталога +```php +// Получение всех записей каталога +$catalogs = UniversalCatalog::find() + ->orderBy(['name' => SORT_ASC]) + ->all(); + +// Поиск по алиасу +$catalog = UniversalCatalog::find() + ->where(['alias' => 'taskTypes']) + ->one(); +``` + +### Группировка по catalog_alias +```php +$grouped = UniversalCatalog::find() + ->orderBy(['catalog_alias' => SORT_ASC, 'name' => SORT_ASC]) + ->all(); + +$byCatalog = []; +foreach ($grouped as $item) { + $byCatalog[$item->catalog_alias][] = $item; +} +``` + +### Получение камер видеонаблюдения +```php +$cameras = UniversalCatalog::getCameras(); + +echo Html::dropDownList('camera_id', null, $cameras, [ + 'prompt' => 'Выберите камеру' +]); +``` + +### Создание записи каталога +```php +$catalog = new UniversalCatalog(); +$catalog->name = 'Новый тип'; +$catalog->alias = 'newType'; +$catalog->catalog_alias = 'types'; +$catalog->bgcolor = '#ff0000'; +$catalog->save(); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `name` | required, string (max 250) | +| `alias` | required, string (max 30), unique | +| `catalog_alias` | required, string (max 30) | +| `bgcolor` | string (max 20) | + +## Связанные модели + +- [UniversalCatalogItem](./UniversalCatalogItem.md) — элементы каталога +- [Dashboard](./Dashboard.md) — дашборды +- [TasksType](./TasksType.md) — типы задач +- [EmployeePosition](./EmployeePosition.md) — должности +- [Products1c](./Products1c.md) — товары из 1С +- [Admin](./Admin.md) — сотрудники +- [Firms](./Firms.md) — фирмы + +## Особенности реализации + +1. **Мета-справочник**: Централизованный доступ к различным спискам +2. **Динамические списки**: getDynamicList() для получения данных из разных таблиц +3. **Статусы заказов**: getStatusList() для интеграции с CRM (AmoCRM) +4. **Внешние API**: getCameras() для интеграции с системой видеонаблюдения +5. **Кэширование**: Камеры кэшируются для производительности +6. **Уникальный alias**: Для программного обращения к записям +7. **Цвет фона**: bgcolor для визуального оформления в UI diff --git a/erp24/docs/models/UniversalCatalogItem.md b/erp24/docs/models/UniversalCatalogItem.md new file mode 100644 index 00000000..427014ac --- /dev/null +++ b/erp24/docs/models/UniversalCatalogItem.md @@ -0,0 +1,266 @@ +# Класс: UniversalCatalogItem + + +## Mindmap + +```mermaid +mindmap + root((UniversalCatalogItem)) + Таблица БД + universal_catalog_item + Свойства + id + int + catalog_alias + string + name + string + posit + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель элементов универсального справочника в ERP24. Хранит записи (элементы) для различных справочников системы, идентифицируемых по catalog_alias. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`universal_catalog_item` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `catalog_alias` | varchar(40) | Алиас списка из UniversalCatalog | +| `name` | varchar(255) | Название элемента | +| `guid` | varchar(40) / null | Уникальный идентификатор внутри списка | +| `color` | varchar(20) / null | Цвет элемента для отображения | +| `posit` | int | Позиция элемента в списке | + +## Диаграмма связей + +```mermaid +erDiagram + UniversalCatalogItem { + int id PK + varchar catalog_alias FK + varchar name + varchar guid + varchar color + int posit + } + + UniversalCatalog { + int id PK + varchar alias + varchar catalog_alias + varchar name + } + + UniversalCatalog ||--o{ UniversalCatalogItem : "catalog_alias" +``` + +## Диаграмма структуры справочника + +```mermaid +flowchart TD + subgraph UniversalCatalog + A[catalog_alias='status'
    name='Статусы'] + B[catalog_alias='priority'
    name='Приоритеты'] + end + + subgraph UniversalCatalogItem + C[catalog_alias='status'
    name='Новый'
    color='#green'] + D[catalog_alias='status'
    name='В работе'
    color='#yellow'] + E[catalog_alias='status'
    name='Закрыт'
    color='#gray'] + + F[catalog_alias='priority'
    name='Высокий'
    color='#red'] + G[catalog_alias='priority'
    name='Средний'
    color='#orange'] + H[catalog_alias='priority'
    name='Низкий'
    color='#blue'] + end + + A --> C + A --> D + A --> E + B --> F + B --> G + B --> H +``` + +## Примеры использования + +### Создание элемента справочника +```php +$item = new UniversalCatalogItem(); +$item->catalog_alias = 'status'; +$item->name = 'В ожидании'; +$item->guid = 'waiting'; +$item->color = '#ffcc00'; +$item->posit = 5; +$item->save(); +``` + +### Получение элементов по алиасу каталога +```php +$statusItems = UniversalCatalogItem::find() + ->where(['catalog_alias' => 'status']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statusItems as $item) { + echo "{$item->name} (#{$item->color})\n"; +} +``` + +### Формирование списка для выбора +```php +$items = UniversalCatalogItem::find() + ->where(['catalog_alias' => 'priority']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +$list = ArrayHelper::map($items, 'id', 'name'); + +echo Html::dropDownList('priority_id', null, $list, [ + 'prompt' => 'Выберите приоритет' +]); +``` + +### Поиск элемента по GUID +```php +$item = UniversalCatalogItem::find() + ->where([ + 'catalog_alias' => 'status', + 'guid' => 'active' + ]) + ->one(); + +if ($item) { + echo "Статус: {$item->name}"; +} +``` + +### Получение с цветами для отображения +```php +$statuses = UniversalCatalogItem::find() + ->where(['catalog_alias' => 'status']) + ->orderBy(['posit' => SORT_ASC]) + ->all(); + +foreach ($statuses as $status) { + $style = $status->color ? "background-color: {$status->color}" : ''; + echo "{$status->name}\n"; +} +``` + +### Обновление порядка элементов +```php +$order = [3, 1, 2, 5, 4]; // Новый порядок ID + +foreach ($order as $position => $itemId) { + UniversalCatalogItem::updateAll( + ['posit' => $position], + ['id' => $itemId] + ); +} +``` + +### Массовое создание элементов +```php +$priorities = [ + ['name' => 'Критический', 'guid' => 'critical', 'color' => '#ff0000'], + ['name' => 'Высокий', 'guid' => 'high', 'color' => '#ff6600'], + ['name' => 'Средний', 'guid' => 'medium', 'color' => '#ffcc00'], + ['name' => 'Низкий', 'guid' => 'low', 'color' => '#00cc00'], +]; + +foreach ($priorities as $index => $data) { + $item = new UniversalCatalogItem(); + $item->catalog_alias = 'priority'; + $item->name = $data['name']; + $item->guid = $data['guid']; + $item->color = $data['color']; + $item->posit = $index; + $item->save(); +} +``` + +### Статистика по каталогам +```php +$stats = UniversalCatalogItem::find() + ->select(['catalog_alias', 'COUNT(*) as count']) + ->groupBy('catalog_alias') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['catalog_alias']}: {$stat['count']} элементов\n"; +} +``` + +### Поиск элементов по названию +```php +$items = UniversalCatalogItem::find() + ->where(['like', 'name', 'актив']) + ->all(); +``` + +### Удаление элементов каталога +```php +// Удаление одного элемента +$item = UniversalCatalogItem::findOne($id); +$item->delete(); + +// Удаление всех элементов каталога +UniversalCatalogItem::deleteAll(['catalog_alias' => 'old_catalog']); +``` + +### Использование в виджетах +```php +// В представлении +$colors = ArrayHelper::map( + UniversalCatalogItem::find() + ->where(['catalog_alias' => 'status']) + ->all(), + 'id', + 'color' +); + +// Применение цвета к строке таблицы +foreach ($items as $item) { + $bgColor = $colors[$item->status_id] ?? '#ffffff'; + echo ""; + // ... + echo ""; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `catalog_alias` | required, string (max 40) | +| `name` | required, string (max 255) | +| `guid` | string (max 40) | +| `color` | string (max 20) | +| `posit` | integer | + +## Связанные модели + +- [UniversalCatalog](./UniversalCatalog.md) — мета-справочник + +## Особенности реализации + +1. **Связь через alias**: catalog_alias связывает с UniversalCatalog +2. **GUID внутри списка**: guid для программного обращения к элементу +3. **Цветовая маркировка**: color для визуального оформления в UI +4. **Сортировка**: posit для управления порядком отображения +5. **Гибкость**: Один механизм для разных справочников +6. **Денормализация**: catalog_alias дублируется для быстрой фильтрации diff --git a/erp24/docs/models/UniversalCatalogItemSearch.md b/erp24/docs/models/UniversalCatalogItemSearch.md new file mode 100644 index 00000000..eacb03be --- /dev/null +++ b/erp24/docs/models/UniversalCatalogItemSearch.md @@ -0,0 +1,192 @@ +# Класс: UniversalCatalogItemSearch + + +## Mindmap + +```mermaid +mindmap + root((UniversalCatalogItemSearch)) + Таблица БД + ActiveRecord + Наследование + extends UniversalCatalogItem +``` + +## Назначение +Search-модель для поиска и фильтрации элементов универсальных справочников в ERP24. Модель для работы с элементами динамически создаваемых каталогов, связанных по алиасу. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`UniversalCatalogItem` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `catalog_alias`, `name`, `guid`, `color`, `posit` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска элементов справочников. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос UniversalCatalogItem::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id + - like: catalog_alias, name, guid, color, posit + +## Диаграмма связей + +```mermaid +erDiagram + UniversalCatalog { + int id PK + varchar name + varchar alias + } + + UniversalCatalogItem { + int id PK + varchar catalog_alias FK + varchar name + varchar guid + varchar color + varchar posit + } + + UniversalCatalog ||--o{ UniversalCatalogItem : "alias -> catalog_alias" +``` + +## Диаграмма универсальных справочников + +```mermaid +flowchart TD + A[UniversalCatalog] --> B[catalog_alias] + + B --> C[payment_methods] + C --> C1[Наличные] + C --> C2[Безнал] + C --> C3[Карта] + + B --> D[delivery_types] + D --> D1[Самовывоз] + D --> D2[Курьер] + D --> D3[Почта] + + B --> E[order_sources] + E --> E1[Сайт] + E --> E2[Телефон] + E --> E3[Магазин] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new UniversalCatalogItemSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск элементов каталога +```php +$searchModel = new UniversalCatalogItemSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogItemSearch' => [ + 'catalog_alias' => 'payment_methods', + ] +]); +``` + +### Поиск по названию +```php +$searchModel = new UniversalCatalogItemSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogItemSearch' => [ + 'name' => 'Наличные', + ] +]); +``` + +### Поиск по GUID +```php +$searchModel = new UniversalCatalogItemSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogItemSearch' => [ + 'guid' => 'abc-123', + ] +]); +``` + +### Поиск по цвету +```php +$searchModel = new UniversalCatalogItemSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogItemSearch' => [ + 'color' => '#FF0000', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'catalog_alias', + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + if ($model->color) { + return Html::tag('span', $model->name, ['style' => "color: {$model->color}"]); + } + return $model->name; + }, + ], + 'guid', + 'posit', + ], +]) ?> +``` + +## Связанные модели + +- [UniversalCatalogItem](./UniversalCatalogItem.md) — базовая модель элементов +- [UniversalCatalog](./UniversalCatalog.md) — справочники + +## Особенности реализации + +1. **Связь по алиасу**: catalog_alias связывает с UniversalCatalog.alias +2. **GUID интеграции**: guid для синхронизации с внешними системами +3. **Визуализация**: color для цветового оформления +4. **Позиция**: posit как строка (like поиск) +5. **like вместо ilike**: Регистрозависимый поиск +6. **Динамические справочники**: Элементы любых пользовательских каталогов diff --git a/erp24/docs/models/UniversalCatalogSearch.md b/erp24/docs/models/UniversalCatalogSearch.md new file mode 100644 index 00000000..586816fa --- /dev/null +++ b/erp24/docs/models/UniversalCatalogSearch.md @@ -0,0 +1,175 @@ +# Класс: UniversalCatalogSearch + + +## Mindmap + +```mermaid +mindmap + root((UniversalCatalogSearch)) + Таблица БД + ActiveRecord + Наследование + extends UniversalCatalog +``` + +## Назначение +Search-модель для поиска и фильтрации универсальных справочников в ERP24. Модель для управления динамически создаваемыми каталогами с настраиваемой структурой. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`UniversalCatalog` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `name`, `alias`, `catalog_alias`, `bgcolor` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска справочников. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос UniversalCatalog::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id + - like: name, alias, catalog_alias, bgcolor + +## Диаграмма структуры + +```mermaid +erDiagram + UniversalCatalog { + int id PK + varchar name + varchar alias + varchar catalog_alias + varchar bgcolor + } + + UniversalCatalogItem { + int id PK + varchar catalog_alias FK + varchar name + } + + UniversalCatalog ||--o{ UniversalCatalogItem : "alias -> catalog_alias" +``` + +## Диаграмма системы справочников + +```mermaid +flowchart TD + A[UniversalCatalog] --> B[Справочники] + + B --> C[payment_methods] + C --> C1[Способы оплаты] + + B --> D[delivery_types] + D --> D1[Типы доставки] + + B --> E[order_sources] + E --> E1[Источники заказов] + + F[bgcolor] --> G[Цвет фона в UI] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new UniversalCatalogSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new UniversalCatalogSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogSearch' => [ + 'name' => 'Способы оплаты', + ] +]); +``` + +### Поиск по алиасу +```php +$searchModel = new UniversalCatalogSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogSearch' => [ + 'alias' => 'payment_methods', + ] +]); +``` + +### Поиск по цвету фона +```php +$searchModel = new UniversalCatalogSearch(); +$dataProvider = $searchModel->search([ + 'UniversalCatalogSearch' => [ + 'bgcolor' => '#FFFFFF', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + [ + 'attribute' => 'name', + 'format' => 'raw', + 'value' => function($model) { + return Html::tag('span', $model->name, [ + 'style' => "background: {$model->bgcolor}; padding: 3px 8px;" + ]); + }, + ], + 'alias', + 'catalog_alias', + 'bgcolor', + ], +]) ?> +``` + +## Связанные модели + +- [UniversalCatalog](./UniversalCatalog.md) — базовая модель справочников +- [UniversalCatalogItem](./UniversalCatalogItem.md) — элементы справочников + +## Особенности реализации + +1. **Уникальный алиас**: alias для программного обращения к справочнику +2. **catalog_alias**: Связь с родительским каталогом (вложенность) +3. **Визуализация**: bgcolor для цветового оформления в UI +4. **Динамические справочники**: Создание произвольных каталогов без миграций +5. **like вместо ilike**: Регистрозависимый поиск diff --git a/erp24/docs/models/UploadForm.md b/erp24/docs/models/UploadForm.md new file mode 100644 index 00000000..99cdade0 --- /dev/null +++ b/erp24/docs/models/UploadForm.md @@ -0,0 +1,195 @@ +# Класс: UploadForm + + +## Mindmap + +```mermaid +mindmap + root((UploadForm)) + Таблица БД + ActiveRecord + Наследование + extends Model +``` + +## Назначение +Форма загрузки файлов в ERP24. Обеспечивает валидацию и сохранение загружаемых файлов с поддержкой форматов CSV, PNG, JPG. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`\yii\base\Model` + +**Примечание:** Это не ActiveRecord модель, а форма (Model) для обработки загрузки файлов. + +## Свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$file` | UploadedFile | Загружаемый файл | + +## Методы + +### rules() +**Описание:** Правила валидации загружаемого файла. + +**Правила:** +- `file` — обязательный файл с расширениями csv, png, jpg + +**Возвращает:** `array` — массив правил валидации + +### upload() +**Описание:** Валидирует и сохраняет загруженный файл. + +**Возвращает:** `bool` — `true` при успешной загрузке, `false` при ошибке + +**Логика работы:** +1. Вызывает validate() для проверки файла +2. При успешной валидации сохраняет файл в директорию `uploads/` +3. Имя файла: `{baseName}.{extension}` + +**Пример:** +```php +$model = new UploadForm(); +$model->file = UploadedFile::getInstance($model, 'file'); + +if ($model->upload()) { + echo "Файл успешно загружен"; +} else { + echo "Ошибка загрузки"; +} +``` + +## Диаграмма процесса загрузки + +```mermaid +flowchart TD + A[HTTP Request
    multipart/form-data] --> B[UploadedFile::getInstance] + B --> C[UploadForm] + C --> D{validate?} + D -->|Нет| E[Ошибка валидации] + D -->|Да| F{Расширение?} + F -->|csv, png, jpg| G[saveAs
    uploads/filename.ext] + F -->|Другое| E + G --> H[Файл сохранён] + E --> I[return false] + H --> J[return true] +``` + +## Поддерживаемые форматы + +| Расширение | Описание | Применение | +|------------|----------|------------| +| `csv` | Comma-Separated Values | Импорт данных | +| `png` | Portable Network Graphics | Изображения | +| `jpg` | JPEG Image | Изображения | + +## Примеры использования + +### Базовая загрузка файла +```php +$model = new UploadForm(); + +if (Yii::$app->request->isPost) { + $model->file = UploadedFile::getInstance($model, 'file'); + + if ($model->upload()) { + Yii::$app->session->setFlash('success', 'Файл загружен'); + return $this->redirect(['index']); + } +} + +return $this->render('upload', ['model' => $model]); +``` + +### Форма в представлении +```php + ['enctype' => 'multipart/form-data']]); ?> + +field($model, 'file')->fileInput() ?> + + 'btn btn-primary']) ?> + + +``` + +### Загрузка с кастомным именем +```php +$model = new UploadForm(); +$model->file = UploadedFile::getInstance($model, 'file'); + +if ($model->validate()) { + $newName = uniqid() . '.' . $model->file->extension; + $model->file->saveAs('uploads/' . $newName); +} +``` + +### Загрузка в контроллере +```php +public function actionUpload() +{ + $model = new UploadForm(); + + if (Yii::$app->request->isPost) { + $model->file = UploadedFile::getInstance($model, 'file'); + + if ($model->upload()) { + return $this->asJson([ + 'success' => true, + 'filename' => $model->file->baseName . '.' . $model->file->extension + ]); + } else { + return $this->asJson([ + 'success' => false, + 'errors' => $model->errors + ]); + } + } + + return $this->render('upload', ['model' => $model]); +} +``` + +### Получение ошибок валидации +```php +$model = new UploadForm(); +$model->file = UploadedFile::getInstance($model, 'file'); + +if (!$model->validate()) { + foreach ($model->errors['file'] as $error) { + echo $error . "\n"; + } +} +``` + +### Проверка размера файла (расширенная валидация) +```php +// В rules() можно добавить: +[['file'], 'file', + 'skipOnEmpty' => false, + 'extensions' => 'csv, png, jpg', + 'maxSize' => 1024 * 1024 * 5, // 5 MB + 'tooBig' => 'Файл не должен превышать 5 МБ' +], +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `file` | required (skipOnEmpty=false), extensions: csv, png, jpg | + +## Связанные модели + +- [Files](./Files.md) — хранение информации о файлах +- [Images](./Images.md) — обработка изображений + +## Особенности реализации + +1. **Не ActiveRecord**: Наследует yii\base\Model, не связан с БД +2. **Валидация расширений**: Только csv, png, jpg +3. **Директория uploads/**: Файлы сохраняются в корневую директорию uploads +4. **Оригинальное имя**: Сохраняется с оригинальным именем файла +5. **skipOnEmpty=false**: Файл обязателен для загрузки +6. **enctype**: Форма должна иметь enctype="multipart/form-data" diff --git a/erp24/docs/models/UserBonusSendToTgLogs.md b/erp24/docs/models/UserBonusSendToTgLogs.md new file mode 100644 index 00000000..78c922d7 --- /dev/null +++ b/erp24/docs/models/UserBonusSendToTgLogs.md @@ -0,0 +1,264 @@ +# Класс: UserBonusSendToTgLogs + + +## Mindmap + +```mermaid +mindmap + root((UserBonusSendToTgLogs)) + Таблица БД + user_bonus_send_to_tg_logs + Свойства + id + int + input_hash + string + status + int + check_id + string + phone + string + bonusCount + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель логирования отправки бонусов пользователям через Telegram в ERP24. Фиксирует все запросы на начисление бонусов с полной информацией о входных/выходных данных и статусах HTTP-ответов. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`user_bonus_send_to_tg_logs` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `input_hash` | varchar(255) | MD5-хеш входных данных для дедупликации | +| `input` | text / null | Входной JSON запроса | +| `output` | text / null | Выходной JSON ответа | +| `status` | int | Количество упоминаний данной записи | +| `check_id` | varchar(36) | GUID чека | +| `phone` | varchar(255) | Телефон получателя | +| `bonusCount` | int | Количество начисленных бонусов | +| `date` | datetime | Дата и время создания записи | +| `http_code` | int / null | HTTP-код ответа сервера | + +## Диаграмма связей + +```mermaid +erDiagram + UserBonusSendToTgLogs { + int id PK + varchar input_hash + text input + text output + int status + varchar check_id FK + varchar phone + int bonusCount + datetime date + int http_code + } + + Sales { + varchar id PK + varchar phone + } + + Users { + int id PK + varchar phone + } + + Sales ||--o| UserBonusSendToTgLogs : "check_id" + Users ||--o{ UserBonusSendToTgLogs : "phone" +``` + +## Диаграмма процесса отправки бонусов + +```mermaid +flowchart TD + A[Продажа
    check_id] --> B[Расчёт бонусов] + B --> C[Формирование запроса
    input JSON] + C --> D[Вычисление
    input_hash = MD5] + + D --> E{Запись с hash
    существует?} + E -->|Да| F[Увеличить status++] + E -->|Нет| G[Создать запись] + + G --> H[Отправка в Telegram API] + H --> I[Получение ответа] + I --> J[Сохранение output, http_code] + + F --> K[Пропуск отправки
    дедупликация] +``` + +## Примеры использования + +### Логирование отправки бонусов +```php +$inputData = [ + 'phone' => '+79001234567', + 'bonus' => 100, + 'check_id' => $checkId, + 'message' => 'Вам начислено 100 бонусов!' +]; + +$inputJson = json_encode($inputData); +$inputHash = md5($inputJson); + +// Проверка дубликата +$existing = UserBonusSendToTgLogs::find() + ->where(['input_hash' => $inputHash]) + ->one(); + +if ($existing) { + $existing->status++; + $existing->save(); +} else { + $log = new UserBonusSendToTgLogs(); + $log->input_hash = $inputHash; + $log->input = $inputJson; + $log->check_id = $checkId; + $log->phone = '+79001234567'; + $log->bonusCount = 100; + $log->date = date('Y-m-d H:i:s'); + $log->status = 1; + $log->save(); + + // Отправка в Telegram + $response = sendToTelegram($inputData); + + $log->output = json_encode($response['body']); + $log->http_code = $response['code']; + $log->save(); +} +``` + +### Получение логов по чеку +```php +$logs = UserBonusSendToTgLogs::find() + ->where(['check_id' => $checkId]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + echo "Телефон: {$log->phone}, Бонусы: {$log->bonusCount}, HTTP: {$log->http_code}\n"; +} +``` + +### Поиск неуспешных отправок +```php +$failedLogs = UserBonusSendToTgLogs::find() + ->where(['NOT IN', 'http_code', [200, 201]]) + ->orWhere(['http_code' => null]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($failedLogs as $log) { + echo "Ошибка: {$log->phone}, HTTP: {$log->http_code}\n"; + $output = json_decode($log->output, true); + print_r($output); +} +``` + +### Статистика по периоду +```php +$stats = UserBonusSendToTgLogs::find() + ->select([ + 'DATE(date) as day', + 'SUM(bonusCount) as total_bonus', + 'COUNT(*) as requests', + 'SUM(CASE WHEN http_code = 200 THEN 1 ELSE 0 END) as success' + ]) + ->where(['>=', 'date', date('Y-m-01')]) + ->groupBy('DATE(date)') + ->orderBy(['day' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $rate = $stat['requests'] > 0 ? round($stat['success'] / $stat['requests'] * 100, 1) : 0; + echo "{$stat['day']}: {$stat['total_bonus']} бонусов, {$rate}% успешно\n"; +} +``` + +### Поиск дубликатов +```php +$duplicates = UserBonusSendToTgLogs::find() + ->where(['>', 'status', 1]) + ->orderBy(['status' => SORT_DESC]) + ->limit(10) + ->all(); + +foreach ($duplicates as $log) { + echo "Дубликат x{$log->status}: {$log->phone} ({$log->bonusCount} бонусов)\n"; +} +``` + +### Повторная отправка неудачных +```php +$failedLogs = UserBonusSendToTgLogs::find() + ->where(['http_code' => null]) + ->orWhere(['NOT IN', 'http_code', [200, 201]]) + ->andWhere(['>=', 'date', date('Y-m-d', strtotime('-1 day'))]) + ->all(); + +foreach ($failedLogs as $log) { + $inputData = json_decode($log->input, true); + + // Повторная отправка + $response = sendToTelegram($inputData); + + $log->output = json_encode($response['body']); + $log->http_code = $response['code']; + $log->save(); +} +``` + +### Очистка старых логов +```php +$oldDate = date('Y-m-d', strtotime('-90 days')); + +$deleted = UserBonusSendToTgLogs::deleteAll(['<', 'date', $oldDate]); + +echo "Удалено {$deleted} старых записей"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `input_hash` | required, string (max 255) | +| `check_id` | required, string (max 36) | +| `phone` | required, string (max 255) | +| `bonusCount` | required, integer | +| `date` | required, string | +| `input` | string | +| `output` | string | +| `status` | integer, default null | +| `http_code` | integer, default null | + +## Связанные модели + +- [Sales](./Sales.md) — продажи (check_id) +- [Users](./Users.md) — пользователи (phone) +- [UsersBonus](./UsersBonus.md) — бонусы пользователей + +## Особенности реализации + +1. **Дедупликация**: input_hash (MD5) для предотвращения повторных отправок +2. **Счётчик дубликатов**: status увеличивается при повторных попытках +3. **Полное логирование**: input/output JSON для отладки +4. **HTTP-код**: http_code для анализа успешности доставки +5. **Связь с чеком**: check_id для привязки к продаже +6. **Telegram API**: Интеграция с ботом для отправки сообщений diff --git a/erp24/docs/models/UserReviews.md b/erp24/docs/models/UserReviews.md new file mode 100644 index 00000000..1819b661 --- /dev/null +++ b/erp24/docs/models/UserReviews.md @@ -0,0 +1,325 @@ +# Класс: UserReviews + + +## Mindmap + +```mermaid +mindmap + root((UserReviews)) + Таблица БД + user_reviews + Свойства + id + int + survey_id + string + sale_date + string + survey_date + string + receipt_number + string + store_id + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель отзывов клиентов в ERP24. Хранит результаты опросов клиентов после покупки с оценками по нескольким критериям: работа флориста, чистота магазина, ассортимент и качество букетов. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`user_reviews` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +### Идентификация +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `survey_id` | varchar(255) | ID опроса (внешний идентификатор) | +| `receipt_number` | varchar(255) | Номер чека | +| `phone` | varchar(255) | Номер телефона клиента | + +### Даты +| Поле | Тип | Описание | +|------|-----|----------| +| `sale_date` | date | Дата продажи | +| `survey_date` | date | Дата прохождения опроса | +| `created_at` | datetime | Дата создания записи | +| `updated_at` | datetime | Дата обновления записи | + +### Привязки +| Поле | Тип | Описание | +|------|-----|----------| +| `store_id` | varchar(255) | GUID магазина | +| `admin_id` | varchar(255) | GUID администратора (флориста) | + +### Оценки (1-5) +| Поле | Тип | Описание | +|------|-----|----------| +| `rating_florist` | int | Оценка работы флориста | +| `rating_cleanliness` | int | Оценка чистоты и комфорта магазина | +| `rating_assortment` | int | Оценка ассортимента цветов | +| `rating_quality` | int | Оценка выбора и качества букетов | + +## Диаграмма связей + +```mermaid +erDiagram + UserReviews { + int id PK + varchar survey_id + date sale_date + date survey_date + varchar receipt_number + varchar store_id FK + varchar admin_id FK + int rating_florist + int rating_cleanliness + int rating_assortment + int rating_quality + varchar phone + datetime created_at + datetime updated_at + } + + CityStore { + varchar guid PK + varchar name + } + + Admin { + varchar guid PK + varchar name + } + + Sales { + varchar id PK + varchar receipt_number + } + + CityStore ||--o{ UserReviews : "store_id" + Admin ||--o{ UserReviews : "admin_id" + Sales ||--o| UserReviews : "receipt_number" +``` + +## Диаграмма критериев оценки + +```mermaid +flowchart TD + A[Опрос клиента
    UserReviews] --> B[Работа флориста
    rating_florist] + A --> C[Чистота магазина
    rating_cleanliness] + A --> D[Ассортимент
    rating_assortment] + A --> E[Качество букетов
    rating_quality] + + B --> F[Средняя оценка
    магазина] + C --> F + D --> F + E --> F + + F --> G[KPI флориста] + F --> H[Рейтинг магазина] +``` + +## Примеры использования + +### Создание отзыва +```php +$review = new UserReviews(); +$review->survey_id = 'survey_' . uniqid(); +$review->sale_date = '2024-12-15'; +$review->survey_date = date('Y-m-d'); +$review->receipt_number = $receiptNumber; +$review->store_id = $storeGuid; +$review->admin_id = $adminGuid; +$review->rating_florist = 5; +$review->rating_cleanliness = 4; +$review->rating_assortment = 5; +$review->rating_quality = 5; +$review->phone = '+79001234567'; +$review->created_at = date('Y-m-d H:i:s'); +$review->save(); +``` + +### Средние оценки магазина +```php +$avgRatings = UserReviews::find() + ->select([ + 'AVG(rating_florist) as avg_florist', + 'AVG(rating_cleanliness) as avg_cleanliness', + 'AVG(rating_assortment) as avg_assortment', + 'AVG(rating_quality) as avg_quality', + 'COUNT(*) as reviews_count' + ]) + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'survey_date', date('Y-m-01')]) + ->asArray() + ->one(); + +echo "Флорист: " . round($avgRatings['avg_florist'], 2) . "\n"; +echo "Чистота: " . round($avgRatings['avg_cleanliness'], 2) . "\n"; +echo "Ассортимент: " . round($avgRatings['avg_assortment'], 2) . "\n"; +echo "Качество: " . round($avgRatings['avg_quality'], 2) . "\n"; +echo "Всего отзывов: " . $avgRatings['reviews_count'] . "\n"; +``` + +### Рейтинг флориста +```php +$floristRating = UserReviews::find() + ->select([ + 'AVG(rating_florist) as avg_rating', + 'COUNT(*) as reviews_count' + ]) + ->where(['admin_id' => $adminGuid]) + ->andWhere(['>=', 'survey_date', date('Y-m-01')]) + ->asArray() + ->one(); + +echo "Средняя оценка: " . round($floristRating['avg_rating'], 2); +echo " ({$floristRating['reviews_count']} отзывов)"; +``` + +### Топ магазинов по оценкам +```php +$topStores = UserReviews::find() + ->select([ + 'store_id', + '(AVG(rating_florist) + AVG(rating_cleanliness) + AVG(rating_assortment) + AVG(rating_quality)) / 4 as avg_total', + 'COUNT(*) as reviews_count' + ]) + ->where(['>=', 'survey_date', date('Y-m-01')]) + ->groupBy('store_id') + ->having(['>=', 'COUNT(*)', 10]) // Минимум 10 отзывов + ->orderBy(['avg_total' => SORT_DESC]) + ->limit(10) + ->asArray() + ->all(); +``` + +### Низкие оценки для внимания +```php +$lowRatings = UserReviews::find() + ->where(['OR', + ['<=', 'rating_florist', 2], + ['<=', 'rating_cleanliness', 2], + ['<=', 'rating_assortment', 2], + ['<=', 'rating_quality', 2] + ]) + ->andWhere(['>=', 'survey_date', date('Y-m-d', strtotime('-7 days'))]) + ->orderBy(['survey_date' => SORT_DESC]) + ->all(); + +foreach ($lowRatings as $review) { + echo "Магазин: {$review->store_id}\n"; + echo "Флорист: {$review->rating_florist}, "; + echo "Чистота: {$review->rating_cleanliness}, "; + echo "Ассортимент: {$review->rating_assortment}, "; + echo "Качество: {$review->rating_quality}\n\n"; +} +``` + +### Динамика оценок по месяцам +```php +$monthlyStats = UserReviews::find() + ->select([ + "DATE_TRUNC('month', survey_date) as month", + 'AVG(rating_florist) as avg_florist', + 'AVG(rating_cleanliness) as avg_cleanliness', + 'AVG(rating_assortment) as avg_assortment', + 'AVG(rating_quality) as avg_quality' + ]) + ->where(['store_id' => $storeGuid]) + ->groupBy("DATE_TRUNC('month', survey_date)") + ->orderBy(['month' => SORT_ASC]) + ->asArray() + ->all(); +``` + +### Распределение оценок +```php +$distribution = UserReviews::find() + ->select([ + 'rating_florist', + 'COUNT(*) as count' + ]) + ->where(['store_id' => $storeGuid]) + ->groupBy('rating_florist') + ->orderBy(['rating_florist' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($distribution as $row) { + echo "{$row['rating_florist']} звёзд: {$row['count']} отзывов\n"; +} +``` + +### NPS-подобный анализ +```php +$reviews = UserReviews::find() + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'survey_date', date('Y-m-01')]) + ->all(); + +$promoters = 0; // 5 по всем критериям +$passives = 0; // 4 по всем критериям +$detractors = 0; // 3 и ниже хотя бы по одному + +foreach ($reviews as $review) { + $minRating = min( + $review->rating_florist, + $review->rating_cleanliness, + $review->rating_assortment, + $review->rating_quality + ); + + if ($minRating == 5) { + $promoters++; + } elseif ($minRating == 4) { + $passives++; + } else { + $detractors++; + } +} + +$total = count($reviews); +$nps = $total > 0 ? round(($promoters - $detractors) / $total * 100) : 0; +echo "NPS: {$nps}"; +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `survey_id` | required, string (max 255) | +| `sale_date` | required, safe | +| `survey_date` | required, safe | +| `receipt_number` | required, string (max 255) | +| `store_id` | required, string (max 255) | +| `phone` | required, string (max 255) | +| `admin_id` | string (max 255) | +| `rating_florist` | required, integer, min 1, max 5 | +| `rating_cleanliness` | required, integer, min 1, max 5 | +| `rating_assortment` | required, integer, min 1, max 5 | +| `rating_quality` | required, integer, min 1, max 5 | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины (store_id) +- [Admin](./Admin.md) — сотрудники (admin_id) +- [Sales](./Sales.md) — продажи (receipt_number) + +## Особенности реализации + +1. **4 критерия оценки**: Флорист, чистота, ассортимент, качество +2. **Шкала 1-5**: Валидация диапазона оценок +3. **GUID-связи**: store_id и admin_id хранятся как строки +4. **Связь с продажей**: receipt_number для привязки к чеку +5. **Две даты**: sale_date (покупка) и survey_date (опрос) +6. **Внешний ID**: survey_id для интеграции с системой опросов diff --git a/erp24/docs/models/UsersAuthCallLog.md b/erp24/docs/models/UsersAuthCallLog.md new file mode 100644 index 00000000..f7d179e2 --- /dev/null +++ b/erp24/docs/models/UsersAuthCallLog.md @@ -0,0 +1,253 @@ +# Класс: UsersAuthCallLog + + +## Mindmap + +```mermaid +mindmap + root((UsersAuthCallLog)) + Таблица БД + users_auth_call_log + Свойства + id + int + store_id + string + seller_id + string + date + string + phone + string + name + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель логирования авторизационных звонков клиентов в ERP24. Фиксирует попытки идентификации клиентов по телефону при обслуживании в магазине для привязки продажи к профилю клиента. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`users_auth_call_log` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `store_id` | varchar(36) | GUID магазина | +| `seller_id` | varchar(36) | GUID продавца (флориста) | +| `date` | datetime | Дата и время звонка | +| `phone` | varchar(13) | Номер телефона клиента | +| `name` | varchar(150) | Имя клиента | + +## Виртуальные свойства + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$cnt` | int | Счётчик (для агрегации) | + +## Диаграмма связей + +```mermaid +erDiagram + UsersAuthCallLog { + int id PK + varchar store_id FK + varchar seller_id FK + datetime date + varchar phone + varchar name + } + + CityStore { + varchar guid PK + varchar name + } + + Admin { + varchar guid PK + varchar name + } + + Users { + int id PK + varchar phone + varchar name + } + + CityStore ||--o{ UsersAuthCallLog : "store_id" + Admin ||--o{ UsersAuthCallLog : "seller_id" + Users ||--o{ UsersAuthCallLog : "phone" +``` + +## Диаграмма процесса авторизации + +```mermaid +flowchart TD + A[Клиент в магазине] --> B[Флорист запрашивает
    номер телефона] + B --> C[Поиск клиента в Users] + + C --> D{Клиент найден?} + D -->|Да| E[Отображение
    данных клиента] + D -->|Нет| F[Создание
    нового клиента] + + E --> G[Логирование в
    UsersAuthCallLog] + F --> G + + G --> H[Привязка
    к продаже] +``` + +## Примеры использования + +### Логирование авторизации клиента +```php +$log = new UsersAuthCallLog(); +$log->store_id = $storeGuid; +$log->seller_id = $sellerGuid; +$log->date = date('Y-m-d H:i:s'); +$log->phone = '+79001234567'; +$log->name = 'Иван Иванов'; +$log->save(); +``` + +### Получение логов магазина за день +```php +$logs = UsersAuthCallLog::find() + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'date', date('Y-m-d 00:00:00')]) + ->andWhere(['<=', 'date', date('Y-m-d 23:59:59')]) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($logs as $log) { + echo "{$log->date}: {$log->name} ({$log->phone})\n"; +} +``` + +### Статистика по продавцам +```php +$sellerStats = UsersAuthCallLog::find() + ->select(['seller_id', 'COUNT(*) as cnt']) + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'date', date('Y-m-01')]) + ->groupBy('seller_id') + ->orderBy(['cnt' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($sellerStats as $stat) { + echo "Продавец {$stat['seller_id']}: {$stat['cnt']} авторизаций\n"; +} +``` + +### Поиск частых клиентов +```php +$frequentCustomers = UsersAuthCallLog::find() + ->select(['phone', 'name', 'COUNT(*) as cnt']) + ->where(['store_id' => $storeGuid]) + ->groupBy(['phone', 'name']) + ->having(['>=', 'COUNT(*)', 3]) + ->orderBy(['cnt' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($frequentCustomers as $customer) { + echo "{$customer['name']} ({$customer['phone']}): {$customer['cnt']} визитов\n"; +} +``` + +### Статистика по магазинам +```php +$storeStats = UsersAuthCallLog::find() + ->select(['store_id', 'COUNT(*) as cnt']) + ->where(['>=', 'date', date('Y-m-d', strtotime('-7 days'))]) + ->groupBy('store_id') + ->orderBy(['cnt' => SORT_DESC]) + ->asArray() + ->all(); +``` + +### Поиск авторизаций по телефону +```php +$customerHistory = UsersAuthCallLog::find() + ->where(['phone' => '+79001234567']) + ->orderBy(['date' => SORT_DESC]) + ->all(); + +foreach ($customerHistory as $log) { + echo "{$log->date} - {$log->store_id}\n"; +} +``` + +### Уникальные клиенты за период +```php +$uniqueCustomers = UsersAuthCallLog::find() + ->select(['phone']) + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'date', date('Y-m-01')]) + ->distinct() + ->count(); + +echo "Уникальных клиентов: {$uniqueCustomers}"; +``` + +### Динамика авторизаций по дням +```php +$dailyStats = UsersAuthCallLog::find() + ->select(['DATE(date) as day', 'COUNT(*) as cnt']) + ->where(['store_id' => $storeGuid]) + ->andWhere(['>=', 'date', date('Y-m-01')]) + ->groupBy('DATE(date)') + ->orderBy(['day' => SORT_ASC]) + ->asArray() + ->all(); + +foreach ($dailyStats as $stat) { + echo "{$stat['day']}: {$stat['cnt']} авторизаций\n"; +} +``` + +### Последние авторизации клиента +```php +$lastAuth = UsersAuthCallLog::find() + ->where(['phone' => $phone]) + ->orderBy(['date' => SORT_DESC]) + ->one(); + +if ($lastAuth) { + echo "Последний визит: {$lastAuth->date} в магазине {$lastAuth->store_id}"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `store_id` | required, string (max 36) | +| `seller_id` | required, string (max 36) | +| `date` | required, safe | +| `phone` | required, string (max 13) | +| `name` | required, string (max 150) | + +## Связанные модели + +- [CityStore](./CityStore.md) — магазины (store_id через GUID) +- [Admin](./Admin.md) — продавцы (seller_id через GUID) +- [Users](./Users.md) — клиенты (phone) + +## Особенности реализации + +1. **GUID-связи**: store_id и seller_id хранятся как varchar(36) +2. **Короткий телефон**: phone до 13 символов (+79001234567) +3. **Виртуальный счётчик**: $cnt для агрегирующих запросов +4. **Идентификация клиента**: Основа для CRM-аналитики +5. **Привязка к продажам**: Логи связываются с чеками по времени +6. **Аналитика посещений**: Отслеживание частоты визитов клиентов diff --git a/erp24/docs/models/UsersBonus.md b/erp24/docs/models/UsersBonus.md index df9ed6f3..409ab9c7 100644 --- a/erp24/docs/models/UsersBonus.md +++ b/erp24/docs/models/UsersBonus.md @@ -1,5 +1,30 @@ # Model: UsersBonus + +## Mindmap + +```mermaid +mindmap + root((UsersBonus)) + Таблица БД + users_bonus + Свойства + id + int + phone + string + name + string + site_id + int + setka_id + int + tip + string + Наследование + extends yiidbActiveRecord +``` + ## Назначение Модель движений бонусов клиентов. Хранит историю начислений, списаний и сгораний бонусов в рамках программы лояльности. diff --git a/erp24/docs/models/UsersBonusLevels.md b/erp24/docs/models/UsersBonusLevels.md new file mode 100644 index 00000000..4463b029 --- /dev/null +++ b/erp24/docs/models/UsersBonusLevels.md @@ -0,0 +1,513 @@ +# Model: UsersBonusLevels + + +## Mindmap + +```mermaid +mindmap + root((UsersBonusLevels)) + Таблица БД + users_bonus_levels + Свойства + id + int + phone + string + user_id + int + bonus_level + string + date_from + string + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель истории изменения уровней бонусной программы клиентов. Отслеживает переходы клиентов между уровнями, фиксирует дату и основание (чек) для каждого изменения уровня. Используется для аналитики прогресса клиентов, аудита изменений и расчета условий начисления бонусов за исторический период. + +Каждая запись представляет период действия определенного уровня для клиента. Активная запись имеет `active = 1` и `date_to = null`. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_bonus_levels` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `phone` | string(255) | **Телефон клиента** (FK к таблице users) | +| `user_id` | int | **ID клиента** (FK к таблице users.id) | +| `bonus_level` | string(255) | **Уровень клиента в бонусной системе** (алиас из BonusLevels) | +| `date_from` | datetime | **Дата начала** действия уровня | +| `date_to` | datetime | **Дата окончания** действия уровня (null - если активен) | +| `check_id` | string(255) | **GUID чека** - основание для повышения уровня | +| `check_name` | string(255) | **Номер чека** - читаемое представление | +| `active` | int | **Активность записи** (0 - закрыта, 1 - действует) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['phone', 'user_id', 'bonus_level'] +``` + +### Целочисленные поля с значением по умолчанию null +```php +['user_id', 'active'] // integer, default: null +``` + +### Строковые поля +```php +[ + 'check_id', 'check_name', 'phone', + 'bonus_level', 'date_from', 'date_to' +] // max:255 +``` + +### Даты +```php +['check_id', 'check_name', 'date_from', 'date_to'] // safe +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'phone' => 'Телефон клиента', + 'user_id' => 'ID клиента', + 'bonus_level' => 'Уровань клиента в БС', + 'date_from' => 'Дата создания', + 'active' => 'Активность записи', + 'date_to' => 'Дата изменения', + 'check_id' => 'Основание для повышения уровня - GUID', + 'check_name' => 'Основание для повышения уровня - номер чека', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['id' => 'user_id']` +**Описание:** Клиент, которому принадлежит история уровней + +**Пример:** +```php +$levelHistory = UsersBonusLevels::findOne($id); +$user = $levelHistory->user; +echo "Клиент: {$user->name}"; +``` + +### getBonusLevel() +**Тип:** `hasOne` +**Модель:** `BonusLevels` +**Ключ:** `['alias' => 'bonus_level']` +**Описание:** Справочник уровня бонусной программы + +**Пример:** +```php +$levelHistory = UsersBonusLevels::findOne($id); +$level = $levelHistory->bonusLevel; +echo "Уровень: {$level->name}, кэшбек: {$level->cashback_rate}%"; +``` + +### getCheck() +**Тип:** `hasOne` +**Модель:** `Sales` +**Ключ:** `['id' => 'check_id']` +**Описание:** Чек, после которого произошло повышение уровня + +**Пример:** +```php +$levelHistory = UsersBonusLevels::findOne($id); +if ($levelHistory->check_id) { + $check = $levelHistory->check; + echo "Чек на сумму: {$check->summ}"; +} +``` + +--- + +## Примеры использования + +### Создание записи при повышении уровня + +```php +use yii_app\records\UsersBonusLevels; + +// Закрытие предыдущего уровня +$previousLevel = UsersBonusLevels::find() + ->where(['phone' => $phone, 'active' => 1]) + ->one(); + +if ($previousLevel) { + $previousLevel->active = 0; + $previousLevel->date_to = date('Y-m-d H:i:s'); + $previousLevel->save(); +} + +// Создание нового уровня +$newLevel = new UsersBonusLevels(); +$newLevel->phone = $phone; +$newLevel->user_id = $userId; +$newLevel->bonus_level = 'gold'; // Новый уровень +$newLevel->date_from = date('Y-m-d H:i:s'); +$newLevel->check_id = $checkGuid; // Чек, после которого повысили +$newLevel->check_name = $checkNumber; +$newLevel->active = 1; +$newLevel->save(); +``` + +### Получение текущего уровня клиента + +```php +$currentLevel = UsersBonusLevels::find() + ->where(['phone' => $phone, 'active' => 1]) + ->one(); + +if ($currentLevel) { + echo "Текущий уровень: {$currentLevel->bonus_level}\n"; + echo "С {$currentLevel->date_from}\n"; +} +``` + +### История уровней клиента + +```php +$history = UsersBonusLevels::find() + ->where(['phone' => $phone]) + ->orderBy(['date_from' => SORT_DESC]) + ->all(); + +echo "История уровней клиента:\n"; +foreach ($history as $record) { + $status = $record->active ? 'Активен' : 'Закрыт'; + $dateTo = $record->date_to ?? 'по настоящее время'; + echo "{$record->bonus_level} - {$status} (с {$record->date_from} до {$dateTo})\n"; +} +``` + +### Статистика по уровням + +```php +$stats = UsersBonusLevels::find() + ->select(['bonus_level', 'COUNT(DISTINCT phone) as clients_count']) + ->where(['active' => 1]) + ->groupBy('bonus_level') + ->asArray() + ->all(); + +echo "Распределение клиентов по уровням:\n"; +foreach ($stats as $stat) { + echo "{$stat['bonus_level']}: {$stat['clients_count']} клиентов\n"; +} +``` + +### Клиенты, достигшие уровня за период + +```php +$startDate = '2025-01-01'; +$endDate = '2025-01-31'; + +$newGoldClients = UsersBonusLevels::find() + ->where(['bonus_level' => 'gold']) + ->andWhere(['>=', 'date_from', $startDate]) + ->andWhere(['<=', 'date_from', $endDate]) + ->all(); + +echo "Клиентов достигло золотого уровня в январе: " . count($newGoldClients); +``` + +### Средняя продолжительность нахождения на уровне + +```php +$avgDuration = UsersBonusLevels::find() + ->select([ + 'bonus_level', + 'AVG(TIMESTAMPDIFF(DAY, date_from, date_to)) as avg_days' + ]) + ->where(['active' => 0]) // Только закрытые периоды + ->andWhere(['IS NOT', 'date_to', null]) + ->groupBy('bonus_level') + ->asArray() + ->all(); + +foreach ($avgDuration as $stat) { + echo "{$stat['bonus_level']}: в среднем {$stat['avg_days']} дней\n"; +} +``` + +### Проверка, был ли клиент на определенном уровне + +```php +$phone = 79991234567; +$levelToCheck = 'platinum'; + +$wasOnLevel = UsersBonusLevels::find() + ->where(['phone' => $phone, 'bonus_level' => $levelToCheck]) + ->exists(); + +if ($wasOnLevel) { + echo "Клиент был на платиновом уровне"; +} else { + echo "Клиент не достигал платинового уровня"; +} +``` + +### Получение чека, который привел к повышению + +```php +$levelRecord = UsersBonusLevels::find() + ->where(['phone' => $phone, 'active' => 1]) + ->one(); + +if ($levelRecord && $levelRecord->check_id) { + $check = Sales::find() + ->where(['id' => $levelRecord->check_id]) + ->one(); + + echo "Повышение произошло после покупки на {$check->summ} руб."; +} +``` + +--- + +## Бизнес-логика + +### Жизненный цикл записи + +1. **Создание** - при достижении клиентом порога нового уровня +2. **Активный период** - `active = 1`, `date_to = null` +3. **Закрытие** - при переходе на следующий уровень или деактивации +4. **История** - сохраняется навсегда для аналитики + +### Переход между уровнями + +При повышении уровня: +```php +// 1. Закрыть текущий активный период +UPDATE users_bonus_levels +SET active = 0, date_to = NOW() +WHERE phone = :phone AND active = 1; + +// 2. Создать новую запись с новым уровнем +INSERT INTO users_bonus_levels (phone, user_id, bonus_level, date_from, active) +VALUES (:phone, :user_id, :new_level, NOW(), 1); + +// 3. Обновить Users.bonus_level +UPDATE users SET bonus_level = :new_level WHERE phone = :phone; +``` + +### Связь с чеком + +Поле `check_id` хранит GUID чека, после которого произошло повышение уровня. Это позволяет: +- Проанализировать, какие покупки приводят к переходам +- Рассчитать ROI бонусной программы +- Аудировать корректность начисления уровней + +### Использование в расчетах + +Историческая таблица используется для: +- Определения условий начисления бонусов за прошлые периоды +- Пересчета бонусов при изменении правил +- Аналитики эффективности бонусной программы +- Построения когортных анализов + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersBonusLevels { + int id PK + string phone FK + int user_id FK + string bonus_level FK + datetime date_from + datetime date_to + string check_id FK + string check_name + int active + } + + Users ||--o{ UsersBonusLevels : "has history" + BonusLevels ||--o{ UsersBonusLevels : "references" + Sales ||--o| UsersBonusLevels : "triggered upgrade" + + Users { + int id PK + string phone UK + string bonus_level + int sale_price + } + + BonusLevels { + int id PK + string alias UK + string name + int threshold + } + + Sales { + string id PK + bigint phone + decimal summ + datetime date + } +``` + +--- + +## Диаграмма процесса повышения уровня + +```mermaid +sequenceDiagram + participant Sale as Покупка + participant Service as BonusService + participant UBL as UsersBonusLevels + participant Users as Users + participant BL as BonusLevels + + Sale->>Service: Новая покупка + Service->>Users: Получить общую сумму покупок + Users-->>Service: total_purchases + Service->>BL: Определить новый уровень + BL-->>Service: new_level + Service->>Service: Сравнить с текущим уровнем + + alt Уровень повысился + Service->>UBL: Закрыть текущий период (active=0) + Service->>UBL: Создать новый период (new_level) + Service->>Users: Обновить Users.bonus_level + Users-->>Service: OK + Service-->>Sale: Уровень повышен + else Уровень не изменился + Service-->>Sale: Уровень прежний + end +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE users_bonus_levels ADD PRIMARY KEY (id); + +-- Индекс для поиска активного уровня клиента +CREATE INDEX idx_users_bonus_levels_phone_active +ON users_bonus_levels(phone, active); + +-- Индекс для поиска по user_id +CREATE INDEX idx_users_bonus_levels_user_id +ON users_bonus_levels(user_id); + +-- Индекс для поиска по уровню +CREATE INDEX idx_users_bonus_levels_bonus_level +ON users_bonus_levels(bonus_level); + +-- Индекс для поиска по дате начала +CREATE INDEX idx_users_bonus_levels_date_from +ON users_bonus_levels(date_from); + +-- Индекс для поиска по чеку +CREATE INDEX idx_users_bonus_levels_check_id +ON users_bonus_levels(check_id); +``` + +### Оптимизация запросов + +```php +// Эффективный запрос для получения текущего уровня +$currentLevel = UsersBonusLevels::find() + ->where(['phone' => $phone, 'active' => 1]) + ->one(); + +// Использование WITH для связанных данных +$history = UsersBonusLevels::find() + ->with(['user', 'bonusLevel']) + ->where(['phone' => $phone]) + ->orderBy(['date_from' => SORT_DESC]) + ->all(); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [BonusLevels](BonusLevels.md) - Справочник уровней +- [Sales](Sales.md) - Продажи +- [UsersBonus](UsersBonus.md) - Движения бонусов + +--- + +## Связанные сервисы + +- **BonusService** - Управление бонусной программой +- **LevelService** - Логика переходов между уровнями +- **AnalyticsService** - Аналитика уровней клиентов + +--- + +## API Endpoints + +- `GET /api2/bonus/level-history` - История уровней клиента +- `GET /api2/bonus/current-level` - Текущий уровень клиента +- `POST /admin/bonus/manual-level-change` - Ручное изменение уровня + +--- + +## Замечания + +1. **Активная запись** - только одна запись на клиента может иметь `active = 1`. + +2. **Историчность** - все периоды сохраняются навсегда для аудита. + +3. **Чек-основание** - не обязательное поле, может быть null при ручном изменении. + +4. **Синхронизация с Users** - поле `Users.bonus_level` всегда соответствует активной записи. + +5. **Даты** - `date_from` обязательна, `date_to = null` для активной записи. + +6. **Дубликаты phone/user_id** - оба поля FK к Users для совместимости. + +7. **String bonus_level** - хранится алиас, а не ID уровня для читаемости. + +8. **Длина полей** - все строковые поля имеют max:255. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersEvents.md b/erp24/docs/models/UsersEvents.md new file mode 100644 index 00000000..fd61ee71 --- /dev/null +++ b/erp24/docs/models/UsersEvents.md @@ -0,0 +1,530 @@ +# Model: UsersEvents + + +## Mindmap + +```mermaid +mindmap + root((UsersEvents)) + Таблица БД + users_events + Свойства + id + int + phone + int + number + int + date + string + date_day + int + date_month + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель памятных дат клиентов в системе ERP24. Хранит информацию о важных событиях в жизни клиентов: дни рождения, годовщины, памятные даты. Используется для персонализированных маркетинговых кампаний, автоматических поздравлений и формирования когорт для целевых рассылок. + +События участвуют в когортном анализе - клиенты с памятными датами попадают в специальные выборки для напоминаний о необходимости сделать подарок. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_events` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `phone` | bigint | **Телефон клиента** (FK к таблице users) | +| `number` | int | **Номер события** (порядковый номер для клиента) | +| `date` | date | **Дата события** (полная дата в формате Y-m-d) | +| `date_day` | int | **День события** (от 1 до 31) | +| `date_month` | int | **Месяц события** (от 1 до 12) | +| `tip_id` | int | **ID типа события** (FK к справочнику типов) | +| `tip` | string(140) | **Тип события** (текстовое описание: "День рождения", "Годовщина свадьбы" и т.д.) | +| `name` | string(155) | **Имя получателя подарка** (для кого предназначено событие) | +| `sex` | text | **Пол получателя** (М - мужской, Ж - женский) | +| `date_add` | datetime | **Дата добавления** записи в систему | +| `date_edit` | datetime | **Дата редактирования** записи | +| `date_edit_info` | text | **Дата изменения информации** о клиенте | +| `cannel` | text | **Источник добавления** даты (1С, Telegram, Web, Mobile App) | + +--- + +## Правила валидации + +### Обязательные поля +```php +[ + 'phone', 'number', 'date', 'date_day', 'date_month', + 'tip', 'name', 'sex', 'date_add', 'date_edit', 'date_edit_info' +] +``` + +### Целочисленные поля +```php +['phone', 'number', 'date_day', 'date_month', 'tip_id'] // integer +``` + +### Строковые поля +```php +['tip'] // max:140 +['name'] // max:155 +['sex', 'date_edit_info', 'cannel'] // text +``` + +### Даты +```php +['date', 'date_add', 'date_edit'] // safe (datetime validation) +``` + +### Уникальность (закомментирована) +```php +// ['phone', 'date_day', 'date_month'] // unique composite +// Закомментирована, так как клиент может иметь несколько событий в один день +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'phone' => 'Phone', + 'number' => 'Number', + 'date' => 'Date', + 'date_day' => 'Date Day', + 'date_month' => 'Date Month', + 'tip_id' => 'Tip ID', + 'tip' => 'Tip', + 'name' => 'Name', + 'sex' => 'Sex', + 'date_add' => 'Date Add', + 'date_edit' => 'Date Edit', + 'date_edit_info' => 'Date Edit Info', + 'cannel' => 'Cannel', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['phone' => 'phone']` +**Описание:** Клиент, которому принадлежит событие + +**Пример:** +```php +$event = UsersEvents::findOne($id); +$user = $event->user; +echo "Событие клиента: {$user->name}"; +``` + +--- + +## Примеры использования + +### Создание нового события + +```php +use yii_app\records\UsersEvents; + +// Добавление дня рождения +$event = new UsersEvents(); +$event->phone = 79991234567; +$event->number = 1; // Первое событие клиента +$event->date = '1990-05-15'; +$event->date_day = 15; +$event->date_month = 5; +$event->tip_id = 1; // ID типа "День рождения" +$event->tip = 'День рождения'; +$event->name = 'Иван'; +$event->sex = 'М'; +$event->date_add = date('Y-m-d H:i:s'); +$event->date_edit = date('Y-m-d H:i:s'); +$event->date_edit_info = date('Y-m-d H:i:s'); +$event->cannel = 'Telegram'; +$event->save(); +``` + +### Добавление годовщины свадьбы + +```php +$anniversary = new UsersEvents(); +$anniversary->phone = 79991234567; +$anniversary->number = 2; // Второе событие клиента +$anniversary->date = '2015-08-20'; +$anniversary->date_day = 20; +$anniversary->date_month = 8; +$anniversary->tip_id = 2; +$anniversary->tip = 'Годовщина свадьбы'; +$anniversary->name = 'Мария'; // Имя супруги +$anniversary->sex = 'Ж'; +$anniversary->date_add = date('Y-m-d H:i:s'); +$anniversary->date_edit = date('Y-m-d H:i:s'); +$anniversary->date_edit_info = date('Y-m-d H:i:s'); +$anniversary->cannel = 'Web'; +$anniversary->save(); +``` + +### Получение всех событий клиента + +```php +$phone = 79991234567; + +$events = UsersEvents::find() + ->where(['phone' => $phone]) + ->orderBy(['date_month' => SORT_ASC, 'date_day' => SORT_ASC]) + ->all(); + +foreach ($events as $event) { + echo "{$event->tip}: {$event->name} - {$event->date_day}/{$event->date_month}\n"; +} +``` + +### Поиск событий на ближайший месяц + +```php +$currentMonth = (int)date('m'); +$currentDay = (int)date('d'); + +// События в текущем месяце после сегодняшнего дня +$upcomingEvents = UsersEvents::find() + ->where(['date_month' => $currentMonth]) + ->andWhere(['>=', 'date_day', $currentDay]) + ->orderBy(['date_day' => SORT_ASC]) + ->all(); + +foreach ($upcomingEvents as $event) { + $daysUntil = $event->date_day - $currentDay; + echo "Через {$daysUntil} дней: {$event->tip} - {$event->name}\n"; +} +``` + +### Выборка событий по конкретной дате (для когорт) + +```php +// Все события на 15 мая +$events = UsersEvents::find() + ->where([ + 'date_month' => 5, + 'date_day' => 15 + ]) + ->all(); + +echo "Найдено событий на 15 мая: " . count($events) . "\n"; + +foreach ($events as $event) { + echo "Телефон: {$event->phone}, событие: {$event->tip}\n"; +} +``` + +### Обновление информации о событии + +```php +$event = UsersEvents::find() + ->where([ + 'phone' => 79991234567, + 'number' => 1 + ]) + ->one(); + +if ($event) { + $event->name = 'Иван Иванович'; + $event->tip = 'День рождения супруга'; + $event->date_edit = date('Y-m-d H:i:s'); + $event->save(); +} +``` + +### Удаление события + +```php +$event = UsersEvents::find() + ->where([ + 'phone' => 79991234567, + 'number' => 2 + ]) + ->one(); + +if ($event) { + $event->delete(); +} +``` + +### Статистика по типам событий + +```php +$stats = UsersEvents::find() + ->select(['tip', 'COUNT(*) as cnt']) + ->groupBy('tip') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "{$stat['tip']}: {$stat['cnt']} событий\n"; +} +``` + +### События по полу получателя + +```php +// Все мужские дни рождения в мае +$maleEvents = UsersEvents::find() + ->where([ + 'date_month' => 5, + 'sex' => 'М' + ]) + ->all(); + +// Все женские события +$femaleEvents = UsersEvents::find() + ->where(['sex' => 'Ж']) + ->count(); + +echo "Женских событий в базе: {$femaleEvents}"; +``` + +### Интеграция с когортами + +```php +use yii_app\records\Users; + +// Формирование когорты на основе событий на завтра +$targetDate = date('Y-m-d', strtotime('+1 day')); +$targetMonth = (int)date('m', strtotime($targetDate)); +$targetDay = (int)date('d', strtotime($targetDate)); + +// Получение событий на целевую дату +$events = UsersEvents::find() + ->where([ + 'date_month' => $targetMonth, + 'date_day' => $targetDay + ]) + ->all(); + +$phones = []; +foreach ($events as $event) { + $phones[] = $event->phone; +} + +// Получение клиентов с этими телефонами +$users = Users::find() + ->where(['phone' => $phones]) + ->all(); + +echo "Клиентов с событиями завтра: " . count($users) . "\n"; +``` + +--- + +## Бизнес-логика + +### Назначение полей date_day и date_month + +Поля `date_day` и `date_month` дублируют информацию из поля `date` для оптимизации когортных запросов. Вместо использования функций извлечения дня/месяца из даты, используются прямые индексы. + +```sql +-- Медленный запрос +SELECT * FROM users_events WHERE MONTH(date) = 5 AND DAY(date) = 15; + +-- Быстрый запрос с индексами +SELECT * FROM users_events WHERE date_month = 5 AND date_day = 15; +``` + +### Нумерация событий (number) + +Поле `number` содержит порядковый номер события для клиента. При добавлении нового события необходимо определить максимальный номер и увеличить на 1: + +```php +$maxNumber = UsersEvents::find() + ->where(['phone' => $phone]) + ->max('number'); + +$newEvent->number = ($maxNumber ?? 0) + 1; +``` + +### Источники данных (cannel) + +События могут быть добавлены из различных источников: +- **1С** - при регистрации клиента в магазине +- **Telegram** - клиент сам добавил через бота +- **Web** - через веб-интерфейс личного кабинета +- **Mobile App** - через мобильное приложение + +### Пол получателя (sex) + +Поле `sex` определяет пол человека, для которого предназначено событие (получатель подарка). Это важно для: +- Подбора подходящих товаров в рекомендациях +- Формирования текста поздравлений +- Сегментации клиентов для таргетированной рекламы + +Значения: +- **М** - мужской +- **Ж** - женский + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersEvents { + int id PK + bigint phone FK + int number + date date + int date_day + int date_month + int tip_id FK + string tip + string name + string sex + datetime date_add + datetime date_edit + text date_edit_info + text cannel + } + + Users ||--o{ UsersEvents : "has events" + + Users { + int id PK + string phone UK + string name + decimal balans + int sale_cnt + datetime date_first_sale + } +``` + +--- + +## Диаграмма использования в когортах + +```mermaid +flowchart TD + A[Когортный анализ] --> B{Выборка на дату} + B --> C[Выбрать события на date_day и date_month] + C --> D[Получить телефоны клиентов] + D --> E[Исключить тех, кто в холде] + E --> F[Исключить подписанных в Telegram] + F --> G[Исключить купивших за период] + G --> H[Сформировать когорту] + H --> I[Отправить в WhatsApp/Таргет/Звонки] + + style C fill:#90EE90 + style D fill:#87CEEB + style H fill:#FFD700 +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Основной индекс для поиска событий клиента +CREATE INDEX idx_users_events_phone ON users_events(phone); + +-- Составной индекс для когортных запросов +CREATE INDEX idx_users_events_date_month_day ON users_events(date_month, date_day); + +-- Индекс для поиска по типу события +CREATE INDEX idx_users_events_tip_id ON users_events(tip_id); + +-- Индекс для поиска по полу +CREATE INDEX idx_users_events_sex ON users_events(sex); + +-- Индекс для поиска по источнику +CREATE INDEX idx_users_events_cannel ON users_events(cannel); +``` + +### Оптимизация запросов + +Для когортных запросов используйте индексированные поля: + +```php +// Оптимизированный запрос +$events = UsersEvents::find() + ->where(['date_month' => 5, 'date_day' => 15]) + ->all(); + +// Неоптимальный запрос (не использует индексы) +$events = UsersEvents::find() + ->where("DATE_FORMAT(date, '%m-%d') = '05-15'") + ->all(); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [SentKogort](SentKogort.md) - Когортный анализ +- [UsersTelegram](UsersTelegram.md) - Telegram профили клиентов + +--- + +## Связанные сервисы + +- **KogortService** - Формирование когорт на основе событий +- **NotificationService** - Отправка напоминаний о событиях +- **RecommendationService** - Рекомендации товаров на основе событий + +--- + +## API Endpoints + +- `GET /api2/events/list` - Получение списка событий клиента +- `POST /api2/events/add` - Добавление нового события +- `PUT /api2/events/update` - Обновление события +- `DELETE /api2/events/delete` - Удаление события + +--- + +## Замечания + +1. **Уникальность** - композитный индекс `[phone, date_day, date_month]` закомментирован, так как клиент может иметь несколько событий в один день (например, день рождения мужа и свекрови). + +2. **Нумерация** - поле `number` должно быть уникальным в рамках телефона клиента для идентификации конкретного события. + +3. **Денормализация** - поля `date_day` и `date_month` дублируют данные из `date` для оптимизации запросов. + +4. **Источники** - поле `cannel` (channel) содержит информацию об источнике добавления для аналитики. + +5. **История изменений** - три поля с датами (`date_add`, `date_edit`, `date_edit_info`) позволяют отслеживать жизненный цикл записи. + +6. **Пол** - важен для персонализации и рекомендаций подарков. + +7. **Когорты** - модель активно используется в методе `Users::getUsersListForKogort()` для формирования маркетинговых когорт. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersMessageManagement.md b/erp24/docs/models/UsersMessageManagement.md new file mode 100644 index 00000000..c941aba3 --- /dev/null +++ b/erp24/docs/models/UsersMessageManagement.md @@ -0,0 +1,317 @@ +# Класс: UsersMessageManagement + + +## Mindmap + +```mermaid +mindmap + root((UsersMessageManagement)) + Таблица БД + users_message_management + Свойства + id + int + bonus + float + day_before_step1 + int + day_before_step2 + int + day_before_step3 + int + date_start + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель управления рассылками сообщений клиентам в ERP24. Конфигурирует многоэтапные маркетинговые кампании через различные каналы: Telegram чатбот, WhatsApp и таргетированную рекламу с начислением бонусов. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`users_message_management` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Константы типов каналов + +```php +const TYPE_TARGET = 'target'; // Таргетированная реклама +const TYPE_WHATSAPP = 'whatsapp'; // WhatsApp рассылка +const TYPE_CALL = 'call'; // Телефонный обзвон +``` + +## Константы маппинга офферов + +```php +const TYPE_MESSAGE = [ + 'target' => ['offer_1', 'offer_text'], + 'whatsapp' => ['offer_whatsapp', 'offer_2'], + 'call' => 'offer_text', +]; + +const TYPE_MESSAGE_LABELS = [ + 'offer_1' => 'Первое сообщение в чатбот', + 'offer_text' => 'Сообщение когорты Таргет', + 'offer_whatsapp' => 'Сообщение когорты Whatsapp', + 'offer_2' => 'Второе сообщение в чатбот', +]; +``` + +## Поля таблицы + +### Бонусы и этапы +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `bonus` | float | Количество бонусов к начислению | +| `day_before_step1` | int | Дней до первого этапа | +| `day_before_step2` | int | Дней до второго этапа | +| `day_before_step3` | int | Дней до третьего этапа | +| `day_before_step1_active` | int | Активность 1 этапа (0/1) | +| `day_before_step2_active` | int | Активность 2 этапа (0/1) | +| `day_before_step3_active` | int | Активность 3 этапа (0/1) | + +### Даты и активность +| Поле | Тип | Описание | +|------|-----|----------| +| `date_start` | datetime | Дата начала кампании | +| `date_end` | datetime / null | Дата окончания кампании | +| `date_last_scenario` | datetime | Дата последнего сценария | +| `active` | int | Общая активность (0/1) | +| `hold` | int | Дней удержания номера от рассылки | +| `hold_active` | int | Активность hold (0/1) | + +### Офферы (тексты сообщений) +| Поле | Тип | Описание | +|------|-----|----------| +| `offer_1` | text | Первое сообщение в чатбот | +| `offer_2` | text | Второе сообщение в чатбот | +| `offer_3` | text / null | Третье сообщение в чатбот | +| `offer_whatsapp` | text | Сообщение в WhatsApp | +| `offer_text` | text | Текст для таргета | + +### Тестирование +| Поле | Тип | Описание | +|------|-----|----------| +| `test_phones_list` | text | Список тестовых телефонов | +| `test_phones_active` | int | Активность тестового режима | + +### Интеграция с внешними сервисами +| Поле | Тип | Описание | +|------|-----|----------| +| `channel_name` | varchar / null | Имя канала | +| `channel_id` | varchar / null | ID канала (подпись) | +| `channel_limit` | int / null | Суточный лимит сообщений | +| `cascade_name` | varchar / null | Имя каскада | +| `cascade_id` | int / null | ID каскада | +| `subject_id` | int / null | ID подписи | +| `template_name` | varchar / null | Имя шаблона | +| `template_id` | int / null | ID шаблона | +| `callback_status_url` | varchar / null | URL для колбеков статусов | + +### Аудит +| Поле | Тип | Описание | +|------|-----|----------| +| `created_at` | datetime | Дата создания | +| `created_by` | int | ID создателя | +| `updated_at` | datetime | Дата обновления | +| `updated_by` | int | ID редактора | + +## Методы + +### getBonusAction() +**Описание:** Возвращает срок действия бонуса в днях. + +**Возвращает:** `int` — day_before_step1 + 1 + +### getTestPhonesList() +**Описание:** Возвращает список тестовых телефонов. + +**Возвращает:** `string` + +### replaceShortcodes($message, $targetDate) +**Описание:** Заменяет шорткоды в сообщении на реальные значения. + +**Параметры:** +- `$message` (string) — текст сообщения с шорткодами +- `$targetDate` (string) — целевая дата + +**Поддерживаемые шорткоды:** +- `[NumberOfBonuses]` — количество бонусов +- `[ValidityOfBonuses]` — дата окончания действия бонусов +- `[StepTwoDaysBeforeTarget]` — дней до второго этапа + +**Возвращает:** `string` — сообщение с заменёнными шорткодами + +## Диаграмма связей + +```mermaid +erDiagram + UsersMessageManagement { + int id PK + float bonus + int day_before_step1 + int day_before_step2 + int day_before_step3 + datetime date_start + datetime date_end + text offer_1 + text offer_2 + text offer_whatsapp + text offer_text + int active + int created_by FK + int updated_by FK + } + + UsersMessageManagementLogs { + int id PK + varchar field_name + text value_old + text value_new + } + + Admin { + int id PK + varchar name + } + + UsersMessageManagement ||--o{ UsersMessageManagementLogs : "логи изменений" + Admin ||--o{ UsersMessageManagement : "created_by" + Admin ||--o{ UsersMessageManagement : "updated_by" +``` + +## Диаграмма многоэтапной кампании + +```mermaid +flowchart TD + A[Клиент в когорте] --> B{hold_active?} + B -->|Да| C[Ожидание hold дней] + B -->|Нет| D[Начало кампании] + C --> D + + D --> E{step1_active?} + E -->|Да| F[Этап 1: offer_1
    Чатбот] + E -->|Нет| G{step2_active?} + + F --> H[Ожидание
    day_before_step2 дней] + H --> G + + G -->|Да| I[Этап 2: offer_2
    Чатбот/WhatsApp] + G -->|Нет| J{step3_active?} + + I --> K[Ожидание
    day_before_step3 дней] + K --> J + + J -->|Да| L[Этап 3: offer_text
    Таргет] + J -->|Нет| M[Завершение] + L --> M +``` + +## Примеры использования + +### Создание кампании +```php +$campaign = new UsersMessageManagement(); +$campaign->bonus = 100; +$campaign->day_before_step1 = 7; +$campaign->day_before_step2 = 14; +$campaign->day_before_step3 = 21; +$campaign->day_before_step1_active = 1; +$campaign->day_before_step2_active = 1; +$campaign->day_before_step3_active = 0; +$campaign->date_start = date('Y-m-d H:i:s'); +$campaign->offer_1 = 'Привет! Вам начислено [NumberOfBonuses] бонусов!'; +$campaign->offer_2 = 'Напоминаем о бонусах до [ValidityOfBonuses]'; +$campaign->offer_whatsapp = 'Бонусы ждут вас!'; +$campaign->offer_text = 'Специальное предложение'; +$campaign->hold = 30; +$campaign->hold_active = 1; +$campaign->active = 1; +$campaign->created_by = Yii::$app->user->id; +$campaign->updated_by = Yii::$app->user->id; +$campaign->created_at = date('Y-m-d H:i:s'); +$campaign->updated_at = date('Y-m-d H:i:s'); +$campaign->save(); +``` + +### Получение активной кампании +```php +$campaign = UsersMessageManagement::find() + ->where(['active' => 1]) + ->andWhere(['<=', 'date_start', date('Y-m-d H:i:s')]) + ->andWhere(['OR', + ['date_end' => null], + ['>=', 'date_end', date('Y-m-d H:i:s')] + ]) + ->one(); +``` + +### Формирование сообщения +```php +$campaign = UsersMessageManagement::findOne($campaignId); +$targetDate = '2024-12-25'; + +$message = $campaign->replaceShortcodes($campaign->offer_1, $targetDate); +// "Привет! Вам начислено 100 бонусов!" +``` + +### Проверка тестового режима +```php +$campaign = UsersMessageManagement::findOne($campaignId); + +if ($campaign->test_phones_active) { + $testPhones = explode("\n", $campaign->getTestPhonesList()); + // Отправка только на тестовые номера +} +``` + +### Расчёт срока действия бонуса +```php +$campaign = UsersMessageManagement::findOne($campaignId); +$bonusValidDays = $campaign->getBonusAction(); +echo "Бонус действителен {$bonusValidDays} дней"; +``` + +### Получение офферов для канала +```php +$type = UsersMessageManagement::TYPE_WHATSAPP; +$offerFields = UsersMessageManagement::TYPE_MESSAGE[$type]; +// ['offer_whatsapp', 'offer_2'] + +foreach ($offerFields as $field) { + echo UsersMessageManagement::TYPE_MESSAGE_LABELS[$field] . "\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `bonus` | required, number | +| `day_before_step1-3` | required, integer | +| `date_start` | required, safe | +| `offer_1`, `offer_2` | required, string (max 10000) | +| `offer_whatsapp`, `offer_text` | required, string (max 900) | +| `hold` | required, integer | +| `created_by`, `updated_by` | required, integer | + +## Связанные модели + +- [UsersMessageManagementLogs](./UsersMessageManagementLogs.md) — логи изменений +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Многоэтапные кампании**: 3 этапа с настраиваемыми интервалами +2. **Мультиканальность**: Чатбот, WhatsApp, таргет +3. **Шорткоды**: Динамическая подстановка значений в тексты +4. **Hold-период**: Защита от частых рассылок +5. **Тестовый режим**: Отправка на ограниченный список номеров +6. **Интеграция**: Каскады, шаблоны, callback-URL для внешних сервисов +7. **Аудит**: created_by/updated_by для отслеживания изменений diff --git a/erp24/docs/models/UsersMessageManagementLogs.md b/erp24/docs/models/UsersMessageManagementLogs.md new file mode 100644 index 00000000..1f2ece61 --- /dev/null +++ b/erp24/docs/models/UsersMessageManagementLogs.md @@ -0,0 +1,282 @@ +# Класс: UsersMessageManagementLogs + + +## Mindmap + +```mermaid +mindmap + root((UsersMessageManagementLogs)) + Таблица БД + users_message_management_logs + Свойства + id + int + field_name + string + created_at + string + created_by + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель логирования изменений настроек рассылок в ERP24. Фиксирует историю изменений полей в UsersMessageManagement для аудита и возможности отката конфигурации. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`users_message_management_logs` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `field_name` | varchar(255) | Название изменённого поля | +| `value_old` | text / null | Старое значение | +| `value_new` | text / null | Новое значение | +| `created_at` | datetime | Время создания записи | +| `created_by` | int | FK на администратора (Admin) | + +## Связи (Relations) + +| Метод | Тип связи | Модель | Описание | +|-------|-----------|--------|----------| +| `getAdmin()` | hasOne | Admin | Автор изменения | + +## Диаграмма связей + +```mermaid +erDiagram + UsersMessageManagementLogs { + int id PK + varchar field_name + text value_old + text value_new + datetime created_at + int created_by FK + } + + UsersMessageManagement { + int id PK + float bonus + text offer_1 + text offer_2 + } + + Admin { + int id PK + varchar name + } + + UsersMessageManagement ||--o{ UsersMessageManagementLogs : "логи изменений" + Admin ||--o{ UsersMessageManagementLogs : "created_by" +``` + +## Диаграмма процесса логирования + +```mermaid +flowchart TD + A[Изменение поля
    UsersMessageManagement] --> B[beforeSave] + B --> C{Поле изменилось?} + C -->|Да| D[Создание записи
    UsersMessageManagementLogs] + C -->|Нет| E[Пропуск] + + D --> F[field_name = имя поля] + F --> G[value_old = старое значение] + G --> H[value_new = новое значение] + H --> I[created_by = текущий пользователь] + + I --> J[Сохранение лога] + E --> K[Продолжение] + J --> K +``` + +## Примеры использования + +### Логирование изменения поля +```php +$log = new UsersMessageManagementLogs(); +$log->field_name = 'bonus'; +$log->value_old = '50'; +$log->value_new = '100'; +$log->created_at = date('Y-m-d H:i:s'); +$log->created_by = Yii::$app->user->id; +$log->save(); +``` + +### Автоматическое логирование при сохранении +```php +// В модели UsersMessageManagement +public function beforeSave($insert) +{ + if (!$insert) { + $oldAttributes = $this->getOldAttributes(); + + foreach ($this->getDirtyAttributes() as $attribute => $newValue) { + $oldValue = $oldAttributes[$attribute] ?? null; + + if ($oldValue !== $newValue) { + $log = new UsersMessageManagementLogs(); + $log->field_name = $attribute; + $log->value_old = $oldValue; + $log->value_new = $newValue; + $log->created_at = date('Y-m-d H:i:s'); + $log->created_by = Yii::$app->user->id; + $log->save(); + } + } + } + + return parent::beforeSave($insert); +} +``` + +### Получение истории изменений поля +```php +$history = UsersMessageManagementLogs::find() + ->where(['field_name' => 'bonus']) + ->with('admin') + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($history as $log) { + echo "{$log->created_at}: {$log->value_old} → {$log->value_new}"; + echo " (изменил: {$log->admin->name})\n"; +} +``` + +### Последние изменения +```php +$recentChanges = UsersMessageManagementLogs::find() + ->with('admin') + ->orderBy(['created_at' => SORT_DESC]) + ->limit(20) + ->all(); + +foreach ($recentChanges as $log) { + $label = UsersMessageManagement::instance()->getAttributeLabel($log->field_name); + echo "{$log->created_at}: {$label}\n"; + echo " {$log->value_old} → {$log->value_new}\n"; +} +``` + +### Изменения за период +```php +$logs = UsersMessageManagementLogs::find() + ->where(['>=', 'created_at', date('Y-m-d 00:00:00')]) + ->andWhere(['<=', 'created_at', date('Y-m-d 23:59:59')]) + ->orderBy(['created_at' => SORT_ASC]) + ->all(); +``` + +### Изменения конкретного пользователя +```php +$userChanges = UsersMessageManagementLogs::find() + ->where(['created_by' => $adminId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Статистика изменений по полям +```php +$fieldStats = UsersMessageManagementLogs::find() + ->select(['field_name', 'COUNT(*) as changes']) + ->groupBy('field_name') + ->orderBy(['changes' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($fieldStats as $stat) { + echo "{$stat['field_name']}: {$stat['changes']} изменений\n"; +} +``` + +### Откат изменения +```php +$log = UsersMessageManagementLogs::find() + ->where(['field_name' => 'bonus']) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($log) { + // Откат к предыдущему значению + $management = UsersMessageManagement::findOne($campaignId); + $management->{$log->field_name} = $log->value_old; + $management->save(); + + echo "Откат: {$log->field_name} = {$log->value_old}"; +} +``` + +### Сравнение версий +```php +function getFieldHistory($fieldName, $limit = 10) +{ + return UsersMessageManagementLogs::find() + ->where(['field_name' => $fieldName]) + ->with('admin') + ->orderBy(['created_at' => SORT_DESC]) + ->limit($limit) + ->all(); +} + +$bonusHistory = getFieldHistory('bonus'); + +echo "История изменений бонуса:\n"; +foreach ($bonusHistory as $log) { + echo "{$log->created_at}: {$log->value_old} → {$log->value_new}\n"; +} +``` + +### Экспорт истории изменений +```php +$logs = UsersMessageManagementLogs::find() + ->with('admin') + ->orderBy(['created_at' => SORT_DESC]) + ->asArray() + ->all(); + +$export = []; +foreach ($logs as $log) { + $export[] = [ + 'date' => $log['created_at'], + 'field' => $log['field_name'], + 'old' => $log['value_old'], + 'new' => $log['value_new'], + 'author' => $log['admin']['name'] ?? 'Unknown' + ]; +} + +file_put_contents('changelog.json', json_encode($export, JSON_PRETTY_PRINT)); +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `field_name` | required, string (max 255) | +| `created_at` | required, safe | +| `created_by` | required, integer | +| `value_old` | string | +| `value_new` | string | + +## Связанные модели + +- [UsersMessageManagement](./UsersMessageManagement.md) — настройки рассылок +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **История изменений**: Полный лог всех изменений полей +2. **Старое и новое значение**: value_old и value_new для сравнения +3. **Аудит**: created_by для отслеживания автора изменения +4. **Гибкость**: field_name позволяет логировать любое поле +5. **Откат**: Возможность восстановления предыдущих значений +6. **Связь с Admin**: Получение информации о пользователе через getAdmin() diff --git a/erp24/docs/models/UsersPhones.md b/erp24/docs/models/UsersPhones.md new file mode 100644 index 00000000..64d3ef11 --- /dev/null +++ b/erp24/docs/models/UsersPhones.md @@ -0,0 +1,526 @@ +# Model: UsersPhones + + +## Mindmap + +```mermaid +mindmap + root((UsersPhones)) + Таблица БД + users_phones + Свойства + phone + int + store_id + int + store_guid + string + seller_id + string + date + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель истории регистраций телефонов клиентов в магазинах. Отслеживает, в каких магазинах и какими продавцами был зарегистрирован телефон клиента. Используется для аналитики работы продавцов, контроля за регистрацией клиентов в бонусной программе и разрешения конфликтов при дублировании регистраций. + +Хранит информацию о первых точках контакта клиента с сетью магазинов, что важно для аналитики эффективности торговых точек и персонала. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_phones` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `phone` | bigint | **Телефон клиента** (FK к таблице users, часть составного PK) | +| `store_id` | int | **ID магазина** (FK к таблице city_store, часть составного PK) | +| `store_guid` | string(36) | **GUID магазина из 1С** (уникальный идентификатор торговой точки) | +| `seller_id` | string(36) | **GUID продавца из 1С** (часть составного PK, кто зарегистрировал клиента) | +| `date` | datetime | **Дата и время регистрации** телефона в данном магазине | + +--- + +## Правила валидации + +### Обязательные поля +```php +['phone', 'store_id', 'store_guid', 'seller_id', 'date'] +``` + +### Целочисленные поля +```php +['phone', 'store_id'] // integer +``` + +### Строковые поля +```php +['store_guid', 'seller_id'] // max:36 (GUID формат) +``` + +### Даты +```php +['date'] // safe (datetime validation) +``` + +### Уникальность +```php +['phone', 'store_id', 'seller_id'] // unique composite +// Один продавец в одном магазине не может зарегистрировать телефон дважды +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'phone' => 'Phone', + 'store_id' => 'Store ID', + 'store_guid' => 'Store Guid', + 'seller_id' => 'Seller ID', + 'date' => 'Date', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['phone' => 'phone']` +**Описание:** Клиент, которому принадлежит телефон + +**Пример:** +```php +$phoneRecord = UsersPhones::find() + ->where(['phone' => 79991234567]) + ->one(); + +$user = $phoneRecord->user; +echo "Клиент: {$user->name}"; +``` + +### getStore() +**Тип:** `hasOne` +**Модель:** `CityStore` +**Ключ:** `['id' => 'store_id']` +**Описание:** Магазин, в котором был зарегистрирован телефон + +**Пример:** +```php +$phoneRecord = UsersPhones::findOne([ + 'phone' => 79991234567, + 'store_id' => 5 +]); + +$store = $phoneRecord->store; +echo "Магазин: {$store->name}"; +``` + +### getSeller() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['guid_1c' => 'seller_id']` +**Описание:** Продавец, который зарегистрировал клиента + +**Пример:** +```php +$phoneRecord = UsersPhones::findOne([ + 'phone' => 79991234567, + 'store_id' => 5 +]); + +$seller = $phoneRecord->seller; +echo "Продавец: {$seller->name}"; +``` + +--- + +## Примеры использования + +### Регистрация нового телефона в магазине + +```php +use yii_app\records\UsersPhones; + +// Проверка существования записи +$exists = UsersPhones::find() + ->where([ + 'phone' => 79991234567, + 'store_id' => 5, + 'seller_id' => 'a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6' + ]) + ->exists(); + +if (!$exists) { + $phoneReg = new UsersPhones(); + $phoneReg->phone = 79991234567; + $phoneReg->store_id = 5; + $phoneReg->store_guid = 'f1e2d3c4-b5a6-7890-1234-567890abcdef'; + $phoneReg->seller_id = 'a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6'; + $phoneReg->date = date('Y-m-d H:i:s'); + + if ($phoneReg->save()) { + echo "Телефон зарегистрирован в магазине"; + } +} +``` + +### Получение истории регистраций телефона + +```php +$phone = 79991234567; + +$history = UsersPhones::find() + ->where(['phone' => $phone]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +echo "История регистраций телефона {$phone}:\n"; +foreach ($history as $record) { + echo "Дата: {$record->date}, Магазин ID: {$record->store_id}, Продавец: {$record->seller_id}\n"; +} +``` + +### Поиск первой регистрации клиента + +```php +$phone = 79991234567; + +$firstRegistration = UsersPhones::find() + ->where(['phone' => $phone]) + ->orderBy(['date' => SORT_ASC]) + ->one(); + +if ($firstRegistration) { + echo "Первая регистрация: {$firstRegistration->date}\n"; + echo "Магазин: {$firstRegistration->store->name}\n"; + echo "Продавец: {$firstRegistration->seller->name}\n"; +} +``` + +### Статистика регистраций по магазинам + +```php +$stats = UsersPhones::find() + ->select(['store_id', 'COUNT(DISTINCT phone) as clients_count']) + ->groupBy('store_id') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + echo "Магазин {$stat['store_id']}: {$stat['clients_count']} клиентов\n"; +} +``` + +### Статистика регистраций по продавцам + +```php +use yii_app\records\Admin; + +$sellerStats = UsersPhones::find() + ->select(['seller_id', 'COUNT(DISTINCT phone) as clients_count']) + ->groupBy('seller_id') + ->asArray() + ->all(); + +foreach ($sellerStats as $stat) { + $seller = Admin::find()->where(['guid_1c' => $stat['seller_id']])->one(); + $sellerName = $seller ? $seller->name : 'Неизвестный'; + echo "{$sellerName}: {$stat['clients_count']} клиентов\n"; +} +``` + +### Регистрации за период + +```php +$startDate = '2025-01-01 00:00:00'; +$endDate = '2025-01-31 23:59:59'; + +$registrations = UsersPhones::find() + ->where(['>=', 'date', $startDate]) + ->andWhere(['<=', 'date', $endDate]) + ->all(); + +echo "Регистраций за январь 2025: " . count($registrations) . "\n"; +``` + +### Проверка, в каких магазинах зарегистрирован телефон + +```php +$phone = 79991234567; + +$stores = UsersPhones::find() + ->select('store_id') + ->where(['phone' => $phone]) + ->column(); + +echo "Телефон зарегистрирован в магазинах: " . implode(', ', $stores) . "\n"; +``` + +### Клиенты, зарегистрированные конкретным продавцом + +```php +$sellerId = 'a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6'; + +$clients = UsersPhones::find() + ->select('phone') + ->where(['seller_id' => $sellerId]) + ->distinct() + ->column(); + +echo "Продавец зарегистрировал клиентов: " . count($clients) . "\n"; +``` + +### Удаление дубликатов регистраций + +```php +$phone = 79991234567; +$storeId = 5; + +// Получение всех регистраций для телефона в магазине +$duplicates = UsersPhones::find() + ->where(['phone' => $phone, 'store_id' => $storeId]) + ->orderBy(['date' => SORT_ASC]) + ->all(); + +// Оставляем только первую (самую раннюю) регистрацию +if (count($duplicates) > 1) { + for ($i = 1; $i < count($duplicates); $i++) { + $duplicates[$i]->delete(); + } + echo "Удалено дубликатов: " . (count($duplicates) - 1) . "\n"; +} +``` + +--- + +## Бизнес-логика + +### Составной первичный ключ + +Таблица использует составной уникальный индекс `[phone, store_id, seller_id]`, что означает: +- Один телефон может быть зарегистрирован в разных магазинах +- В одном магазине телефон может быть зарегистрирован разными продавцами +- Один продавец в одном магазине не может зарегистрировать телефон дважды + +### Использование GUID + +Поля `store_guid` и `seller_id` хранят GUID из системы 1С: +- **store_guid** - уникальный идентификатор торговой точки в 1С +- **seller_id** - уникальный идентификатор сотрудника в 1С + +Это обеспечивает консистентность данных при синхронизации между ERP24 и 1С. + +### Сценарии использования + +1. **Первая регистрация** - клиент приходит в магазин, продавец регистрирует его телефон +2. **Повторное посещение** - клиент приходит в другой магазин сети, создается новая запись +3. **Аналитика продавцов** - подсчет количества зарегистрированных клиентов каждым продавцом +4. **Аналитика магазинов** - определение эффективности точек по привлечению новых клиентов + +### Разрешение конфликтов + +При синхронизации с 1С могут возникать конфликты: +- Телефон уже зарегистрирован другим продавцом в том же магазине +- Различия в GUID магазина между системами + +В таких случаях необходимо: +1. Проверять существование записи перед добавлением +2. Использовать первую регистрацию как основную +3. Логировать попытки дублирования для аудита + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersPhones { + bigint phone PK,FK + int store_id PK,FK + string store_guid + string seller_id PK,FK + datetime date + } + + Users ||--o{ UsersPhones : "has registrations" + CityStore ||--o{ UsersPhones : "has registrations" + Admin ||--o{ UsersPhones : "registered by" + + Users { + int id PK + string phone UK + string name + datetime date + } + + CityStore { + int id PK + string name + string guid_1c + } + + Admin { + int id PK + string name + string guid_1c + } +``` + +--- + +## Диаграмма процесса регистрации + +```mermaid +sequenceDiagram + participant Client as Клиент + participant Seller as Продавец + participant POS as Касса/1С + participant ERP as ERP24 + participant DB as UsersPhones + + Client->>Seller: Предоставляет телефон + Seller->>POS: Вводит телефон + POS->>ERP: Синхронизация данных + ERP->>DB: Проверка существования записи + alt Запись не существует + ERP->>DB: Создание новой записи + DB-->>ERP: Успешно + else Запись существует + ERP->>DB: Пропуск (дубликат) + DB-->>ERP: Уже существует + end + ERP-->>Seller: Подтверждение регистрации +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Уникальный составной индекс +CREATE UNIQUE INDEX idx_users_phones_unique +ON users_phones(phone, store_id, seller_id); + +-- Индекс для поиска по телефону +CREATE INDEX idx_users_phones_phone ON users_phones(phone); + +-- Индекс для поиска по магазину +CREATE INDEX idx_users_phones_store ON users_phones(store_id); + +-- Индекс для поиска по продавцу +CREATE INDEX idx_users_phones_seller ON users_phones(seller_id); + +-- Индекс для поиска по дате +CREATE INDEX idx_users_phones_date ON users_phones(date); + +-- Индекс для поиска по GUID магазина +CREATE INDEX idx_users_phones_store_guid ON users_phones(store_guid); +``` + +### Оптимизация запросов + +```php +// Эффективный запрос - использует индекс +$count = UsersPhones::find() + ->where(['phone' => $phone]) + ->count(); + +// Эффективный запрос - использует составной индекс +$exists = UsersPhones::find() + ->where([ + 'phone' => $phone, + 'store_id' => $storeId, + 'seller_id' => $sellerId + ]) + ->exists(); + +// Группировка с индексами +$stats = UsersPhones::find() + ->select(['store_id', 'COUNT(*) as cnt']) + ->groupBy('store_id') + ->asArray() + ->all(); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [CityStore](CityStore.md) - Магазины +- [Admin](Admin.md) - Сотрудники/продавцы + +--- + +## Связанные сервисы + +- **RegistrationService** - Сервис регистрации клиентов +- **SyncService** - Синхронизация с 1С +- **AnalyticsService** - Аналитика регистраций + +--- + +## API Endpoints + +- `POST /api1/users/register-phone` - Регистрация телефона в магазине +- `GET /api1/users/phone-history` - История регистраций телефона +- `GET /api1/analytics/registrations` - Статистика регистраций + +--- + +## Замечания + +1. **Композитный уникальный ключ** - `[phone, store_id, seller_id]` предотвращает дублирование регистраций одним продавцом в одном магазине. + +2. **GUID формат** - поля `store_guid` и `seller_id` используют формат UUID для совместимости с 1С. + +3. **История** - таблица хранит полную историю регистраций, не перезаписывая данные. + +4. **Аналитика** - используется для KPI продавцов (количество привлеченных клиентов). + +5. **Синхронизация** - данные поступают из 1С при регистрации клиента в магазине. + +6. **Множественные регистрации** - клиент может быть зарегистрирован в разных магазинах разными продавцами. + +7. **Первая регистрация** - важна для определения точки входа клиента в сеть. + +8. **Отсутствие удаления** - записи обычно не удаляются, сохраняется вся история для аудита. + +--- + +## Связанные документы + +- [Интеграция с 1С](../guides/1c-integration.md) +- [Аналитика продавцов](../guides/seller-analytics.md) +- [Регистрация клиентов](../guides/client-registration.md) + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersStopList.md b/erp24/docs/models/UsersStopList.md new file mode 100644 index 00000000..961ec3f4 --- /dev/null +++ b/erp24/docs/models/UsersStopList.md @@ -0,0 +1,552 @@ +# Model: UsersStopList + + +## Mindmap + +```mermaid +mindmap + root((UsersStopList)) + Таблица БД + users_stop_list + Свойства + phone + int + name + string + date + string + admin_id + int + Связи + Author + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель стоп-листа клиентов для когортного маркетинга. Содержит телефоны клиентов, которых необходимо исключить из автоматических рассылок, таргетированной рекламы и холодных звонков. Используется для соблюдения пожеланий клиентов об отказе от маркетинговых коммуникаций, а также для блокировки проблемных контактов. + +Стоп-лист проверяется при формировании когорт в методе `Users::filterTelegramUsersForSending()` для исключения заблокированных телефонов из рассылок. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_stop_list` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `phone` | bigint | **Телефон клиента** (PK, уникальный идентификатор) | +| `name` | string(255) | **Комментарий** (причина добавления в стоп-лист) | +| `date` | datetime | **Дата добавления** в стоп-лист | +| `admin_id` | int | **ID администратора** (FK к таблице admin, кто добавил) | + +### Виртуальные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `authorName` | string | **Имя автора** (вычисляемое свойство через связь) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['phone', 'name', 'date', 'admin_id'] +``` + +### Целочисленные поля +```php +['admin_id'] // integer +``` + +### Строковые поля +```php +['name'] // max:255 (комментарий) +``` + +### Даты +```php +['date', 'authorName'] // safe +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'phone' => 'Телефон', + 'name' => 'Комментарий', + 'date' => 'Дата', + 'admin_id' => 'Admin ID', + 'authorName' => 'Автор', +] +``` + +--- + +## Связи (Relations) + +### getAuthor() +**Тип:** `hasOne` +**Модель:** `Admin` +**Ключ:** `['id' => 'admin_id']` +**Описание:** Администратор, который добавил телефон в стоп-лист + +**Пример:** +```php +$stopItem = UsersStopList::find() + ->where(['phone' => 79991234567]) + ->one(); + +$admin = $stopItem->author; +echo "Добавил в стоп-лист: {$admin->name}"; +``` + +--- + +## Методы + +### getAuthorName() +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `string` — имя автора + +**Описание:** Получение имени администратора, который добавил телефон в стоп-лист + +**Логика работы:** +1. Обращается к связи `author` +2. Возвращает свойство `name` из модели `Admin` + +**Пример:** +```php +$stopItem = UsersStopList::findOne(79991234567); +echo "Автор: " . $stopItem->getAuthorName(); +// Выведет: "Автор: Иванов Иван" +``` + +--- + +## Примеры использования + +### Добавление телефона в стоп-лист + +```php +use yii_app\records\UsersStopList; +use Yii; + +$stopItem = new UsersStopList(); +$stopItem->phone = 79991234567; +$stopItem->name = 'Клиент попросил не беспокоить'; +$stopItem->date = date('Y-m-d H:i:s'); +$stopItem->admin_id = Yii::$app->user->id; + +if ($stopItem->save()) { + echo "Телефон добавлен в стоп-лист"; +} else { + echo "Ошибка: " . implode(', ', $stopItem->getFirstErrors()); +} +``` + +### Проверка наличия телефона в стоп-листе + +```php +$phone = 79991234567; + +$isBlocked = UsersStopList::find() + ->where(['phone' => $phone]) + ->exists(); + +if ($isBlocked) { + echo "Телефон в стоп-листе, рассылка запрещена"; +} else { + echo "Телефон не заблокирован"; +} +``` + +### Получение причины блокировки + +```php +$phone = 79991234567; + +$stopItem = UsersStopList::findOne($phone); + +if ($stopItem) { + echo "Причина блокировки: {$stopItem->name}\n"; + echo "Дата блокировки: {$stopItem->date}\n"; + echo "Заблокировал: {$stopItem->authorName}\n"; +} +``` + +### Получение всего стоп-листа + +```php +$stopList = UsersStopList::find() + ->orderBy(['date' => SORT_DESC]) + ->all(); + +echo "Всего в стоп-листе: " . count($stopList) . " телефонов\n"; + +foreach ($stopList as $item) { + echo "{$item->phone} - {$item->name} (добавлен {$item->date})\n"; +} +``` + +### Удаление телефона из стоп-листа + +```php +$phone = 79991234567; + +$stopItem = UsersStopList::findOne($phone); + +if ($stopItem && $stopItem->delete()) { + echo "Телефон удален из стоп-листа"; +} else { + echo "Телефон не найден в стоп-листе"; +} +``` + +### Массовая загрузка телефонов в стоп-лист + +```php +$phones = [ + ['phone' => 79991234567, 'reason' => 'Спам'], + ['phone' => 79991234568, 'reason' => 'Отказ от рассылок'], + ['phone' => 79991234569, 'reason' => 'Жалоба клиента'], +]; + +$adminId = Yii::$app->user->id; +$currentDate = date('Y-m-d H:i:s'); +$added = 0; + +foreach ($phones as $data) { + // Проверка существования + if (!UsersStopList::find()->where(['phone' => $data['phone']])->exists()) { + $stopItem = new UsersStopList(); + $stopItem->phone = $data['phone']; + $stopItem->name = $data['reason']; + $stopItem->date = $currentDate; + $stopItem->admin_id = $adminId; + + if ($stopItem->save()) { + $added++; + } + } +} + +echo "Добавлено телефонов в стоп-лист: {$added}"; +``` + +### Экспорт стоп-листа + +```php +$stopList = UsersStopList::find() + ->with('author') + ->asArray() + ->all(); + +$csv = "Phone,Reason,Date,Author\n"; + +foreach ($stopList as $item) { + $csv .= "{$item['phone']},\"{$item['name']}\",{$item['date']},{$item['author']['name']}\n"; +} + +file_put_contents('stop_list_export.csv', $csv); +echo "Стоп-лист экспортирован в CSV"; +``` + +### Статистика по причинам блокировки + +```php +$stats = UsersStopList::find() + ->select(['name', 'COUNT(*) as cnt']) + ->groupBy('name') + ->orderBy(['cnt' => SORT_DESC]) + ->asArray() + ->all(); + +echo "Статистика по причинам блокировки:\n"; +foreach ($stats as $stat) { + echo "{$stat['name']}: {$stat['cnt']} телефонов\n"; +} +``` + +### Телефоны, добавленные администратором + +```php +$adminId = 5; + +$phones = UsersStopList::find() + ->where(['admin_id' => $adminId]) + ->all(); + +echo "Администратор добавил в стоп-лист: " . count($phones) . " телефонов\n"; + +foreach ($phones as $item) { + echo "{$item->phone} - {$item->name}\n"; +} +``` + +### Фильтрация когорты с учетом стоп-листа + +```php +use yii_app\records\Users; + +// Получение когорты +$kogort = Users::formKogortByDateAndType('2025-12-15', 'whatsapp'); + +// Получение стоп-листа +$stopListPhones = UsersStopList::find() + ->select('phone') + ->column(); + +// Фильтрация когорты +$filteredKogort = array_diff($kogort, $stopListPhones); + +echo "Когорта до фильтрации: " . count($kogort) . "\n"; +echo "Исключено по стоп-листу: " . count($stopListPhones) . "\n"; +echo "Когорта после фильтрации: " . count($filteredKogort) . "\n"; +``` + +--- + +## Бизнес-логика + +### Причины добавления в стоп-лист + +Телефоны добавляются в стоп-лист по следующим причинам: + +1. **Отказ клиента** - клиент явно попросил не беспокоить +2. **Жалобы** - клиент пожаловался на рассылки +3. **Спам** - номер используется для спама +4. **Неактуальный номер** - номер не принадлежит клиенту +5. **Технические причины** - проблемы с доставкой сообщений +6. **Юридические причины** - требование прекратить коммуникацию + +### Интеграция с когортным маркетингом + +Стоп-лист проверяется в методе `Users::filterTelegramUsersForSending()`: + +```php +// Из модели Users +public static function filterTelegramUsersForSending($telegramUsers, $sentStatusKogort) +{ + // Загрузка стоп-листа + $stopList = KogortStopList::find()->select('phone')->column(); + + // Фильтрация + return array_filter($telegramUsers, function($user) use ($sentStatusKogort, $stopList) { + return !in_array($user['phone'], $sentStatusKogort, true) + && !in_array($user['phone'], $stopList, true); + }); +} +``` + +### Соответствие законодательству + +Стоп-лист помогает соблюдать требования законодательства о защите персональных данных: +- **152-ФЗ** - право на отказ от обработки данных +- **Федеральный закон о рекламе** - запрет на спам +- **GDPR** (если есть клиенты из ЕС) - право на забвение + +### Аудит действий + +Каждая запись содержит: +- `admin_id` - кто добавил +- `date` - когда добавил +- `name` - почему добавил + +Это обеспечивает полную прослеживаемость действий для аудита и разрешения спорных ситуаций. + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersStopList { + bigint phone PK + string name + datetime date + int admin_id FK + } + + Admin ||--o{ UsersStopList : "added by" + Users ||--o| UsersStopList : "blocked" + + Admin { + int id PK + string name + string email + } + + Users { + int id PK + string phone UK + string name + int black_list + } +``` + +--- + +## Диаграмма процесса блокировки + +```mermaid +flowchart TD + A[Запрос на блокировку] --> B{Причина блокировки} + B -->|Жалоба клиента| C[Проверка обращения] + B -->|Спам| D[Подтверждение спама] + B -->|Отказ клиента| E[Регистрация отказа] + + C --> F[Добавление в UsersStopList] + D --> F + E --> F + + F --> G[Сохранение записи] + G --> H[Логирование действия] + H --> I[Исключение из когорт] + + I --> J{Проверка в Users} + J -->|Да| K[Обновление Users.black_list] + J -->|Нет| L[Только стоп-лист] + + style F fill:#FF6B6B + style I fill:#FFA500 +``` + +--- + +## Использование в когортах + +```mermaid +sequenceDiagram + participant Kogort as Формирование когорты + participant Users as Users::filterTelegramUsersForSending() + participant StopList as UsersStopList + participant Result as Отфильтрованная когорта + + Kogort->>Users: Передача списка телефонов + Users->>StopList: Загрузка стоп-листа + StopList-->>Users: Массив заблокированных телефонов + Users->>Users: Фильтрация array_filter() + Users-->>Result: Телефоны без заблокированных + Result-->>Kogort: Готовая когорта для рассылки +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ (автоматически создается) +ALTER TABLE users_stop_list ADD PRIMARY KEY (phone); + +-- Индекс для поиска по администратору +CREATE INDEX idx_users_stop_list_admin ON users_stop_list(admin_id); + +-- Индекс для поиска по дате +CREATE INDEX idx_users_stop_list_date ON users_stop_list(date); + +-- Индекс для поиска по причине +CREATE INDEX idx_users_stop_list_name ON users_stop_list(name); +``` + +### Оптимизация запросов + +```php +// Эффективный запрос - использует PRIMARY KEY +$exists = UsersStopList::find() + ->where(['phone' => $phone]) + ->exists(); + +// Массовая проверка через IN +$blockedPhones = UsersStopList::find() + ->select('phone') + ->where(['phone' => $phonesToCheck]) + ->column(); + +// Использование кэша для стоп-листа +$stopList = Yii::$app->cache->getOrSet('stop_list_phones', function() { + return UsersStopList::find()->select('phone')->column(); +}, 3600); // Кэш на 1 час +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [Admin](Admin.md) - Администраторы +- [SentKogort](SentKogort.md) - Когортный анализ + +--- + +## Связанные сервисы + +- **KogortService** - Формирование когорт (фильтрация стоп-листа) +- **NotificationService** - Проверка перед отправкой уведомлений +- **ComplianceService** - Соблюдение требований законодательства + +--- + +## API Endpoints + +- `POST /api2/stoplist/add` - Добавление телефона в стоп-лист +- `DELETE /api2/stoplist/remove` - Удаление из стоп-листа +- `GET /api2/stoplist/check` - Проверка телефона +- `GET /api2/stoplist/list` - Получение списка + +--- + +## Замечания + +1. **Первичный ключ** - телефон является уникальным идентификатором, один телефон не может быть добавлен дважды. + +2. **Обязательный комментарий** - поле `name` обязательно для заполнения, всегда должна быть указана причина блокировки. + +3. **Аудит** - каждая запись содержит информацию о том, кто и когда добавил телефон. + +4. **Автоматическая фильтрация** - стоп-лист автоматически проверяется при формировании всех типов когорт. + +5. **Кэширование** - рекомендуется кэшировать список телефонов для оптимизации производительности. + +6. **Связь с Users.black_list** - при добавлении в стоп-лист рекомендуется также установить `Users.black_list = 1`. + +7. **GDPR compliance** - стоп-лист помогает соблюдать право клиента на отказ от маркетинговых коммуникаций. + +8. **Виртуальное свойство** - `authorName` является вычисляемым и не хранится в БД. + +--- + +## Связанные документы + +- [Когортный маркетинг](../guides/kogort-marketing.md) +- [GDPR Compliance](../guides/gdpr-compliance.md) +- [Управление рассылками](../guides/mailing-management.md) + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersStopListSearch.md b/erp24/docs/models/UsersStopListSearch.md new file mode 100644 index 00000000..d30d9999 --- /dev/null +++ b/erp24/docs/models/UsersStopListSearch.md @@ -0,0 +1,189 @@ +# Класс: UsersStopListSearch + + +## Mindmap + +```mermaid +mindmap + root((UsersStopListSearch)) + Таблица БД + ActiveRecord + Наследование + extends UsersStopList +``` + +## Назначение +Search-модель для поиска и фильтрации стоп-листа пользователей в ERP24. Модель с JOIN к автору записи и поиском по имени автора через прямой SQL в joinWith. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`UsersStopList` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$authorName` | string | Имя администратора, добавившего в стоп-лист | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `phone`, `admin_id` — integer +- `name`, `date`, `authorName` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с JOIN к автору записи. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос UsersStopList::find() +2. Оборачивает в ActiveDataProvider +3. Если не загружены параметры или невалидны: + - Выполняет joinWith(['author']) и возвращает +4. Применяет фильтры: + - Точное совпадение: phone, date, admin_id + - like: users_stop_list.name (с алиасом таблицы) +5. Выполняет joinWith с callback для author: + - Добавляет WHERE admin.name LIKE '%authorName%' + +**Особенность:** Использует прямой SQL в joinWith callback для поиска по имени автора + +## Диаграмма связей + +```mermaid +erDiagram + UsersStopList { + int phone PK + varchar name + date date + int admin_id FK + } + + Admin { + int id PK + varchar name + } + + UsersStopList }o--|| Admin : "admin_id" +``` + +## Диаграмма логики поиска + +```mermaid +flowchart TD + A[search] --> B{load && validate} + + B -->|false| C[joinWith author] + C --> D[return dataProvider] + + B -->|true| E[Фильтры] + E --> F[phone, date, admin_id - точное] + E --> G[users_stop_list.name - like] + + H[joinWith author] --> I[callback] + I --> J[admin.name LIKE authorName] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new UsersStopListSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по телефону +```php +$searchModel = new UsersStopListSearch(); +$dataProvider = $searchModel->search([ + 'UsersStopListSearch' => [ + 'phone' => 79991234567, + ] +]); +``` + +### Поиск по имени клиента +```php +$searchModel = new UsersStopListSearch(); +$dataProvider = $searchModel->search([ + 'UsersStopListSearch' => [ + 'name' => 'Иванов', + ] +]); +``` + +### Поиск по автору добавления +```php +$searchModel = new UsersStopListSearch(); +$dataProvider = $searchModel->search([ + 'UsersStopListSearch' => [ + 'authorName' => 'Петров', + ] +]); +``` + +### Поиск по дате добавления +```php +$searchModel = new UsersStopListSearch(); +$dataProvider = $searchModel->search([ + 'UsersStopListSearch' => [ + 'date' => '2024-01-15', + ] +]); +``` + +### GridView с именем автора +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'phone', + 'name', + 'date:date', + [ + 'attribute' => 'authorName', + 'value' => 'author.name', + 'label' => 'Добавил', + ], + ], +]) ?> +``` + +## Связанные модели + +- [UsersStopList](./UsersStopList.md) — базовая модель стоп-листа +- [Admin](./Admin.md) — администраторы + +## Особенности реализации + +1. **Виртуальное свойство**: authorName для поиска по имени автора +2. **joinWith с callback**: Прямой SQL для фильтрации по связанной таблице +3. **Ранний return**: При невалидных параметрах — простой joinWith и возврат +4. **Алиас таблицы**: users_stop_list.name для избежания конфликтов +5. **like вместо ilike**: Регистрозависимый поиск +6. **SQL injection**: Потенциальная уязвимость при конкатенации $this->authorName без экранирования diff --git a/erp24/docs/models/UsersTelegram.md b/erp24/docs/models/UsersTelegram.md new file mode 100644 index 00000000..25c579cf --- /dev/null +++ b/erp24/docs/models/UsersTelegram.md @@ -0,0 +1,257 @@ +# Модель UsersTelegram + + +## Mindmap + +```mermaid +mindmap + root((UsersTelegram)) + Таблица БД + users_telegram + Свойства + chat_id + string + is_blocked + int + is_registered + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `UsersTelegram` представляет данные пользователей Telegram-бота. Хранит информацию о подписчиках бота: идентификатор чата, телефон, имя пользователя и статусы блокировки/регистрации. Используется для отправки уведомлений и рассылок клиентам через Telegram. + +**Файл модели:** `erp24/records/UsersTelegram.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `users_telegram` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `chat_id` | VARCHAR(255) | Chat ID пользователя в Telegram (первичный ключ) | +| `phone` | VARCHAR(255) | Телефон пользователя | +| `username` | VARCHAR(255) | Username в Telegram (@username) | +| `first_name` | VARCHAR(255) | Имя пользователя | +| `is_blocked` | INTEGER | Заблокирован: 0 — нет, 1 — да | +| `is_registered` | INTEGER | Зарегистрирован: 0 — нет, 1 — да | + +--- + +## Описание полей + +### `chat_id` — Идентификатор чата + +Уникальный идентификатор чата пользователя в Telegram. Используется для отправки сообщений через Telegram Bot API. + +**Формат:** Строка с числовым значением (может быть большим числом) + +**Пример:** `"123456789"`, `"987654321012"` + +### `phone` — Телефон + +Номер телефона пользователя. Может быть получен при авторизации через Telegram или введён вручную при регистрации в боте. + +**Формат:** Строка, обычно в международном формате + +**Пример:** `"+79001234567"` + +### `username` — Username в Telegram + +Публичный username пользователя в Telegram (без символа @). + +**Пример:** `"ivan_petrov"`, `"flower_lover"` + +### `first_name` — Имя пользователя + +Имя пользователя из профиля Telegram. + +**Пример:** `"Иван"`, `"Maria"` + +### `is_blocked` — Статус блокировки + +Флаг, указывающий заблокировал ли пользователь бота. + +| Значение | Описание | +|----------|----------| +| 0 | Пользователь не заблокировал бота | +| 1 | Пользователь заблокировал бота | + +**Логика:** При попытке отправки сообщения заблокировавшему боту возвращается ошибка, и флаг устанавливается в 1. + +### `is_registered` — Статус регистрации + +Флаг, указывающий прошёл ли пользователь регистрацию в боте. + +| Значение | Описание | +|----------|----------| +| 0 | Пользователь не завершил регистрацию | +| 1 | Пользователь зарегистрирован | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + users_telegram }o--|| users : "linked_to" + + users_telegram { + string chat_id PK + string phone + string username + string first_name + int is_blocked + int is_registered + } + + users { + string id PK + string phone + string name + int bonus + } +``` + +--- + +## Примеры использования + +### Получение пользователя по chat_id + +```php +$telegramUser = UsersTelegram::findOne(['chat_id' => $chatId]); +if ($telegramUser) { + echo "Пользователь: " . $telegramUser->first_name; +} +``` + +### Поиск по телефону + +```php +$telegramUser = UsersTelegram::findOne(['phone' => $phone]); +if ($telegramUser && !$telegramUser->is_blocked) { + // Можно отправить сообщение + $chatId = $telegramUser->chat_id; +} +``` + +### Получение активных подписчиков для рассылки + +```php +$activeUsers = UsersTelegram::find() + ->where(['is_blocked' => 0]) + ->andWhere(['is_registered' => 1]) + ->all(); + +foreach ($activeUsers as $user) { + // Отправка сообщения через Telegram Bot API + sendTelegramMessage($user->chat_id, $messageText); +} +``` + +### Создание/обновление пользователя при старте бота + +```php +$telegramUser = UsersTelegram::findOne(['chat_id' => $chatId]); + +if (!$telegramUser) { + $telegramUser = new UsersTelegram(); + $telegramUser->chat_id = $chatId; +} + +$telegramUser->username = $updateMessage->from->username ?? null; +$telegramUser->first_name = $updateMessage->from->first_name ?? null; +$telegramUser->is_blocked = 0; +$telegramUser->save(); +``` + +### Пометка заблокированного пользователя + +```php +// При получении ошибки отправки сообщения +try { + sendTelegramMessage($chatId, $message); +} catch (TelegramBlockedException $e) { + UsersTelegram::updateAll( + ['is_blocked' => 1], + ['chat_id' => $chatId] + ); +} +``` + +### Статистика подписчиков + +```php +$stats = [ + 'total' => UsersTelegram::find()->count(), + 'registered' => UsersTelegram::find() + ->where(['is_registered' => 1]) + ->count(), + 'active' => UsersTelegram::find() + ->where(['is_blocked' => 0, 'is_registered' => 1]) + ->count(), + 'blocked' => UsersTelegram::find() + ->where(['is_blocked' => 1]) + ->count(), +]; +``` + +### Связь с основной таблицей клиентов + +```php +// Получение клиента по данным Telegram +$telegramUser = UsersTelegram::findOne(['chat_id' => $chatId]); +if ($telegramUser && $telegramUser->phone) { + $user = Users::findOne(['phone' => $telegramUser->phone]); + if ($user) { + echo "Бонусный баланс: " . $user->bonus; + } +} +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `chat_id` | Обязательное, макс. 255 символов | +| `phone` | Макс. 255 символов | +| `username` | Макс. 255 символов | +| `first_name` | Макс. 255 символов | +| `is_blocked` | Целое число, по умолчанию null | +| `is_registered` | Целое число, по умолчанию null | + +--- + +## Связанные модели + +- **[Users](./Users.md)** — клиенты (связь по телефону) +- **TelegramMessage** — история сообщений бота +- **TelegramNotification** — очередь уведомлений + +--- + +## Интеграция с Telegram Bot API + +Модель используется совместно с контроллерами Telegram-бота: + +- `TelegramController` — обработка входящих сообщений +- `TelegramSalebotController` — интеграция с Salebot + +При получении webhook от Telegram: +1. Извлекается `chat_id` из сообщения +2. Создаётся/обновляется запись `UsersTelegram` +3. Обрабатывается команда/сообщение +4. Формируется и отправляется ответ + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/UsersTelegramLog.md b/erp24/docs/models/UsersTelegramLog.md new file mode 100644 index 00000000..d2e78b09 --- /dev/null +++ b/erp24/docs/models/UsersTelegramLog.md @@ -0,0 +1,401 @@ +# Model: UsersTelegramLog + + +## Mindmap + +```mermaid +mindmap + root((UsersTelegramLog)) + Таблица БД + users_telegram_log + Свойства + phone + string + is_blocked + int + is_registered + int + active + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель журнала истории статусов клиентов в Telegram. Отслеживает изменения состояния подписки клиента на Telegram-бота: блокировку, регистрацию, активность. Используется для ведения истории изменений статусов, анализа оттока подписчиков и аудита активности в боте. + +Хранит исторические данные о статусах клиентов, каждое изменение создает новую запись с датой окончания предыдущего состояния. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_telegram_log` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `phone` | string(255) | **Телефон клиента** (PK, уникальный идентификатор) | +| `is_blocked` | int | **Заблокирован**: 0 - нет, 1 - да (бот заблокирован клиентом) | +| `is_registered` | int | **Зарегистрирован**: 0 - нет, 1 - да (прошел регистрацию в боте) | +| `active` | int | **Активный**: 0 - нет, 1 - да (текущее состояние) | +| `date_end` | datetime | **Дата окончания** активности статуса | + +--- + +## Правила валидации + +### Обязательные поля +```php +['phone'] +``` + +### Целочисленные поля с значением по умолчанию null +```php +['is_blocked', 'is_registered', 'active'] // integer, default: null +``` + +### Строковые поля +```php +['phone'] // max:255 +``` + +### Уникальность +```php +['phone'] // unique +``` + +### Даты +```php +['date_end'] // safe +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'phone' => 'Телефон', + 'is_blocked' => 'Заблокирован: 0 - нет, 1 - да', + 'is_registered' => 'Зарегистрирован: 0 - нет, 1 - да', + 'active' => 'Активный: 0 - нет, 1 - да', + 'date_end' => 'Дата окончания активности', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['phone' => 'phone']` +**Описание:** Клиент, которому принадлежит запись + +**Пример:** +```php +$logRecord = UsersTelegramLog::findOne($phone); +$user = $logRecord->user; +echo "Клиент: {$user->name}"; +``` + +--- + +## Примеры использования + +### Создание записи при изменении статуса + +```php +use yii_app\records\UsersTelegramLog; + +// Закрытие предыдущей записи +$previousRecord = UsersTelegramLog::findOne($phone); +if ($previousRecord && $previousRecord->active == 1) { + $previousRecord->active = 0; + $previousRecord->date_end = date('Y-m-d H:i:s'); + $previousRecord->save(); +} + +// Создание новой записи +$logRecord = new UsersTelegramLog(); +$logRecord->phone = $phone; +$logRecord->is_blocked = 0; +$logRecord->is_registered = 1; +$logRecord->active = 1; +$logRecord->save(); +``` + +### Получение текущего статуса клиента + +```php +$phone = '79991234567'; + +$currentStatus = UsersTelegramLog::find() + ->where(['phone' => $phone, 'active' => 1]) + ->one(); + +if ($currentStatus) { + echo "Зарегистрирован: " . ($currentStatus->is_registered ? 'Да' : 'Нет') . "\n"; + echo "Заблокирован: " . ($currentStatus->is_blocked ? 'Да' : 'Нет') . "\n"; +} +``` + +### История статусов клиента + +```php +$phone = '79991234567'; + +$history = UsersTelegramLog::find() + ->where(['phone' => $phone]) + ->orderBy(['date_end' => SORT_DESC]) + ->all(); + +foreach ($history as $record) { + $status = $record->active ? 'Активен' : 'Закрыт'; + $registered = $record->is_registered ? 'Да' : 'Нет'; + $blocked = $record->is_blocked ? 'Да' : 'Нет'; + + echo "Статус: {$status}, Регистрация: {$registered}, Блокировка: {$blocked}\n"; + if ($record->date_end) { + echo "До: {$record->date_end}\n"; + } +} +``` + +### Статистика заблокированных пользователей + +```php +$blockedCount = UsersTelegramLog::find() + ->where(['is_blocked' => 1, 'active' => 1]) + ->count(); + +echo "Заблокировано ботов: {$blockedCount}"; +``` + +### Статистика зарегистрированных пользователей + +```php +$registeredCount = UsersTelegramLog::find() + ->where(['is_registered' => 1, 'active' => 1]) + ->count(); + +echo "Зарегистрированных пользователей: {$registeredCount}"; +``` + +### Пользователи, заблокировавшие бота за период + +```php +$startDate = '2025-01-01'; +$endDate = '2025-01-31'; + +$blocked = UsersTelegramLog::find() + ->where(['is_blocked' => 1]) + ->andWhere(['>=', 'date_end', $startDate]) + ->andWhere(['<=', 'date_end', $endDate]) + ->all(); + +echo "Заблокировали бота в январе: " . count($blocked); +``` + +### Отток подписчиков + +```php +// Пользователи, которые были активны, но отписались +$churnedUsers = UsersTelegramLog::find() + ->where(['is_blocked' => 1, 'active' => 0]) + ->andWhere(['>=', 'date_end', date('Y-m-d', strtotime('-30 days'))]) + ->count(); + +echo "Отток за последние 30 дней: {$churnedUsers}"; +``` + +--- + +## Бизнес-логика + +### Жизненный цикл статусов + +1. **Регистрация** - `is_registered = 1, is_blocked = 0, active = 1` +2. **Блокировка бота** - `is_blocked = 1`, создается новая запись +3. **Разблокировка** - `is_blocked = 0`, создается новая запись +4. **Отписка** - `active = 0, date_end = NOW()` + +### Связь с UsersTelegram + +Таблица `UsersTelegramLog` хранит историю, а `UsersTelegram` - текущее состояние: + +```php +// UsersTelegram - текущий статус +$telegram = UsersTelegram::findOne(['phone' => $phone]); +echo "Текущий статус: is_blocked={$telegram->is_blocked}"; + +// UsersTelegramLog - история +$history = UsersTelegramLog::find() + ->where(['phone' => $phone]) + ->all(); +echo "Изменений статуса: " . count($history); +``` + +### Аналитика оттока + +Анализ причин отписки: +- Период активности до блокировки +- Количество отправленных сообщений +- Последняя покупка +- Реакция на рассылки + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersTelegramLog { + string phone PK,UK + int is_blocked + int is_registered + int active + datetime date_end + } + + Users ||--o{ UsersTelegramLog : "has history" + UsersTelegram ||--o| UsersTelegramLog : "current status" + + Users { + int id PK + string phone UK + string name + } + + UsersTelegram { + int id PK + bigint phone UK + int is_blocked + int is_registered + } +``` + +--- + +## Диаграмма изменения статусов + +```mermaid +stateDiagram-v2 + [*] --> Registered: Регистрация + Registered --> Blocked: Блокировка бота + Blocked --> Registered: Разблокировка + Registered --> Churned: Отписка + Blocked --> Churned: Удаление + Churned --> [*] + + note right of Registered + is_registered=1 + is_blocked=0 + active=1 + end note + + note right of Blocked + is_registered=1 + is_blocked=1 + active=1 + end note + + note right of Churned + active=0 + date_end=NOW() + end note +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ и уникальность +ALTER TABLE users_telegram_log ADD PRIMARY KEY (phone); +CREATE UNIQUE INDEX idx_utl_phone ON users_telegram_log(phone); + +-- Индекс для поиска активных записей +CREATE INDEX idx_utl_active ON users_telegram_log(active); + +-- Индекс для поиска заблокированных +CREATE INDEX idx_utl_blocked ON users_telegram_log(is_blocked); + +-- Индекс для поиска зарегистрированных +CREATE INDEX idx_utl_registered ON users_telegram_log(is_registered); + +-- Индекс для поиска по дате окончания +CREATE INDEX idx_utl_date_end ON users_telegram_log(date_end); + +-- Составной индекс для статистики +CREATE INDEX idx_utl_active_blocked +ON users_telegram_log(active, is_blocked); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [UsersTelegram](UsersTelegram.md) - Telegram профили (текущее состояние) +- [UsersTelegramMessage](UsersTelegramMessage.md) - Отправленные сообщения + +--- + +## Связанные сервисы + +- **TelegramService** - Управление Telegram-ботом +- **AnalyticsService** - Аналитика активности +- **ChurnAnalysisService** - Анализ оттока подписчиков + +--- + +## API Endpoints + +- `GET /api2/telegram/status-history` - История статусов клиента +- `GET /api2/telegram/churn-stats` - Статистика оттока +- `GET /api2/telegram/active-users` - Активные пользователи + +--- + +## Замечания + +1. **Уникальность phone** - один телефон может иметь только одну активную запись. + +2. **Активная запись** - `active = 1` для текущего статуса, `active = 0` для закрытых. + +3. **История** - все изменения сохраняются навсегда для аудита. + +4. **date_end** - null для активной записи, заполняется при закрытии. + +5. **Связь с UsersTelegram** - дублирование данных для производительности. + +6. **Блокировка != Отписка** - блокировка временная, отписка постоянная. + +7. **Аналитика оттока** - используется для улучшения контента и снижения отписок. + +8. **Default values** - все флаги по умолчанию null для явного указания. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersTelegramMessage.md b/erp24/docs/models/UsersTelegramMessage.md new file mode 100644 index 00000000..00344489 --- /dev/null +++ b/erp24/docs/models/UsersTelegramMessage.md @@ -0,0 +1,483 @@ +# Model: UsersTelegramMessage + + +## Mindmap + +```mermaid +mindmap + root((UsersTelegramMessage)) + Таблица БД + users_telegram_message + Свойства + id + int + chat_id + string + phone + string + message + string + kogort_date + string + target_date + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель журнала отправленных сообщений в Telegram для когортного маркетинга. Хранит историю всех сообщений, отправленных клиентам через Telegram-бота в рамках маркетинговых кампаний. Используется для отслеживания коммуникаций, предотвращения дублирования сообщений и аналитики эффективности рассылок. + +Каждое сообщение связано с конкретной когортой (`kogort_date`, `target_date`) и типом рассылки (`type`). + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`users_telegram_message` + +--- + +## Константы + +### Типы сообщений + +```php +const TYPE_FIRST_MESSAGE = 1; // Первое сообщение когорты +const TYPE_SECOND_MESSAGE = 2; // Второе сообщение (напоминание) +``` + +--- + +## Основные свойства + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** Первичный ключ | +| `chat_id` | string(255) | **Chat ID** пользователя в Telegram | +| `phone` | string(255) | **Телефон** клиента | +| `message` | text | **Текст сообщения** отправленного в Telegram | +| `kogort_date` | date | **Дата когорты** (дата формирования) | +| `target_date` | date | **Целевая дата** (дата памятного события) | +| `type` | int | **Номер рассылки** (1 - первое сообщение, 2 - второе) | +| `created_at` | datetime | **Дата создания** записи (отправки сообщения) | + +--- + +## Правила валидации + +### Обязательные поля +```php +['chat_id', 'phone', 'message', 'kogort_date', 'target_date'] +``` + +### Целочисленные поля с значением по умолчанию null +```php +['type'] // integer, default: null +``` + +### Даты +```php +['kogort_date', 'target_date', 'created_at'] // safe +``` + +### Строковые поля +```php +['chat_id', 'phone'] // max:255 +['message'] // text (без ограничения длины) +``` + +--- + +## Атрибуты (Labels) + +```php +[ + 'id' => 'ID', + 'chat_id' => 'Chat ID', + 'phone' => 'Телефон', + 'message' => 'Текст сообщения в Телеграм', + 'kogort_date' => 'Дата когорты', + 'target_date' => 'Целевая дата', + 'type' => 'Номер рассылки', + 'created_at' => 'Дата создания записи', +] +``` + +--- + +## Связи (Relations) + +### getUser() +**Тип:** `hasOne` +**Модель:** `Users` +**Ключ:** `['phone' => 'phone']` +**Описание:** Клиент, которому отправлено сообщение + +**Пример:** +```php +$message = UsersTelegramMessage::findOne($id); +$user = $message->user; +echo "Отправлено клиенту: {$user->name}"; +``` + +### getUserTelegram() +**Тип:** `hasOne` +**Модель:** `UsersTelegram` +**Ключ:** `['chat_id' => 'chat_id']` +**Описание:** Telegram профиль получателя + +**Пример:** +```php +$message = UsersTelegramMessage::findOne($id); +$telegram = $message->userTelegram; +echo "Telegram: @{$telegram->username}"; +``` + +--- + +## Примеры использования + +### Сохранение отправленного сообщения + +```php +use yii_app\records\UsersTelegramMessage; + +$messageRecord = new UsersTelegramMessage(); +$messageRecord->chat_id = $chatId; +$messageRecord->phone = $phone; +$messageRecord->message = "Здравствуйте! Напоминаем, что 15 мая у вашего близкого день рождения."; +$messageRecord->kogort_date = '2025-05-10'; // Дата формирования когорты +$messageRecord->target_date = '2025-05-15'; // Дата события +$messageRecord->type = UsersTelegramMessage::TYPE_FIRST_MESSAGE; +$messageRecord->created_at = date('Y-m-d H:i:s'); +$messageRecord->save(); +``` + +### Проверка, было ли отправлено сообщение + +```php +$phone = '79991234567'; +$targetDate = '2025-05-15'; +$type = UsersTelegramMessage::TYPE_FIRST_MESSAGE; + +$wasS ent = UsersTelegramMessage::find() + ->where([ + 'phone' => $phone, + 'target_date' => $targetDate, + 'type' => $type + ]) + ->exists(); + +if ($wasS ent) { + echo "Первое сообщение уже отправлено"; +} +``` + +### Получение истории сообщений клиента + +```php +$phone = '79991234567'; + +$messages = UsersTelegramMessage::find() + ->where(['phone' => $phone]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($messages as $msg) { + echo "Дата: {$msg->created_at}\n"; + echo "Тип: " . ($msg->type == 1 ? 'Первое' : 'Второе') . "\n"; + echo "Сообщение: {$msg->message}\n\n"; +} +``` + +### Статистика отправленных сообщений за период + +```php +$startDate = '2025-01-01'; +$endDate = '2025-01-31'; + +$stats = UsersTelegramMessage::find() + ->select([ + 'type', + 'COUNT(*) as cnt', + 'COUNT(DISTINCT phone) as unique_users' + ]) + ->where(['>=', 'created_at', $startDate]) + ->andWhere(['<=', 'created_at', $endDate]) + ->groupBy('type') + ->asArray() + ->all(); + +foreach ($stats as $stat) { + $typeName = $stat['type'] == 1 ? 'Первые сообщения' : 'Вторые сообщения'; + echo "{$typeName}: {$stat['cnt']} шт., {$stat['unique_users']} клиентов\n"; +} +``` + +### Сообщения по конкретной когорте + +```php +$kogortDate = '2025-05-10'; +$targetDate = '2025-05-15'; + +$messages = UsersTelegramMessage::find() + ->where([ + 'kogort_date' => $kogortDate, + 'target_date' => $targetDate + ]) + ->all(); + +echo "Отправлено сообщений по когорте: " . count($messages); +``` + +### Клиенты, которым отправлено второе сообщение + +```php +$secondMessages = UsersTelegramMessage::find() + ->where(['type' => UsersTelegramMessage::TYPE_SECOND_MESSAGE]) + ->all(); + +$phones = []; +foreach ($secondMessages as $msg) { + $phones[] = $msg->phone; +} + +echo "Вторые сообщения отправлены " . count(array_unique($phones)) . " клиентам"; +``` + +### Последнее сообщение клиенту + +```php +$phone = '79991234567'; + +$lastMessage = UsersTelegramMessage::find() + ->where(['phone' => $phone]) + ->orderBy(['created_at' => SORT_DESC]) + ->one(); + +if ($lastMessage) { + echo "Последнее сообщение: {$lastMessage->created_at}\n"; + echo "Текст: {$lastMessage->message}"; +} +``` + +### Удаление старых записей + +```php +// Удаление сообщений старше 1 года +$oldDate = date('Y-m-d', strtotime('-1 year')); + +$deleted = UsersTelegramMessage::deleteAll(['<', 'created_at', $oldDate]); +echo "Удалено записей: {$deleted}"; +``` + +--- + +## Бизнес-логика + +### Двухэтапная рассылка + +Система отправляет сообщения в два этапа: + +1. **Первое сообщение** (`TYPE_FIRST_MESSAGE`) - за 5 дней до события + - Напоминание о предстоящем событии + - Предложение заказать подарок со скидкой + +2. **Второе сообщение** (`TYPE_SECOND_MESSAGE`) - за 2 дня до события + - Напоминание для тех, кто не совершил покупку + - Последний шанс получить скидку + +### Связь с когортами + +Каждое сообщение связано с записью в `SentKogort`: +- `kogort_date` - когда была сформирована когорта +- `target_date` - целевая дата события +- После отправки обновляется `SentKogort.status` + +### Предотвращение дублирования + +Перед отправкой проверяется наличие записи: +```php +$exists = UsersTelegramMessage::find() + ->where([ + 'phone' => $phone, + 'target_date' => $targetDate, + 'type' => $type + ]) + ->exists(); + +if (!$exists) { + // Отправить сообщение +} +``` + +### Персонализация сообщений + +Текст сообщения может содержать: +- Имя клиента +- Дату события +- Имя получателя подарка +- Персональный промокод +- Ссылку на каталог + +--- + +## Диаграмма структуры + +```mermaid +erDiagram + UsersTelegramMessage { + int id PK + string chat_id FK + string phone FK + text message + date kogort_date + date target_date + int type + datetime created_at + } + + Users ||--o{ UsersTelegramMessage : "receives" + UsersTelegram ||--o{ UsersTelegramMessage : "chat" + SentKogort ||--o{ UsersTelegramMessage : "kogort" + + Users { + int id PK + string phone UK + string name + } + + UsersTelegram { + int id PK + bigint chat_id UK + bigint phone + string username + } + + SentKogort { + int id PK + string phone + date kogort_date + date target_date + int status + } +``` + +--- + +## Диаграмма процесса рассылки + +```mermaid +sequenceDiagram + participant Cron as Планировщик + participant Kogort as SentKogort + participant Service as TelegramService + participant Bot as Telegram Bot + participant UTM as UsersTelegramMessage + + Cron->>Kogort: Получить когорту на сегодня + Kogort-->>Cron: Список телефонов + Cron->>Service: Отправить первые сообщения + + loop Для каждого телефона + Service->>UTM: Проверка дублирования + UTM-->>Service: Не отправлено + Service->>Bot: Отправка сообщения + Bot-->>Service: OK + Service->>UTM: Сохранение записи + Service->>Kogort: Обновление статуса + end + + Service-->>Cron: Завершено +``` + +--- + +## Индексы и производительность + +### Рекомендуемые индексы + +```sql +-- Первичный ключ +ALTER TABLE users_telegram_message ADD PRIMARY KEY (id); + +-- Индекс для проверки дублирования +CREATE INDEX idx_utm_phone_target_type +ON users_telegram_message(phone, target_date, type); + +-- Индекс для поиска по chat_id +CREATE INDEX idx_utm_chat_id ON users_telegram_message(chat_id); + +-- Индекс для поиска по когорте +CREATE INDEX idx_utm_kogort_target +ON users_telegram_message(kogort_date, target_date); + +-- Индекс для поиска по дате создания +CREATE INDEX idx_utm_created_at ON users_telegram_message(created_at); + +-- Индекс для поиска по типу +CREATE INDEX idx_utm_type ON users_telegram_message(type); +``` + +--- + +## Связанные модели + +- [Users](Users.md) - Клиенты +- [UsersTelegram](UsersTelegram.md) - Telegram профили +- [SentKogort](SentKogort.md) - Когорты +- [UsersEvents](UsersEvents.md) - События клиентов + +--- + +## Связанные сервисы + +- **TelegramService** - Отправка сообщений в Telegram +- **KogortService** - Формирование когорт +- **MessageTemplateService** - Шаблоны сообщений + +--- + +## API Endpoints + +- `POST /api2/telegram/send-message` - Отправка сообщения +- `GET /api2/telegram/message-history` - История сообщений +- `GET /api2/telegram/stats` - Статистика рассылок + +--- + +## Замечания + +1. **Типы сообщений** - константы TYPE_FIRST_MESSAGE и TYPE_SECOND_MESSAGE определены в классе. + +2. **Хранение текста** - полный текст сообщения сохраняется для аудита. + +3. **Даты когорты** - kogort_date и target_date связывают с SentKogort. + +4. **Chat ID** - связь с UsersTelegram для получения дополнительной информации. + +5. **Удаление старых записей** - рекомендуется периодическая очистка для экономии места. + +6. **Статусы доставки** - не отслеживаются в этой таблице, только факт отправки. + +7. **Персонализация** - текст сообщения уже содержит подставленные переменные. + +8. **Аналитика** - используется для расчета конверсии когортных кампаний. + +--- + +**Последнее обновление:** 2025-12-11 diff --git a/erp24/docs/models/UsersWhatsappMessage.md b/erp24/docs/models/UsersWhatsappMessage.md new file mode 100644 index 00000000..a9cce280 --- /dev/null +++ b/erp24/docs/models/UsersWhatsappMessage.md @@ -0,0 +1,267 @@ +# Класс: UsersWhatsappMessage + + +## Mindmap + +```mermaid +mindmap + root((UsersWhatsappMessage)) + Таблица БД + users_whatsapp_message + Свойства + id + int + request_id + string + phone + string + message + string + kogort_date + string + target_date + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение +Модель сообщений WhatsApp клиентам в ERP24. Хранит историю отправленных сообщений через WhatsApp Business API с отслеживанием статусов доставки для когортных маркетинговых кампаний. + +## Пространство имён +`yii_app\records` + +## Таблица БД +`users_whatsapp_message` + +## Родительский класс +`\yii\db\ActiveRecord` + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | int | Первичный ключ (auto-increment) | +| `request_id` | varchar(255) | ID сообщения (внешний идентификатор WhatsApp API) | +| `phone` | varchar(255) | Телефон получателя | +| `message` | text | Текст сообщения | +| `kogort_date` | date | Дата когорты | +| `target_date` | date | Целевая дата (дата планируемого визита) | +| `status` | varchar(20) | Статус сообщения (default: 'sent') | +| `created_at` | datetime | Дата создания записи | + +## Статусы сообщений + +| Статус | Описание | +|--------|----------| +| `sent` | Отправлено | +| `delivered` | Доставлено | +| `read` | Прочитано | +| `undelivered` | Не доставлено | +| `cancelled` | Отменено | +| `expired` | Истёк срок доставки | +| `failed` | Ошибка обработки | + +## Диаграмма связей + +```mermaid +erDiagram + UsersWhatsappMessage { + int id PK + varchar request_id UK + varchar phone + text message + date kogort_date + date target_date + varchar status + datetime created_at + } + + Users { + int id PK + varchar phone + } + + UsersMessageManagement { + int id PK + text offer_whatsapp + } + + Users ||--o{ UsersWhatsappMessage : "phone" + UsersMessageManagement ||--o{ UsersWhatsappMessage : "когорта" +``` + +## Диаграмма жизненного цикла сообщения + +```mermaid +stateDiagram-v2 + [*] --> sent: Отправка + sent --> delivered: Доставлено + sent --> undelivered: Не доставлено + sent --> failed: Ошибка + sent --> expired: Таймаут + + delivered --> read: Прочитано + delivered --> [*] + + read --> [*] + undelivered --> [*] + cancelled --> [*] + expired --> [*] + failed --> [*] +``` + +## Примеры использования + +### Создание записи сообщения +```php +$message = new UsersWhatsappMessage(); +$message->request_id = 'wamid.' . uniqid(); +$message->phone = '+79001234567'; +$message->message = 'Здравствуйте! Вам начислено 100 бонусов!'; +$message->kogort_date = '2024-12-01'; +$message->target_date = '2024-12-15'; +$message->status = 'sent'; +$message->created_at = date('Y-m-d H:i:s'); +$message->save(); +``` + +### Обновление статуса по колбеку +```php +$requestId = $webhookData['request_id']; +$newStatus = $webhookData['status']; + +$message = UsersWhatsappMessage::find() + ->where(['request_id' => $requestId]) + ->one(); + +if ($message) { + $message->status = $newStatus; + $message->save(); +} +``` + +### Получение сообщений когорты +```php +$kohortMessages = UsersWhatsappMessage::find() + ->where(['kogort_date' => '2024-12-01']) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($kohortMessages as $msg) { + echo "{$msg->phone}: {$msg->status}\n"; +} +``` + +### Статистика доставки +```php +$stats = UsersWhatsappMessage::find() + ->select([ + 'status', + 'COUNT(*) as count' + ]) + ->where(['kogort_date' => $kohortDate]) + ->groupBy('status') + ->asArray() + ->all(); + +$total = array_sum(array_column($stats, 'count')); +foreach ($stats as $stat) { + $percent = $total > 0 ? round($stat['count'] / $total * 100, 1) : 0; + echo "{$stat['status']}: {$stat['count']} ({$percent}%)\n"; +} +``` + +### Поиск недоставленных сообщений +```php +$failed = UsersWhatsappMessage::find() + ->where(['IN', 'status', ['undelivered', 'failed', 'expired']]) + ->andWhere(['>=', 'created_at', date('Y-m-d', strtotime('-7 days'))]) + ->all(); +``` + +### Статистика по целевым датам +```php +$targetStats = UsersWhatsappMessage::find() + ->select([ + 'target_date', + 'COUNT(*) as total', + 'SUM(CASE WHEN status = \'delivered\' THEN 1 ELSE 0 END) as delivered', + 'SUM(CASE WHEN status = \'read\' THEN 1 ELSE 0 END) as read_count' + ]) + ->groupBy('target_date') + ->orderBy(['target_date' => SORT_ASC]) + ->asArray() + ->all(); +``` + +### Повторная отправка неудачных +```php +$failedMessages = UsersWhatsappMessage::find() + ->where(['status' => 'failed']) + ->andWhere(['>=', 'created_at', date('Y-m-d', strtotime('-1 day'))]) + ->all(); + +foreach ($failedMessages as $msg) { + // Повторная отправка через WhatsApp API + $newRequestId = sendWhatsappMessage($msg->phone, $msg->message); + + // Создание новой записи + $newMessage = new UsersWhatsappMessage(); + $newMessage->request_id = $newRequestId; + $newMessage->phone = $msg->phone; + $newMessage->message = $msg->message; + $newMessage->kogort_date = $msg->kogort_date; + $newMessage->target_date = $msg->target_date; + $newMessage->status = 'sent'; + $newMessage->created_at = date('Y-m-d H:i:s'); + $newMessage->save(); +} +``` + +### Конверсия по когортам +```php +$conversion = UsersWhatsappMessage::find() + ->select([ + 'kogort_date', + 'COUNT(DISTINCT phone) as total_phones', + 'SUM(CASE WHEN status IN (\'delivered\', \'read\') THEN 1 ELSE 0 END) as success' + ]) + ->groupBy('kogort_date') + ->orderBy(['kogort_date' => SORT_DESC]) + ->asArray() + ->all(); + +foreach ($conversion as $row) { + $rate = $row['total_phones'] > 0 + ? round($row['success'] / $row['total_phones'] * 100, 1) + : 0; + echo "{$row['kogort_date']}: {$rate}% доставлено\n"; +} +``` + +## Валидация + +| Поле | Правила валидации | +|------|-------------------| +| `request_id` | required, string (max 255) | +| `phone` | required, string (max 255) | +| `message` | required, string | +| `kogort_date` | required, safe | +| `target_date` | required, safe | +| `status` | string (max 20), default 'sent' | +| `created_at` | safe | + +## Связанные модели + +- [Users](./Users.md) — клиенты (phone) +- [UsersMessageManagement](./UsersMessageManagement.md) — настройки кампаний + +## Особенности реализации + +1. **WhatsApp Business API**: Интеграция для массовых рассылок +2. **Request ID**: Внешний идентификатор для отслеживания статуса +3. **Статусы доставки**: 7 статусов жизненного цикла сообщения +4. **Когортный маркетинг**: kogort_date и target_date для сегментации +5. **Колбеки статусов**: Обновление через webhook от WhatsApp +6. **Default status**: По умолчанию 'sent' при создании записи diff --git a/erp24/docs/models/UsersWhatsappMessageSearch.md b/erp24/docs/models/UsersWhatsappMessageSearch.md new file mode 100644 index 00000000..1d99a50c --- /dev/null +++ b/erp24/docs/models/UsersWhatsappMessageSearch.md @@ -0,0 +1,191 @@ +# Класс: UsersWhatsappMessageSearch + + +## Mindmap + +```mermaid +mindmap + root((UsersWhatsappMessageSearch)) + Таблица БД + ActiveRecord + Наследование + extends UsersWhatsappMessage +``` + +## Назначение +Search-модель для поиска и фильтрации WhatsApp-сообщений пользователям в ERP24. Модель с поддержкой кастомного formName в методе load() и ilike поиском для текстовых полей. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`UsersWhatsappMessage` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id` — integer +- `request_id`, `phone`, `message`, `kogort_date`, `target_date`, `status`, `created_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, $formName = null): ActiveDataProvider +**Описание:** Создаёт провайдер данных с поддержкой кастомного formName. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$formName` (string|null) — кастомное имя формы для load() + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос UsersWhatsappMessage::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры через load($params, $formName) +4. Применяет фильтры: + - Точное совпадение: id, kogort_date, target_date, created_at + - ilike: request_id, phone, message, status + +## Диаграмма структуры + +```mermaid +erDiagram + UsersWhatsappMessage { + int id PK + varchar request_id + varchar phone + text message + date kogort_date + date target_date + varchar status + datetime created_at + } +``` + +## Диаграмма когортной рассылки + +```mermaid +flowchart TD + A[UsersWhatsappMessage] --> B[Когортная рассылка] + + B --> C[kogort_date] + C --> C1[Дата когорты] + + B --> D[target_date] + D --> D1[Целевая дата] + + E[status] --> F{Статус} + F --> G[pending - Ожидает] + F --> H[sent - Отправлено] + F --> I[delivered - Доставлено] + F --> J[read - Прочитано] + F --> K[failed - Ошибка] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new UsersWhatsappMessageSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск с кастомным formName +```php +$searchModel = new UsersWhatsappMessageSearch(); +$dataProvider = $searchModel->search($params, 'WhatsappFilter'); +// Загрузит из $params['WhatsappFilter'] +``` + +### Поиск по телефону +```php +$searchModel = new UsersWhatsappMessageSearch(); +$dataProvider = $searchModel->search([ + 'UsersWhatsappMessageSearch' => [ + 'phone' => '79991234567', + ] +]); +``` + +### Поиск по статусу +```php +$searchModel = new UsersWhatsappMessageSearch(); +$dataProvider = $searchModel->search([ + 'UsersWhatsappMessageSearch' => [ + 'status' => 'sent', + ] +]); +``` + +### Поиск по когорте +```php +$searchModel = new UsersWhatsappMessageSearch(); +$dataProvider = $searchModel->search([ + 'UsersWhatsappMessageSearch' => [ + 'kogort_date' => '2024-01-15', + ] +]); +``` + +### Поиск в тексте сообщения +```php +$searchModel = new UsersWhatsappMessageSearch(); +$dataProvider = $searchModel->search([ + 'UsersWhatsappMessageSearch' => [ + 'message' => 'Ваш заказ', + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'phone', + [ + 'attribute' => 'message', + 'format' => 'ntext', + 'contentOptions' => ['style' => 'max-width: 300px;'], + ], + [ + 'attribute' => 'status', + 'filter' => ['pending' => 'Ожидает', 'sent' => 'Отправлено', 'delivered' => 'Доставлено'], + ], + 'kogort_date:date', + 'target_date:date', + 'created_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [UsersWhatsappMessage](./UsersWhatsappMessage.md) — базовая модель сообщений +- [Users](./Users.md) — пользователи + +## Особенности реализации + +1. **Кастомный formName**: Второй параметр в search() для гибкой загрузки +2. **ilike поиск**: Регистронезависимый поиск по текстовым полям +3. **Когортный маркетинг**: kogort_date и target_date для когортной рассылки +4. **request_id**: Идентификатор запроса к WhatsApp API +5. **Статусы доставки**: status отслеживает жизненный цикл сообщения diff --git a/erp24/docs/models/WaybillIncoming.md b/erp24/docs/models/WaybillIncoming.md new file mode 100644 index 00000000..795edefe --- /dev/null +++ b/erp24/docs/models/WaybillIncoming.md @@ -0,0 +1,349 @@ +# Модель WaybillIncoming + + +## Mindmap + +```mermaid +mindmap + root((WaybillIncoming)) + Таблица БД + waybill_incoming + Свойства + id + int + guid + string + status + int + created_admin_id + int + store_id + int + store_guid + string + Связи + ShiftTransfer + 1:1 ShiftTransfer + CreatedAdmin + 1:1 Admin + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WaybillIncoming` представляет накладные приходования недостающих товаров, выявленных при передаче смены. Автоматически создаётся на основе данных о расхождениях между фактическим и учётным остатком товаров. + +**Файл модели:** `erp24/records/WaybillIncoming.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `waybill_incoming` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | Уникальный GUID документа для 1С | +| `shift_transfer_id` | INTEGER | ID записи передачи смены (FK) | +| `status` | INTEGER | Статус документа | +| `created_admin_id` | INTEGER | ID создателя документа (FK) | +| `updated_admin_id` | INTEGER | ID редактора документа (FK) | +| `store_id` | INTEGER | ID магазина в ERP (FK) | +| `store_guid` | VARCHAR(100) | GUID магазина из 1С | +| `number` | VARCHAR(100) | Номер документа | +| `number_1c` | VARCHAR(100) | Номер документа в 1С | +| `date` | TIMESTAMP | Дата документа | +| `comment` | TEXT | Комментарий | +| `quantity` | NUMERIC | Общее количество товаров | +| `summ` | NUMERIC | Сумма розничная | +| `summ_self_cost` | NUMERIC | Сумма себестоимости | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `send_at` | TIMESTAMP | Дата отправки в 1С | +| `error_text` | TEXT | Текст ошибки от 1С | + +--- + +## Константы + +### Статусы документа + +```php +const STATUS_NEW = 1; +``` + +Статус нового документа (аналогично `WriteOffsErp::STATUS_CREATED`). + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Автоматическое заполнение дат создания и обновления. + +**Атрибуты:** +- `created_at` - устанавливается при создании +- `updated_at` - обновляется при изменении + +### BlameableBehavior + +Автоматическое заполнение ID администраторов. + +**Атрибуты:** +- `created_admin_id` - устанавливается при создании +- `updated_admin_id` - обновляется при изменении + +--- + +## Методы модели + +### Связи (Relations) + +#### `getShiftTransfer()` + +Возвращает запись передачи смены. + +```php +$shiftTransfer = $waybill->shiftTransfer; // ShiftTransfer +``` + +**Тип:** hasOne +**Связанная модель:** `ShiftTransfer` +**FK:** `shift_transfer_id` → `id` + +#### `getCreatedAdmin()` + +Возвращает администратора, создавшего документ. + +```php +$admin = $waybill->createdAdmin; // Admin +``` + +**Тип:** hasOne +**Связанная модель:** `Admin` +**FK:** `created_admin_id` → `id` + +--- + +### Статические методы + +#### `setData($shiftTransfer): void` + +Создаёт накладную приходования на основе данных передачи смены. + +**Параметры:** +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** void + +**Логика:** +1. Создаёт новый экземпляр `WaybillIncoming` +2. Генерирует GUID через `DataHelper::createGuidMy()` +3. Устанавливает начальные значения: + - `shift_transfer_id` из передачи смены + - `status = WriteOffsErp::STATUS_CREATED` + - `store_id` получает из `CityStore::getAllActiveGuidId()` по `store_guid` + - `store_guid` из передачи смены + - `date`, `comment` из передачи смены + - `quantity`, `summ`, `summ_self_cost` = 0 (будут пересчитаны) +4. Сохраняет документ +5. Генерирует номер: `'ЕРП_РПН_' . date("Y-m-d_H-i") . '_' . $model->id` +6. Вызывает `WaybillIncomingProducts::setData()` для создания позиций товаров +7. Проверяет наличие созданных позиций, если их нет - удаляет документ +8. Рассчитывает итоги по позициям: + - Выполняет запрос с `SUM(product_count)`, `SUM(summ)`, `SUM(summ_self_cost)` + - Обновляет поля `quantity`, `summ`, `summ_self_cost` документа + +**Вызовы сторонних методов:** +- `DataHelper::createGuidMy()` - генерация GUID +- `CityStore::getAllActiveGuidId()` - получение соответствия GUID и ID магазинов +- `WaybillIncomingProducts::setData()` - создание позиций товаров +- `WaybillIncomingProducts::find()` - поиск и агрегация сумм + +**Пример:** +```php +$shiftTransfer = ShiftTransfer::findOne($id); +WaybillIncoming::setData($shiftTransfer); +// Создаётся накладная приходования с автоматическим заполнением товаров +``` + +**Исключения:** +- Выбрасывает `\Exception` при ошибках сохранения + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `guid` | Обязательное, уникальное, макс. 100 символов | +| `store_id` | Обязательное, целое число | +| `store_guid` | Обязательное, макс. 100 символов | +| `date` | Обязательное, безопасное | +| `quantity` | Обязательное, числовое | +| `summ` | Обязательное, числовое | +| `shift_transfer_id` | Целое число, по умолчанию null | +| `status` | Целое число, по умолчанию null | +| `created_admin_id` | Целое число, по умолчанию null | +| `updated_admin_id` | Целое число, по умолчанию null | +| `comment`, `error_text` | Текстовые | +| `summ_self_cost` | Числовое | +| `number`, `number_1c` | Макс. 100 символов | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + waybill_incoming }o--|| shift_transfer : "belongs_to" + waybill_incoming ||--o{ waybill_incoming_products : "has_many" + waybill_incoming }o--|| admin : "created_by" + waybill_incoming }o--|| city_store : "store" + waybill_incoming_products }o--|| products_1c : "product" + + waybill_incoming { + int id PK + string guid UK + int shift_transfer_id FK + int status + int store_id FK + string store_guid + string number + timestamp date + numeric quantity + numeric summ + numeric summ_self_cost + timestamp created_at + } + + shift_transfer { + int id PK + string store_guid + timestamp date + string comment + } + + waybill_incoming_products { + int id PK + int waybill_incoming_id FK + string product_id FK + float product_count + numeric summ + } +``` + +--- + +## Потоки данных + +### Создание накладной приходования + +```mermaid +sequenceDiagram + participant System + participant WaybillIncoming + participant WaybillIncomingProducts + participant ShiftTransfer + participant ShiftRemains + participant CityStore + + System->>ShiftTransfer: Получить данные передачи смены + System->>WaybillIncoming: setData(shiftTransfer) + WaybillIncoming->>WaybillIncoming: Генерация GUID + WaybillIncoming->>CityStore: Получить store_id по store_guid + WaybillIncoming->>WaybillIncoming: Сохранить документ + WaybillIncoming->>WaybillIncoming: Генерировать номер + WaybillIncoming->>WaybillIncomingProducts: setData(waybill, shiftTransfer) + WaybillIncomingProducts->>ShiftRemains: Получить товары с излишками + WaybillIncomingProducts->>WaybillIncomingProducts: Создать позиции + WaybillIncomingProducts-->>WaybillIncoming: Позиции созданы + WaybillIncoming->>WaybillIncomingProducts: Агрегация сумм + WaybillIncomingProducts-->>WaybillIncoming: Итоги + WaybillIncoming->>WaybillIncoming: Обновить quantity, summ + WaybillIncoming-->>System: Накладная создана +``` + +--- + +## Примеры использования + +### Автоматическое создание накладной при передаче смены + +```php +$shiftTransfer = ShiftTransfer::findOne(['id' => $shiftTransferId]); + +try { + WaybillIncoming::setData($shiftTransfer); + echo "Накладная приходования создана\n"; +} catch (\Exception $e) { + echo "Ошибка создания накладной: {$e->getMessage()}\n"; +} +``` + +### Получение накладных магазина + +```php +$waybills = WaybillIncoming::find() + ->where(['store_id' => $storeId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($waybills as $waybill) { + echo "Накладная: {$waybill->number}, "; + echo "Дата: {$waybill->date}, "; + echo "Сумма: {$waybill->summ}\n"; +} +``` + +### Просмотр деталей накладной + +```php +$waybill = WaybillIncoming::findOne($id); + +echo "Номер: {$waybill->number}\n"; +echo "Магазин: {$waybill->store_guid}\n"; +echo "Количество позиций: {$waybill->quantity}\n"; +echo "Сумма розничная: {$waybill->summ}\n"; +echo "Себестоимость: {$waybill->summ_self_cost}\n"; + +// Администратор +$admin = $waybill->createdAdmin; +echo "Создал: {$admin->name}\n"; + +// Передача смены +$shiftTransfer = $waybill->shiftTransfer; +echo "Передача смены от: {$shiftTransfer->date}\n"; +``` + +--- + +## Связанные модели + +- **[WaybillIncomingProducts](./WaybillIncomingProducts.md)** — позиции товаров накладной приходования +- **ShiftTransfer** — передача смены магазина +- **ShiftRemains** — остатки товаров при передаче смены +- **[CityStore](./CityStore.md)** — справочник магазинов +- **[Admin](./Admin.md)** — администраторы системы +- **[WriteOffsErp](./WriteOffsErp.md)** — документы списаний (используется статус) + +--- + +## Примечания + +1. Документ создаётся **автоматически** при передаче смены, если обнаружены излишки товаров +2. Номер документа генерируется в формате: `ЕРП_РПН_YYYY-MM-DD_HH-MM_ID` +3. РПН = Расходная Накладная на Недостачу (приходование излишков) +4. Если при создании не обнаружено товаров для приходования, документ удаляется +5. Использует behaviors для автоматического заполнения дат и ID администраторов +6. Связан с системой передачи смен (`ShiftTransfer`) +7. Подготовлен для интеграции с 1С (поля `guid`, `number_1c`, `send_at`, `error_text`) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WaybillIncomingProducts.md b/erp24/docs/models/WaybillIncomingProducts.md new file mode 100644 index 00000000..13681936 --- /dev/null +++ b/erp24/docs/models/WaybillIncomingProducts.md @@ -0,0 +1,281 @@ +# Модель WaybillIncomingProducts + + +## Mindmap + +```mermaid +mindmap + root((WaybillIncomingProducts)) + Таблица БД + waybill_incoming_products + Свойства + id + int + waybill_incoming_id + int + name + string + summ + float + created_at + string + waybillIncoming + WaybillIncoming + Связи + WaybillIncoming + 1:1 WaybillIncoming + Product + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WaybillIncomingProducts` представляет позиции товаров в накладных приходования недостающих товаров. Создаётся автоматически на основе данных об излишках, выявленных при передаче смены. + +**Файл модели:** `erp24/records/WaybillIncomingProducts.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `waybill_incoming_products` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `waybill_incoming_id` | INTEGER | ID накладной из таблицы `waybill_incoming` (FK) | +| `name` | VARCHAR(100) | Название товара | +| `product_id` | VARCHAR(255) | GUID товара (FK) | +| `product_count` | NUMERIC | Количество товара с недостатком (излишек) | +| `product_price` | NUMERIC | Цена товара розничная | +| `product_self_cost` | NUMERIC | Себестоимость товара | +| `summ` | NUMERIC | Сумма розничная (product_count * product_price) | +| `summ_self_cost` | NUMERIC | Сумма себестоимости (product_count * product_self_cost) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Автоматическое заполнение дат создания и обновления. + +--- + +## Методы модели + +### Связи (Relations) + +#### `getWaybillIncoming()` + +Возвращает родительскую накладную. + +```php +$waybill = $product->waybillIncoming; // WaybillIncoming +``` + +**Тип:** hasOne +**Связанная модель:** `WaybillIncoming` +**FK:** `waybill_incoming_id` → `id` + +#### `getProduct()` + +Возвращает товар из справочника Products1c. + +```php +$product = $item->product; // Products1c +``` + +**Тип:** hasOne +**Связанная модель:** `Products1c` +**FK:** `product_id` → `id` + +--- + +### Статические методы + +#### `setData($waybillIncoming, $shiftTransfer): void` + +Создаёт позиции товаров накладной на основе данных передачи смены. + +**Параметры:** +- `$waybillIncoming` (WaybillIncoming) - объект накладной +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** void + +**Логика:** +1. Находит все записи `ShiftRemains` с положительной разницей `fact_and_1c_diff > 0` (излишки) +2. Для каждой записи: + - Вычисляет количество излишка: `abs(fact_and_1c_diff)` + - Проверяет наличие выравниваний (`EqualizationRemains`): + - Если есть выравнивание на полное количество - пропускает товар + - Если выравнивание частичное - уменьшает количество на выравненное + - Если выравнивания больше недостачи - пропускает товар + - Если после выравнивания количество = 0 - пропускает товар +3. Создаёт позицию `WaybillIncomingProducts`: + - `waybill_incoming_id` - ID накладной + - `name` - название из `product->name` + - `product_id` - GUID товара + - `product_count` - количество излишка + - `product_price` - розничная цена из `retail_price` + - `product_self_cost` - себестоимость из `self_cost` + - `summ` - количество * розничная цена + - `summ_self_cost` - количество * себестоимость +4. Сохраняет позицию + +**Вызовы сторонних методов:** +- `ShiftRemains::find()` - поиск остатков с излишками +- `EqualizationRemains::find()` - поиск выравниваний товаров + +**Пример:** +```php +$waybill = WaybillIncoming::findOne($id); +$shiftTransfer = $waybill->shiftTransfer; + +WaybillIncomingProducts::setData($waybill, $shiftTransfer); +// Создаются позиции товаров с излишками +``` + +**Исключения:** +- Выбрасывает `\Exception` при ошибках сохранения + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `waybill_incoming_id` | Обязательное, целое число, существует в `WaybillIncoming` | +| `name` | Обязательное, макс. 100 символов | +| `summ` | Обязательное, числовое | +| `product_id` | Макс. 255 символов | +| `product_count` | Числовое | +| `product_price` | Числовое | +| `product_self_cost` | Числовое | +| `summ_self_cost` | Числовое | +| `created_at`, `updated_at` | Безопасные | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + waybill_incoming ||--o{ waybill_incoming_products : "has_many" + waybill_incoming_products }o--|| products_1c : "belongs_to" + shift_remains ||--o{ waybill_incoming_products : "source_data" + + waybill_incoming { + int id PK + string guid + int shift_transfer_id + numeric summ + } + + waybill_incoming_products { + int id PK + int waybill_incoming_id FK + string product_id FK + string name + numeric product_count + numeric product_price + numeric summ + } + + products_1c { + string id PK + string name + } + + shift_remains { + int id PK + int shift_transfer_id + string product_guid + numeric fact_and_1c_diff + } +``` + +--- + +## Примеры использования + +### Получение позиций накладной + +```php +$products = WaybillIncomingProducts::find() + ->where(['waybill_incoming_id' => $waybillId]) + ->all(); + +foreach ($products as $product) { + echo "Товар: {$product->name}\n"; + echo "Количество: {$product->product_count}\n"; + echo "Цена: {$product->product_price}\n"; + echo "Сумма: {$product->summ}\n\n"; +} +``` + +### Расчёт итогов накладной + +```php +$totals = WaybillIncomingProducts::find() + ->where(['waybill_incoming_id' => $waybillId]) + ->select([ + 'total_count' => 'SUM(product_count)', + 'total_summ' => 'SUM(summ)', + 'total_cost' => 'SUM(summ_self_cost)' + ]) + ->asArray() + ->one(); + +echo "Всего товаров: {$totals['total_count']}\n"; +echo "Сумма розничная: {$totals['total_summ']}\n"; +echo "Себестоимость: {$totals['total_cost']}\n"; +``` + +### Просмотр деталей позиции + +```php +$item = WaybillIncomingProducts::findOne($id); + +// Накладная +$waybill = $item->waybillIncoming; +echo "Накладная: {$waybill->number}\n"; + +// Товар +$product = $item->product; +echo "Товар: {$product->name}\n"; +echo "Артикул: {$product->vendor_code}\n"; +``` + +--- + +## Связанные модели + +- **[WaybillIncoming](./WaybillIncoming.md)** — накладные приходования +- **[Products1c](./Products1c.md)** — справочник товаров из 1С +- **ShiftRemains** — остатки товаров при передаче смены +- **EqualizationRemains** — выравнивания остатков товаров +- **ShiftTransfer** — передача смены магазина + +--- + +## Примечания + +1. Позиции создаются **автоматически** при создании накладной `WaybillIncoming` +2. Обрабатываются только товары с положительной разницей (`fact_and_1c_diff > 0`) - излишки +3. Учитываются выравнивания товаров из `EqualizationRemains` +4. Если после выравнивания количество = 0, позиция не создаётся +5. Хранит как розничную цену, так и себестоимость +6. Использует TimestampBehavior для автоматического заполнения дат +7. Название товара дублируется в поле `name` для удобства отображения + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WaybillWriteOffs.md b/erp24/docs/models/WaybillWriteOffs.md new file mode 100644 index 00000000..be556720 --- /dev/null +++ b/erp24/docs/models/WaybillWriteOffs.md @@ -0,0 +1,291 @@ +# Модель WaybillWriteOffs + + +## Mindmap + +```mermaid +mindmap + root((WaybillWriteOffs)) + Таблица БД + waybill_write_offs + Свойства + id + int + guid + string + status + int + created_admin_id + int + store_id + int + store_guid + string + Связи + ShiftTransfer + 1:1 ShiftTransfer + WaybillWriteOffsProducts + 1:N WaybillWriteOffsProducts + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WaybillWriteOffs` представляет накладные списания недостающих товаров, выявленных при передаче смены. Автоматически создаётся на основе данных о расхождениях между фактическим и учётным остатком товаров (недостача). + +**Файл модели:** `erp24/records/WaybillWriteOffs.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `waybill_write_offs` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | Уникальный GUID документа для 1С | +| `shift_transfer_id` | INTEGER | ID записи передачи смены (FK) | +| `status` | INTEGER | Статус документа | +| `created_admin_id` | INTEGER | ID создателя документа (FK) | +| `updated_admin_id` | INTEGER | ID редактора документа (FK) | +| `store_id` | INTEGER | ID магазина в ERP (FK) | +| `store_guid` | VARCHAR(100) | GUID магазина из 1С | +| `number` | VARCHAR(100) | Номер документа | +| `number_1c` | VARCHAR(100) | Номер документа в 1С | +| `name_1c` | TEXT | Название документа в 1С | +| `date` | TIMESTAMP | Дата документа | +| `comment` | TEXT | Комментарий | +| `quantity` | NUMERIC | Общее количество товаров | +| `summ` | NUMERIC | Сумма розничная | +| `summ_self_cost` | NUMERIC | Сумма себестоимости | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | +| `send_at` | TIMESTAMP | Дата отправки в 1С | +| `error_text` | TEXT | Текст ошибки от 1С | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Автоматическое заполнение дат создания и обновления. + +### BlameableBehavior + +Автоматическое заполнение ID администраторов. + +--- + +## Методы модели + +### Связи (Relations) + +#### `getShiftTransfer()` + +Возвращает запись передачи смены. + +```php +$shiftTransfer = $waybill->shiftTransfer; // ShiftTransfer +``` + +**Тип:** hasOne +**Связанная модель:** `ShiftTransfer` +**FK:** `shift_transfer_id` → `id` + +#### `getWaybillWriteOffsProducts()` + +Возвращает позиции товаров накладной. + +```php +$products = $waybill->waybillWriteOffsProducts; // WaybillWriteOffsProducts[] +``` + +**Тип:** hasMany +**Связанная модель:** `WaybillWriteOffsProducts` +**FK:** `id` → `waybill_write_offs_id` + +--- + +### Статические методы + +#### `setData($shiftTransfer): void` + +Создаёт накладную списания на основе данных передачи смены. + +**Параметры:** +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** void + +**Логика:** +1. Создаёт новый экземпляр `WaybillWriteOffs` +2. Генерирует GUID через `DataHelper::createGuidMy()` +3. Устанавливает начальные значения: + - `shift_transfer_id` из передачи смены + - `status = WriteOffsErp::STATUS_CREATED` + - `store_id` получает из `CityStore::getAllActiveGuidId()` по `store_guid` + - `store_guid` из передачи смены + - `date`, `comment` из передачи смены + - `quantity`, `summ`, `summ_self_cost` = 0 (будут пересчитаны) +4. Сохраняет документ +5. Генерирует номер: `'ЕРП_РНС_' . date("Y-m-d_H-i") . '_' . $model->id` +6. Вызывает `WaybillWriteOffsProducts::setData()` для создания позиций товаров +7. Проверяет наличие созданных позиций, если их нет - удаляет документ +8. Рассчитывает итоги по позициям: + - Выполняет запрос с `SUM(product_count)`, `SUM(summ)`, `SUM(summ_self_cost)` + - Обновляет поля `quantity`, `summ`, `summ_self_cost` документа + +**Вызовы сторонних методов:** +- `DataHelper::createGuidMy()` - генерация GUID +- `CityStore::getAllActiveGuidId()` - получение соответствия GUID и ID магазинов +- `WaybillWriteOffsProducts::setData()` - создание позиций товаров +- `WaybillWriteOffsProducts::find()` - поиск и агрегация сумм + +**Пример:** +```php +$shiftTransfer = ShiftTransfer::findOne($id); +WaybillWriteOffs::setData($shiftTransfer); +// Создаётся накладная списания с автоматическим заполнением товаров +``` + +**Исключения:** +- Выбрасывает `\Exception` при ошибках сохранения + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `guid` | Обязательное, уникальное, макс. 100 символов | +| `store_id` | Обязательное, целое число | +| `store_guid` | Обязательное, макс. 100 символов | +| `date` | Обязательное, безопасное | +| `quantity` | Обязательное, числовое | +| `summ` | Обязательное, числовое | +| `shift_transfer_id` | Существует в `ShiftTransfer` | +| Остальные поля | По умолчанию null, безопасные или числовые | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + waybill_write_offs }o--|| shift_transfer : "belongs_to" + waybill_write_offs ||--o{ waybill_write_offs_products : "has_many" + waybill_write_offs }o--|| admin : "created_by" + waybill_write_offs }o--|| city_store : "store" + waybill_write_offs_products }o--|| products_1c : "product" + + waybill_write_offs { + int id PK + string guid UK + int shift_transfer_id FK + int status + int store_id FK + string store_guid + string number + timestamp date + numeric quantity + numeric summ + numeric summ_self_cost + } + + shift_transfer { + int id PK + string store_guid + timestamp date + string comment + } + + waybill_write_offs_products { + int id PK + int waybill_write_offs_id FK + string product_id FK + numeric product_count + numeric summ + } +``` + +--- + +## Примеры использования + +### Автоматическое создание накладной при передаче смены + +```php +$shiftTransfer = ShiftTransfer::findOne(['id' => $shiftTransferId]); + +try { + WaybillWriteOffs::setData($shiftTransfer); + echo "Накладная списания создана\n"; +} catch (\Exception $e) { + echo "Ошибка создания накладной: {$e->getMessage()}\n"; +} +``` + +### Получение накладных магазина + +```php +$waybills = WaybillWriteOffs::find() + ->where(['store_id' => $storeId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($waybills as $waybill) { + echo "Накладная: {$waybill->number}, "; + echo "Дата: {$waybill->date}, "; + echo "Сумма: {$waybill->summ}\n"; +} +``` + +### Просмотр деталей накладной + +```php +$waybill = WaybillWriteOffs::findOne($id); + +echo "Номер: {$waybill->number}\n"; +echo "Магазин: {$waybill->store_guid}\n"; +echo "Количество позиций: {$waybill->quantity}\n"; +echo "Сумма розничная: {$waybill->summ}\n"; +echo "Себестоимость: {$waybill->summ_self_cost}\n"; + +// Позиции товаров +$products = $waybill->waybillWriteOffsProducts; +foreach ($products as $product) { + echo "- {$product->name}: {$product->product_count} шт.\n"; +} +``` + +--- + +## Связанные модели + +- **[WaybillWriteOffsProducts](./WaybillWriteOffsProducts.md)** — позиции товаров накладной списания +- **ShiftTransfer** — передача смены магазина +- **ShiftRemains** — остатки товаров при передаче смены +- **[CityStore](./CityStore.md)** — справочник магазинов +- **[Admin](./Admin.md)** — администраторы системы +- **[WriteOffsErp](./WriteOffsErp.md)** — документы списаний (используется статус) + +--- + +## Примечания + +1. Документ создаётся **автоматически** при передаче смены, если обнаружена недостача товаров +2. Номер документа генерируется в формате: `ЕРП_РНС_YYYY-MM-DD_HH-MM_ID` +3. РНС = Расходная Накладная на Списание +4. Если при создании не обнаружено товаров для списания, документ удаляется +5. Использует behaviors для автоматического заполнения дат и ID администраторов +6. Связан с системой передачи смен (`ShiftTransfer`) +7. Подготовлен для интеграции с 1С (поля `guid`, `number_1c`, `name_1c`, `send_at`, `error_text`) + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WaybillWriteOffsProducts.md b/erp24/docs/models/WaybillWriteOffsProducts.md new file mode 100644 index 00000000..0cc0efb9 --- /dev/null +++ b/erp24/docs/models/WaybillWriteOffsProducts.md @@ -0,0 +1,285 @@ +# Модель WaybillWriteOffsProducts + + +## Mindmap + +```mermaid +mindmap + root((WaybillWriteOffsProducts)) + Таблица БД + waybill_write_offs_products + Свойства + id + int + waybill_write_offs_id + int + name + string + summ + float + created_at + string + waybillWriteOffs + WaybillWriteOffs + Связи + WaybillWriteOffs + 1:1 WaybillWriteOffs + Product + 1:1 Products1c + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WaybillWriteOffsProducts` представляет позиции товаров в накладных списания недостающих товаров. Создаётся автоматически на основе данных о недостаче, выявленной при передаче смены. + +**Файл модели:** `erp24/records/WaybillWriteOffsProducts.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `waybill_write_offs_products` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `waybill_write_offs_id` | INTEGER | ID накладной из таблицы `waybill_write_offs` (FK) | +| `name` | VARCHAR(100) | Название товара | +| `product_id` | VARCHAR(255) | GUID товара (FK) | +| `product_count` | NUMERIC | Количество недостающего товара | +| `product_price` | NUMERIC | Цена товара розничная | +| `product_self_cost` | NUMERIC | Себестоимость товара | +| `summ` | NUMERIC | Сумма розничная (product_count * product_price) | +| `summ_self_cost` | NUMERIC | Сумма себестоимости (product_count * product_self_cost) | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата обновления записи | + +--- + +## Поведения (Behaviors) + +### TimestampBehavior + +Автоматическое заполнение дат создания и обновления. + +--- + +## Методы модели + +### Связи (Relations) + +#### `getWaybillWriteOffs()` + +Возвращает родительскую накладную списания. + +```php +$waybill = $product->waybillWriteOffs; // WaybillWriteOffs +``` + +**Тип:** hasOne +**Связанная модель:** `WaybillWriteOffs` +**FK:** `waybill_write_offs_id` → `id` + +#### `getProduct()` + +Возвращает товар из справочника Products1c. + +```php +$product = $item->product; // Products1c +``` + +**Тип:** hasOne +**Связанная модель:** `Products1c` +**FK:** `product_id` → `id` + +--- + +### Статические методы + +#### `setData($waybillWriteOffs, $shiftTransfer): bool` + +Создаёт позиции товаров накладной на основе данных передачи смены. + +**Параметры:** +- `$waybillWriteOffs` (WaybillWriteOffs) - объект накладной +- `$shiftTransfer` (ShiftTransfer) - объект передачи смены + +**Возвращает:** `bool` - true если были ошибки сохранения, false если всё успешно + +**Логика:** +1. Находит все записи `ShiftRemains` с отрицательной разницей `fact_and_1c_diff < 0` (недостача) +2. Для каждой записи: + - Вычисляет количество недостачи: `abs(fact_and_1c_diff)` + - Проверяет наличие выравниваний (`EqualizationRemains`): + - Если есть выравнивание на полное количество - пропускает товар + - Если выравнивание частичное - уменьшает количество на выравненное + - Если выравнивания больше недостачи - пропускает товар + - Если после выравнивания количество = 0 - пропускает товар +3. Создаёт позицию `WaybillWriteOffsProducts`: + - `waybill_write_offs_id` - ID накладной + - `name` - название из `product->name` + - `product_id` - GUID товара + - `product_count` - количество недостачи + - `product_price` - розничная цена из `retail_price` + - `product_self_cost` - себестоимость из `self_cost` + - `summ` - количество * розничная цена + - `summ_self_cost` - количество * себестоимость +4. Сохраняет позицию +5. Возвращает флаг наличия ошибок при сохранении + +**Вызовы сторонних методов:** +- `ShiftRemains::find()` - поиск остатков с недостачей +- `EqualizationRemains::find()` - поиск выравниваний товаров + +**Пример:** +```php +$waybill = WaybillWriteOffs::findOne($id); +$shiftTransfer = $waybill->shiftTransfer; + +$hasErrors = WaybillWriteOffsProducts::setData($waybill, $shiftTransfer); +if ($hasErrors) { + echo "Были ошибки при создании позиций\n"; +} +``` + +**Исключения:** +- Выбрасывает `\Exception` при критических ошибках сохранения + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `waybill_write_offs_id` | Обязательное, целое число, существует в `WaybillWriteOffs` | +| `name` | Обязательное, макс. 100 символов | +| `summ` | Обязательное, числовое | +| `product_id` | Макс. 255 символов | +| `product_count` | Числовое | +| `product_price` | Числовое | +| `product_self_cost` | Числовое | +| `summ_self_cost` | Числовое | +| `created_at`, `updated_at` | Безопасные | + +--- + +## Диаграмма связей + +```mermaid +erDiagram + waybill_write_offs ||--o{ waybill_write_offs_products : "has_many" + waybill_write_offs_products }o--|| products_1c : "belongs_to" + shift_remains ||--o{ waybill_write_offs_products : "source_data" + + waybill_write_offs { + int id PK + string guid + int shift_transfer_id + numeric summ + } + + waybill_write_offs_products { + int id PK + int waybill_write_offs_id FK + string product_id FK + string name + numeric product_count + numeric product_price + numeric summ + } + + products_1c { + string id PK + string name + } + + shift_remains { + int id PK + int shift_transfer_id + string product_guid + numeric fact_and_1c_diff + } +``` + +--- + +## Примеры использования + +### Получение позиций накладной + +```php +$products = WaybillWriteOffsProducts::find() + ->where(['waybill_write_offs_id' => $waybillId]) + ->all(); + +foreach ($products as $product) { + echo "Товар: {$product->name}\n"; + echo "Количество: {$product->product_count}\n"; + echo "Цена: {$product->product_price}\n"; + echo "Сумма: {$product->summ}\n\n"; +} +``` + +### Расчёт итогов накладной + +```php +$totals = WaybillWriteOffsProducts::find() + ->where(['waybill_write_offs_id' => $waybillId]) + ->select([ + 'total_count' => 'SUM(product_count)', + 'total_summ' => 'SUM(summ)', + 'total_cost' => 'SUM(summ_self_cost)' + ]) + ->asArray() + ->one(); + +echo "Всего товаров: {$totals['total_count']}\n"; +echo "Сумма розничная: {$totals['total_summ']}\n"; +echo "Себестоимость: {$totals['total_cost']}\n"; +``` + +### Просмотр деталей позиции + +```php +$item = WaybillWriteOffsProducts::findOne($id); + +// Накладная +$waybill = $item->waybillWriteOffs; +echo "Накладная: {$waybill->number}\n"; + +// Товар +$product = $item->product; +echo "Товар: {$product->name}\n"; +echo "Артикул: {$product->vendor_code}\n"; +``` + +--- + +## Связанные модели + +- **[WaybillWriteOffs](./WaybillWriteOffs.md)** — накладные списания +- **[Products1c](./Products1c.md)** — справочник товаров из 1С +- **ShiftRemains** — остатки товаров при передаче смены +- **EqualizationRemains** — выравнивания остатков товаров +- **ShiftTransfer** — передача смены магазина + +--- + +## Примечания + +1. Позиции создаются **автоматически** при создании накладной `WaybillWriteOffs` +2. Обрабатываются только товары с отрицательной разницей (`fact_and_1c_diff < 0`) - недостача +3. Учитываются выравнивания товаров из `EqualizationRemains` +4. Если после выравнивания количество = 0, позиция не создаётся +5. Хранит как розничную цену, так и себестоимость +6. Использует TimestampBehavior для автоматического заполнения дат +7. Название товара дублируется в поле `name` для удобства отображения +8. Метод `setData()` возвращает флаг ошибок, а не выбрасывает исключение + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WikiArticle.md b/erp24/docs/models/WikiArticle.md new file mode 100644 index 00000000..71b03250 --- /dev/null +++ b/erp24/docs/models/WikiArticle.md @@ -0,0 +1,422 @@ +# Class: WikiArticle + +## Mindmap: Модель WikiArticle + +```mermaid +mindmap + root((WikiArticle)) + Идентификация + id PK + slug ЧПУ уникальный + title название + category_id FK категория + Содержимое + description краткое описание + content полный контент + resource_link ссылка на ресурс + Аудит + created_at дата создания + created_by FK автор + updated_at дата обновления + updated_by FK редактор + Связи + WikiCategory категория + Admin автор + Admin редактор + Behaviors + TimestampBehavior авто даты + BlameableBehavior авто авторы +``` + +--- + +## Назначение + +Модель статьи внутренней Wiki-базы знаний в системе ERP24. Представляет собой структурированный информационный материал, организованный по категориям. Используется для хранения корпоративной документации, инструкций, FAQ, полезных материалов для сотрудников. + +Каждая статья имеет ЧПУ (человекопонятный URL), категорию, автора и поддерживает полнотекстовый контент с форматированием. Автоматически отслеживает время создания/обновления и авторов изменений через Yii2 behaviors. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`wiki_article` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** ID статьи | +| `slug` | string(255) | **ЧПУ** (человекопонятный URL, уникальный) | +| `title` | string(255) | **Название статьи** (обязательное) | +| `category_id` | int | **FK** Категория статьи (обязательное) | + +### Содержимое + +| Имя | Тип | Описание | +|-----|-----|----------| +| `description` | text | Краткое описание (для превью) | +| `content` | text | **Полный контент статьи** (HTML/Markdown, обязательное) | +| `resource_link` | string(255) | Ссылка на внешний ресурс | + +### Аудит + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | string(255) | Дата создания (автоматически) | +| `created_by` | int | **FK** Кем создано (автоматически) | +| `updated_at` | string(255) | Дата обновления (автоматически) | +| `updated_by` | int | **FK** Кем обновлено (автоматически) | + +--- + +## Behaviors + +### TimestampBehavior + +**Назначение:** Автоматическое заполнение полей created_at и updated_at + +**Логика:** +При создании записи устанавливает created_at в текущую дату-время. При обновлении записи обновляет updated_at. Использует формат 'Y-m-d H:i:s'. + +**Конфигурация:** +```php +[ + 'class' => TimestampBehavior::class, + 'value' => function () { + return date('Y-m-d H:i:s'); + }, +] +``` + +--- + +### BlameableBehavior + +**Назначение:** Автоматическое заполнение полей created_by и updated_by + +**Логика:** +При создании записи устанавливает created_by в ID текущего пользователя (Yii::$app->user->id). При обновлении обновляет updated_by. Если пользователь не авторизован, поля остаются NULL. + +--- + +## Отношения (Relations) + +### getCategory() + +**Тип:** `hasOne` +**Модель:** `WikiCategory` +**Ключ:** `['id' => 'category_id']` +**Описание:** Категория статьи + +**Логика:** +Связь с моделью WikiCategory для получения информации о категории: название, описание, родительская категория, разрешённые группы сотрудников. + +**Вызовы сторонних методов:** +- `WikiCategory::hasOne()` - построение связи с таблицей wiki_category + +**Пример:** +```php +$article = WikiArticle::findOne($id); +echo "Статья: {$article->title}\n"; +echo "Категория: {$article->category->title}\n"; +``` + +--- + +## Методы + +### beforeValidate() + +**Тип:** `protected` +**Параметры:** нет +**Возвращает:** `bool` — результат валидации родителя + +**Описание:** +Хук, выполняемый перед валидацией модели. Автоматически генерирует slug из title при создании новой записи, если slug не был указан вручную. + +**Логика работы:** +1. Проверяет, является ли запись новой через $this->isNewRecord +2. Проверяет, не задан ли slug вручную +3. Если обе проверки пройдены, генерирует slug из title через Inflector::slug() +4. Inflector транслитерирует кириллицу, убирает спецсимволы, преобразует пробелы в дефисы +5. Вызывает родительский beforeValidate() и возвращает его результат + +**Вызовы сторонних методов:** +- `$this->isNewRecord` - проверка, новая ли запись +- `Inflector::slug($this->title)` - генерация ЧПУ из названия +- `parent::beforeValidate()` - валидация родительского класса + +**Пример:** +```php +$article = new WikiArticle(); +$article->title = 'Как работать с кассой'; +$article->category_id = 1; +$article->content = 'Инструкция...'; +// slug будет автоматически установлен в 'kak-rabotat-s-kassoj' +$article->save(); + +echo $article->slug; // kak-rabotat-s-kassoj +``` + +--- + +### hasCategories() + +**Тип:** `static` +**Параметры:** нет +**Возвращает:** `bool` — true если есть хотя бы одна категория + +**Описание:** +Статический метод проверки наличия категорий в системе. Используется перед созданием статьи для валидации — нельзя создать статью без категорий. + +**Логика работы:** +1. Выполняет запрос к таблице wiki_category через WikiCategory::find() +2. Проверяет существование хотя бы одной записи через exists() +3. Возвращает true если категории есть, false если категорий нет + +**Вызовы сторонних методов:** +- `WikiCategory::find()->exists()` - проверка наличия записей в таблице + +**Пример:** +```php +if (WikiArticle::hasCategories()) { + // Можно создавать статью + $article = new WikiArticle(); + // ... +} else { + echo "Сначала создайте хотя бы одну категорию"; +} +``` + +--- + +### getCreatorName() + +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `string|null` — имя создателя или null + +**Описание:** +Получает имя сотрудника, создавшего статью. Используется для отображения информации об авторе в интерфейсе. + +**Логика работы:** +1. Создаёт связь hasOne с моделью Admin через поле created_by +2. Выбирает только поле name +3. Извлекает скалярное значение через scalar() +4. Возвращает имя автора или null если автор не найден + +**Вызовы сторонних методов:** +- `$this->hasOne(Admin::class, ['id' => 'created_by'])` - связь с таблицей admin +- `->select('name')` - выборка только имени +- `->scalar()` - получение скалярного значения + +**Пример:** +```php +$article = WikiArticle::findOne($id); +$creatorName = $article->getCreatorName(); + +if ($creatorName) { + echo "Автор: {$creatorName}\n"; +} else { + echo "Автор неизвестен\n"; +} +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['title', 'category_id', 'content'] // required +``` + +### Целочисленные поля +```php +['category_id', 'created_by', 'updated_by'] // integer, default: null +``` + +### Строковые поля +```php +['slug', 'title', 'created_at', 'updated_at', 'resource_link'] // string, max: 255 +['description', 'content'] // text (без ограничения) +``` + +### Безопасные поля +```php +['slug', 'created_at', 'created_by', 'resource_link'] // safe +``` + +### Уникальность +```php +['slug'] // unique +``` + +### Внешние ключи +```php +['category_id'] // exist в WikiCategory +``` + +--- + +## Примеры использования + +### Создание статьи + +```php +use yii_app\records\WikiArticle; + +$article = new WikiArticle(); +$article->title = 'Инструкция по работе с CRM'; +$article->category_id = 5; +$article->description = 'Краткое руководство по основным функциям CRM'; +$article->content = '

    CRM система

    Описание...

    '; +$article->resource_link = 'https://docs.example.com/crm'; +// slug, created_at, created_by заполнятся автоматически + +if ($article->save()) { + echo "Статья создана с slug: {$article->slug}"; +} +``` + +--- + +### Получение статьи по slug + +```php +$article = WikiArticle::find() + ->where(['slug' => 'instrukciya-po-rabote-s-crm']) + ->with('category') + ->one(); + +if ($article) { + echo "Статья: {$article->title}\n"; + echo "Категория: {$article->category->title}\n"; + echo "Автор: {$article->getCreatorName()}\n"; + echo "Создана: {$article->created_at}\n"; +} +``` + +--- + +### Обновление статьи + +```php +$article = WikiArticle::findOne($id); +$article->content = $newContent; +// updated_at и updated_by обновятся автоматически +$article->save(); +``` + +--- + +### Поиск статей по категории + +```php +$categoryId = 5; + +$articles = WikiArticle::find() + ->where(['category_id' => $categoryId]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); + +foreach ($articles as $article) { + echo "- {$article->title} ({$article->slug})\n"; +} +``` + +--- + +### Полнотекстовый поиск + +```php +$searchTerm = 'касса'; + +$articles = WikiArticle::find() + ->where(['OR', + ['like', 'title', $searchTerm], + ['like', 'description', $searchTerm], + ['like', 'content', $searchTerm] + ]) + ->all(); + +echo "Найдено статей: " . count($articles) . "\n"; +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + WikiArticle ||--|| WikiCategory : "belongs to" + WikiArticle ||--o| Admin : "created by" + WikiArticle ||--o| Admin : "updated by" + + WikiArticle { + int id PK + string slug UK + string title + int category_id FK + text description + text content + string resource_link + string created_at + int created_by FK + string updated_at + int updated_by FK + } + + WikiCategory { + int id PK + string slug UK + string title + int parent_id FK + text description + } + + Admin { + int id PK + string name + string email + } +``` + +--- + +## Замечания + +1. **slug** — автоматически генерируется из title при создании +2. **slug** — уникальный, используется в URL: /wiki/{slug} +3. **created_at**, **updated_at** — заполняются автоматически через TimestampBehavior +4. **created_by**, **updated_by** — заполняются автоматически через BlameableBehavior +5. **category_id** — обязательное поле, статья всегда принадлежит категории +6. **resource_link** — опциональная ссылка на внешний ресурс +7. **content** — поддерживает HTML/Markdown форматирование +8. **description** — краткое описание для списков и превью +9. Перед созданием первой статьи должна существовать хотя бы одна категория +10. Используется в паре с WikiCategory для построения базы знаний + +--- + +## Связанные документы + +- [WikiCategory.md](./WikiCategory.md) — категории Wiki +- [Admin.md](./Admin.md) — модель сотрудников diff --git a/erp24/docs/models/WikiArticleSearch.md b/erp24/docs/models/WikiArticleSearch.md new file mode 100644 index 00000000..27454f3d --- /dev/null +++ b/erp24/docs/models/WikiArticleSearch.md @@ -0,0 +1,209 @@ +# Класс: WikiArticleSearch + + +## Mindmap + +```mermaid +mindmap + root((WikiArticleSearch)) + Таблица БД + ActiveRecord + Наследование + extends WikiArticle +``` + +## Назначение +Search-модель для поиска и фильтрации Wiki-статей в ERP24. Модель с проверкой наличия категорий перед поиском — возвращает пустой результат при отсутствии категорий. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`WikiArticle` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `category_id`, `created_by`, `updated_by` — integer +- `slug`, `title`, `description`, `content`, `created_at`, `updated_at` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных с предварительной проверкой категорий. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. **Проверяет hasCategories()**: Если категорий нет — возвращает пустой результат (WHERE 0=1) +2. Создаёт запрос WikiArticle::find() +3. Оборачивает в ActiveDataProvider +4. Загружает параметры +5. Применяет фильтры: + - Точное совпадение: id, category_id, created_by, updated_by + - ilike: slug, title, description, content, created_at, updated_at + +## Диаграмма связей + +```mermaid +erDiagram + WikiArticle { + int id PK + int category_id FK + varchar slug + varchar title + text description + text content + int created_by FK + int updated_by FK + datetime created_at + datetime updated_at + } + + WikiCategory { + int id PK + varchar name + } + + Admin { + int id PK + varchar name + } + + WikiArticle }o--|| WikiCategory : "category_id" + WikiArticle }o--|| Admin : "created_by" + WikiArticle }o--|| Admin : "updated_by" +``` + +## Диаграмма логики поиска + +```mermaid +flowchart TD + A[search] --> B{hasCategories?} + + B -->|false| C[WHERE 0=1] + C --> D[Пустой результат] + + B -->|true| E[WikiArticle::find] + E --> F[ActiveDataProvider] + + F --> G[Фильтры] + G --> H[id, category_id - точное] + G --> I[slug, title - ilike] + G --> J[content - ilike] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new WikiArticleSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по заголовку +```php +$searchModel = new WikiArticleSearch(); +$dataProvider = $searchModel->search([ + 'WikiArticleSearch' => [ + 'title' => 'Инструкция', + ] +]); +``` + +### Поиск по категории +```php +$searchModel = new WikiArticleSearch(); +$dataProvider = $searchModel->search([ + 'WikiArticleSearch' => [ + 'category_id' => 5, + ] +]); +``` + +### Поиск по slug +```php +$searchModel = new WikiArticleSearch(); +$dataProvider = $searchModel->search([ + 'WikiArticleSearch' => [ + 'slug' => 'kak-oformit-zakaz', + ] +]); +``` + +### Полнотекстовый поиск в контенте +```php +$searchModel = new WikiArticleSearch(); +$dataProvider = $searchModel->search([ + 'WikiArticleSearch' => [ + 'content' => 'букет', + ] +]); +``` + +### Поиск статей автора +```php +$searchModel = new WikiArticleSearch(); +$dataProvider = $searchModel->search([ + 'WikiArticleSearch' => [ + 'created_by' => Yii::$app->user->id, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'slug', + 'title', + [ + 'attribute' => 'category_id', + 'value' => 'category.name', + ], + [ + 'attribute' => 'created_by', + 'value' => 'creator.name', + ], + 'created_at:datetime', + 'updated_at:datetime', + ], +]) ?> +``` + +## Связанные модели + +- [WikiArticle](./WikiArticle.md) — базовая модель статей +- [WikiCategory](./WikiCategory.md) — категории Wiki +- [Admin](./Admin.md) — авторы статей + +## Особенности реализации + +1. **Проверка категорий**: hasCategories() перед поиском +2. **Пустой результат**: WHERE 0=1 при отсутствии категорий +3. **SEO-friendly slug**: Человекочитаемый URL статьи +4. **ilike поиск**: Регистронезависимый поиск по всем текстовым полям +5. **Аудит изменений**: created_by, updated_by для отслеживания авторов +6. **ilike на датах**: Нестандартное использование ilike для created_at/updated_at diff --git a/erp24/docs/models/WikiCategory.md b/erp24/docs/models/WikiCategory.md new file mode 100644 index 00000000..5c79b1f2 --- /dev/null +++ b/erp24/docs/models/WikiCategory.md @@ -0,0 +1,482 @@ +# Class: WikiCategory + +## Mindmap: Модель WikiCategory + +```mermaid +mindmap + root((WikiCategory)) + Идентификация + id PK + slug ЧПУ уникальный + title название + parent_id FK родитель + Содержимое + description описание + allow_group_id разрешённые группы + Аудит + created_at дата создания + created_by FK автор + updated_at дата обновления + updated_by FK редактор + Связи + WikiCategory parent родитель + WikiCategory children дочерние + WikiArticle articles статьи + Admin автор + Admin редактор + Behaviors + TimestampBehavior авто даты + BlameableBehavior авто авторы + Методы + getParentCategories цепочка родителей +``` + +--- + +## Назначение + +Модель категории Wiki-базы знаний в системе ERP24. Организует статьи в иерархическую структуру через вложенность (parent_id). Поддерживает многоуровневую вложенность, ограничение доступа по группам сотрудников, автоматическую генерацию ЧПУ. + +Используется для структурирования корпоративной документации по темам, разделам, проектам. Категории могут быть корневыми (parent_id = NULL) или вложенными. + +--- + +## Пространство имён + +`yii_app\records` + +--- + +## Родительский класс + +`yii\db\ActiveRecord` + +--- + +## Таблица базы данных + +`wiki_category` + +--- + +## Основные свойства + +### Идентификация + +| Имя | Тип | Описание | +|-----|-----|----------| +| `id` | int | **PK** ID категории | +| `slug` | string(255) | **ЧПУ** (человекопонятный URL, уникальный) | +| `title` | string(255) | **Название категории** (обязательное) | +| `parent_id` | int | **FK** Родительская категория (NULL для корневых) | + +### Содержимое + +| Имя | Тип | Описание | +|-----|-----|----------| +| `description` | text | Описание категории | +| `allow_group_id` | string(255) | Разрешённые группы сотрудников (через запятую) | + +### Аудит + +| Имя | Тип | Описание | +|-----|-----|----------| +| `created_at` | string(255) | Дата создания (автоматически) | +| `created_by` | int | **FK** Кем создано (автоматически) | +| `updated_at` | string(255) | Дата обновления (автоматически) | +| `updated_by` | int | **FK** Кем обновлено (автоматически) | + +--- + +## Behaviors + +### TimestampBehavior + +**Конфигурация:** +```php +[ + 'class' => TimestampBehavior::class, + 'value' => function () { + return date('Y-m-d H:i:s'); + }, +] +``` + +### BlameableBehavior + +**Конфигурация:** +```php +BlameableBehavior::class +``` + +--- + +## Отношения (Relations) + +### getParent() + +**Тип:** `hasOne` +**Модель:** `WikiCategory` (self) +**Ключ:** `['id' => 'parent_id']` +**Описание:** Родительская категория + +**Логика:** +Self-relation для получения родительской категории. Используется для построения breadcrumbs, навигации вверх по иерархии. + +**Вызовы сторонних методов:** +- `WikiCategory::hasOne()` - self-relation + +**Пример:** +```php +$category = WikiCategory::findOne($id); +if ($category->parent) { + echo "Родитель: {$category->parent->title}\n"; +} else { + echo "Это корневая категория\n"; +} +``` + +--- + +### getWikiArticles() + +**Тип:** `hasMany` +**Модель:** `WikiArticle` +**Ключ:** `['category_id' => 'id']` +**Описание:** Статьи категории + +**Логика:** +Связь один-ко-многим с моделью WikiArticle. Возвращает все статьи, принадлежащие данной категории. + +**Вызовы сторонних методов:** +- `WikiArticle::hasMany()` - связь с таблицей wiki_article + +**Пример:** +```php +$category = WikiCategory::findOne($id); +echo "Категория: {$category->title}\n"; +echo "Статей: " . count($category->wikiArticles) . "\n"; + +foreach ($category->wikiArticles as $article) { + echo "- {$article->title}\n"; +} +``` + +--- + +### getWikiCategories() + +**Тип:** `hasMany` +**Модель:** `WikiCategory` (self) +**Ключ:** `['parent_id' => 'id']` +**Описание:** Дочерние категории + +**Логика:** +Self-relation для получения подкатегорий. Используется для построения дерева категорий, навигации вниз по иерархии. + +**Вызовы сторонних методов:** +- `WikiCategory::hasMany()` - self-relation + +**Пример:** +```php +$category = WikiCategory::findOne($id); +$children = $category->wikiCategories; + +if (count($children) > 0) { + echo "Подкатегории:\n"; + foreach ($children as $child) { + echo "- {$child->title}\n"; + } +} +``` + +--- + +## Методы + +### beforeValidate() + +**Тип:** `protected` +**Параметры:** нет +**Возвращает:** `bool` + +**Описание:** +Автоматически генерирует slug из title при создании новой категории, если slug не был указан вручную. + +**Логика работы:** +1. Проверяет $this->isNewRecord +2. Проверяет отсутствие slug +3. Генерирует slug через Inflector::slug($this->title) +4. Вызывает parent::beforeValidate() + +**Вызовы сторонних методов:** +- `Inflector::slug()` - транслитерация и очистка строки +- `parent::beforeValidate()` - валидация родителя + +**Пример:** +```php +$category = new WikiCategory(); +$category->title = 'Работа с клиентами'; +// slug будет автоматически: 'rabota-s-klientami' +$category->save(); +``` + +--- + +### getParentCategories() + +**Тип:** `public` +**Параметры:** нет +**Возвращает:** `array` — массив категорий от корня до текущей + +**Описание:** +Возвращает цепочку родительских категорий от корневой до текущей. Используется для построения breadcrumbs (хлебных крошек) в интерфейсе. + +**Логика работы:** +1. Инициализирует пустой массив $categories +2. Присваивает текущую категорию в переменную $category +3. Запускает цикл while, пока $category не станет NULL +4. В цикле добавляет текущую категорию в начало массива через array_unshift() +5. Переходит к родительской категории через $this->parent +6. ВНИМАНИЕ: В коде ошибка — используется $this->parent вместо $category->parent +7. Из-за ошибки метод работает только для одного уровня вложенности +8. Возвращает массив категорий + +**Вызовы сторонних методов:** +- `array_unshift()` - добавление элемента в начало массива +- `$this->parent` - получение родительской категории + +**Пример (с учётом бага):** +```php +$category = WikiCategory::findOne($id); +$breadcrumbs = $category->getParentCategories(); + +foreach ($breadcrumbs as $cat) { + echo "{$cat->title} > "; +} +echo "\n"; + +// ИСПРАВЛЕННАЯ ВЕРСИЯ (для документации): +// public function getParentCategories() +// { +// $categories = []; +// $category = $this; +// while ($category !== null) { +// array_unshift($categories, $category); +// $category = $category->parent; // ← Исправление +// } +// return $categories; +// } +``` + +--- + +## Правила валидации + +### Обязательные поля +```php +['title'] // required +``` + +### Целочисленные поля +```php +['parent_id', 'created_by', 'updated_by'] // integer, default: null +``` + +### Строковые поля +```php +['slug', 'title', 'created_at', 'updated_at', 'allow_group_id'] // string, max: 255 +['description'] // text (без ограничения) +``` + +### Безопасные поля +```php +['created_at', 'updated_at', 'slug', 'created_at', 'created_by'] // safe +``` + +### Уникальность +```php +['slug'] // unique +``` + +### Внешние ключи +```php +['parent_id'] // exist в WikiCategory (self-reference) +``` + +--- + +## Примеры использования + +### Создание корневой категории + +```php +use yii_app\records\WikiCategory; + +$category = new WikiCategory(); +$category->title = 'Документация'; +$category->parent_id = null; // Корневая категория +$category->description = 'Все документы компании'; +// slug, created_at, created_by заполнятся автоматически + +if ($category->save()) { + echo "Категория создана с slug: {$category->slug}"; +} +``` + +--- + +### Создание подкатегории + +```php +$subcategory = new WikiCategory(); +$subcategory->title = 'Инструкции'; +$subcategory->parent_id = $parentCategoryId; +$subcategory->description = 'Пошаговые инструкции'; +$subcategory->save(); +``` + +--- + +### Получение дерева категорий + +```php +// Корневые категории +$rootCategories = WikiCategory::find() + ->where(['parent_id' => null]) + ->orderBy(['title' => SORT_ASC]) + ->all(); + +foreach ($rootCategories as $root) { + echo $root->title . "\n"; + + // Подкатегории первого уровня + foreach ($root->wikiCategories as $child) { + echo " └─ {$child->title}\n"; + + // Подкатегории второго уровня + foreach ($child->wikiCategories as $grandChild) { + echo " └─ {$grandChild->title}\n"; + } + } +} +``` + +--- + +### Получение всех статей категории и подкатегорий + +```php +function getAllArticles($category) { + $articles = $category->wikiArticles; + + // Рекурсивно добавляем статьи из подкатегорий + foreach ($category->wikiCategories as $child) { + $articles = array_merge($articles, getAllArticles($child)); + } + + return $articles; +} + +$category = WikiCategory::findOne($id); +$allArticles = getAllArticles($category); + +echo "Всего статей в категории и подкатегориях: " . count($allArticles); +``` + +--- + +### Breadcrumbs (хлебные крошки) + +```php +$category = WikiCategory::findOne($id); +$breadcrumbs = []; + +$current = $category; +while ($current !== null) { + array_unshift($breadcrumbs, $current); + $current = $current->parent; +} + +foreach ($breadcrumbs as $crumb) { + echo "{$crumb->title} > "; +} +``` + +--- + +### Ограничение доступа по группам + +```php +$category = WikiCategory::findOne($id); + +if ($category->allow_group_id) { + $allowedGroups = explode(',', $category->allow_group_id); + $userGroupId = Yii::$app->user->identity->group_id; + + if (!in_array($userGroupId, $allowedGroups)) { + throw new ForbiddenHttpException('Доступ запрещён'); + } +} +``` + +--- + +## Диаграмма отношений + +```mermaid +erDiagram + WikiCategory ||--o| WikiCategory : "parent" + WikiCategory ||--o{ WikiCategory : "children" + WikiCategory ||--o{ WikiArticle : "has articles" + WikiCategory ||--o| Admin : "created by" + WikiCategory ||--o| Admin : "updated by" + + WikiCategory { + int id PK + string slug UK + string title + int parent_id FK + text description + string allow_group_id + string created_at + int created_by FK + string updated_at + int updated_by FK + } + + WikiArticle { + int id PK + string slug UK + string title + int category_id FK + text content + } + + Admin { + int id PK + string name + } +``` + +--- + +## Замечания + +1. **slug** — автоматически генерируется из title +2. **parent_id** — NULL для корневых категорий +3. **allow_group_id** — список ID через запятую, например: "1,5,7" +4. Поддерживает неограниченную вложенность +5. **getParentCategories()** имеет баг в исходном коде (использует $this->parent вместо $category->parent) +6. При удалении категории нужно обработать дочерние категории и статьи +7. Self-relations используются для построения дерева +8. **created_at**, **updated_at** заполняются автоматически +9. **created_by**, **updated_by** заполняются автоматически +10. Используется совместно с WikiArticle для построения базы знаний + +--- + +## Связанные документы + +- [WikiArticle.md](./WikiArticle.md) — статьи Wiki +- [Admin.md](./Admin.md) — модель сотрудников diff --git a/erp24/docs/models/WriteOffs.md b/erp24/docs/models/WriteOffs.md new file mode 100644 index 00000000..c4f0c5d5 --- /dev/null +++ b/erp24/docs/models/WriteOffs.md @@ -0,0 +1,668 @@ +# Модель WriteOffs + + +## Mindmap + +```mermaid +mindmap + root((WriteOffs)) + Таблица БД + write_offs + Свойства + id + string + status_id + integer + write_downs + string + store_id + string + number + string + date + string + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WriteOffs` представляет документы списания товаров в старой системе учёта. Используется для хранения информации о списаниях по различным причинам (брак, возврат и т.д.) и связи с внешней системой 1С. + +**Файл модели:** `erp24/records/WriteOffs.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `write_offs` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | VARCHAR(36) | Первичный ключ (GUID документа) | +| `status_id` | INTEGER | Статус документа | +| `write_downs` | TEXT | Дополнительная информация о списаниях | +| `store_id` | VARCHAR(36) | GUID магазина | +| `number` | VARCHAR(255) | Номер документа списания | +| `date` | TIMESTAMP | Дата документа списания | +| `based_on` | TEXT | Документ-основание для списания | +| `type` | VARCHAR(255) | Тип списания (строковое значение) | +| `cause` | TEXT | Причина списания | +| `comment` | TEXT | Комментарий к документу | +| `items` | TEXT | JSON с позициями товаров | +| `summ` | NUMERIC | Сумма списания (закупочная цена) | +| `summ_retail` | NUMERIC | Сумма в розничных ценах | +| `type_id` | INTEGER | ID типа списания | +| `type_guid` | VARCHAR(255) | GUID типа списания из 1С | + +--- + +## Константы + +Модель не содержит явных констант статусов или типов, так как это старая система. + +--- + +## Методы модели + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех основных полей. + +#### `getId(): string` + +Возвращает GUID документа списания. + +**Параметры:** нет + +**Возвращает:** `string` - GUID документа + +**Логика:** Возвращает значение поля `id`. + +**Пример:** +```php +$writeOff = WriteOffs::findOne($id); +$guid = $writeOff->getId(); +``` + +#### `setId(string $id): void` + +Устанавливает GUID документа списания. + +**Параметры:** +- `$id` (string) - GUID документа + +**Возвращает:** void + +**Логика:** Присваивает значение полю `id`. + +**Пример:** +```php +$writeOff = new WriteOffs(); +$writeOff->setId('550e8400-e29b-41d4-a716-446655440000'); +``` + +#### `getStoreId(): string` + +Возвращает GUID магазина. + +**Параметры:** нет + +**Возвращает:** `string` - GUID магазина + +**Логика:** Возвращает значение поля `store_id`. + +#### `setStoreId(string $store_id): void` + +Устанавливает GUID магазина. + +**Параметры:** +- `$store_id` (string) - GUID магазина + +**Возвращает:** void + +**Логика:** Присваивает значение полю `store_id`. + +#### `getNumber(): string` + +Возвращает номер документа списания. + +**Параметры:** нет + +**Возвращает:** `string` - номер документа + +**Логика:** Возвращает значение поля `number`. + +#### `setNumber(string $number): void` + +Устанавливает номер документа списания. + +**Параметры:** +- `$number` (string) - номер документа + +**Возвращает:** void + +**Логика:** Присваивает значение полю `number`. + +#### `getDate(): string` + +Возвращает дату документа списания. + +**Параметры:** нет + +**Возвращает:** `string` - дата документа + +**Логика:** Возвращает значение поля `date`. + +#### `setDate(string $date): void` + +Устанавливает дату документа списания. + +**Параметры:** +- `$date` (string) - дата документа + +**Возвращает:** void + +**Логика:** Присваивает значение полю `date`. + +#### `getBasedOn(): string` + +Возвращает документ-основание. + +**Параметры:** нет + +**Возвращает:** `string` - документ-основание + +**Логика:** Возвращает значение поля `based_on`. + +#### `setBasedOn(string $based_on): void` + +Устанавливает документ-основание. + +**Параметры:** +- `$based_on` (string) - документ-основание + +**Возвращает:** void + +**Логика:** Присваивает значение полю `based_on`. + +#### `getType(): string` + +Возвращает тип списания. + +**Параметры:** нет + +**Возвращает:** `string` - тип списания + +**Логика:** Возвращает значение поля `type`. + +#### `setType(string $type): void` + +Устанавливает тип списания. + +**Параметры:** +- `$type` (string) - тип списания + +**Возвращает:** void + +**Логика:** Присваивает значение полю `type`. + +#### `getCause(): string` + +Возвращает причину списания. + +**Параметры:** нет + +**Возвращает:** `string` - причина списания + +**Логика:** Возвращает значение поля `cause`. + +#### `setCause(string $cause): void` + +Устанавливает причину списания. + +**Параметры:** +- `$cause` (string) - причина списания + +**Возвращает:** void + +**Логика:** Присваивает значение полю `cause`. + +#### `getComment(): string` + +Возвращает комментарий к документу. + +**Параметры:** нет + +**Возвращает:** `string` - комментарий + +**Логика:** Возвращает значение поля `comment`. + +#### `setComment(string $comment): void` + +Устанавливает комментарий к документу. + +**Параметры:** +- `$comment` (string) - комментарий + +**Возвращает:** void + +**Логика:** Присваивает значение полю `comment`. + +#### `getItems(): string` + +Возвращает строку с позициями товаров (обычно JSON). + +**Параметры:** нет + +**Возвращает:** `string` - позиции товаров + +**Логика:** Возвращает значение поля `items`. + +#### `setItems(string $items): void` + +Устанавливает позиции товаров. + +**Параметры:** +- `$items` (string) - позиции товаров (JSON) + +**Возвращает:** void + +**Логика:** Присваивает значение полю `items`. + +#### `getSumm(): float` + +Возвращает сумму списания в закупочных ценах. + +**Параметры:** нет + +**Возвращает:** `float` - сумма списания + +**Логика:** Возвращает значение поля `summ`. + +#### `setSumm(float $summ): void` + +Устанавливает сумму списания. + +**Параметры:** +- `$summ` (float) - сумма списания + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ`. + +#### `getSummRetail(): float` + +Возвращает сумму списания в розничных ценах. + +**Параметры:** нет + +**Возвращает:** `float` - сумма в розничных ценах + +**Логика:** Возвращает значение поля `summ_retail`. + +#### `setSummRetail(float $summ_retail): void` + +Устанавливает сумму в розничных ценах. + +**Параметры:** +- `$summ_retail` (float) - сумма в розничных ценах + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ_retail`. + +--- + +### Статические методы + +#### `getWriteOffByStore($dateFrom, $dateTo, $storeId, $type = 'Брак'): array` + +Получает сумму списаний по магазину за период. + +**Параметры:** +- `$dateFrom` (string) - дата начала периода +- `$dateTo` (string) - дата окончания периода +- `$storeId` (string) - GUID магазина +- `$type` (string) - тип списания (по умолчанию "Брак") + +**Возвращает:** `array` - массив с суммой списаний + +**Логика:** +1. Определяет поле для суммирования: до 2022-12-01 используется `summ`, после - `summ_retail` +2. Формирует SQL-запрос с группировкой по `store_id` +3. Применяет фильтры по магазину, типу и датам +4. Использует `DateHelper::getDateTimeStartDay()` и `DateHelper::getDateTimeEndDay()` для точных временных рамок +5. Возвращает агрегированную сумму списаний + +**Вызовы сторонних методов:** +- `DateHelper::getDateTimeStartDay()` - получение начала дня +- `DateHelper::getDateTimeEndDay()` - получение конца дня + +**Пример:** +```php +$writeOffs = WriteOffs::getWriteOffByStore('2024-01-01', '2024-01-31', 'store-guid-123', 'Брак'); +// [['store_id' => 'store-guid-123', 'summ' => 15000.00]] +``` + +#### `setRetailPriceWriteOff($dateFrom = '2022-12-01 00:00:00', $dateTo = null, $type = 'Брак'): array` + +Устанавливает розничные цены для списаний и их товаров за период. + +**Параметры:** +- `$dateFrom` (string) - дата начала периода (по умолчанию '2022-12-01 00:00:00') +- `$dateTo` (string|null) - дата окончания периода (по умолчанию текущая дата) +- `$type` (string) - тип списания (по умолчанию "Брак") + +**Возвращает:** `array` - массив со счётчиками обновлённых записей + +**Логика:** +1. Если `$dateTo` не задан, устанавливает текущую дату +2. Вызывает `setWriteOffProductsWithoutRetailPrice()` для установки розничных цен товаров +3. Вызывает `setWriteOffWithoutRetailPrice()` для установки розничных цен документов +4. Возвращает массив с количеством обновлённых записей: + - `productsRetailPriceSetCounter` - количество обновлённых товаров + - `writeOffRetailPriceSetCounter` - количество обновлённых документов + +**Вызовы сторонних методов:** +- `setWriteOffProductsWithoutRetailPrice()` - установка розничных цен для товаров +- `setWriteOffWithoutRetailPrice()` - установка розничных цен для документов + +**Пример:** +```php +$result = WriteOffs::setRetailPriceWriteOff('2022-12-01', '2023-12-31'); +// ['productsRetailPriceSetCounter' => 150, 'writeOffRetailPriceSetCounter' => 50] +``` + +#### `setWriteOffWithoutRetailPrice($dateFrom, $dateTo, $type): int` + +Устанавливает розничные цены для документов списаний без них. + +**Параметры:** +- `$dateFrom` (string) - дата начала периода +- `$dateTo` (string) - дата окончания периода +- `$type` (string) - тип списания + +**Возвращает:** `int` - количество обновлённых документов + +**Логика:** +1. Если `$dateTo` не задан, устанавливает текущую дату +2. Находит все документы с `summ_retail = 0` в заданном периоде +3. Для каждого документа: + - Пытается получить сумму розничных цен из `WriteOffsProducts` (запрос с `SUM(summ_retail)`) + - Если розничная сумма не найдена, берёт сумму закупочных цен `SUM(summ)` + - Если и это не найдено, использует `summ` самого документа +4. Валидирует и сохраняет документ с обновлённой розничной суммой +5. Увеличивает счётчик обновлённых записей + +**Вызовы сторонних методов:** +- `DateHelper::getDateTimeStartDay()` - получение начала дня +- `DateHelper::getDateTimeEndDay()` - получение конца дня +- `WriteOffsProducts::find()` - поиск товаров списания +- `ArrayHelper::getValue()` - безопасное получение значения из массива + +**Пример:** +```php +$updatedCount = WriteOffs::setWriteOffWithoutRetailPrice('2022-12-01', '2023-12-31', 'Брак'); +// 25 +``` + +#### `setWriteOffProductsWithoutRetailPrice($dateFrom, $dateTo): int` + +Устанавливает розничные цены для товаров списаний без них. + +**Параметры:** +- `$dateFrom` (string) - дата начала периода +- `$dateTo` (string) - дата окончания периода + +**Возвращает:** `int` - количество обновлённых позиций товаров + +**Логика:** +1. Находит все товары списаний с `price_retail = 0` или `summ_retail = 0` в заданном периоде +2. Для каждого товара: + - Получает `product_id`, `write_offs_id`, `quantity` и дату списания + - Вызывает `getPriceDynamic()` для получения розничной цены на дату списания + - Если цена найдена в динамике цен: + - Устанавливает `price_retail` из динамики + - Рассчитывает `summ_retail = price_retail * quantity` + - Если цена не найдена: + - Использует закупочную цену `price` + - Рассчитывает `summ_retail = price * quantity` +3. Валидирует и сохраняет товар с обновлённой розничной ценой +4. Увеличивает счётчик обновлённых записей + +**Вызовы сторонних методов:** +- `DateHelper::getDateTimeStartDay()` - получение начала дня +- `DateHelper::getDateTimeEndDay()` - получение конца дня +- `getPriceDynamic()` - получение динамической розничной цены +- `InfoLogService::setInfoLog()` - логирование ошибок + +**Пример:** +```php +$updatedCount = WriteOffs::setWriteOffProductsWithoutRetailPrice('2022-12-01', '2023-12-31'); +// 150 +``` + +#### `getPriceDynamic($productId, $writeOffDate, $regionId = 52): float|null` + +Получает розничную цену товара на определённую дату из справочника динамики цен. + +**Параметры:** +- `$productId` (string) - GUID товара +- `$writeOffDate` (string) - дата списания +- `$regionId` (int) - ID региона (по умолчанию 52 - Нижегородская область) + +**Возвращает:** `float|null` - розничная цена или null, если не найдена + +**Логика:** +1. Ищет в таблице `PricesDynamic` запись по условиям: + - `region_id = $regionId` + - `product_id = $productId` + - `date_from <= $writeOffDate` + - `date_to >= $writeOffDate` +2. Выбирает только поле `price` +3. Возвращает скалярное значение цены или null + +**Вызовы сторонних методов:** +- `PricesDynamic::find()` - поиск в справочнике динамики цен + +**Пример:** +```php +$price = WriteOffs::getPriceDynamic('product-guid-123', '2023-06-15', 52); +// 1250.00 +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `id` | Обязательное, уникальное, макс. 36 символов | +| `store_id` | Обязательное, макс. 36 символов | +| `number` | Обязательное, макс. 255 символов | +| `date` | Обязательное | +| `type` | Обязательное, макс. 255 символов | +| `items` | Обязательное, текстовое поле | +| `summ` | Обязательное, числовое | +| `summ_retail` | Безопасное, числовое | +| `based_on` | Безопасное, текстовое | +| `cause` | Безопасное, текстовое | +| `comment` | Текстовое | +| `status_id` | Числовое | +| `type_id` | Числовое | +| `type_guid` | Макс. 255 символов | + +--- + +## Связи (Relations) + +Модель не содержит явных связей через методы `hasOne` или `hasMany`, но связывается с другими моделями через GUID: + +- **WriteOffsProducts** - позиции товаров списания (связь через `write_offs_id`) +- **CityStore** - магазин (связь через `store_id`) +- **Products1c** - товары (связь через поле `items`) +- **PricesDynamic** - динамика цен (используется в методе `getPriceDynamic()`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + write_offs ||--o{ write_offs_products : "has_many" + write_offs }o--|| city_store : "belongs_to" + write_offs_products }o--|| products_1c : "product" + write_offs }o--o| prices_dynamic : "dynamic_pricing" + + write_offs { + string id PK + integer status_id + string store_id FK + string number + timestamp date + string type + text items + numeric summ + numeric summ_retail + integer type_id + string type_guid + } + + write_offs_products { + string write_offs_id FK + string product_id FK + integer quantity + numeric price + numeric summ + numeric price_retail + numeric summ_retail + } +``` + +--- + +## Потоки данных + +### Создание документа списания + +```mermaid +sequenceDiagram + participant User + participant WriteOffs + participant WriteOffsProducts + participant Store + participant Product + + User->>WriteOffs: Создание нового списания + WriteOffs->>Store: Проверка существования магазина + WriteOffs->>WriteOffs: Генерация ID и номера + WriteOffs->>WriteOffs: Сохранение документа + User->>WriteOffsProducts: Добавление товаров + WriteOffsProducts->>Product: Проверка товаров + WriteOffsProducts->>WriteOffs: Пересчёт суммы + WriteOffs->>User: Документ создан +``` + +### Обновление розничных цен + +```mermaid +sequenceDiagram + participant System + participant WriteOffs + participant WriteOffsProducts + participant PricesDynamic + + System->>WriteOffs: setRetailPriceWriteOff() + WriteOffs->>WriteOffsProducts: setWriteOffProductsWithoutRetailPrice() + WriteOffsProducts->>PricesDynamic: getPriceDynamic() + PricesDynamic-->>WriteOffsProducts: Розничная цена + WriteOffsProducts->>WriteOffsProducts: Обновление price_retail, summ_retail + WriteOffsProducts-->>WriteOffs: Количество обновлённых товаров + WriteOffs->>WriteOffs: setWriteOffWithoutRetailPrice() + WriteOffs->>WriteOffsProducts: Получение SUM(summ_retail) + WriteOffsProducts-->>WriteOffs: Сумма + WriteOffs->>WriteOffs: Обновление summ_retail + WriteOffs-->>System: Результат обновления +``` + +--- + +## Примеры использования + +### Получение списаний по магазину за период + +```php +$storeId = '550e8400-e29b-41d4-a716-446655440000'; +$writeOffs = WriteOffs::getWriteOffByStore('2024-01-01', '2024-01-31', $storeId, 'Брак'); + +foreach ($writeOffs as $writeOff) { + echo "Магазин: {$writeOff['store_id']}, Сумма: {$writeOff['summ']}\n"; +} +``` + +### Обновление розничных цен для старых списаний + +```php +$result = WriteOffs::setRetailPriceWriteOff('2022-12-01', '2023-12-31'); +echo "Обновлено товаров: {$result['productsRetailPriceSetCounter']}\n"; +echo "Обновлено документов: {$result['writeOffRetailPriceSetCounter']}\n"; +``` + +### Создание нового документа списания + +```php +$writeOff = new WriteOffs(); +$writeOff->setId(DataHelper::createGuidMy()); +$writeOff->setStoreId('550e8400-e29b-41d4-a716-446655440000'); +$writeOff->setNumber('WO-2024-001'); +$writeOff->setDate(date('Y-m-d H:i:s')); +$writeOff->setType('Брак'); +$writeOff->setCause('Повреждение упаковки'); +$writeOff->setComment('Товар повреждён при выкладке'); +$writeOff->setItems(json_encode($items)); +$writeOff->setSumm(5000.00); +$writeOff->setSummRetail(7500.00); + +if ($writeOff->validate()) { + $writeOff->save(); +} +``` + +### Поиск списаний определённого типа + +```php +$writeOffs = WriteOffs::find() + ->where(['type' => 'Брак']) + ->andWhere(['>=', 'date', '2024-01-01']) + ->andWhere(['<=', 'date', '2024-12-31']) + ->all(); +``` + +--- + +## Связанные модели + +- **[WriteOffsProducts](./WriteOffsProducts.md)** — позиции товаров списания (старая система) +- **[WriteOffsErp](./WriteOffsErp.md)** — списания в новой системе ERP +- **[WriteOffsProductsErp](./WriteOffsProductsErp.md)** — товары списаний ERP +- **[CityStore](./CityStore.md)** — справочник магазинов +- **[Products1c](./Products1c.md)** — справочник товаров из 1С +- **PricesDynamic** — динамика цен товаров + +--- + +## Примечания + +1. Это **старая система** списаний, для новых документов используется модель **WriteOffsErp** +2. Модель использует GUID в качестве первичного ключа +3. Поддерживает хранение товаров в виде JSON в поле `items` +4. С декабря 2022 года используются розничные цены (`summ_retail`) +5. Модель содержит методы миграции данных для установки розничных цен +6. Связана с системой 1С через GUID магазинов и типов списаний + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WriteOffsErp.md b/erp24/docs/models/WriteOffsErp.md new file mode 100644 index 00000000..3fc26a2e --- /dev/null +++ b/erp24/docs/models/WriteOffsErp.md @@ -0,0 +1,430 @@ +# Модель WriteOffsErp + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsErp)) + Таблица БД + write_offs_erp + Свойства + id + int + guid + string + status + int + active + int + created_admin_id + int + store_id + int + Связи + WriteOffsProductsErps + 1:N WriteOffsProductsErp + ProductClass + 1:1 ProductsClass + CityStore + 1:1 CityStore + Product + 1:1 Products1c + ProductsImages + 1:N WriteOffsProductsErp + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WriteOffsErp` представляет документы списания товаров. Является центральной моделью для учёта брака, возвратов и других типов списаний товаров с интеграцией в 1С. + +**Файл модели:** `erp24/records/WriteOffsErp.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `write_offs_erp` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `guid` | VARCHAR(100) | Уникальный GUID документа для 1С | +| `status` | INTEGER | Статус документа (см. константы) | +| `active` | INTEGER | Активность записи (0/1) | +| `created_admin_id` | INTEGER | ID сотрудника, создавшего документ | +| `updated_admin_id` | INTEGER | ID сотрудника, изменившего документ | +| `confirm_admin_id` | INTEGER | ID сотрудника, подтвердившего документ | +| `deleted_admin_id` | INTEGER | ID сотрудника, удалившего документ | +| `store_id` | INTEGER | ID магазина в ERP | +| `store_guid` | VARCHAR(100) | GUID магазина из 1С | +| `cause_id` | INTEGER | ID причины списания | +| `cause_group_id` | INTEGER | ID группы причины списания | +| `number` | VARCHAR(100) | Номер документа списания | +| `number_1c` | VARCHAR(100) | Номер документа в 1С | +| `date` | VARCHAR(100) | Дата документа | +| `based_on` | TEXT | Документ-основание | +| `write_offs_type` | TEXT | Тип списания | +| `comment` | TEXT | Комментарий к списанию | +| `error_text` | TEXT | Текст ошибки от 1С | +| `summ` | NUMERIC | Сумма в закупочных ценах | +| `summ_retail` | NUMERIC | Сумма в розничных ценах | +| `quantity` | NUMERIC | Общее количество товаров | +| `type_id` | INTEGER | ID типа списания | +| `type_guid` | VARCHAR(100) | GUID типа списания | +| `created_at` | VARCHAR(100) | Дата создания | +| `updated_at` | VARCHAR(100) | Дата изменения | +| `deleted_at` | VARCHAR(100) | Дата удаления | +| `confirm_at` | VARCHAR(100) | Дата подтверждения | +| `send_at` | VARCHAR(100) | Дата отправки в 1С | +| `attachment_cleared` | INTEGER | Флаг очистки вложений (0/1) | +| `attachment_cleared_at` | VARCHAR(100) | Дата очистки вложений | + +--- + +## Константы статусов + +```php +const STATUS_CREATED = 1; // Создан (ожидает согласования) +const STATUS_CONFIRM = 2; // Согласован (одобрен) +const STATUS_SEND = 3; // Отправлен в 1С +const STATUS_CREATED_1C = 4; // Создан в 1С (успешно) +const STATUS_DISABLE = 5; // Отклонён +const STATUS_ERROR_1C = 8; // Ошибка загрузки в 1С +``` + +### Словарь статусов + +```php +public const STATUSES = [ + 1 => "Создан", + 2 => "Одобрен", + 3 => "Отправлен в 1С", + 4 => "Создан в 1С", + 5 => "Отклонен", + 8 => "Ошибка в 1С", +]; +``` + +--- + +## Константы типов списания + +```php +const WRITE_OFFS_TYPE_BRAK = "Брак"; +const WRITE_OFFS_TYPE_RETURN_KALUGA = "Возврат нереализованного товара (Калуга)"; +const WRITE_OFFS_TYPE_DELIVERY_BRAK = "Брак с поставки"; +const WRITE_OFFS_TYPE_DUE_TO_EQUIPMENT_FAILURE_BRAK = "Брак из-за поломки оборудования"; +const WRITE_OFFS_TYPE_RESORTING_DOES_NOT_COUNT_TOWARDS_COST = "Пересорт, не идет в затраты"; +``` + +--- + +## Методы модели + +### Геттеры и сеттеры + +Модель содержит полный набор геттеров и сеттеров для всех полей. Ключевые методы: + +#### Управление GUID + +```php +// Генерация нового GUID +$doc->setGuidCreated(); + +// Получение GUID +$guid = $doc->getGuid(); +``` + +#### Управление статусом + +```php +// Установка статуса "Создан" +$doc->setStatusCreated(); + +// Установка статуса "Подтверждён" +$doc->setStatusConfirm(); + +// Получение текущего статуса +$status = $doc->getStatus(); +``` + +#### Управление номером документа + +```php +// Генерация номера по умолчанию +$doc->setNumberDefault(); // "0000-{timestamp}" + +// Генерация номера при создании +$doc->setNumberCreated(); // "ЕРП_2025-12-11_10-30_{count}" + +// Генерация номера по фирме +$doc->setNumberCreatedByFirm(); // "{prefix}-000001" +``` + +#### Управление датами + +```php +$doc->setCreatedAt(); // Дата создания = now +$doc->setUpdatedAt(); // Дата изменения = now +$doc->setDeletedAt(); // Дата удаления = now +$doc->setConfirmAt(); // Дата подтверждения = now +$doc->setCreatedDate(); // Дата документа = now +``` + +### Методы работы с магазином + +```php +// Установка GUID магазина +$doc->setStoreGuidCreated(); + +// Получение GUID магазина из 1С по ID +$guid = $doc->get1cStoreGuid($storeId); +``` + +### Статические методы + +#### `getStatusDict(): array` + +Возвращает словарь статусов для отображения. + +```php +$statuses = WriteOffsErp::getStatusDict(); +// [1 => 'Ожидает согласования', 2 => 'Согласован', ...] +``` + +#### `newFromBase(WriteOffsErp $base, int $adminId): WriteOffsErp` + +Создаёт новый документ на основе существующего (копирование). + +```php +$newDoc = WriteOffsErp::newFromBase($existingDoc, $currentAdminId); +``` + +#### `recalcTotals(WriteOffsErp $doc): void` + +Пересчитывает суммы и количество документа по активным строкам. + +```php +WriteOffsErp::recalcTotals($doc); +``` + +#### `isManager(int $storeId): bool` + +Проверяет, является ли текущий пользователь менеджером для магазина. + +#### `isTestStore(int $storeId): bool` + +Проверяет, входит ли магазин в тестовую группу для новой системы списаний. + +--- + +## Методы работы с вложениями + +### `getAttachments(): array` + +Возвращает все вложения документа (изображения и видео). + +```php +$attachments = $doc->getAttachments(); +// [ +// ['type' => 'image', 'product_item_id' => 1, 'url' => '/images/...', 'name' => '...'], +// ['type' => 'video', 'product_item_id' => 2, 'url' => '/video/...', 'mime' => 'video/mp4'], +// ] +``` + +### `getAttachmentsOlderThanMonth(?string $borderDate = null): array` + +Возвращает вложения документов старше 2 месяцев (для очистки). + +```php +$oldAttachments = WriteOffsErp::getAttachmentsOlderThanMonth(); +``` + +### `getImagesList($imagesWriteOffsErp, $forWidget = true): array` + +Формирует список изображений для виджета или галереи. + +--- + +## Связи (Relations) + +### `getWriteOffsProductsErps()` + +Позиции (товары) документа списания. + +```php +$products = $doc->writeOffsProductsErps; // WriteOffsProductsErp[] +``` + +**Тип:** hasMany +**Связанная модель:** `WriteOffsProductsErp` +**FK:** `write_offs_erp_id` → `id` +**Условие:** `active_product = 1` +**Сортировка:** `num_row ASC` + +### `getCityStore()` + +Магазин списания. + +```php +$store = $doc->cityStore; // CityStore +``` + +**Тип:** hasOne +**Связанная модель:** `CityStore` +**FK:** `store_id` → `id` + +### `getCreatedAdmin()` + +Сотрудник, создавший документ. + +```php +$admin = $doc->createdAdmin; // Admin +``` + +### `getConfirmAdmin()` + +Сотрудник, подтвердивший документ. + +### `getImagesWriteOffsErp()` + +Изображения документа через связь с `ImageDocumentLink`. + +### `getProduct()` + +Продукт из 1С через строки документа. + +--- + +## Жизненный цикл документа + +```mermaid +stateDiagram-v2 + [*] --> STATUS_CREATED: Создание + STATUS_CREATED --> STATUS_CONFIRM: Согласование + STATUS_CREATED --> STATUS_DISABLE: Отклонение + STATUS_CONFIRM --> STATUS_SEND: Отправка в 1С + STATUS_SEND --> STATUS_CREATED_1C: Успешно создан + STATUS_SEND --> STATUS_ERROR_1C: Ошибка + STATUS_ERROR_1C --> STATUS_SEND: Повторная отправка + STATUS_CREATED_1C --> [*] + STATUS_DISABLE --> [*] +``` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + write_offs_erp ||--o{ write_offs_products_erp : "has_many" + write_offs_erp }o--|| city_store : "belongs_to" + write_offs_erp }o--|| admin : "created_by" + write_offs_erp }o--|| admin : "confirmed_by" + write_offs_erp }o--|| write_offs_erp_cause_dict : "cause" + write_offs_products_erp }o--|| products_1c : "product" + write_offs_products_erp ||--o{ image_document_link : "images" + + write_offs_erp { + int id PK + string guid UK + int status + int store_id FK + string number + float summ + float quantity + string write_offs_type + } +``` + +--- + +## Примеры использования + +### Создание нового документа списания + +```php +$doc = new WriteOffsErp(); +$doc->loadDefaultValues(); + +$doc->setGuidCreated(); +$doc->setNumberCreated(); +$doc->setStatusCreated(); +$doc->setCreatedDate(); +$doc->setStoreId($storeId); +$doc->setStoreGuidCreated(); +$doc->setCreatedAt(); +$doc->setCreatedAdminId(Yii::$app->user->id); +$doc->setQuantity(0); +$doc->setSumm(0); +$doc->write_offs_type = WriteOffsErp::WRITE_OFFS_TYPE_BRAK; + +if ($doc->save()) { + // Документ создан +} +``` + +### Подтверждение документа + +```php +$doc = WriteOffsErp::findOne($id); +$doc->setStatusConfirm(); +$doc->setConfirmAt(); +$doc->setConfirmAdminId(Yii::$app->user->id); +$doc->save(); +``` + +### Получение документов магазина + +```php +$documents = WriteOffsErp::find() + ->where(['store_id' => $storeId]) + ->andWhere(['active' => 1]) + ->orderBy(['created_at' => SORT_DESC]) + ->all(); +``` + +### Фильтрация по статусу + +```php +$pendingDocs = WriteOffsErp::find() + ->where(['status' => WriteOffsErp::STATUS_CREATED]) + ->all(); +``` + +--- + +## Валидация + +| Поле | Правило | +|------|---------| +| `guid` | Обязательное, уникальное, макс. 100 символов | +| `created_admin_id` | Обязательное, целое число | +| `store_id` | Обязательное, целое число | +| `store_guid` | Обязательное, макс. 100 символов | +| `number` | Обязательное, макс. 100 символов | +| `date` | Обязательное | +| `write_offs_type` | Обязательное | +| `quantity` | Обязательное при сохранении | +| `summ`, `summ_retail` | Число | + +--- + +## Связанные модели + +- **WriteOffsProductsErp** — позиции документа списания +- **WriteOffsErpCauseDict** — справочник причин списания +- **[CityStore](./CityStore.md)** — магазин +- **[Admin](./Admin.md)** — сотрудники (создатель, подтвердивший) +- **[Products1c](./Products1c.md)** — товары +- **ImageDocumentLink** — изображения документа +- **[Files](./Files.md)** — видео-вложения + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WriteOffsErpCauseDict.md b/erp24/docs/models/WriteOffsErpCauseDict.md new file mode 100644 index 00000000..68122a66 --- /dev/null +++ b/erp24/docs/models/WriteOffsErpCauseDict.md @@ -0,0 +1,222 @@ +# Модель WriteOffsErpCauseDict + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsErpCauseDict)) + Таблица БД + write_offs_erp_cause_dict + Свойства + id + int + name + string + status + int + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WriteOffsErpCauseDict` представляет справочник причин списания товаров. Поддерживает иерархическую структуру с группами и подпричинами, а также контроль доступа по группам пользователей. + +**Файл модели:** `erp24/records/WriteOffsErpCauseDict.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `write_offs_erp_cause_dict` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `name` | VARCHAR(100) | Название причины или группы | +| `parent_id` | INTEGER | ID родительской группы (NULL для групп верхнего уровня) | +| `status` | INTEGER | Статус записи (1 - активна, 0 - неактивна) | +| `access_group_id` | INTEGER | ID группы доступа (для ограничения видимости) | + +--- + +## Константы + +Модель не содержит констант. + +--- + +## Методы модели + +Модель содержит только стандартные методы ActiveRecord: + +### `tableName(): string` + +Возвращает название таблицы БД. + +**Возвращает:** `'write_offs_erp_cause_dict'` + +### `rules(): array` + +Правила валидации полей модели. + +**Возвращает:** массив правил валидации + +**Правила:** +- `name` - обязательное поле, макс. 100 символов +- `parent_id`, `status`, `access_group_id` - целые числа + +### `attributeLabels(): array` + +Метки полей для форм. + +**Возвращает:** массив меток + +--- + +## Структура справочника + +Справочник имеет двухуровневую иерархию: + +### Уровень 1: Группы причин +- `parent_id = NULL` +- Примеры: "Брак", "Просроченные", "Возврат" + +### Уровень 2: Конкретные причины +- `parent_id` указывает на группу +- Примеры: "Повреждение упаковки", "Производственный брак", "Истёк срок годности" + +--- + +## Диаграмма связей + +```mermaid +erDiagram + write_offs_erp_cause_dict ||--o{ write_offs_erp_cause_dict : "parent" + write_offs_erp_cause_dict ||--o{ write_offs_products_erp : "used_in" + + write_offs_erp_cause_dict { + int id PK + string name + int parent_id FK + int status + int access_group_id + } + + write_offs_products_erp { + int id PK + int cause_id FK + string product_id + float quantity + } +``` + +--- + +## Примеры использования + +### Получение всех активных групп + +```php +$groups = WriteOffsErpCauseDict::find() + ->where(['status' => 1]) + ->andWhere(['parent_id' => null]) + ->all(); + +foreach ($groups as $group) { + echo "Группа: {$group->name}\n"; +} +``` + +### Получение причин определённой группы + +```php +$causes = WriteOffsErpCauseDict::find() + ->where(['status' => 1, 'parent_id' => $groupId]) + ->all(); + +foreach ($causes as $cause) { + echo "- {$cause->name}\n"; +} +``` + +### Создание новой причины + +```php +$cause = new WriteOffsErpCauseDict(); +$cause->name = 'Повреждение при транспортировке'; +$cause->parent_id = 1; // ID группы "Брак" +$cause->status = 1; +$cause->access_group_id = null; + +if ($cause->save()) { + echo "Причина создана с ID: {$cause->id}\n"; +} +``` + +### Получение иерархической структуры + +```php +// Получить все группы +$groups = WriteOffsErpCauseDict::find() + ->where(['status' => 1, 'parent_id' => null]) + ->indexBy('id') + ->all(); + +// Получить все причины +$causes = WriteOffsErpCauseDict::find() + ->where(['status' => 1]) + ->andWhere(['IS NOT', 'parent_id', new \yii\db\Expression('null')]) + ->all(); + +// Сгруппировать причины по группам +$tree = []; +foreach ($causes as $cause) { + if (isset($groups[$cause->parent_id])) { + $groupName = $groups[$cause->parent_id]->name; + $tree[$groupName][$cause->id] = $cause->name; + } +} + +print_r($tree); +// [ +// 'Брак' => [1 => 'Повреждение упаковки', 2 => 'Производственный брак'], +// 'Просроченные' => [3 => 'Истёк срок годности'] +// ] +``` + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `name` | Обязательное, макс. 100 символов | +| `parent_id` | Целое число, необязательное | +| `status` | Целое число | +| `access_group_id` | Целое число, необязательное | + +--- + +## Связанные модели + +- **[WriteOffsProductsErp](./WriteOffsProductsErp.md)** — позиции списаний, использующие причины +- **[WriteOffsErp](./WriteOffsErp.md)** — документы списаний + +--- + +## Примечания + +1. Справочник имеет двухуровневую иерархию (группы → причины) +2. Поле `status` управляет видимостью записи (1 - активна, 0 - скрыта) +3. Поле `access_group_id` позволяет ограничить доступ к определённым причинам +4. Для групп верхнего уровня `parent_id = NULL` +5. Для конкретных причин `parent_id` указывает на ID группы +6. Используется в модели `WriteOffsProductsErp` для выбора причины списания + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WriteOffsErpCauseDictSearch.md b/erp24/docs/models/WriteOffsErpCauseDictSearch.md new file mode 100644 index 00000000..c60562b8 --- /dev/null +++ b/erp24/docs/models/WriteOffsErpCauseDictSearch.md @@ -0,0 +1,201 @@ +# Класс: WriteOffsErpCauseDictSearch + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsErpCauseDictSearch)) + Таблица БД + ActiveRecord + Наследование + extends WriteOffsErpCauseDict +``` + +## Назначение +Search-модель для поиска и фильтрации справочника причин списания в ERP24. Иерархический справочник с родительскими элементами, статусом и группой доступа. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`WriteOffsErpCauseDict` + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `parent_id`, `status`, `access_group_id` — integer +- `name` — safe + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params): ActiveDataProvider +**Описание:** Создаёт провайдер данных для поиска причин списания. + +**Параметры:** +- `$params` (array) — параметры поиска + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос WriteOffsErpCauseDict::find() +2. Оборачивает в ActiveDataProvider +3. Загружает параметры +4. Применяет фильтры: + - Точное совпадение: id, parent_id, status, access_group_id + - like: name + +## Диаграмма структуры + +```mermaid +erDiagram + WriteOffsErpCauseDict { + int id PK + int parent_id FK + varchar name + int status + int access_group_id FK + } + + AccessGroup { + int id PK + varchar name + } + + WriteOffsErpCauseDict ||--o{ WriteOffsErpCauseDict : "parent_id" + WriteOffsErpCauseDict }o--|| AccessGroup : "access_group_id" +``` + +## Диаграмма иерархии причин + +```mermaid +flowchart TD + A[Причины списания] --> B[Брак] + A --> C[Утилизация] + A --> D[Пересортица] + + B --> B1[Механические повреждения] + B --> B2[Заводской брак] + B --> B3[Порча при хранении] + + C --> C1[Просрочка] + C --> C2[Неликвид] + + D --> D1[Пересортица при приёмке] + D --> D2[Ошибка сборки] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new WriteOffsErpCauseDictSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск по названию +```php +$searchModel = new WriteOffsErpCauseDictSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpCauseDictSearch' => [ + 'name' => 'Брак', + ] +]); +``` + +### Поиск дочерних причин +```php +$searchModel = new WriteOffsErpCauseDictSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpCauseDictSearch' => [ + 'parent_id' => 1, // Дочерние элементы категории Брак + ] +]); +``` + +### Поиск активных причин +```php +$searchModel = new WriteOffsErpCauseDictSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpCauseDictSearch' => [ + 'status' => 1, + ] +]); +``` + +### Поиск по группе доступа +```php +$searchModel = new WriteOffsErpCauseDictSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpCauseDictSearch' => [ + 'access_group_id' => 3, // Доступно менеджерам + ] +]); +``` + +### Поиск корневых причин +```php +$searchModel = new WriteOffsErpCauseDictSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpCauseDictSearch' => [ + 'parent_id' => null, + ] +]); +``` + +### GridView +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'name', + [ + 'attribute' => 'parent_id', + 'value' => 'parent.name', + ], + [ + 'attribute' => 'status', + 'value' => function($model) { + return $model->status ? 'Активна' : 'Неактивна'; + }, + 'filter' => [0 => 'Неактивна', 1 => 'Активна'], + ], + [ + 'attribute' => 'access_group_id', + 'value' => 'accessGroup.name', + ], + ], +]) ?> +``` + +## Связанные модели + +- [WriteOffsErpCauseDict](./WriteOffsErpCauseDict.md) — базовая модель причин +- [WriteOffsErp](./WriteOffsErp.md) — документы списания +- [AccessGroup](./AccessGroup.md) — группы доступа + +## Особенности реализации + +1. **Иерархическая структура**: parent_id для древовидной организации +2. **Статус активности**: status для включения/выключения причин +3. **Разграничение доступа**: access_group_id для RBAC +4. **Самосвязь**: Модель ссылается сама на себя через parent_id +5. **like вместо ilike**: Регистрозависимый поиск по названию diff --git a/erp24/docs/models/WriteOffsErpSearch.md b/erp24/docs/models/WriteOffsErpSearch.md new file mode 100644 index 00000000..d0562cea --- /dev/null +++ b/erp24/docs/models/WriteOffsErpSearch.md @@ -0,0 +1,264 @@ +# Класс: WriteOffsErpSearch + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsErpSearch)) + Таблица БД + ActiveRecord + Наследование + extends WriteOffsErp +``` + +## Назначение +Search-модель для поиска и фильтрации документов списания ERP в ERP24. Расширенная модель с JOIN к магазину, фильтрацией по доступным магазинам, базовыми условиями (active=1, visible=1) и кастомной сортировкой по магазину. + +## Пространство имён +`yii_app\records` + +## Родительский класс +`WriteOffsErp` + +## Дополнительные свойства поиска + +| Свойство | Тип | Описание | +|----------|-----|----------| +| `$cityStoreName` | int | ID магазина для фильтрации (используется как store_id) | + +## Методы + +### rules() +**Описание:** Правила валидации параметров поиска. + +**Возвращает:** `array` — массив правил + +**Правила:** +- `id`, `status`, `created_admin_id`, `updated_admin_id`, `confirm_admin_id` — integer +- `guid`, `store_guid`, `number`, `date`, `based_on`, `comment`, `created_at`, `send_at`, `write_offs_type`, `updated_at`, `cityStoreName` — safe +- `summ`, `summ_retail` — number + +### scenarios() +**Описание:** Возвращает сценарии базового класса Model. + +**Возвращает:** `array` — сценарии из yii\base\Model + +### search($params, array $storeIds = []): object +**Описание:** Создаёт провайдер данных с JOIN к магазину и предустановленными фильтрами. + +**Параметры:** +- `$params` (array) — параметры поиска +- `$storeIds` (array) — массив ID доступных магазинов для фильтрации + +**Возвращает:** `ActiveDataProvider` — провайдер данных + +**Логика:** +1. Создаёт запрос с базовыми условиями: + - WHERE active = 1 + - ORDER BY write_offs_erp.id DESC +2. Если передан $storeIds — фильтрует по доступным магазинам +3. Выполняет joinWith(['cityStore']) +4. Добавляет условие city_store.visible = '1' +5. Настраивает кастомную сортировку по cityStoreName (city_store.id) +6. Применяет фильтры: + - Точное совпадение: id, status, created_admin_id, updated_admin_id, confirm_admin_id, store_id, summ, summ_retail, created_at, send_at, updated_at + - Фильтр по store_id: через cityStoreName + - like: guid, store_guid, number, date, write_offs_type, based_on, comment + +## Диаграмма связей + +```mermaid +erDiagram + WriteOffsErp { + int id PK + varchar guid + int store_id FK + varchar store_guid + varchar number + date date + int status + varchar write_offs_type + varchar based_on + text comment + decimal summ + decimal summ_retail + int created_admin_id FK + int updated_admin_id FK + int confirm_admin_id FK + datetime created_at + datetime send_at + datetime updated_at + int active + } + + CityStore { + int id PK + varchar name + int visible + } + + Admin { + int id PK + varchar name + } + + WriteOffsErp }o--|| CityStore : "store_id" + WriteOffsErp }o--|| Admin : "created_admin_id" + WriteOffsErp }o--|| Admin : "updated_admin_id" + WriteOffsErp }o--|| Admin : "confirm_admin_id" +``` + +## Диаграмма базовых условий + +```mermaid +flowchart TD + A[WriteOffsErpSearch] --> B[Базовые условия] + + B --> C[active = 1] + C --> D[Только активные документы] + + B --> E[city_store.visible = 1] + E --> F[Только видимые магазины] + + G[storeIds] --> H{Не пустой?} + H -->|Да| I[store_id IN storeIds] + H -->|Нет| J[Все магазины] + + K[Сортировка] --> L[id DESC по умолчанию] +``` + +## Диаграмма статусов списания + +```mermaid +flowchart LR + A[status] --> B{Значение} + + B --> C[0 - Черновик] + B --> D[1 - На согласовании] + B --> E[2 - Согласован] + B --> F[3 - Отправлен в 1С] + B --> G[4 - Отклонён] +``` + +## Примеры использования + +### Стандартный поиск +```php +public function actionIndex() +{ + $searchModel = new WriteOffsErpSearch(); + $dataProvider = $searchModel->search(Yii::$app->request->queryParams); + + return $this->render('index', [ + 'searchModel' => $searchModel, + 'dataProvider' => $dataProvider, + ]); +} +``` + +### Поиск с ограничением по магазинам +```php +$searchModel = new WriteOffsErpSearch(); +$userStoreIds = Yii::$app->user->identity->getAvailableStoreIds(); +$dataProvider = $searchModel->search(Yii::$app->request->queryParams, $userStoreIds); +``` + +### Поиск по номеру документа +```php +$searchModel = new WriteOffsErpSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpSearch' => [ + 'number' => 'СП-00123', + ] +]); +``` + +### Поиск по статусу +```php +$searchModel = new WriteOffsErpSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpSearch' => [ + 'status' => 2, // Согласован + ] +]); +``` + +### Поиск по магазину +```php +$searchModel = new WriteOffsErpSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpSearch' => [ + 'cityStoreName' => 5, // store_id + ] +]); +``` + +### Поиск по типу списания +```php +$searchModel = new WriteOffsErpSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpSearch' => [ + 'write_offs_type' => 'Брак', + ] +]); +``` + +### Поиск по сумме +```php +$searchModel = new WriteOffsErpSearch(); +$dataProvider = $searchModel->search([ + 'WriteOffsErpSearch' => [ + 'summ' => 5000, + ] +]); +``` + +### GridView с сортировкой по магазину +```php + $dataProvider, + 'filterModel' => $searchModel, + 'columns' => [ + 'id', + 'number', + 'date:date', + [ + 'attribute' => 'cityStoreName', + 'value' => 'cityStore.name', + 'filter' => ArrayHelper::map(CityStore::find()->all(), 'id', 'name'), + ], + [ + 'attribute' => 'status', + 'value' => function($model) { + return $model->getStatusLabel(); + }, + ], + 'write_offs_type', + 'summ:decimal', + 'summ_retail:decimal', + [ + 'attribute' => 'created_admin_id', + 'value' => 'createdAdmin.name', + ], + ], +]) ?> +``` + +## Связанные модели + +- [WriteOffsErp](./WriteOffsErp.md) — базовая модель списаний +- [CityStore](./CityStore.md) — магазины +- [Admin](./Admin.md) — администраторы (создатель, редактор, согласовавший) +- [WriteOffsErpCauseDict](./WriteOffsErpCauseDict.md) — причины списания + +## Особенности реализации + +1. **Второй параметр storeIds**: Фильтрация по доступным магазинам пользователя +2. **Базовые условия**: active=1 и city_store.visible='1' всегда применяются +3. **Сортировка по умолчанию**: id DESC (новые сверху) +4. **Кастомная сортировка**: cityStoreName сортирует по city_store.id +5. **cityStoreName как store_id**: Виртуальное свойство фильтрует по store_id +6. **Алиас таблицы**: write_offs_erp.* для избежания конфликтов с JOIN +7. **like вместо ilike**: Регистрозависимый поиск +8. **Три администратора**: created, updated, confirm для аудита документа diff --git a/erp24/docs/models/WriteOffsMetrics.md b/erp24/docs/models/WriteOffsMetrics.md new file mode 100644 index 00000000..ad7f2f4d --- /dev/null +++ b/erp24/docs/models/WriteOffsMetrics.md @@ -0,0 +1,347 @@ +# Class: WriteOffsMetrics + +## Mindmap + +```mermaid +mindmap + root((WriteOffsMetrics)) + Таблица БД + Metrics наследник + Свойства + calculateShifts + bool false + alias + array + Метрики + write_offs + Сумма списаний + Связи + WriteOffs + 1:N данные + ExportImportTable + маппинг ID + Наследование + extends Metrics +``` + +## Назначение + +Класс WriteOffsMetrics наследует абстрактный класс Metrics и реализует расчёт метрик списаний (брака) по магазинам в системе ERP24. Агрегирует данные из таблицы `write_offs` (списания продукции), вычисляет сумму списанных товаров по типу "Брак". Используется для контроля потерь, анализа качества продукции и учёта убытков магазинов. Рассчитывается только по полным дням (shift_type = 4), без разбивки по сменам. + +## Пространство имён + +```php +namespace yii_app\records\metrics; +``` + +## Родительский класс + +```php +\yii_app\records\metrics\Metrics +``` + +## Использования (Dependencies) + +- `yii\db\Expression` - SQL выражения Yii2 +- `yii_app\records\metrics\Metrics` - базовый класс метрик +- `yii_app\records\WriteOffs` - модель списаний + +## Свойства (Properties) + +### Защищённые флаги + +```php +protected bool $calculateShifts = false; // Не рассчитываем по сменам (только дни) +``` + +### Защищённые массивы псевдонимов + +```php +protected array $alias = [ + 'write_offs' // Сумма списаний (брака) +]; +``` + +## Методы + +### getQueryDataDay() + +**Описание:** Реализует абстрактный метод родителя. Возвращает запрос для расчёта метрик списаний за полный день (shift_type = 4). + +**Параметры:** Нет + +**Возвращает:** `ActiveQuery` - запрос к таблице write_offs с агрегацией по дням + +**Логика работы:** +1. Создаёт запрос к таблице `write_offs` +2. Выполняет SELECT с агрегирующими функциями: + - `date` - дата списания (форматируется к виду Y-m-d) + - `shift_type` - всегда 4 (полный день) + - `store_dynamic_id` - ID записи динамики магазина + - `store_id` - ID магазина из city_store + - `write_offs` - SUM суммы всех списаний +3. INNER JOIN с `export_import_table` для сопоставления: + - write_offs.store_id → export_import_table.export_val + - Это связывает внешний ID магазина из 1С с внутренним ID +4. INNER JOIN с `city_store`: + - export_import_table.entity_id → city_store.id +5. INNER JOIN с `store_dynamic` для определения кластера: + - store_dynamic.store_id = export_import_table.entity_id + - date_from <= write_offs.date + - date_to > write_offs.date +6. Фильтрует по: + - Диапазону дат (dateStart, dateEnd) + - Кластеру (если указан через $this->cluster) + - Магазину (если указан через $this->store) + - Типу списания (только 'Брак') +7. Группирует по дате, store_dynamic.id, city_store.id +8. Возвращает ActiveQuery + +**Вызовы сторонних методов:** +- `WriteOffs::find()` - создание запроса +- `->select([...])` - выбор полей с агрегацией +- `->innerJoin('export_import_table', ...)` - соединение с таблицей экспорта/импорта +- `->innerJoin('city_store', ...)` - соединение с магазинами +- `->innerJoin('store_dynamic', [...])` - соединение с кластерами +- `->andFilterWhere([...])` - условная фильтрация по кластеру и магазину +- `->andWhere([...])` - фильтрация по дате и типу +- `->addGroupBy([...])` - группировка результатов +- `new Expression()` - SQL выражения для форматирования дат + +**Пример результирующего запроса:** +```sql +SELECT + DATE_FORMAT(write_offs.date, '%Y-%m-%d') AS date, + 4 AS shift_type, + store_dynamic.id AS store_dynamic_id, + city_store.id AS store_id, + SUM(write_offs.summ) AS write_offs +FROM write_offs +INNER JOIN export_import_table ON write_offs.store_id = export_import_table.export_val +INNER JOIN city_store ON export_import_table.entity_id = city_store.id +INNER JOIN store_dynamic ON store_dynamic.store_id = export_import_table.entity_id + AND DATE_FORMAT(store_dynamic.date_from, '%Y-%m-%d') <= DATE_FORMAT(write_offs.date, '%Y-%m-%d') + AND DATE_FORMAT(store_dynamic.date_to, '%Y-%m-%d') > DATE_FORMAT(write_offs.date, '%Y-%m-%d') +WHERE store_dynamic.value_int = 1 -- Кластер (если указан) + AND city_store.id = 5 -- Магазин (если указан) + AND write_offs.type = 'Брак' + AND DATE_FORMAT(write_offs.date, '%Y-%m-%d') >= '2025-01-01' + AND DATE_FORMAT(write_offs.date, '%Y-%m-%d') <= '2025-01-31' +GROUP BY DATE_FORMAT(write_offs.date, '%Y-%m-%d'), + store_dynamic.id, + city_store.id +``` + +--- + +### getQueryDataShifts() + +**Описание:** Реализует абстрактный метод родителя. Возвращает false, так как метрики списаний рассчитываются только по дням, без разбивки по сменам. + +**Параметры:** Нет + +**Возвращает:** `bool` - всегда false + +**Логика работы:** +Метод просто возвращает false, указывая родительскому классу не создавать LEFT JOIN для данных смен. + +**Пример:** +```php +$writeOffsMetrics = new WriteOffsMetrics(); +$shiftsQuery = $writeOffsMetrics->getQueryDataShifts(); +var_dump($shiftsQuery); // bool(false) +``` + +## Связи (Relations) + +```mermaid +erDiagram + WRITE_OFFS_METRICS --|> METRICS : extends + WRITE_OFFS_METRICS --> WRITE_OFFS : reads from + WRITE_OFFS_METRICS --> EXPORT_IMPORT_TABLE : joins with + WRITE_OFFS_METRICS --> CITY_STORE : joins with + WRITE_OFFS_METRICS --> STORE_DYNAMIC : joins with + WRITE_OFFS_METRICS --> RNP_INDEX : writes to + WRITE_OFFS_METRICS --> RNP_DATA : writes to + WRITE_OFFS_METRICS --> RNP_ALIAS : uses + + WRITE_OFFS { + int id PK + string store_id + date date + float summ + string type + } + + EXPORT_IMPORT_TABLE { + int id PK + string export_val + int entity_id + } +``` + +## Примеры использования + +### Расчёт метрик списаний за месяц + +```php +$metrics = new WriteOffsMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +$result = $metrics->insertData(); +echo $result; +// |Время поиска: 1.850 sec.|Время записи индексов: 0.280 sec.|Время записи данных: 0.950 sec.| +``` + +### Расчёт для конкретного кластера + +```php +$metrics = new WriteOffsMetrics(); +$metrics->dateStart = '2025-01-15'; +$metrics->dateEnd = '2025-01-15'; +$metrics->cluster = 3; // Только кластер 3 + +$metrics->insertData(); +``` + +### Расчёт для одного магазина за квартал + +```php +$metrics = new WriteOffsMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-03-31'; +$metrics->store = 8; // Магазин ID 8 + +$metrics->insertData(); +``` + +### Получение рассчитанных данных списаний + +```php +$metrics = new WriteOffsMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +// Получить данные по дням (shift_type = 4) +$data = $metrics->getDataArray(1, 5, 4); + +foreach ($data as $row) { + echo "{$row['date']} - Списания: {$row['value']} руб.\n"; +} +// Результат: +// 2025-01-01 - Списания: 2500.00 руб. +// 2025-01-02 - Списания: 3200.50 руб. +// 2025-01-03 - Списания: 1800.00 руб. +``` + +### Анализ динамики списаний + +```php +$metrics = new WriteOffsMetrics(); +$metrics->dateStart = '2025-01-01'; +$metrics->dateEnd = '2025-01-31'; + +$data = $metrics->getDataArray(1, null, 4); // Кластер 1, все магазины + +// Группировка по магазинам +$byStore = []; +foreach ($data as $row) { + if (!isset($byStore[$row['store_id']])) { + $byStore[$row['store_id']] = 0; + } + $byStore[$row['store_id']] += $row['value']; +} + +// Сортировка по убыванию списаний +arsort($byStore); + +echo "Топ магазинов по списаниям:\n"; +foreach (array_slice($byStore, 0, 5) as $storeId => $total) { + echo "Магазин {$storeId}: " . number_format($total, 2) . " руб.\n"; +} +``` + +### Расчёт процента списаний к выручке + +```php +$salesMetrics = new SalesMetrics(); +$salesMetrics->dateStart = '2025-01-01'; +$salesMetrics->dateEnd = '2025-01-31'; +$salesData = $salesMetrics->getDataArray(1, 5, 4); + +$writeOffsMetrics = new WriteOffsMetrics(); +$writeOffsMetrics->dateStart = '2025-01-01'; +$writeOffsMetrics->dateEnd = '2025-01-31'; +$writeOffsData = $writeOffsMetrics->getDataArray(1, 5, 4); + +// Группировка по датам +$salesByDate = []; +foreach ($salesData as $row) { + if ($row['alias'] === 'sales_sum') { + $salesByDate[$row['date']] = $row['value']; + } +} + +$writeOffsByDate = []; +foreach ($writeOffsData as $row) { + $writeOffsByDate[$row['date']] = $row['value']; +} + +// Расчёт процентов +foreach ($writeOffsByDate as $date => $writeOffs) { + $sales = $salesByDate[$date] ?? 0; + $percent = $sales > 0 ? ($writeOffs / $sales) * 100 : 0; + + echo "{$date}: Списания {$writeOffs} руб. ({$percent}% от выручки {$sales} руб.)\n"; +} +``` + +## Поток данных + +```mermaid +flowchart TD + A[WriteOffsMetrics::insertData] --> B[Валидация дат] + B --> C[getQueryDataCollection] + C --> D[getQueryDataShifts = false] + C --> E[getQueryDataDay - смена 4] + D --> F[Пропуск данных смен] + E --> G[WriteOffs: SUM по дням type=Брак] + G --> H[JOIN через export_import_table] + H --> I[Формирование selectQuery] + I --> J[Batch 1000 записей] + J --> K[Расчёт индексов RnpIndex] + K --> L[Маппинг RnpAlias: write_offs] + L --> M[RnpData::deleteAll старые] + M --> N[RnpData::batchInsert новые] + N --> O[Возврат статистики] +``` + +## Связанные компоненты + +| Компонент | Тип | Описание | +|-----------|-----|----------| +| [Metrics](./Metrics.md) | Abstract | Базовый класс метрик | +| [WriteOffs](./WriteOffs.md) | Model | Модель списаний | +| [SalesMetrics](./SalesMetrics.md) | Model | Метрики продаж | +| [FotMetrics](./FotMetrics.md) | Model | Метрики ФОТ | +| `WriteOffsMetricsJob` | Job | Фоновый расчёт метрик списаний | +| `RnpData` | Model | Хранение данных метрик | + +## Примечания + +1. **Только дни**: Метрики списаний рассчитываются только по полным дням (shift_type = 4), без разбивки по сменам +2. **Только брак**: Фильтруется только тип списания 'Брак', остальные типы (например, 'Утилизация') не учитываются +3. **Маппинг магазинов**: Используется таблица export_import_table для сопоставления внешних ID магазинов из 1С с внутренними ID системы +4. **Кластеризация**: Использует store_dynamic для определения принадлежности магазина к кластеру на конкретную дату +5. **Аналитика**: Данные используются для контроля потерь, расчёта показателей "процент списаний к выручке", выявления проблемных магазинов + +--- + +**Связанная документация:** +- [Metrics](./Metrics.md) +- [SalesMetrics](./SalesMetrics.md) +- [FotMetrics](./FotMetrics.md) +- [WriteOffs](./WriteOffs.md) +- [Архитектура системы метрик](../architecture/metrics-system.md) +- [Руководство по контролю списаний](../guides/write-offs-control.md) diff --git a/erp24/docs/models/WriteOffsProducts.md b/erp24/docs/models/WriteOffsProducts.md new file mode 100644 index 00000000..c7ab3f6a --- /dev/null +++ b/erp24/docs/models/WriteOffsProducts.md @@ -0,0 +1,411 @@ +# Модель WriteOffsProducts + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsProducts)) + Таблица БД + write_offs_products + Свойства + write_offs_id + string + date + string + product_id + string + color + string + quantity + int + price + float + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WriteOffsProducts` представляет позиции товаров в документах списания старой системы. Хранит детальную информацию о каждом списанном товаре: количество, цены, суммы. + +**Файл модели:** `erp24/records/WriteOffsProducts.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `write_offs_products` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `write_offs_id` | VARCHAR(36) | GUID документа списания из таблицы `write_offs` (FK) | +| `date` | TIMESTAMP | Дата списания (дублируется для удобства выборки) | +| `product_id` | VARCHAR(36) | GUID товара из таблицы `products_1c` (FK) | +| `color` | VARCHAR(200) | Цвет товара (текстовое описание) | +| `quantity` | INTEGER | Количество списанного товара | +| `price` | NUMERIC | Цена закупочная (розничная в старой системе) | +| `summ` | NUMERIC | Сумма позиции (price * quantity) | +| `price_retail` | NUMERIC | Розничная цена товара | +| `summ_retail` | NUMERIC | Сумма в розничных ценах (price_retail * quantity) | + +**Первичный ключ:** композитный (`write_offs_id`, `product_id`) + +--- + +## Константы + +Модель не содержит констант. + +--- + +## Методы модели + +### Геттеры и сеттеры + +#### `getWriteOffsId(): string` + +Возвращает GUID документа списания. + +**Параметры:** нет + +**Возвращает:** `string` - GUID документа списания + +**Логика:** Возвращает значение поля `write_offs_id`. + +**Пример:** +```php +$product = WriteOffsProducts::findOne(['write_offs_id' => $id, 'product_id' => $productId]); +$writeOffId = $product->getWriteOffsId(); +``` + +#### `setWriteOffsId(string $write_offs_id): void` + +Устанавливает GUID документа списания. + +**Параметры:** +- `$write_offs_id` (string) - GUID документа списания + +**Возвращает:** void + +**Логика:** Присваивает значение полю `write_offs_id`. + +**Пример:** +```php +$product = new WriteOffsProducts(); +$product->setWriteOffsId('550e8400-e29b-41d4-a716-446655440000'); +``` + +#### `getDate(): string` + +Возвращает дату списания товара. + +**Параметры:** нет + +**Возвращает:** `string` - дата списания + +**Логика:** Возвращает значение поля `date`. + +#### `setDate(string $date): void` + +Устанавливает дату списания. + +**Параметры:** +- `$date` (string) - дата списания + +**Возвращает:** void + +**Логика:** Присваивает значение полю `date`. + +#### `getProductId(): string` + +Возвращает GUID товара. + +**Параметры:** нет + +**Возвращает:** `string` - GUID товара + +**Логика:** Возвращает значение поля `product_id`. + +#### `setProductId(string $product_id): void` + +Устанавливает GUID товара. + +**Параметры:** +- `$product_id` (string) - GUID товара + +**Возвращает:** void + +**Логика:** Присваивает значение полю `product_id`. + +#### `getColor(): string` + +Возвращает цвет товара. + +**Параметры:** нет + +**Возвращает:** `string` - цвет товара + +**Логика:** Возвращает значение поля `color`. + +#### `setColor(string $color): void` + +Устанавливает цвет товара. + +**Параметры:** +- `$color` (string) - цвет товара + +**Возвращает:** void + +**Логика:** Присваивает значение полю `color`. + +#### `getQuantity(): int` + +Возвращает количество списанного товара. + +**Параметры:** нет + +**Возвращает:** `int` - количество + +**Логика:** Возвращает значение поля `quantity`. + +#### `setQuantity(int $quantity): void` + +Устанавливает количество товара. + +**Параметры:** +- `$quantity` (int) - количество + +**Возвращает:** void + +**Логика:** Присваивает значение полю `quantity`. + +#### `getPrice(): float` + +Возвращает закупочную цену товара. + +**Параметры:** нет + +**Возвращает:** `float` - цена + +**Логика:** Возвращает значение поля `price`. + +#### `setPrice(float $price): void` + +Устанавливает цену товара. + +**Параметры:** +- `$price` (float) - цена + +**Возвращает:** void + +**Логика:** Присваивает значение полю `price`. + +#### `getSumm(): float` + +Возвращает сумму позиции в закупочных ценах. + +**Параметры:** нет + +**Возвращает:** `float` - сумма + +**Логика:** Возвращает значение поля `summ`. + +#### `setSumm(float $summ): void` + +Устанавливает сумму позиции. + +**Параметры:** +- `$summ` (float) - сумма + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ`. + +#### `getPriceRetail(): float` + +Возвращает розничную цену товара. + +**Параметры:** нет + +**Возвращает:** `float` - розничная цена + +**Логика:** Возвращает значение поля `price_retail`. + +#### `setPriceRetail(float $price_retail): void` + +Устанавливает розничную цену товара. + +**Параметры:** +- `$price_retail` (float) - розничная цена + +**Возвращает:** void + +**Логика:** Присваивает значение полю `price_retail`. + +#### `getSummRetail(): float` + +Возвращает сумму позиции в розничных ценах. + +**Параметры:** нет + +**Возвращает:** `float` - сумма в розничных ценах + +**Логика:** Возвращает значение поля `summ_retail`. + +#### `setSummRetail(float $summ_retail): void` + +Устанавливает сумму в розничных ценах. + +**Параметры:** +- `$summ_retail` (float) - сумма в розничных ценах + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ_retail`. + +--- + +## Валидация + +| Поле | Правила | +|------|---------| +| `write_offs_id` | Обязательное, макс. 36 символов | +| `date` | Обязательное | +| `product_id` | Обязательное, макс. 36 символов | +| `quantity` | Обязательное, целое число | +| `price` | Обязательное, числовое | +| `summ` | Обязательное, числовое | +| `price_retail` | Обязательное, числовое | +| `summ_retail` | Обязательное, числовое | +| `color` | Безопасное, макс. 200 символов | +| (`write_offs_id`, `product_id`) | Уникальная комбинация | + +--- + +## Связи (Relations) + +Модель не содержит явных связей через методы, но связывается с другими моделями: + +- **WriteOffs** - документ списания (связь через `write_offs_id`) +- **Products1c** - товар (связь через `product_id`) + +--- + +## Диаграмма связей + +```mermaid +erDiagram + write_offs ||--o{ write_offs_products : "has_many" + write_offs_products }o--|| products_1c : "belongs_to" + + write_offs { + string id PK + string store_id + string number + numeric summ + numeric summ_retail + } + + write_offs_products { + string write_offs_id PK,FK + string product_id PK,FK + timestamp date + string color + integer quantity + numeric price + numeric summ + numeric price_retail + numeric summ_retail + } + + products_1c { + string id PK + string name + string tip + } +``` + +--- + +## Примеры использования + +### Создание позиции товара в списании + +```php +$product = new WriteOffsProducts(); +$product->setWriteOffsId('550e8400-e29b-41d4-a716-446655440000'); +$product->setDate(date('Y-m-d H:i:s')); +$product->setProductId('660e8400-e29b-41d4-a716-446655440001'); +$product->setColor('Красный'); +$product->setQuantity(5); +$product->setPrice(100.00); +$product->setSumm(500.00); +$product->setPriceRetail(150.00); +$product->setSummRetail(750.00); + +if ($product->validate()) { + $product->save(); +} +``` + +### Получение товаров документа списания + +```php +$products = WriteOffsProducts::find() + ->where(['write_offs_id' => '550e8400-e29b-41d4-a716-446655440000']) + ->all(); + +foreach ($products as $product) { + echo "Товар: {$product->getProductId()}, "; + echo "Количество: {$product->getQuantity()}, "; + echo "Сумма: {$product->getSummRetail()}\n"; +} +``` + +### Расчёт общей суммы по документу + +```php +$totalRetail = WriteOffsProducts::find() + ->where(['write_offs_id' => $writeOffId]) + ->sum('summ_retail'); +``` + +### Обновление розничных цен товара + +```php +$product = WriteOffsProducts::findOne([ + 'write_offs_id' => $writeOffId, + 'product_id' => $productId +]); + +$product->setPriceRetail(200.00); +$product->setSummRetail($product->getQuantity() * 200.00); +$product->save(); +``` + +--- + +## Связанные модели + +- **[WriteOffs](./WriteOffs.md)** — документы списания (старая система) +- **[WriteOffsProductsErp](./WriteOffsProductsErp.md)** — товары списаний в новой системе ERP +- **[Products1c](./Products1c.md)** — справочник товаров из 1С +- **PricesDynamic** — динамика цен товаров + +--- + +## Примечания + +1. Модель является частью **старой системы** списаний +2. Использует композитный первичный ключ (`write_offs_id`, `product_id`) +3. Поле `date` дублируется из родительского документа для удобства выборки +4. С декабря 2022 года обязательно заполнение полей `price_retail` и `summ_retail` +5. Поле `color` хранит текстовое описание цвета товара +6. Розничные цены могут заполняться автоматически из справочника `PricesDynamic` + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11 diff --git a/erp24/docs/models/WriteOffsProductsErp.md b/erp24/docs/models/WriteOffsProductsErp.md new file mode 100644 index 00000000..c8f0a53b --- /dev/null +++ b/erp24/docs/models/WriteOffsProductsErp.md @@ -0,0 +1,853 @@ +# Модель WriteOffsProductsErp + + +## Mindmap + +```mermaid +mindmap + root((WriteOffsProductsErp)) + Таблица БД + write_offs_products_erp + Свойства + id + int + write_offs_erp_id + int + date + string + product_id + string + name + string + quantity + float + Связи + Video + 1:1 Files + WriteOffsErp + 1:1 WriteOffsErp + ImagesWriteOffsErp + 1:N ImageDocumentLink + Наследование + extends yiidbActiveRecord +``` + +## Назначение + +Модель `WriteOffsProductsErp` представляет позиции товаров в документах списания новой системы ERP. Содержит расширенную информацию о списанных товарах: причины списания, изображения, видео-доказательства, комментарии. + +**Файл модели:** `erp24/records/WriteOffsProductsErp.php` +**Namespace:** `yii_app\records` +**Таблица БД:** `write_offs_products_erp` +**Родительский класс:** `yii\db\ActiveRecord` + +--- + +## Поля таблицы + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | INTEGER | Первичный ключ (автоинкремент) | +| `write_offs_erp_id` | INTEGER | ID документа списания из таблицы `write_offs_erp` (FK) | +| `date` | TIMESTAMP | Дата списания | +| `product_id` | VARCHAR(36) | GUID товара из таблицы `products_1c` (FK) | +| `comment` | VARCHAR(600) | Комментарий к списанию товара | +| `color` | VARCHAR(100) | Цвет товара | +| `name` | VARCHAR(100) | Название товара | +| `quantity` | NUMERIC | Количество списанного товара | +| `cause_id` | INTEGER | ID причины списания (FK) | +| `num_row` | INTEGER | Номер строки в документе | +| `active_product` | INTEGER | Флаг активности позиции (0/1) | +| `price` | NUMERIC | Цена закупочная | +| `summ` | NUMERIC | Сумма позиции (price * quantity) | +| `price_retail` | NUMERIC | Розничная цена товара | +| `summ_retail` | NUMERIC | Сумма в розничных ценах | +| `created_at` | TIMESTAMP | Дата создания записи | +| `updated_at` | TIMESTAMP | Дата изменения записи | +| `deleted_at` | TIMESTAMP | Дата удаления записи | +| `created_admin_id` | INTEGER | ID создателя (FK) | +| `updated_admin_id` | INTEGER | ID редактора (FK) | +| `deleted_admin_id` | INTEGER | ID удалившего (FK) | + +--- + +## Константы + +### Типы вложений + +```php +const WRITE_OFFS_VIDEO = 'write_offs_products_erp_video'; +``` + +Константа для идентификации видео-вложений позиции списания. + +--- + +## Методы модели + +### Геттеры и сеттеры + +#### `getId(): int` + +Возвращает ID позиции товара. + +**Параметры:** нет + +**Возвращает:** `int` - ID позиции + +**Логика:** Возвращает значение поля `id`. + +#### `getName(): string` + +Возвращает название товара. + +**Параметры:** нет + +**Возвращает:** `string` - название товара + +**Логика:** Возвращает значение поля `name`. + +#### `setName(string $name): void` + +Устанавливает название товара. + +**Параметры:** +- `$name` (string) - название товара + +**Возвращает:** void + +**Логика:** Присваивает значение полю `name`. + +#### `getWriteOffsErpId(): int` + +Возвращает ID документа списания. + +**Параметры:** нет + +**Возвращает:** `int` - ID документа + +**Логика:** Возвращает значение поля `write_offs_erp_id`. + +#### `setWriteOffsErpId(int $write_offs_erp_id): void` + +Устанавливает ID документа списания. + +**Параметры:** +- `$write_offs_erp_id` (int) - ID документа + +**Возвращает:** void + +**Логика:** Присваивает значение полю `write_offs_erp_id`. + +#### `getDate(): string` + +Возвращает дату списания. + +**Параметры:** нет + +**Возвращает:** `string` - дата + +**Логика:** Возвращает значение поля `date`. + +#### `setDate(string $date): void` + +Устанавливает дату списания. + +**Параметры:** +- `$date` (string) - дата + +**Возвращает:** void + +**Логика:** Присваивает значение полю `date`. + +#### `getProductId(): string` + +Возвращает GUID товара. + +**Параметры:** нет + +**Возвращает:** `string` - GUID товара + +**Логика:** Возвращает значение поля `product_id`. + +#### `setProductId(string $product_id): void` + +Устанавливает GUID товара. + +**Параметры:** +- `$product_id` (string) - GUID товара + +**Возвращает:** void + +**Логика:** Присваивает значение полю `product_id`. + +#### `getColor(): string` + +Возвращает цвет товара. + +**Параметры:** нет + +**Возвращает:** `string` - цвет + +**Логика:** Возвращает значение поля `color`. + +#### `setColor(string $color): void` + +Устанавливает цвет товара. + +**Параметры:** +- `$color` (string) - цвет + +**Возвращает:** void + +**Логика:** Присваивает значение полю `color`. + +#### `getQuantity(): float` + +Возвращает количество товара с приведением к float. + +**Параметры:** нет + +**Возвращает:** `float` - количество (0 если null) + +**Логика:** Приводит значение `quantity` к float, возвращает 0 если null. + +**Пример:** +```php +$quantity = $product->getQuantity(); // 5.0 +``` + +#### `setQuantity(float $quantity): object` + +Устанавливает количество товара с приведением к float. + +**Параметры:** +- `$quantity` (float) - количество + +**Возвращает:** `object` - текущий объект (fluent interface) + +**Логика:** Приводит значение к float (0.0 если null) и присваивает полю `quantity`. Возвращает $this для цепочки вызовов. + +**Пример:** +```php +$product->setQuantity(10.5)->setPrice(100.00)->save(); +``` + +#### `getPrice(): float` + +Возвращает закупочную цену. + +**Параметры:** нет + +**Возвращает:** `float` - цена + +**Логика:** Возвращает значение поля `price`. + +#### `setPrice(float $price): void` + +Устанавливает закупочную цену. + +**Параметры:** +- `$price` (float) - цена + +**Возвращает:** void + +**Логика:** Присваивает значение полю `price`. + +#### `getSumm(): float` + +Возвращает сумму позиции. + +**Параметры:** нет + +**Возвращает:** `float` - сумма + +**Логика:** Возвращает значение поля `summ`. + +#### `setSumm(float $summ): void` + +Устанавливает сумму позиции. + +**Параметры:** +- `$summ` (float) - сумма + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ`. + +#### `getPriceRetail(): float` + +Возвращает розничную цену. + +**Параметры:** нет + +**Возвращает:** `float` - розничная цена + +**Логика:** Возвращает значение поля `price_retail`. + +#### `setPriceRetail(float $price_retail): void` + +Устанавливает розничную цену. + +**Параметры:** +- `$price_retail` (float) - розничная цена + +**Возвращает:** void + +**Логика:** Присваивает значение полю `price_retail`. + +#### `getSummRetail(): float` + +Возвращает сумму в розничных ценах. + +**Параметры:** нет + +**Возвращает:** `float` - сумма + +**Логика:** Возвращает значение поля `summ_retail`. + +#### `setSummRetail(float $summ_retail): void` + +Устанавливает сумму в розничных ценах. + +**Параметры:** +- `$summ_retail` (float) - сумма + +**Возвращает:** void + +**Логика:** Присваивает значение полю `summ_retail`. + +#### `setCauseId(int $cause_id): void` + +Устанавливает ID причины списания. + +**Параметры:** +- `$cause_id` (int) - ID причины + +**Возвращает:** void + +**Логика:** Присваивает значение полю `cause_id`. + +#### `getNumRow(): int` + +Возвращает номер строки в документе. + +**Параметры:** нет + +**Возвращает:** `int` - номер строки + +**Логика:** Возвращает значение поля `num_row`. + +#### `setNumRow(int $num_row): void` + +Устанавливает номер строки в документе. + +**Параметры:** +- `$num_row` (int) - номер строки + +**Возвращает:** void + +**Логика:** Присваивает значение полю `num_row`. + +#### `getActive(): int` + +Возвращает флаг активности позиции. + +**Параметры:** нет + +**Возвращает:** `int` - 0 или 1 + +**Логика:** Возвращает значение поля `active_product`. + +#### `setActive(int $active): void` + +Устанавливает флаг активности позиции. + +**Параметры:** +- `$active` (int) - 0 (неактивна) или 1 (активна) + +**Возвращает:** void + +**Логика:** Присваивает значение полю `active_product`. + +#### `setCreatedAt(): void` + +Устанавливает дату создания записи. + +**Параметры:** нет + +**Возвращает:** void + +**Логика:** Присваивает полю `created_at` текущую дату и время. + +**Пример:** +```php +$product->setCreatedAt(); // created_at = '2025-12-11 10:30:00' +``` + +#### `setUpdatedAt(): void` + +Устанавливает дату изменения записи. + +**Параметры:** нет + +**Возвращает:** void + +**Логика:** Устанавливает `updated_at` на текущее время. Если `created_at` пустое, также устанавливает его. + +**Пример:** +```php +$product->setUpdatedAt(); // updated_at = '2025-12-11 10:35:00' +``` + +#### `setDeletedAt(): object` + +Устанавливает дату удаления записи. + +**Параметры:** нет + +**Возвращает:** `object` - текущий объект + +**Логика:** Присваивает полю `deleted_at` текущую дату и время. Возвращает $this. + +**Пример:** +```php +$product->setDeletedAt()->setActive(0)->save(); +``` + +#### `setCreatedAdminId(int $created_admin_id): object` + +Устанавливает ID создателя. + +**Параметры:** +- `$created_admin_id` (int) - ID администратора + +**Возвращает:** `object` - текущий объект + +**Логика:** Присваивает значение полю `created_admin_id`. Возвращает $this. + +#### `setUpdatedAdminId(int $updated_admin_id): object` + +Устанавливает ID редактора. + +**Параметры:** +- `$updated_admin_id` (int) - ID администратора + +**Возвращает:** `object` - текущий объект + +**Логика:** Присваивает значение полю `updated_admin_id`. Если `created_admin_id` пустое, также устанавливает его. Возвращает $this. + +#### `setDeletedAdminId(?int $deleted_admin_id): object` + +Устанавливает ID удалившего. + +**Параметры:** +- `$deleted_admin_id` (int|null) - ID администратора + +**Возвращает:** `object` - текущий объект + +**Логика:** Присваивает значение полю `deleted_admin_id`. Возвращает $this. + +--- + +### Статические методы + +#### `getCauseList(): array` + +Возвращает список активных причин списания (без групп). + +**Параметры:** нет + +**Возвращает:** `array` - ассоциативный массив [id => name] + +**Логика:** +1. Выбирает из `WriteOffsErpCauseDict` все записи с непустым `parent_id` (причины, не группы) +2. Фильтрует по `status = 1` (активные) +3. Индексирует по `id` +4. Преобразует в простой массив [id => name] через `ArrayHelper::map()` + +**Вызовы сторонних методов:** +- `WriteOffsErpCauseDict::find()` - поиск причин списания +- `ArrayHelper::map()` - преобразование массива + +**Пример:** +```php +$causes = WriteOffsProductsErp::getCauseList(); +// [1 => 'Повреждение упаковки', 2 => 'Истёк срок годности', ...] +``` + +#### `getCauseDict($userGroupId = null): array` + +Возвращает иерархический словарь причин списания с группами. + +**Параметры:** +- `$userGroupId` (int|null) - ID группы пользователя для фильтрации (не используется) + +**Возвращает:** `array` - многоуровневый массив [группа => [id => название]] + +**Логика:** +1. Запрашивает причины: записи с непустым `parent_id`, `status = 1` +2. Запрашивает группы: записи с пустым `parent_id`, `status = 1` +3. Для каждой причины: + - Находит родительскую группу по `parent_id` + - Добавляет причину в массив под названием группы +4. Возвращает структуру: ['Группа 1' => [1 => 'Причина 1', 2 => 'Причина 2'], ...] + +**Вызовы сторонних методов:** +- `WriteOffsErpCauseDict::find()` - поиск причин и групп + +**Пример:** +```php +$causeDict = WriteOffsProductsErp::getCauseDict(); +// [ +// 'Брак' => [1 => 'Повреждение упаковки', 2 => 'Производственный брак'], +// 'Просроченные' => [3 => 'Истёк срок годности'] +// ] +``` + +#### `getCuseName(): string` + +Возвращает название причины списания текущей позиции. + +**Параметры:** нет + +**Возвращает:** `string` - название причины + +**Логика:** +1. Вызывает `getCauseDict()` для получения всего словаря +2. Извлекает значение по `cause_id` текущей записи через `ArrayHelper::getValue()` +3. Возвращает название причины + +**Вызовы сторонних методов:** +- `getCauseDict()` - получение словаря причин +- `ArrayHelper::getValue()` - безопасное получение значения +- `getCauseId()` - получение ID причины + +**Пример:** +```php +$product = WriteOffsProductsErp::findOne($id); +$causeName = $product->getCuseName(); // 'Повреждение упаковки' +``` + +#### `getGroupAccess(int $accessGroupId): bool` + +Проверяет доступ группы пользователей к причинам списания. + +**Параметры:** +- `$accessGroupId` (int) - ID группы доступа + +**Возвращает:** `bool` - ключ группы или false + +**Логика:** +1. Определяет конфигурацию групп доступа: + - Группа 1: доступ к причинам [1, 7, 85] + - Группа 2: доступ к причинам [1, 85] +2. Ищет `$accessGroupId` в массивах групп +3. Возвращает ключ группы (1 или 2) или false + +**Примечание:** Метод не используется в коде (закомментирован в `getCauseDict`). + +**Пример:** +```php +$group = WriteOffsProductsErp::getGroupAccess(7); // 1 +``` + +#### `deleteByIDs($deletedIDs, $adminId): ?int` + +Мягкое удаление позиций по массиву ID. + +**Параметры:** +- `$deletedIDs` (array) - массив ID позиций для удаления +- `$adminId` (int) - ID администратора, выполняющего удаление + +**Возвращает:** `int|null` - количество удалённых записей или null + +**Логика:** +1. Проверяет, что `$deletedIDs` не пустой +2. Находит все записи с указанными ID +3. Для каждой записи: + - Устанавливает `active_product = 0` + - Устанавливает `deleted_admin_id = $adminId` + - Устанавливает `deleted_at` на текущее время + - Сохраняет без валидации +4. Возвращает количество обработанных записей + +**Вызовы сторонних методов:** +- `setActive()` - установка флага активности +- `setDeletedAdminId()` - установка ID удалившего +- `setDeletedAt()` - установка даты удаления + +**Пример:** +```php +$deletedCount = WriteOffsProductsErp::deleteByIDs([1, 2, 3], Yii::$app->user->id); +// 3 +``` + +#### `deleteByParentId(int $parentId, int $adminId): ?int` + +Мягкое удаление всех позиций документа. + +**Параметры:** +- `$parentId` (int) - ID документа списания +- `$adminId` (int) - ID администратора + +**Возвращает:** `int|null` - количество удалённых записей или null + +**Логика:** +1. Проверяет, что `$parentId` не пустой +2. Находит все записи с `write_offs_erp_id = $parentId` +3. Для каждой записи выполняет мягкое удаление (аналогично `deleteByIDs`) +4. Возвращает количество обработанных записей + +**Вызовы сторонних методов:** +- `setActive()`, `setDeletedAdminId()`, `setDeletedAt()` - установка параметров удаления + +**Пример:** +```php +$deletedCount = WriteOffsProductsErp::deleteByParentId($writeOffId, $adminId); +// 15 +``` + +#### `getProduct($productId): Products1c|null` + +Получает товар из справочника Products1c. + +**Параметры:** +- `$productId` (string) - GUID товара + +**Возвращает:** `Products1c|null` - объект товара или null + +**Логика:** +1. Ищет в таблице `Products1c` запись по условиям: + - `id = $productId` + - `tip = 'products'` +2. Возвращает первую найденную запись или null + +**Вызовы сторонних методов:** +- `Products1c::find()` - поиск товара + +**Пример:** +```php +$product = $productItem->getProduct('product-guid-123'); +echo $product->name; // 'Молоко 2,5% 1л' +``` + +--- + +## Связи (Relations) + +### `getWriteOffsErp()` + +Документ списания. + +```php +$document = $product->writeOffsErp; // WriteOffsErp +``` + +**Тип:** hasOne +**Связанная модель:** `WriteOffsErp` +**FK:** `write_offs_erp_id` → `id` + +### `getImagesWriteOffsErp()` + +Изображения позиции списания. + +```php +$images = $product->imagesWriteOffsErp; // ImageDocumentLink[] +``` + +**Тип:** hasMany +**Связанная модель:** `ImageDocumentLink` +**FK:** `id` → `document_item_id` +**Условия:** +- `active = 1` +- `document_group_id = 1` + +### `getVideo()` + +Видео-вложение позиции списания. + +```php +$video = $product->video; // Files +``` + +**Тип:** hasOne +**Связанная модель:** `Files` +**FK:** `id` → `entity_id` +**Условие:** `entity = 'write_offs_products_erp_video'` + +--- + +## Диаграмма связей + +```mermaid +erDiagram + write_offs_erp ||--o{ write_offs_products_erp : "has_many" + write_offs_products_erp }o--|| products_1c : "belongs_to" + write_offs_products_erp }o--|| write_offs_erp_cause_dict : "cause" + write_offs_products_erp ||--o{ image_document_link : "images" + write_offs_products_erp ||--o| files : "video" + write_offs_products_erp }o--|| admin : "created_by" + + write_offs_erp { + int id PK + string guid + int status + int store_id + string number + } + + write_offs_products_erp { + int id PK + int write_offs_erp_id FK + string product_id FK + int cause_id FK + string name + float quantity + float price + float summ + int active_product + int num_row + string comment + } + + write_offs_erp_cause_dict { + int id PK + string name + int parent_id + int status + } + + products_1c { + string id PK + string name + string tip + } + + image_document_link { + int id PK + int document_item_id FK + int document_group_id + int active + } + + files { + int id PK + int entity_id FK + string entity + string url + } +``` + +--- + +## Примеры использования + +### Создание позиции товара в списании + +```php +$product = new WriteOffsProductsErp(); +$product->setWriteOffsErpId($documentId); +$product->setDate(date('Y-m-d H:i:s')); +$product->setProductId('product-guid-123'); +$product->setName('Молоко 2,5% 1л'); +$product->setQuantity(5); +$product->setPrice(50.00); +$product->setSumm(250.00); +$product->setPriceRetail(75.00); +$product->setSummRetail(375.00); +$product->setCauseId(1); +$product->setNumRow(1); +$product->setActive(1); +$product->setCreatedAt(); +$product->setCreatedAdminId(Yii::$app->user->id); + +if ($product->save()) { + echo "Позиция создана с ID: {$product->getId()}\n"; +} +``` + +### Получение причин списания для формы + +```php +$causeDict = WriteOffsProductsErp::getCauseDict(); + +echo Html::dropDownList('cause_id', null, $causeDict, [ + 'class' => 'form-control', + 'prompt' => 'Выберите причину' +]); +``` + +### Мягкое удаление позиций + +```php +// Удаление конкретных позиций +$deletedIds = [1, 3, 5]; +$count = WriteOffsProductsErp::deleteByIDs($deletedIds, Yii::$app->user->id); +echo "Удалено позиций: {$count}\n"; + +// Удаление всех позиций документа +$count = WriteOffsProductsErp::deleteByParentId($writeOffId, $adminId); +echo "Удалено позиций документа: {$count}\n"; +``` + +### Получение активных позиций документа + +```php +$products = WriteOffsProductsErp::find() + ->where(['write_offs_erp_id' => $documentId]) + ->andWhere(['active_product' => 1]) + ->orderBy(['num_row' => SORT_ASC]) + ->all(); + +foreach ($products as $product) { + echo "{$product->getNumRow()}. {$product->getName()} - "; + echo "{$product->getQuantity()} шт. - {$product->getSummRetail()} руб.\n"; +} +``` + +### Работа с изображениями и видео + +```php +$product = WriteOffsProductsErp::findOne($id); + +// Получение изображений +$images = $product->imagesWriteOffsErp; +foreach ($images as $image) { + echo "Фото брака\n"; +} + +// Получение видео +$video = $product->video; +if ($video) { + echo "\n"; +} +``` + +--- + +## Связанные модели + +- **[WriteOffsErp](./WriteOffsErp.md)** — документы списания ERP +- **[WriteOffsErpCauseDict](./WriteOffsErpCauseDict.md)** — справочник причин списания +- **[Products1c](./Products1c.md)** — справочник товаров из 1С +- **[Admin](./Admin.md)** — сотрудники системы +- **ImageDocumentLink** — связь документов с изображениями +- **[Files](./Files.md)** — файловые вложения + +--- + +## Примечания + +1. Модель является частью **новой системы ERP** списаний +2. Поддерживает мягкое удаление через флаг `active_product` +3. Хранит расширенную информацию: комментарии, причины, изображения, видео +4. Связана с иерархическим справочником причин списания +5. Использует fluent interface для некоторых сеттеров +6. Поддерживает аудит изменений (created_admin_id, updated_admin_id, deleted_admin_id) +7. Номер строки (`num_row`) определяет порядок отображения позиций в документе + +--- + +**Версия:** 1.0 +**Дата:** 2025-12-11