From 2c2e830f0a2293d4329f4b9b67947733c305db33 Mon Sep 17 00:00:00 2001 From: Arsen Mirzaev Tatyano-Muradovich Date: Wed, 1 Jun 2022 14:24:48 +1000 Subject: [PATCH] =?UTF-8?q?=D0=BA=D1=80=D1=83=D0=BF=D0=BD=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/commands/TestController.php | 58 +++ .../system/controllers/CartController.php | 16 +- .../system/controllers/OrderController.php | 185 ++++---- .../system/controllers/ProfileController.php | 24 +- mirzaev/skillparts/system/models/Account.php | 24 +- .../system/models/AccountEdgeOrder.php | 16 +- mirzaev/skillparts/system/models/Document.php | 2 +- mirzaev/skillparts/system/models/Edge.php | 32 +- .../system/models/ImportEdgeSupply.php | 59 ++- mirzaev/skillparts/system/models/Order.php | 419 ++++++------------ .../system/models/OrderEdgeSupply.php | 19 +- mirzaev/skillparts/system/models/Product.php | 276 +++--------- .../system/models/ProductEdgeProductGroup.php | 13 + .../skillparts/system/models/ProductGroup.php | 160 ++++++- mirzaev/skillparts/system/models/Search.php | 10 +- mirzaev/skillparts/system/models/Supply.php | 339 +++++++------- .../system/models/traits/SearchByEdge.php | 12 +- .../skillparts/system/views/cart/index.php | 228 ++++------ .../system/views/invoice/order/pattern.php | 33 +- .../views/notification/system/orders/new.php | 2 +- .../skillparts/system/views/orders/index.php | 394 +++++++--------- .../system/views/profile/sidebar.php | 20 +- .../skillparts/system/views/search/index.php | 56 +-- mirzaev/skillparts/system/web/js/cart.js | 2 +- 24 files changed, 1161 insertions(+), 1238 deletions(-) diff --git a/mirzaev/skillparts/system/commands/TestController.php b/mirzaev/skillparts/system/commands/TestController.php index 2822d39..35072ef 100644 --- a/mirzaev/skillparts/system/commands/TestController.php +++ b/mirzaev/skillparts/system/commands/TestController.php @@ -6,6 +6,9 @@ use yii\console\Controller; use yii\console\ExitCode; use app\models\Invoice; +use app\models\Product; +use app\models\ProductGroup; +use app\models\ImportEdgeSupply; class TestController extends Controller { @@ -75,4 +78,59 @@ class TestController extends Controller return ExitCode::OK; } + + + public function actionAnalogs($_id = 'product/51987159') + { + + + return ExitCode::OK; + } + + public function actionWriteAnalog($_id = 'product/51987159', $analog = 'product/12051485') + { + // Инициализация товара + $product = Product::searchById($_id); + + // Инициализация аналога + $analog = Product::searchById($analog); + + if (!$group = ProductGroup::searchByProduct($product)) { + // Не найдена группа товаров + + // Запись новой группы + $group = ProductGroup::writeEmpty(active: true); + + // Запись товара в группу + $group->writeProduct($product); + } + + if ($_group = ProductGroup::searchByProduct($analog)) { + // Найдена друга группа у товара который надо добавить в группу + + // Перенос всех участников (включая целевой товар) + return $group->transfer($_group); + } else { + // Не найдена группа у товара который надо добавить в группу + + // Запись целевого товара в группу + return $group->writeProduct($analog); + } + + return ExitCode::OK; + } + + public function actionReadAnalog($_id = 'product/51987159') + { + var_dump((ProductGroup::searchByProduct(Product::searchById($_id))->searchProducts())); + + return ExitCode::OK; + } + + public function actionEdgeMax() + { + var_dump(ImportEdgeSupply::generateVersion()); + + return ExitCode::OK; + } } diff --git a/mirzaev/skillparts/system/controllers/CartController.php b/mirzaev/skillparts/system/controllers/CartController.php index 2bb7d2e..7188fde 100644 --- a/mirzaev/skillparts/system/controllers/CartController.php +++ b/mirzaev/skillparts/system/controllers/CartController.php @@ -86,26 +86,26 @@ class CartController extends Controller $account = Account::initAccount(); // Поиск корзины (текущего заказа) - $model = Order::searchByType(); + $data = Order::searchByType()[0] ?? null; - if (empty($model)) { + if (empty($data['order'])) { // Корзина не инициализирована // Инициализация - $model = new Order(); + $data['order'] = new Order(); - if ($model->save()) { + if ($data['order']->save()) { // Удалось инициализировать заказ // Подключение заказа к аккаунту - $model->connect($account); + $data['order']->connect($account); } else { throw new Exception('Не удалось инициализировать заказ'); } } // Инициализация содержимого корзины - $connections = $model->content(10, $page); + $data['supplies'] = $data['order']->supplies(10, $page, test: true); // Инициализация данных списка для выбора терминала $delivery_to_terminal_list = $account->genListTerminalsTo(); @@ -138,14 +138,14 @@ class CartController extends Controller yii::$app->response->format = Response::FORMAT_JSON; return [ - 'main' => $this->renderPartial('index', compact('account', 'model', 'connections', 'delivery_to_terminal_list')), + 'main' => $this->renderPartial('index', compact('account', 'data', 'delivery_to_terminal_list')), 'title' => 'Корзина', 'redirect' => '/cart', '_csrf' => yii::$app->request->getCsrfToken() ]; } - return $this->render('index', compact('account', 'model', 'connections', 'delivery_to_terminal_list')); + return $this->render('index', compact('account', 'data', 'delivery_to_terminal_list')); } public function actionEditComm(string $catn, string $prod): array|string|null diff --git a/mirzaev/skillparts/system/controllers/OrderController.php b/mirzaev/skillparts/system/controllers/OrderController.php index be3e197..c19038b 100644 --- a/mirzaev/skillparts/system/controllers/OrderController.php +++ b/mirzaev/skillparts/system/controllers/OrderController.php @@ -266,11 +266,10 @@ class OrderController extends Controller } // Инициализация заказов - $orders = Order::searchByType( + $data = Order::searchByType( type: $type, limit: 10, page: 1, - select: '{account_edge_order, order}', supplies: true, from: $from, to: $to, @@ -278,7 +277,6 @@ class OrderController extends Controller ); // Фильтрация - if ( !yii::$app->user->isGuest && yii::$app->user->identity->type === 'administrator' @@ -287,20 +285,20 @@ class OrderController extends Controller // Пользователь имеет доступ // Инициализация заказов для модератора - $moderator_orders = self::genOrdersForModeration(); + $moderator_data = Order::searchByType(account: '@all', type: 'requested', limit: 10, page: 1, supplies: true); } else { // Пользователь не имеет доступ // Инициализация заглушки - $moderator_orders = null; + $moderator_data = null; } + // Инициализация аккаунта + $account ?? $account = Account::initAccount(); + if (yii::$app->request->isPost) { // POST-запрос - // Инициализация аккаунта - $account ?? $account = Account::initAccount(); - // Конвертация из UNIXTIME в формат поддерживаемый календарём по спецификации HTML $from = DateTime::createFromFormat('U', (string) $from)->format('Y-m-d'); $to = DateTime::createFromFormat('U', (string) $to)->format('Y-m-d'); @@ -309,15 +307,15 @@ class OrderController extends Controller yii::$app->response->format = Response::FORMAT_JSON; return [ - 'main' => $this->renderPartial('/orders/index', compact('orders', 'moderator_orders', 'search', 'from', 'to', 'window') - + ['panel' => $this->renderPartial('/orders/search/panel', compact('account') + ['response' => @$orders[0]['supplies']] ?? null)]), + 'main' => $this->renderPartial('/orders/index', compact('data', 'moderator_data', 'account', 'search', 'from', 'to', 'window') + + ['panel' => $this->renderPartial('/orders/search/panel', compact('account') + ['data' => $data] ?? null)]), 'title' => 'Заказы', 'redirect' => '/orders', '_csrf' => yii::$app->request->getCsrfToken() ]; } - return $this->render('/orders/index', compact('orders', 'moderator_orders')); + return $this->render('/orders/index', compact('data', 'moderator_data', 'account')); } /** @@ -394,19 +392,19 @@ class OrderController extends Controller ]; // Инициализация корзины - if (!$model = Order::searchByType($account)) { + if (!$data = Order::searchByType($account)[0]) { // Корзина не найдена (текущий заказ) // Инициализация - $model = new Order(); - $model->save() or throw new Exception('Не удалось инициализировать заказ'); + $data['order'] = new Order(); + $data['order']->save() or throw new Exception('Не удалось инициализировать заказ'); // Запись ребра: АККАУНТ -> ЗАКАЗ - AccountEdgeOrder::write($account->readId(), $model->readId(), 'current') or $model->addError('errors', 'Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ'); + AccountEdgeOrder::write($account->readId(), $data['order']->readId(), 'current') or $data['order']->addError('errors', 'Не удалось инициализировать ребро: АККАУНТ -> ЗАКАЗ'); } // Если запись не удалась, то вернуть код: 500 Internal Server Error - $model->writeSupply($supply_id, $delivery_type, (int) $amount) or yii::$app->response->statusCode = 500; + $data['order']->writeSupply($supply_id, $delivery_type, (int) $amount) or yii::$app->response->statusCode = 500; return $return; } @@ -585,105 +583,98 @@ class OrderController extends Controller */ public function actionRequest(): string|array|null { - // Инициализация - $model = Order::searchByType(supplies: true); + // Инициализация аккаунта + $account = Account::initAccount(); - if (($account_edge_order = AccountEdgeOrder::searchByVertex(yii::$app->user->id, $model->readId(), 'current')[0]) ?? false) { - // Найдено ребро: АККАУНТ -> ЗАКАЗ + if (isset($account)) { + // Найден аккаунт + if ($account->filled() === true) { + // Заполнены все необходимые поля для оформления заказа (подразумевается прохождение второго этапа регистрации) - if ($order_edge_supply = OrderEdgeSupply::searchByDirection($account_edge_order->_to, type: 'write', limit: 500)) { - // Найдены рёбра: ЗАКАЗ -> ПОСТАВКА + // Инициализация данных о заказе и поставках + $data = Order::searchByType(supplies: true)[0]; - foreach ($order_edge_supply as $edge) { - // Перебор рёбер: ЗАКАЗ -> ПОСТАВКА + if (($account_edge_order = AccountEdgeOrder::searchByVertex(yii::$app->user->id, $data['order']->readId(), 'current')[0]) ?? false) { + // Найдено ребро: АККАУНТ -> ЗАКАЗ - if ($product = Product::searchBySupplyId($edge->_to)) { - // Найден товар + // Инициализация статуса необходимости реинициализации + $reinitialization = false; - // Проверка на активность товара - if ($product['stts'] === 'active'); - else $edge->delete(); + if ($order_edge_supply = OrderEdgeSupply::searchByDirection($account_edge_order->_to, type: 'write', limit: 500)) { + // Найдены рёбра: ЗАКАЗ -> ПОСТАВКА + + foreach ($order_edge_supply as $edge) { + // Перебор рёбер: ЗАКАЗ -> ПОСТАВКА + + if ($product = Product::searchBySupplyId($edge->_to)) { + // Найден товар + + if ($product['stts'] !== 'active') { + // Не активен товар + + // Удаление из заказа + $edge->delete(); + + // Статус необходимости реинициализации + $reinitialization = true; + } + } + } + } + + // Реинициализация + if ($reinitialization) $data = Order::searchByType(supplies: true); + + // Запись + $account_edge_order->type = 'requested'; + + if ($account_edge_order->update() > 0) { + // Удалось сохранить изменения + + // Запись в журнал + $data['order']->journal('requested'); + + Invoice::generate($data['order']->_key, $this->renderPartial('/invoice/order/pattern', [ + 'buyer' => [ + 'id' => yii::$app->user->identity->_key, + 'info' => 'Неизвестно' + ], + 'data' => $data, + 'date' => $account_edge_order->date ?? time() + ])); + + // Отправка уведомлений модераторам + Notification::_write($this->renderPartial('/notification/system/orders/new', ['id' => $account_edge_order->_key]), true, '@auth', Notification::TYPE_MODERATOR_ORDER_NEW); + + // Отправка уведомления покупателю + Notification::_write($this->renderPartial('/notification/system/orders/new', ['id' => $account_edge_order->_key]), true, $account->_key, Notification::TYPE_NOTICE); } } - } - // Реиницилазация - $model = Order::searchByType(supplies: true); + return $this->actionIndex(); + } else { + // Не заполнены все необходимые поля для оформления заказа (подразумевается прохождение второго этапа регистрации) - // Запись - $account_edge_order->type = 'requested'; + var_dump($account->getErrors()); - if ($account_edge_order->update()) { - // Удалось сохранить изменения + foreach ($account->getErrors() as $parameter => $errors) { + // Перебор параметров с ошибками - // Запись в журнал - $model->journal('requested'); + foreach ($errors as $error) { + // Перебор ошибок - // Инициализация буфера поставок - $supplies = []; + // Инициализация ярлыка + $label = $account->getAttributeLabel($parameter) ?? $parameter; - foreach ($model['supplies'] as $supply) { - // Перебор поставок - - $supplies[] = [ - 'title' => $supply['supply']['catn'], - 'delivery' => 0, - 'amount' => [ - // 'value' => $supply['amount'][$supply['order_edge_supply'][]] ?? 0, - // 'value' => $supply['amount'] ?? 0, - 'value' => 0, - 'unit' => 'шт' - ], - 'cost' => [ - // 'value' => $supply['cost'] ?? 0, - 'value' => 0, - 'unit' => 'руб' - ], - 'type' => 'supply' - ]; + // Отправка уведомления покупателю + Notification::_write($error . " ($label)", account: $account->_key, type: Notification::TYPE_ERROR); + } } - Invoice::generate($model->_key, $this->renderPartial('/invoice/order/pattern', [ - 'buyer' => [ - 'id' => yii::$app->user->identity->_key, - 'info' => 'Неизвестно' - ], - 'order' => [ - 'id' => $model->_key, - 'date' => $account_edge_order->date ?? time(), - 'entries' => $supplies - ] - ])); - - // Отправка уведомлений модераторам - Notification::_write($this->renderPartial('/notification/system/orders/new', ['id' => $account_edge_order->_key]), true, '@auth', Notification::TYPE_MODERATOR_ORDER_NEW); + return null; } } - - return $this->actionIndex(); - } - - /** - * Генерация данных заказов для модераторов - * - * Включает поиск запрошенных заказов и связанных с ними поставках - * - * @return array ['order' => array, 'order_edge_account' => array, 'supplies' => array] - * - * @todo Уничтожить заменив на Order::searchByType(supplies: true) - */ - protected static function genOrdersForModeration(int $page = 1): array - { - $orders = Order::searchByType(account: '@all', type: 'requested', limit: 10, page: 1, select: '{account_edge_order, order}'); - - foreach ($orders as &$order) { - // Перебор заказов - - $order['supplies'] = Order::searchById($order['order']['_id'])->content(10, $page); - } - - return $orders; } /** diff --git a/mirzaev/skillparts/system/controllers/ProfileController.php b/mirzaev/skillparts/system/controllers/ProfileController.php index 89cd1d3..f566f53 100644 --- a/mirzaev/skillparts/system/controllers/ProfileController.php +++ b/mirzaev/skillparts/system/controllers/ProfileController.php @@ -791,6 +791,8 @@ class ProfileController extends Controller return self::syncGeolocationWithDellin($account); } + + return false; } /** @@ -1227,6 +1229,9 @@ class ProfileController extends Controller * Удалить склад * * @return array|string|null + * + * @todo + * 1. Удаление всех ImportEdgeSupply с устаревшими версиями */ public function actionWarehousesDelete(): array|string|null { @@ -1277,11 +1282,22 @@ class ProfileController extends Controller foreach (Supply::searchByImport($import->readId(), limit: 9999) as $supply) { // Перебор найденных поставок - if (ImportEdgeSupply::searchBySupply($supply, limit: 1)?->delete() === 1) { - // Удалено ребро: ИНСТАНЦИЯ ПОСТАВКИ -> ПОСТАВКА + if ($edge = ImportEdgeSupply::searchBySupply($supply)) { + // Найдено ребро: ИНСТАНЦИЯ ПОСТАВКИ -> ПОСТАВКА - // Удаление поставки - if ($supply->delete() === 1) ++$deleted; + // Инициализация версии + $version = $edge->vrsn; + + if ($edge->delete() === 1) { + // Удалено ребро: ИНСТАНЦИЯ ПОСТАВКИ -> ПОСТАВКА + + if (ImportEdgeSupply::searchMaxVersion($supply) <= $version) { + // Не найдена более новая версия поставки (обрабатываемая считается актуальной) + + // Удаление поставки + if ($supply->delete() === 1) ++$deleted; + } + } } } diff --git a/mirzaev/skillparts/system/models/Account.php b/mirzaev/skillparts/system/models/Account.php index 7c3ee36..23c093c 100644 --- a/mirzaev/skillparts/system/models/Account.php +++ b/mirzaev/skillparts/system/models/Account.php @@ -989,7 +989,7 @@ class Account extends Document implements IdentityInterface, PartnerInterface public static function isModer($account = null): bool { if ($account = self::initAccount($account)) { - // Аккаунт инициализирован + // Инициализирован аккаунт if ($account->type === 'moderator') { return true; @@ -1022,4 +1022,26 @@ class Account extends Document implements IdentityInterface, PartnerInterface default => 'Неизвестно' }; } + + public static function filled($account = null): bool|self + { + if ($account = self::initAccount($account)) { + // Инициализирован аккаунт + + // Проверка на заполненность обязательных полей + if (empty($account->name)) $account->addError('name', 'Не заполнено необходимое поле для заказа'); + if (empty($account->boss)) $account->addError('boss', 'Не заполнено необходимое поле для заказа'); + if (empty($account->simc)) $account->addError('simc', 'Не заполнено необходимое поле для заказа'); + if (empty($account->comp)) $account->addError('comp', 'Не заполнено необходимое поле для заказа'); + if (empty($account->mail)) $account->addError('mail', 'Не заполнено необходимое поле для заказа'); + if (empty($account->taxn)) $account->addError('taxn', 'Не заполнено необходимое поле для заказа'); + if (empty($account->cntg)) $account->addError('cntg', 'Не заполнено необходимое поле для заказа'); + if (empty($account->fadd)) $account->addError('fadd', 'Не заполнено необходимое поле для заказа'); + if (empty($account->ladd)) $account->addError('ladd', 'Не заполнено необходимое поле для заказа'); + + return $account->hasErrors() ? $account : true; + } + + return false; + } } diff --git a/mirzaev/skillparts/system/models/AccountEdgeOrder.php b/mirzaev/skillparts/system/models/AccountEdgeOrder.php index b971752..5d59965 100644 --- a/mirzaev/skillparts/system/models/AccountEdgeOrder.php +++ b/mirzaev/skillparts/system/models/AccountEdgeOrder.php @@ -24,10 +24,20 @@ class AccountEdgeOrder extends Edge return self::find()->where(['_to' => $order_id])->limit($limit)->all(); } - public static function convertStatusToRussian(string $status): string + /** + * Генерация ярлыка на русском языке для статуса заказа + * + * @param string $status Статус заказа + * + * @return string Ярлык статуса на русском языке + */ + public static function statusToRussian(string $status = ''): string { - return match($status) { - 'accepted' => 'Доставляется', + return match ($status) { + 'requested' => 'Запрошен', + 'accepted' => 'Ожидается отправка', + 'going' => 'Доставляется', + 'completed' => 'Завершен', default => 'Обрабатывается' }; } diff --git a/mirzaev/skillparts/system/models/Document.php b/mirzaev/skillparts/system/models/Document.php index c6528d1..8ae593a 100644 --- a/mirzaev/skillparts/system/models/Document.php +++ b/mirzaev/skillparts/system/models/Document.php @@ -150,7 +150,7 @@ abstract class Document extends ActiveRecord return static::findOne(['_id' => $_id]); } - public static function readLast(): ?static + public static function readLast(): static|null|bool { return static::find()->orderBy(['DESC'])->one(); } diff --git a/mirzaev/skillparts/system/models/Edge.php b/mirzaev/skillparts/system/models/Edge.php index 8bc59d7..faf2054 100644 --- a/mirzaev/skillparts/system/models/Edge.php +++ b/mirzaev/skillparts/system/models/Edge.php @@ -167,20 +167,44 @@ abstract class Edge extends Document /** * Поиск рёбер по направлению */ - public static function searchByDirection(string $target, string $direction = 'OUTBOUND', string $type = '', int $limit = 1): static|array|null + public static function searchByDirection(string $_from, string $direction = 'OUTBOUND', string $type = '', array $where = [], int $limit = 1): static|array|null { if (str_contains($direction, 'OUTBOUND')) { // Исходящие рёбра - $query = static::find()->where(['_from' => $target, 'type' => $type]); + if (isset($where)) { + // Получен параметр $where + + // Реинициализация + $where = array_merge($where + ['_from' => $_from]); + } else { + // Не получен параметр $where + + // Реинициализация + $where = array_merge(['_from' => $_from, 'type' => $type]); + } + + $query = static::find()->where($where); } else if (str_contains($direction, 'INBOUND')) { // Входящие рёбра - $query = static::find()->where(['_to' => $target, 'type' => $type]); + if (isset($where)) { + // Получен параметр $where + + // Реинициализация + $where = array_merge($where + ['_to' => $_from]); + } else { + // Не получен параметр $where + + // Реинициализация + $where = array_merge(['_to' => $_from, 'type' => $type]); + } + + $query = static::find()->where($where); } else if (str_contains($direction, 'ANY')) { // Исходящие и входящие рёбра - return static::searchByDirection(target: $target, direction: 'OUTBOUND', type: $type, limit: $limit) + static::searchByDirection(target: $target, direction: 'INBOUND', type: $type, limit: $limit); + return static::searchByDirection(_from: $_from, direction: 'OUTBOUND', type: $type, where: $where, limit: $limit) + static::searchByDirection(_from: $_from, direction: 'INBOUND', type: $type, where: $where, limit: $limit); } if ($limit < 2) { diff --git a/mirzaev/skillparts/system/models/ImportEdgeSupply.php b/mirzaev/skillparts/system/models/ImportEdgeSupply.php index f9f50ca..a4ef76d 100644 --- a/mirzaev/skillparts/system/models/ImportEdgeSupply.php +++ b/mirzaev/skillparts/system/models/ImportEdgeSupply.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace app\models; +use mirzaev\yii2\arangodb\Query; + /** * Связь инстанций импорта с поставками */ @@ -25,6 +27,7 @@ class ImportEdgeSupply extends Edge return array_merge( parent::attributes(), [ + 'vrsn' ] ); } @@ -37,6 +40,7 @@ class ImportEdgeSupply extends Edge return array_merge( parent::attributeLabels(), [ + 'vrsn' => 'Версия' ] ); } @@ -49,12 +53,63 @@ class ImportEdgeSupply extends Edge return array_merge( parent::rules(), [ + [ + [ + 'vrsn' + ], + 'integer', + 'message' => '{attribute} должен быть числом.' + ] ] ); } - public static function searchBySupply(Supply $supply): static + /** + * Перед сохранением + * + * @todo Подождать обновление от ебаного Yii2 и добавить + * проверку типов передаваемых параметров + */ + public function beforeSave($data): bool { - return static::find()->where(['_to' => $supply->readId()])->one(); + if (parent::beforeSave($data)) { + if ($this->isNewRecord) { + } + + return true; + } + + return false; + } + + /** + * Поиск максимальной версии + * + * Ищет максимальную версию у поставок + * + * @param Supply $supply Поставка + * + * @return int Версия, если найдена + */ + public static function searchMaxVersion(Supply $supply): ?int + { + return static::find()->execute("RETURN MAX( + FOR import_edge_supply IN import_edge_supply + FILTER (import_edge_supply._to == 'supply/$supply->_key') + LIMIT 0,999 + RETURN import_edge_supply.vrsn + )")[0] ?? null; + } + + /** + * Поиск по поставке + * + * @param Supply $supply + * + * @return static|null + */ + public static function searchBySupply(Supply $supply): ?static + { + return static::find()->where(['_to' => $supply->readId()])->one()[0] ?? null; } } diff --git a/mirzaev/skillparts/system/models/Order.php b/mirzaev/skillparts/system/models/Order.php index 6d3e7b9..2e5b44d 100644 --- a/mirzaev/skillparts/system/models/Order.php +++ b/mirzaev/skillparts/system/models/Order.php @@ -274,7 +274,7 @@ class Order extends Document implements DocumentInterface int|null $from = null, int|null $to = null, bool $count = false - ): self|int|array|null { + ): int|array|null { // Инициализация аккаунта if (empty($account) && isset(yii::$app->user->identity)) { // Данные не переданы @@ -335,7 +335,7 @@ class Order extends Document implements DocumentInterface } // Поиск заказов в базе данных - $return = self::searchByEdge( + $orders = self::searchByEdge( from: 'account', to: 'order', subquery_where: $subquery_where, @@ -346,73 +346,81 @@ class Order extends Document implements DocumentInterface sort: ['DESC'], select: $select, direction: 'INBOUND', - count: !$supplies && $count ? true : false + count: !$supplies && $count ); if (!$supplies && $count) { // Запрошен подсчет заказов - return $return; + return $orders; + } + + // Инициализация буфера возврата + $return = []; + + // Инициализация архитектуры буфера вывода + foreach ($orders as $key => $order) { + // Перебор заказов + + // Запись в буфер возврата + $return[$key]['order'] = $order; } if ($supplies) { // Запрошен поиск поставок - foreach ($return as &$container) { + foreach ($return as $key => &$container) { // Перебор заказов - if ($container instanceof Order) { - $buffer = $container; + + if ($container['order'] instanceof Order) { + // Инстанция заказа + + // Инициализация заказа + $order = $container['order']; } else { + // Массив с заказом (подразумевается) + // Инициализация настроек $config = $container['order']; unset($config['_id'], $config['_rev'], $config['_id']); - // Инициализация буфера - $buffer = new Order($config); + // Инициализация заказа + $order = new Order($config); } // Чтение полного содержания - $container['supplies'] = $buffer->content($limit, $page, $search, count: $count); + $return[$key]['supplies'] = $order->supplies($limit, $page, $search, count: $count); if ($count) { - // Запрошен подсчет поставок + // Запрошен подсчет поставок (переделать под подсчёт) - return $container['supplies']; + return $return[$key]['supplies']; } } } - return $limit === 1 ? $return[0] ?? null : $return; + return $return; } - // /** - // * Объеденить дубликаты поставок - // * - // * @return array Данные заказа - // */ - // public function mergeDuplicateSupplies(array $order): array { - // foreach ($orders as $order) { - - // } - // } - /** - * Поиск содержимого заказа + * Поиск содержимых поставок заказа * * @todo В будущем возможно заказ не только поставок реализовать * Переписать реестр и проверку на дубликаты, не понимаю как они работают */ - public function content(int $limit = 1, int $page = 1, string|null $search = null, bool $count = false): Supply|int|array|null + public function supplies(int $limit = 1, int $page = 1, string|null $search = null, bool $count = false, $test = false): Supply|int|array|null { + // Инициализация аккаунта + $account = Account::initAccount(); + // Генерация сдвига по запрашиваемым данным (пагинация) $offset = $limit * ($page - 1); if (!empty($search)) { // Передан поиск по продуктам в заказах - // Добавить ограничитель - + // Запись ограничения по максимальному значению $limit = 9999; } @@ -426,286 +434,125 @@ class Order extends Document implements DocumentInterface 'order._id' => $this->readId() ] ], - foreach: ['edge' => 'order_edge_supply'], - where: 'edge._to == supply._id', + where: 'order_edge_supply != []', limit: $limit, offset: $offset, filterStart: ['catn' => $search], direction: 'INBOUND', - select: '{supply, order_edge_supply}', + select: '{order_edge_supply}', count: $count ); if ($count) { - // Запрошен подсчет + // Подсчёт запрошен return $connections; } - // Рекурсивная фильтрации соединений - recursive_connections_filtration: - - // Инициализация статуса об изменении списка поставок - $changed = false; - - // Инициализация глобального буфера поставок - $buffer_global = []; - - // Разделение поставок по типам доставки - // foreach ($connections as &$connection) { - // // Перебор поставок - - // if (count($connection['order_edge_supply'] ?? []) > 1) { - // // Поставок более чем одна - - // // Инициализация (выбор значения для фильтрации) - // $target_dlvr_type = reset($connection['order_edge_supply'])['dlvr']['type'] ?? 'auto'; - - // // Инициализация буфера поставок - // $buffer = $connection; - // unset($buffer['order_edge_supply']); - - // foreach ($connection['order_edge_supply'] as $key => &$order_edge_supply) { - // // Перебор рёбер: ЗАКАЗ -> ПОСТАВКА - - // // if ((empty($order_edge_supply['dlvr']['type']) && $target_dlvr_type === 'auto') - // // || (isset($order_edge_supply['dlvr']['type']) && $order_edge_supply['dlvr']['type'] !== $target_dlvr_type) - // // ) { - // if ((isset($order_edge_supply['dlvr']['type']) && $order_edge_supply['dlvr']['type'] === $target_dlvr_type) - // || (empty($order_edge_supply['dlvr']['type']) && $target_dlvr_type === 'auto') - // ) { - // // Тип доставки для одной поставки отличается - - // // Запись в буфер - // $buffer['order_edge_supply'][$key] = $order_edge_supply; - - // // Удаление из списка - // unset($connection['order_edge_supply'][$key]); - - // // Перезапись статуса - // $changed = true; - // } - // } - - // // Запись из буфера в глобальный буфер - // $buffer_global[] = $buffer; - // } - // } - - // $connections = array_merge($connections, $buffer_global); - - // if ($changed) goto recursive_connections_filtration; - // Инициализация реестра дубликатов - $registry = []; + $supplies = []; - // Подсчёт и перестройка массива для очистки от дубликатов foreach ($connections as $key => &$connection) { - // Перебор поставок + // Перебор объектов для заказа - if (in_array([ - 'catn' => $connection['supply']['catn'], - 'type' => $type = reset($connection['order_edge_supply'])['dlvr']['type'] ?? 'auto' - ], $registry)) { - // Если данная поставка найдена в реестре - - // Удаление - unset($connections[$key]); - - // Пропуск итерации - continue; - } - - // Повторный перебор для поиска дубликатов - foreach ($connections as &$connection_for_check) { - if ($connection == $connection_for_check) { - // Найден дубликат - - // Запись в реестр - $registry[$key] = [ - 'catn' => $connection_for_check['supply']['catn'], - 'type' => $type - ]; - } - } - - // Инициализация счетчиков - $connection['amount'] = [ - 'auto' => 0, - 'avia' => 0 - ]; - - // Подсчет количества поставок foreach ($connection['order_edge_supply'] as $edge) { // Перебор связанных поставок - if ($edge['dlvr']['type'] === 'auto') ++$connection['amount']['auto']; - if ($edge['dlvr']['type'] === 'avia') ++$connection['amount']['avia']; + // Инициализация связанной с заказом поставки + $supply = Supply::searchById($edge['_to']); + + // Инициализация поставщика в буфере + if (empty($supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']])) $supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']] = [ + 'supply' => $supply, + 'account' => Account::searchBySupplyId($supply->readId()), + 'product' => Product::searchBySupplyId($supply->readId()), + 'currency' => 'руб', + 'amount' => 1 + ]; + else ++$supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']]['amount']; + + // Инициализация буфера с обрабатываемой поставкой + $buffer = &$supplies[$supply->prod][$supply->catn][$edge['dlvr']['type']]; + + // Запись ребра + $buffer['edge'] = $edge; + + if (empty($buffer['supply']->cost) || $buffer['supply']->cost < 1) { + // Если стоимость не найдена или равна нулю (явная ошибка) + + // Удаление из базы данных + $this->deleteSupply($buffer['supply']); + + // Инициализация стоимости товара для уведомления (чтобы там не было NULL) + $cost = $buffer['supply']->cost ?? 0; + + // Отправка уведомлений покупателю + Notification::_write("Стоимость товара $supply->catn равна $cost", account: $account->_key, type: Notification::TYPE_ERROR); + Notification::_write("Товар $supply->catn удалён", account: $account->_key, type: Notification::TYPE_ERROR); + + // Отправка уведомления поставщику + + // Отправка уведомления модератору + + // Удаление из списка + unset($connections[$key]); + + // Выход из цикла + break; + } + + try { + // Инициализация данных геолокации + try { + $from = (int) $buffer['account']['opts']['delivery_from_terminal'] ?? empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default; + } catch (Exception $e) { + $from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default; + } + + try { + $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36; + } catch (Exception $e) { + $to = 36; + } + + if (($buffer_connection = $buffer['product']['bffr']["$from-$to-" . $edge['dlvr']['type']] ?? false) && time() < $buffer_connection['expires']) { + // Найдены данные доставки в буфере и их срок хранения не превышен, информация актуальна + + // Запись в буфер вывода + $buffer['delivery'] = $buffer_connection['data']; + } else { + // Инициализация инстанции продукта в базе данных (реинициализация под ActiveRecord) + $product = Product::searchByCatnAndProd($buffer['product']['catn'], $buffer['product']['prod']); + + // Инициализация доставки Dellin (автоматическая) + $product->bffr = ($product->bffr ?? []) + [ + "$from-$to-" . $edge['dlvr']['type'] => [ + 'data' => $buffer['delivery'] = Dellin::calcDeliveryAdvanced( + $from, + $to, + (int) ($buffer['product']['wght'] ?? 0), + (int) ($buffer['product']['dmns']['x'] ?? 0), + (int) ($buffer['product']['dmns']['y'] ?? 0), + (int) ($buffer['product']['dmns']['z'] ?? 0), + avia: $edge['dlvr']['type'] === 'avia' + ), + 'expires' => time() + 86400 + ] + ]; + + // Отправка в базу данных + $product->update(); + } + + // Запись цены (цена поставки + цена доставки + наша наценка) + $buffer['cost'] = ($supply->cost ?? $supply->onec['Цены']['Цена']['ЦенаЗаЕдиницу'] ?? throw new exception('Не найдена цена товара')) + ($buffer['delivery']['price']['all'] ?? $buffer['delivery']['price']['one'] ?? 0) + ($settings['increase'] ?? 0) ?? 0; + } catch (Exception $e) { + $buffer['delivery'] = null; + } } } - // Инициализация дополнительных данных - foreach ($connections as $key => &$connection) { - // Перебор поставок - - // Чтение стоимости - $cost = Supply::readCostById($connection['supply']['_id']); - - if (empty($cost) || $cost < 1) { - // Если стоимость не найдена или равна нулю (явная ошибка) - - // Удаление из базы данных - $this->deleteSupply($connection['supply']['_id']); - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Добавить уведомление об ошибочном товаре - - // Удаление из списка - unset($connections[$key]); - - // Пропуск итерации - continue; - } - - // Поиск ребра до аккаунта - $connection['account'] = Account::searchBySupplyId($connection['supply']['_id']); - - // Поиск привязанного товара - $connection['product'] = Product::searchBySupplyId($connection['supply']['_id']); - - // if (empty(reset($connection['order_edge_supply'])['dlvr']['type']) || reset($connection['order_edge_supply'])['dlvr']['type'] === 'auto') { - // // Доставка автоматическая - - try { - // Инициализация данных геолокации - try { - $from = (int) $connection['account']['opts']['delivery_from_terminal'] ?? empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default; - } catch (Exception $e) { - $from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default; - } - - try { - $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36; - } catch (Exception $e) { - $to = 36; - } - - if ($buffer_connection = $connection['product']['bffr']["$from-$to"] ?? false) { - // Найдены данные доставки в буфере - - if (time() < $buffer_connection['expires']) { - // Срок хранения не превышен, информация актуальна - - // Запись в буфер вывода - $connection['delivery']['auto'] = $buffer_connection['data']; - } - } else { - // Инициализация инстанции продукта в базе данных - $product = Product::searchByCatnAndProd($connection['product']['catn'], $connection['product']['prod']); - - // Инициализация доставки Dellin (автоматическая) - $product->bffr = ($product->bffr ?? []) + [ - "$from-$to" => [ - 'data' => $connection['delivery']['auto'] = Dellin::calcDeliveryAdvanced( - $from, - $to, - (int) ($connection['product']['wght'] ?? 0), - (int) ($connection['product']['dmns']['x'] ?? 0), - (int) ($connection['product']['dmns']['y'] ?? 0), - (int) ($connection['product']['dmns']['z'] ?? 0), - count($connection['order_edge_supply']) - ), - 'expires' => time() + 86400 - ] - ]; - - // Отправка в базу данных - $product->update(); - } - } catch (Exception $e) { - $connection['delivery']['auto']['error'] = true; - - // echo '
';
-                // var_dump($e->getMessage());
-                // var_dump($e->getTrace());
-                // var_dump($e->getFile());
-                // die;
-
-                // var_dump(json_decode($e->getMessage(), true)['errors']); die;
-            }
-
-            // Запись цены (цена поставки + цена доставки + наша наценка)
-            $connection['cost']['auto'] = ($cost ?? $connection['supply']->onec['Цены']['Цена']['ЦенаЗаЕдиницу']) + ($connection['delivery']['auto']['price']['all'] ?? $connection['delivery']['auto']['price']['one'] ?? 0) + ($settings['increase'] ?? 0) ?? 0;
-            // } else {
-            // Доставка самолётом
-
-            try {
-                // Инициализация данных геолокации
-                try {
-                    $from = (int) $connection['account']['opts']['delivery_from_terminal'] ?? empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
-                } catch (Exception $e) {
-                    $from = empty(Settings::searchActive()->delivery_from_default) ? 36 : (int) Settings::searchActive()->delivery_from_default;
-                }
-
-                try {
-                    $to = (int) yii::$app->user->identity->opts['delivery_to_terminal'] ?? 36;
-                } catch (Exception $e) {
-                    $to = 36;
-                }
-
-                if ($buffer_connection = $connection['product']['bffr']["$from-$to-avia"] ?? false) {
-                    // Найдены данные доставки в буфере
-
-                    if (time() < $buffer_connection['expires']) {
-                        // Срок хранения не превышен, информация актуальна
-
-                        // Запись в буфер вывода
-                        $connection['delivery']['avia'] = $buffer_connection['data'];
-                    }
-                } else {
-                    // Инициализация инстанции продукта в базе данных
-                    $product = Product::searchByCatnAndProd($connection['product']['catn'], $connection['product']['prod']);
-
-                    // Инициализация доставки Dellin (автоматическая)
-                    $product->bffr = ($product->bffr ?? []) + [
-                        "$from-$to-avia" => [
-                            'data' => $connection['delivery']['avia'] = Dellin::calcDeliveryAdvanced(
-                                $from,
-                                $to,
-                                (int) ($connection['product']['wght'] ?? 0),
-                                (int) ($connection['product']['dmns']['x'] ?? 0),
-                                (int) ($connection['product']['dmns']['y'] ?? 0),
-                                (int) ($connection['product']['dmns']['z'] ?? 0),
-                                count($connection['order_edge_supply']),
-                                avia: true
-                            ),
-                            'expires' => time() + 86400
-                        ]
-                    ];
-
-                    // Отправка в базу данных
-                    $product->update();
-                }
-            } catch (Exception $e) {
-                $connection['delivery']['avia']['error'] = true;
-
-                // var_dump($e->getMessage());
-                // var_dump($e->getTrace());
-                // var_dump($e->getFile());
-                // die;
-
-                // var_dump(json_decode($e->getMessage(), true)['errors']); die;
-            }
-
-            // Запись цены (цена поставки + цена доставки + наша наценка)
-            $connection['cost']['avia'] = ($cost ?? $connection['supply']->onec['Цены']['Цена']['ЦенаЗаЕдиницу']) + ($connection['delivery']['avia']['price']['all'] ?? $connection['delivery']['avia']['price']['one'] ?? 0) + ($settings['increase'] ?? 0) ?? 0;
-            // }
-
-            // Запись валюты
-            $connection['currency'] = 'руб';
-        }
-
-        return $connections;
+        return $supplies;
     }
 
     /**
diff --git a/mirzaev/skillparts/system/models/OrderEdgeSupply.php b/mirzaev/skillparts/system/models/OrderEdgeSupply.php
index e8b8976..a3ad355 100644
--- a/mirzaev/skillparts/system/models/OrderEdgeSupply.php
+++ b/mirzaev/skillparts/system/models/OrderEdgeSupply.php
@@ -143,11 +143,22 @@ class OrderEdgeSupply extends Edge
         return [];
     }
 
-    public static function convertStatusToRussian(string|int $status): string
+
+    /**
+     * Генерация ярлыка на русском языке для статуса заказа поставки
+     *
+     * @param string $status Статус заказа поставки
+     *
+     * @return string Ярлык статуса на русском языке
+     */
+    public static function statusToRussian(string $status = ''): string
     {
-        return match($status) {
-            'accepted', 1 => 'Ожидается отправка',
-            default => 'Запрошен'
+        return match ($status) {
+            'requested' => 'Запрошен',
+            'accepted' => 'Ожидается отправка',
+            'going' => 'Доставляется',
+            'completed' => 'Завершен',
+            default => 'Обрабатывается'
         };
     }
 }
diff --git a/mirzaev/skillparts/system/models/Product.php b/mirzaev/skillparts/system/models/Product.php
index 542cb95..859e246 100644
--- a/mirzaev/skillparts/system/models/Product.php
+++ b/mirzaev/skillparts/system/models/Product.php
@@ -59,11 +59,6 @@ class Product extends Document
      */
     public UploadedFile|string|array|null $file_image = null;
 
-    /**
-     * Группа в которой состоит товар
-     */
-    public ProductGroup|null $group = null;
-
     /**
      * Имя коллекции
      */
@@ -113,7 +108,6 @@ class Product extends Document
                 'stts' => 'Статус',
                 'file_excel' => 'Документ (file_excel)',
                 'file_image' => 'Изображение (file_image)',
-                'group' => 'Группа (group)',
                 'account' => 'Аккаунт'
             ]
         );
@@ -233,7 +227,7 @@ class Product extends Document
     }
 
     /**
-     * Запись пустого продукта
+     * Запись пустого товара
      */
     public static function writeEmpty(string $catn, string $prod = 'Неизвестный', bool $active = false): ?self
     {
@@ -275,7 +269,6 @@ class Product extends Document
                     };
                 }
 
-
                 if (!file_exists(YII_PATH_PUBLIC . $catalog_h150 = '/img/products/' . $this->_key . '/h150')) {
                     // Директория для обложек изображений продукта не найдена
 
@@ -462,138 +455,35 @@ class Product extends Document
         )[0];
     }
 
-    /**
-     * Найти все аналоги
-     *
-     * @param string $prod Производитель
-     * @param string $catn Артикул
-     * @param int $limit Ограничение по количеству
-     *
-     * @return array|null Найденные аналоги
-     */
-    public static function searchAnalogs(string $prod, string $catn, int $limit = 30): ?array
-    {
-        // Инициализация буфера возврата
-        $return = [];
-
-        // Поиск ключей аналогов
-        $analogs = ProductEdgeProduct::searchConnections(self::searchByCatnAndProd($catn, $prod)?->_key, $limit);
-
-        foreach ($analogs as $analog) {
-            // Перебор найденных ключей (_key) аналогов
-
-            if ($analog = Product::searchById(self::collectionName() . "/$analog")) {
-                // Найден товар
-
-                if (isset($analog->stts) && $analog->stts === 'active') {
-                    // Пройдена проверка по статусу
-
-                    // Запись в буфер вывода
-                    $return[] = $analog;
-                }
-            } else {
-                // Не найден товар
-            }
-        }
-
-        return $return;
-    }
-
-    /**
-     * Синхронизация аналогов
-     *
-     * Связывает с товаром и связанными с ним товарами в одну группу
-     *
-     * @param self $to Цель
-     *
-     * @return array Созданные рёбра
-     */
-    public function synchronization(self $to): array
-    {
-        // Инициализация буфера сохранённых рёбер
-        $edges = [];
-
-        // Инициализация списка товаров в группе и буфера для связей "всех со всеми"
-        $products = $_products =  array_unique(array_merge($this->connections(), $to->connections(), [Product::collectionName() . "/$to->_key"]));
-
-        foreach ($products as $product) {
-            // Перебор связей для создания связей во всех товарах
-
-            // Удаление обрабатываемого товара из буферного списка товаров
-            // unset($_products[array_search($product, $_products)]);
-
-            foreach ($_products as $_product) {
-                // Перебор товаров из буфера
-
-                // if ($from = self::searchById($product)) {
-                //     // Товар найден
-                // } else {
-                //     if ($from = Product::writeEmpty($product)) {
-                //         // Удалось записать товар
-                //     } else {
-                //         // Не удалось записать товар
-
-                //         continue;
-                //     }
-                // }
-
-                if ($to = self::searchById($_product)) {
-                    // Товар найден
-                } else {
-                    if ($to = Product::writeEmpty($_product)) {
-                        // Удалось записать товар
-                    } else {
-                        // Не удалось записать товар
-
-                        continue;
-                    }
-                }
-
-                if ($edge = $this->connect($to)) {
-                    // Ребро создано
-
-                    // Запись в буфер
-                    $edges[] = $edge;
-                }
-            }
-        }
-
-        return $edges;
-    }
-
     /**
      * Подключение аналога
      *
-     * @param self $to Цель
+     * @param Product $target Товар который надо подключить
      *
-     * @return ProductEdgeProduct|null Ребро между товарами, если создалось
+     * @return ProductEdgeProductGroup|null Ребро между товаром и группой, если создалось
      */
-    public function connect(self $to): ?ProductEdgeProduct
+    public function connect(Product $product): ?ProductEdgeProductGroup
     {
-        if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", type: 'analogue')) {
-            // Найдено ребро
-        } else if (ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", type: 'analogue')) {
-            // Найдено ребро (наоборот)
+        if (!$group = ProductGroup::searchByProduct($this)) {
+            // Не найдена группа товаров
 
-            // !!! Вероятно эта проверка здесь не нужна, так как мы знаем входные данные
+            // Запись новой группы
+            $group = ProductGroup::writeEmpty(active: true);
+
+            // Запись товара в группу
+            $group->writeProduct($this);
+        }
+
+        if ($_group = ProductGroup::searchByProduct($product)) {
+            // Найдена другая группа у товара который надо добавить в группу
+
+            // Перенос всех участников (включая целевой товар)
+            return $group->transfer($_group);
         } else {
-            // Не найдены ребра
+            // Не найдена группа у товара который надо добавить в группу
 
-            if ($edge = ProductEdgeProduct::write(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", data: ['type' => 'analogue'])) {
-                // Ребро сохранено
-
-                // Запись в журнал о соединении
-                $this->journal('connect analogue', [
-                    'to' => Product::collectionName() . "/$to->_key"
-                ]);
-
-                // Запись в журнал о соединении
-                $to->journal('connect analogue', [
-                    'from' => Product::collectionName() . "/$this->_key"
-                ]);
-
-                return $edge;
-            }
+            // Запись целевого товара в группу
+            return $group->writeProduct($product);
         }
 
         return null;
@@ -602,67 +492,21 @@ class Product extends Document
     /**
      * Отключение аналога
      *
-     * @param self|null $to Цель (если null, то целью являются все подключенные аналоги)
-     * @param bool $all Удалить соединения со всеми членами группы
+     * @return bool Статус выполнения
      */
-    public function disconnect(self|null $to = null, bool $all = true): bool
+    public function disconnect(): bool
     {
-        if (isset($to)) {
-            // Передана цель для удаления (из её группы)
+        if ($group = ProductGroup::searchByProduct($this)) {
+            // Найдена группа товаров
 
-            if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$this->_key", Product::collectionName() . "/$to->_key", type: 'analogue')[0]) {
-                // Найдено ребро
-            } else if ($edge = @ProductEdgeProduct::searchByVertex(Product::collectionName() . "/$to->_key", Product::collectionName() . "/$this->_key", type: 'analogue')[0]) {
-                // Найдено ребро (наоборот)
-            } else {
-                // Не найдены ребра
-
-                return false;
-            }
-        } else {
-            // Не передана цель для удаления (из её группы)
-
-            foreach ($this->connections() as $edge) {
-                // Перебор всех рёбер
-
-                if (Product::collectionName() . "/$this->_key" !== $edge && $to = Product::searchById($edge)) {
-                    // Найден товар (проверен на то, что не является самим собой)
-
-                    // Разъединение
-                    $this->disconnect($to, all: false);
-                }
-            }
+            // Удаление из группы
+            $group->deleteProduct($this);
 
             return true;
-        }
+        } else {
+            // Не найдена группа товаров
 
-        if ($edge->delete() > 0) {
-            // Удалось удалить ребро (связь)
-
-            if ($all) {
-                // Запрошено удаление соединений со всеми членами группы
-
-                foreach ($to->connections() as $edge) {
-                    // Перебор рёбер (найденных соединений с группой в которой находилась цель)
-
-                    if (Product::collectionName() . "/$this->_key" !== $edge && $to = Product::searchById($edge)) {
-                        // Найден товар (проверен на то, что не является самим собой)
-
-                        // Разъединение
-                        $this->disconnect($to, all: false);
-                    }
-                }
-            }
-
-            // Запись в журнал о разъединении
-            $this->journal('disconnect analogue', [
-                'to' => Product::collectionName() . "/$to->_key"
-            ]);
-
-            // Запись в журнал о соединении
-            $to->journal('disconnect analogue', [
-                'from' => Product::collectionName() . "/$this->_key"
-            ]);
+            // Заебись
 
             return true;
         }
@@ -670,42 +514,16 @@ class Product extends Document
         return false;
     }
 
-    /**
-     * Найти все связанные товары
-     *
-     * @param int $limit Ограничение по максимальному значению
-     */
-    public function connections(int $limit = 100): array
-    {
-        // Инициализация буфера связанных товаров
-        $products = [];
-
-        foreach (ProductEdgeProduct::searchByDirection(self::collectionName() . "/$this->_key", direction: 'ANY', type: 'analogue', limit: $limit) as $edge) {
-            // Перебор связей для создания списка товаров (вершин)
-
-            // Добавление товаров (вершин) в буфер (подразумевается, что без дубликатов)
-            if (!in_array($edge->_from, $products, true)) $products[] = $edge->_from;
-            if (!in_array($edge->_to, $products, true)) $products[] = $edge->_to;
-        }
-
-        return $products;
-    }
-
     /**
      * Проверка на уникальность
      *
-     * @param static|null $account — Аккаунт
-     *
-     * @return bool|static true если создать новую запись, static если найден дубликат
+     * @return bool|static Товар, если найден
      *
      * @todo
      * 1. Обработка дубликатов
      */
-    public function validateForUniqueness(Account|int|null $account = null): bool|static
+    public function validateForUniqueness(): bool|static
     {
-        // Инициализация аккаунта
-        $account = Account::initAccount($account);
-
         if ($supplies = self::search(['catn' => $this->catn, 'prod' => $this->prod], limit: 100)) {
             // Найдены поставки с таким же артикулом (catn) и производителем (prod)
 
@@ -717,9 +535,9 @@ class Product extends Document
 
             // Возврат (найден дубликат в базе данных)
             return $supply;
-        } else return true;
+        }
 
-        // Возврат (подразумевается ошибка)
+        // Возврат (подразумевается отсутствие дубликатов в базе данных)
         return false;
     }
 
@@ -796,4 +614,30 @@ class Product extends Document
 
         return false;
     }
+
+    /**
+     * Найти товары по группе
+     *
+     * @param string|null $_id Идентификатор группы
+     *
+     * @return array|null Товары (Product)
+     */
+    public static function searchByProductGroup(string $_id): ?array
+    {
+        return static::searchByEdge(
+            from: 'product_group',
+            to: 'product',
+            edge: 'product_edge_product_group',
+            direction: 'INBOUND',
+            subquery_where: [
+                [
+                    'product_edge_product_group._from == "' . $_id . '"'
+                ]
+            ],
+            subquery_select: 'product',
+            where: 'product_edge_product_group[0]._id != null',
+            limit: 1,
+            select: 'product_edge_product_group[0]'
+        )[0];
+    }
 }
diff --git a/mirzaev/skillparts/system/models/ProductEdgeProductGroup.php b/mirzaev/skillparts/system/models/ProductEdgeProductGroup.php
index 09669b8..3c9304d 100644
--- a/mirzaev/skillparts/system/models/ProductEdgeProductGroup.php
+++ b/mirzaev/skillparts/system/models/ProductEdgeProductGroup.php
@@ -10,4 +10,17 @@ class ProductEdgeProductGroup extends Edge
     {
         return 'product_edge_product_group';
     }
+
+    /**
+     * Поиск по товару
+     *
+     * @param Product $product Товар
+     * @param int $amount Ограничение по максимальному количеству
+     *
+     * @return null|self Ребро, если найдено
+     */
+    public static function searchByProduct(Product $product, int $limit = 1): ?self
+    {
+        return self::find()->where(['_from' => $product->readId()])->limit($limit)->all()[0] ?? null;
+    }
 }
diff --git a/mirzaev/skillparts/system/models/ProductGroup.php b/mirzaev/skillparts/system/models/ProductGroup.php
index 557014b..238421d 100644
--- a/mirzaev/skillparts/system/models/ProductGroup.php
+++ b/mirzaev/skillparts/system/models/ProductGroup.php
@@ -4,15 +4,19 @@ declare(strict_types=1);
 
 namespace app\models;
 
+use app\models\traits\SearchByEdge;
+
 use carono\exchange1c\interfaces\GroupInterface;
 
 use Zenwalker\CommerceML\Model\Group;
 
 /**
- * Группировка продуктов
+ * Группировка продуктов для соединения их в аналоги
  */
 class ProductGroup extends Document implements GroupInterface
 {
+    use SearchByEdge;
+
     /**
      * Имя коллекции
      */
@@ -29,7 +33,7 @@ class ProductGroup extends Document implements GroupInterface
         return array_merge(
             parent::attributes(),
             [
-                'name'
+                'stts'
             ]
         );
     }
@@ -42,7 +46,7 @@ class ProductGroup extends Document implements GroupInterface
         return array_merge(
             parent::attributeLabels(),
             [
-                'name' => 'Название (name)'
+                'stts' => 'Статус'
             ]
         );
     }
@@ -55,23 +59,138 @@ class ProductGroup extends Document implements GroupInterface
         return array_merge(
             parent::rules(),
             [
-                // [
-                //     'name',
-                //     'required',
-                //     'message' => 'Заполните поле: {attribute}'
-                // ]
+                [
+                    'stts',
+                    'string',
+                    'length' => [4, 20],
+                    'message' => '{attribute} должен быть строкой от 4 до 20 символов'
+                ]
             ]
         );
     }
 
+    /**
+     * Запись пустой группы
+     *
+     * @param bool $active Статус активации
+     *
+     * @return self Группа товаров, если создана
+     */
+    public static function writeEmpty(bool $active = false): ?self
+    {
+        // Инициализация
+        $model = new self;
+
+        // Настройки
+        $model->stts = $active ? 'active' : 'inactive';
+
+        // Запись
+        return $model->save() ? $model : null;
+    }
+
     /**
      * Запись члена группы
+     *
+     * @deprecated
      */
     public function writeMember(Product $member): ProductEdgeProductGroup
     {
         return ProductEdgeProductGroup::write($member->readId(), $this->readId(), 'member');
     }
 
+    /**
+     * Запись товара в группу
+     *
+     * @param Product $product Товар
+     */
+    public function writeProduct(Product $product): ?ProductEdgeProductGroup
+    {
+        // Запись товара в группу
+        $edge = ProductEdgeProductGroup::write($product->readId(), $this->readId(), data: ['type' => 'member']);
+
+        // Запись в журнал
+        $this->journal('write member', [
+            'product' => $product->readId()
+        ]);
+
+        return $edge;
+    }
+    /**
+     * Удаление товара из группы
+     *
+     * @param Product $product Товар
+     *
+     * @return void
+     */
+    public function deleteProduct(Product $product): void
+    {
+        // Удаление товара из группы (подразумевается, что будет только одно)
+        foreach (ProductEdgeProductGroup::searchByVertex($product->readId(), $this->readId(), filter: ['type' => 'member']) as $edge) $edge->delete;
+
+        // Запись в журнал
+        $this->journal('delete member', [
+            'product' => $product->readId()
+        ]);
+    }
+
+    /**
+     * Найти рёбра до товаров
+     *
+     * @param int $limit Ограничение по максимальному количеству
+     */
+    public function searchEdges(int $limit = 999): ?array
+    {
+        return ProductEdgeProductGroup::searchByDirection($this->readId(), 'INBOUND', where: ['type' => 'member'], limit: $limit);
+    }
+
+    /**
+     * Прочитать связанные товары
+     *
+     * @param int $limit Ограничение по максимальному количеству
+     */
+    public function searchProducts(int $limit = 999): ?array
+    {
+        // Инициализация буфера товаров
+        $products = [];
+
+        foreach ($this->searchEdges($limit) as $edge) {
+            // Перебор рёбер
+
+            $products[] = Product::searchById($edge->_from);
+        }
+
+        return $products;
+    }
+
+    /**
+     * Перенос членов группы из другой
+     *
+     * @param ProductGroup $group Группа из которой нужен перенос
+     *
+     * @return null|int Количество перенесённых товаров, если произведён перенос
+     */
+    public function transfer(ProductGroup $group): ?int
+    {
+        // Проверка на то, что запрошен перенос "из себя в себя"
+        if ($this->readId() === $group->readId()) return null;
+
+        // Инициализация счётчика записанных товаров
+        $transfered = 0;
+
+        // Перенос
+        foreach ($group->searchProducts() as $product) if ($this->writeProduct($product)) ++$transfered;
+
+        // Деактивация целевой группы (пустой)
+        $group->deactivate();
+
+        // Запись в журнал
+        $this->journal('transfer', [
+            'from' => $group->readId()
+        ]);
+
+        return $transfered;
+    }
+
     /**
      * Запись рёбер групп
      *
@@ -171,4 +290,29 @@ class ProductGroup extends Document implements GroupInterface
     {
         return static::findOne(['onec_id' => $onec_id]);
     }
+
+    /**
+     * Найти по идентификатору товара
+     *
+     * @param Product $product Товар
+     *
+     * @return self|null Группа (ProductGroup)
+     */
+    public static function searchByProduct(Product $product): ?self
+    {
+        return static::searchByEdge(
+            from: 'product',
+            to: 'product_group',
+            edge: 'product_edge_product_group',
+            direction: 'INBOUND',
+            subquery_where: [
+                [
+                    'product_edge_product_group._from == "' . $product->readId() . '"'
+                ]
+            ],
+            subquery_select: 'product_group',
+            where: 'product_edge_product_group[0]._id != null',
+            limit: 1
+        )[0] ?? null;
+    }
 }
diff --git a/mirzaev/skillparts/system/models/Search.php b/mirzaev/skillparts/system/models/Search.php
index 0d49de6..0e21f65 100644
--- a/mirzaev/skillparts/system/models/Search.php
+++ b/mirzaev/skillparts/system/models/Search.php
@@ -392,12 +392,12 @@ class Search extends Document
      *
      * @param array $row Товар сгенерированный через Search::content()
      * @param string|null $cover Обложка
-     * @param array $empties Реестр не найденных товаров
+     * @param array $list Реестр найденных товаров
      * @param bool $analogs Запрошены аналоги (не выведет пустые товары)
      *
      * @return string HTML-элемент с товаром
      */
-    public static function generate(array &$row, string|null &$cover = null, array &$empties = [], bool $analogs = false): string
+    public static function generate(array &$row, string|null &$cover = null, array &$list = [], bool $analogs = false): string
     {
         foreach ($row['imgs'] ?? [] as &$img) {
             // Перебор изображений для обложки
@@ -437,6 +437,9 @@ class Search extends Document
         foreach (empty($row['supplies']) || $supplies_amount === 0 ? [null] : $row['supplies'] as &$supply) {
             // Перебор поставок
 
+            // Запись в список найденных
+            $list[$row['prod']] = [$row['catn']] + (isset($list[$row['prod']]) ? $list[$row['prod']] : []);
+
             // Инициализация модификатора класса
             if ($supplies_amount > $supply_iterator) {
                 // Это не последняя строка с товаром и его поставками
@@ -491,9 +494,6 @@ class Search extends Document
                     
                 HTML;
 
-                // Запись в список ненайденных
-                $empties[$row['prod']] = [$row['catn']] + (isset($empties[$row['prod']]) ? $empties[$row['prod']] : []);
-
                 // Запись блокировщика
                 $empty_block = true;
 
diff --git a/mirzaev/skillparts/system/models/Supply.php b/mirzaev/skillparts/system/models/Supply.php
index a2088ea..dc86ce1 100644
--- a/mirzaev/skillparts/system/models/Supply.php
+++ b/mirzaev/skillparts/system/models/Supply.php
@@ -92,15 +92,7 @@ class Supply extends Product implements ProductInterface, OfferInterface
     {
         return array_merge(
             parent::rules(),
-            [
-                // [
-                //     [
-                //         'oemn'
-                //     ],
-                //     'arrayValidator',
-                //     'message' => '{attribute} должен быть массивом.'
-                // ]
-            ]
+            []
         );
     }
 
@@ -341,7 +333,7 @@ class Supply extends Product implements ProductInterface, OfferInterface
             // Пользователь аутентифицирован и авторизован
 
             // Инициализация п̸̨͇͑͋͠р̷̬̂́̀̊о̸̜̯̹̅͒͘͝д̴̨̨̨̟̈́̆у̴̨̭̮̠́͋̈́к̴̭͊̋̎т̵̛̣͈̔̐͆а̵̨͖͑
-            $product = self::initEmpty($this->catn);
+            $product = self::initEmpty($this->catn, $this->prod);
 
             if (!is_array($product)) {
                 // Создался только один товар и вернулся в виде модели
@@ -356,7 +348,7 @@ class Supply extends Product implements ProductInterface, OfferInterface
                     // Перебор артикулов из массива ОЕМ-номеров
 
                     // Инициализация и запись
-                    $product[] = self::initEmpty($oem);
+                    $product[] = self::initEmpty($oem, $this->prod);
                 }
             }
 
@@ -456,23 +448,51 @@ class Supply extends Product implements ProductInterface, OfferInterface
                     foreach ($data as $doc) {
                         // Перебор полученных документов
 
+                        // Проверка на пустую страницу или документ (пустой массив)
+                        if (empty($doc)) continue;
+
                         foreach ($doc as $row) {
                             // Перебор строк
 
+                            if (!is_array($row)) {
+                                // Строка является не массивом со значением колонок, а самим значением колонки (значит в $data и так хранился $doc - у файла всего одна страница)
+
+                                // Универсализация
+                                $row = $doc;
+                            }
+
                             // Поиск артикула
-                            $article = $row['Артикул'] ?? $row['артикул'] ?? $row['Article'] ?? $row['article'] ?? $row['catn'];
+                            $article = (string) ($row['Артикул'] ?? $row['артикул'] ?? $row['Article'] ?? $row['article'] ?? $row['catn'] ?? null);
 
                             // Поиск количества товаров
-                            $amount = $row['Количество'] ?? $row['количество'] ?? $row['Amount'] ?? $row['amount'] ?? $row['amnt'] ?? 1;
+                            $amount = $row['Количество'] ?? $row['количество'] ?? $row['Amount'] ?? $row['amount'] ?? $row['amnt'] ?? null;
+
+                            // Поиск производителя
+                            $prod = (string) ($row['Производитель'] ?? $row['производитель'] ?? $row['Production'] ?? $row['production'] ?? $row['prod'] ?? null);
 
                             // Поиск аналогов
                             $analogs = explode(',', (string) ($row['Аналоги'] ?? $row['аналоги'] ?? $row['Analogs'] ?? $row['analogs'] ?? $row['ОЕМ'] ?? $row['eom'] ?? ''), 50);
 
-                            // Поиск производителя
-                            $prod = $row['Производитель'] ?? $row['производитель'] ?? $row['Production'] ?? $row['production'] ?? $row['prod'] ?? 'Неизвестный';
+                            // Инициализация буфера аналогов
+                            $buffer = [];
+
+                            // Дополнительная фильтрация аналогов
+                            foreach ($analogs as $analog) foreach (explode('/', $analog, 50) as $value) $buffer[] = $value;
+
+                            // Запись аналогов из буфера
+                            $analogs = $buffer;
+
+                            // Пропуск пустых строк (подразумевается)
+                            if ($article === null || $amount === null || $prod === null) continue;
+
+                            // Инициализация товара для инициализации группы
+                            if (!$product = Product::searchByCatnAndProd($article, $prod)) $product = Product::writeEmpty($article, $prod, Account::isMinimalAuthorized($account));
+
+                            // Инициализация группы товаров
+                            if (!$group = ProductGroup::searchByProduct($product)) $group = ProductGroup::writeEmpty(active: true);
 
                             // Инициализация функции создания поставки
-                            $create = function (string $_supply) use ($article, $row, $prod, $analogs, &$created, &$updated, &$imported, $amount, $account): bool {
+                            $create = function (string $_supply, int|null $amount = null) use ($group, $row, $prod, $analogs, &$created, &$updated, &$imported, $account): bool {
                                 // Очистка
                                 $_supply = trim($_supply);
 
@@ -484,7 +504,7 @@ class Supply extends Product implements ProductInterface, OfferInterface
 
                                 // Запись артикула (каталожного номера) в буфер
                                 $_row['catn'] = $_supply;
-                                $_row['cost'] = (float) preg_replace('/[^\d\.]+/', '', preg_replace('/\,+/', ' ', $row['Стоимость'] ?? $row['стоимость'] ?? $row['Цена'] ?? $row['цена'] ?? $row['Cost'] ?? $row['cost'] ?? $row['Price'] ?? $row['price'])) ?? 0;
+                                $_row['cost'] = (float) preg_replace('/[^\d\.]+/', '', preg_replace('/\,+/', ' ', (string) ($row['Стоимость'] ?? $row['стоимость'] ?? $row['Цена'] ?? $row['цена'] ?? $row['Cost'] ?? $row['cost'] ?? $row['Price'] ?? $row['price']))) ?? 0;
                                 $_row['prod'] = $prod;
                                 $_row['oemn'] = array_walk($analogs, 'trim');
 
@@ -496,137 +516,12 @@ class Supply extends Product implements ProductInterface, OfferInterface
                                 if ($supply->validate()) {
                                     // Проверка пройдена
 
-                                    if (($_supply = $supply->validateForUniqueness($account)) instanceof static) {
+                                    if ((($_supply = $supply->validateForUniqueness()) instanceof Supply)) {
                                         // Найдена поставка с такими параметрами (артикул и производитель)
 
-                                        if ($_supply->cost !== $_row['cost']) {
-                                            // Стоимость изменилась
-
-                                            if ($product = Product::searchByCatnAndProd($supply->catn, $supply->prod)) {
-                                                // Найден товар подходящий для привязки с только что созданной поставкой (подразумевается что уже был привязан в коде выше)
-
-                                                // Приведение типа (для анализатора)
-                                                if (is_array($product)) $product = $product[0];
-                                            } else {
-                                                // Не найден товар подходящий для привязки с только что созданной поставкой
-
-                                                if ($product = Product::writeEmpty($supply->catn, $supply->prod, Account::isMinimalAuthorized($account))) {
-                                                    // Удалось записать новый товар (НЕАКТИВНЫЙ)
-
-                                                    // Отправка уведомления
-                                                    // Notification::_write("Не найден товар подходящий для связи с поставкой: $supply->catn", account: '@authorized');
-                                                } else {
-                                                    // Не удалось записать новый товар
-
-                                                    // Отправка уведомления
-                                                    Notification::_write("Не удалось создать новый товар: $supply->catn", account: '@authorized');
-
-                                                    // Запись статуса об ошибке
-                                                    $error = true;
-                                                }
-                                            }
-
-                                            // Завершение выполнения при ошибке
-                                            if ($error) return !$error;
-
-                                            if ($product = Product::searchByCatnAndProd($_supply->catn, $_supply->prod)) {
-                                                // Найден товар подходящий для привязки с этой поставкой
-
-                                                for ($i = 0; $i++ < $amount;) {
-                                                    // Перебор создаваемых рёбер (так работает обозначение количества товаров в наличии)
-
-                                                    // Поиск ребёр
-                                                    $edges = SupplyEdgeProduct::searchByVertex($_supply->readId(), $product->readId(), limit: 999);
-
-                                                    if (count($edges) === 0) {
-                                                        // Ребёр нет, но должны быть (если количество загружаемых поставок более нуля)
-
-                                                        for ($i = 0; $i++ < $amount;) {
-                                                            // Перебор создаваемых рёбер (так работает обозначение количества товаров в наличии)
-
-                                                            // Запись ребра
-                                                            SupplyEdgeProduct::write($_supply->readId(), $product->readId(), data: ['type' => 'connect']);
-                                                        }
-                                                    } else if ($amount === count($edges)) {
-                                                        // Количество товаров в поставке не изменилось
-
-                                                        // Раз изменений нет, то обновлять ничего не нужно
-                                                        continue;
-                                                    } else if ($amount < count($edges)) {
-                                                        // Количество товаров в поставке стало МЕНЬШЕ
-
-                                                        // Расчёт разницы
-                                                        $delete = count($edges) - $amount;
-
-                                                        // Инициализация количества рёбер которые не удалось удалить
-                                                        $failed = 0;
-
-                                                        for ($i = 0; $i < $delete; $i++) {
-                                                            // Перебор рёбер на удаление (синхронизация)
-
-                                                            if ($edges[$i]->delete() >= 1) {
-                                                                // Удалено ребро
-                                                            } else {
-                                                                // Не удалено ребро
-
-                                                                // Обновление количества рёбер которые не удалось удалить
-                                                                ++$failed;
-                                                            }
-                                                        }
-
-                                                        // Отправка уведомления
-                                                        Notification::_write("Не удалось удалить $failed рёбер у поставки $_supply->catn");
-                                                    } else if ($amount > count($edges)) {
-                                                        // Количество товаров в поставке стало БОЛЬШЕ
-
-                                                        // Расчёт разницы
-                                                        $write = $amount - count($edges);
-
-                                                        // Инициализация количества рёбер которые не удалось записать
-                                                        $failed = 0;
-
-                                                        for ($i = 0; $i < $write; $i++) {
-                                                            // Перебор рёбер на запись (синхронизация)
-
-                                                            if (SupplyEdgeProduct::write($_supply->readId(), $product->readId(), data: ['type' => 'connect'])) {
-                                                                // Записано ребро
-                                                            } else {
-                                                                // Не записано ребро
-
-                                                                // Обновление количества рёбер которые не удалось записать
-                                                                ++$failed;
-                                                            }
-                                                        }
-
-                                                        // Отправка уведомления
-                                                        Notification::_write("Не удалось записать $failed рёбер у поставки $_supply->catn");
-                                                    }
-                                                }
-                                            }
-                                        }
-
-                                        // Инициализация буфера с параметрами загружаемого товара
-                                        $vars = $supply->getAttributes();
-
-                                        // Удаление _key, чтобы не перезаписать его при замене параметров документа в буфере
-                                        unset($vars['_key']);
-
-                                        // Перенос данных в буфер (существующий в базе данных дубликат)
-                                        $_supply->setAttributes($vars, false);
-
-                                        // Перезапись существующего документа
-                                        $_supply->update();
-
-                                        // Обновление счётчика
-                                        $updated++;
-
-                                        // Запись поставки в буфер
-                                        $imported[] = $_supply;
-
-                                        // Запись в буфер (для универсальной обработки)
                                         $supply = $_supply;
                                     } else {
-                                        // Не найден документ с такими параметрами
+                                        // Не найдена поставка с такими параметрами
 
                                         if ($supply->save()) {
                                             // Поставка записана в базу данных
@@ -638,6 +533,141 @@ class Supply extends Product implements ProductInterface, OfferInterface
                                             $imported[] = $supply;
                                         };
                                     }
+
+                                    if ($supply->cost !== $_row['cost']) {
+                                        // Стоимость изменилась
+
+                                        if ($product = Product::searchByCatnAndProd($supply->catn, $supply->prod)) {
+                                            // Найден товар подходящий для привязки
+
+                                            // Приведение типа (для анализатора)
+                                            if (is_array($product)) $product = $product[0];
+                                        } else {
+                                            // Не найден товар подходящий для привязки с только что созданной поставкой
+
+                                            if ($product = Product::writeEmpty($supply->catn, $supply->prod, Account::isMinimalAuthorized($account))) {
+                                                // Удалось записать новый товар (НЕАКТИВНЫЙ)
+
+                                                // Отправка уведомления
+                                                // Notification::_write("Не найден товар подходящий для связи с поставкой: $supply->catn", account: '@authorized');
+                                            } else {
+                                                // Не удалось записать новый товар
+
+                                                // Отправка уведомления
+                                                Notification::_write("Не удалось создать новый товар: $supply->catn", account: '@authorized');
+
+                                                // Запись статуса об ошибке
+                                                $error = true;
+                                            }
+                                        }
+
+                                        // Завершение выполнения при ошибке
+                                        if ($error) return false;
+
+                                        if ($product = Product::searchByCatnAndProd($supply->catn, $supply->prod)) {
+                                            // Найден товар подходящий для привязки с этой поставкой
+
+                                            // Поиск ребёр
+                                            $edges = SupplyEdgeProduct::searchByVertex($supply->readId(), $product->readId(), limit: 999);
+
+                                            if (isset($amount)) {
+                                                // Передано количество
+                                                if (count($edges) === 0) {
+                                                    // Ребёр нет, но должны быть (если количество загружаемых поставок более нуля)
+
+                                                    // Инициализация количества рёбер которые не удалось записать
+                                                    $failed = 0;
+
+                                                    for ($i = 0; $i++ < $amount;) {
+                                                        // Перебор создаваемых рёбер (так работает обозначение количества товаров в наличии)
+
+                                                        if (SupplyEdgeProduct::write($supply->readId(), $product->readId(), data: ['type' => 'connect'])) {
+                                                            // Записано ребро
+                                                        } else {
+                                                            // Не записано ребро
+
+                                                            // Обновление количества рёбер которые не удалось записать
+                                                            ++$failed;
+                                                        }
+                                                    }
+
+                                                    // Отправка уведомления
+                                                    if ($failed > 0) Notification::_write("Не удалось записать $failed рёбер у поставки $supply->catn");
+                                                } else if ($amount === count($edges)) {
+                                                    // Количество товаров в поставке не изменилось
+
+                                                    // Раз изменений нет, то обновлять ничего не нужно
+                                                    // continue;
+                                                } else if ($amount < count($edges)) {
+                                                    // Количество товаров в поставке стало МЕНЬШЕ
+
+                                                    // Расчёт разницы
+                                                    $delete = count($edges) - $amount;
+
+                                                    // Инициализация количества рёбер которые не удалось удалить
+                                                    $failed = 0;
+
+                                                    for ($i = 0; $i < $delete; $i++) {
+                                                        // Перебор рёбер на удаление (синхронизация)
+
+                                                        if ($edges[$i]->delete() >= 1) {
+                                                            // Удалено ребро
+                                                        } else {
+                                                            // Не удалено ребро
+
+                                                            // Обновление количества рёбер которые не удалось удалить
+                                                            ++$failed;
+                                                        }
+                                                    }
+
+                                                    // Отправка уведомления
+                                                    Notification::_write("Не удалось удалить $failed рёбер у поставки $supply->catn");
+                                                } else if ($amount > count($edges)) {
+                                                    // Количество товаров в поставке стало БОЛЬШЕ
+
+                                                    // Расчёт разницы
+                                                    $write = $amount - count($edges);
+
+                                                    // Инициализация количества рёбер которые не удалось записать
+                                                    $failed = 0;
+
+                                                    for ($i = 0; $i < $write; $i++) {
+                                                        // Перебор рёбер на запись (синхронизация)
+
+                                                        if (SupplyEdgeProduct::write($supply->readId(), $product->readId(), data: ['type' => 'connect'])) {
+                                                            // Записано ребро
+                                                        } else {
+                                                            // Не записано ребро
+
+                                                            // Обновление количества рёбер которые не удалось записать
+                                                            ++$failed;
+                                                        }
+                                                    }
+
+                                                    // Отправка уведомления
+                                                    if ($failed > 0) Notification::_write("Не удалось записать $failed рёбер у поставки $supply->catn");
+                                                }
+                                            }
+                                        }
+                                    }
+
+                                    // Инициализация буфера с параметрами загружаемого товара
+                                    $vars = $supply->getAttributes();
+
+                                    // Удаление _key, чтобы не перезаписать его при замене параметров документа в буфере
+                                    unset($vars['_key']);
+
+                                    // Перенос данных в буфер (существующий в базе данных дубликат)
+                                    $supply->setAttributes($vars, false);
+
+                                    // Перезапись существующего документа
+                                    $supply->update();
+
+                                    // Обновление счётчика
+                                    $updated++;
+
+                                    // Запись поставки в буфер
+                                    $imported[] = $supply;
                                 } else {
                                     // Проверка не пройдена
 
@@ -654,26 +684,19 @@ class Supply extends Product implements ProductInterface, OfferInterface
                                     // Активация товара
                                     $product->activate();
 
-                                    // Инициализация списка артикулов группы для добавления аналогов
-                                    $group = [$article] + $analogs;
+                                    // foreach (Product::searchByCatn($product->catn) as $target) {
+                                    //     // Перебор товаров по артикулу (все производители)
+                                    // }
 
-                                    foreach ($group as $catn) {
-                                        // Перебор артикулов для добавления аналогов
-
-                                        foreach (Product::searchByCatn((string) $catn) as $target) {
-                                            // Перебор товаров для добавления аналогов
-
-                                            // Добавление в группу аналогов
-                                            if ($to = Product::searchByCatn((string) $target['catn'], 1)) $product->synchronization($to);
-                                        }
-                                    }
+                                    // Добавление в группу аналогов
+                                    $group->writeProduct($product);
                                 }
 
                                 return !$error;
                             };
 
                             // Запись поставки
-                            $create((string) $article);
+                            $create($article, (int) $amount);
 
                             foreach ($analogs as $_supply) {
                                 // Перебор аналогов (если найдены)
@@ -710,7 +733,7 @@ class Supply extends Product implements ProductInterface, OfferInterface
                     foreach ($imported as $supply) {
                         // Перебор импортированных поставок
 
-                        if (ImportEdgeSupply::write($import->collectionName() . "/$import->_key", $supply->collectionName() . "/$supply->_key", data: ['type' => 'imported'])) {
+                        if (ImportEdgeSupply::write($import->readId(), $supply->readId(), data: ['type' => 'imported', 'vrsn' => ImportEdgeSupply::searchMaxVersion($supply) + 1])) {
                             // Записано ребро: ИНСТАНЦИЯ ПОСТАВОК -> ПОСТАВКА
 
                             // Запись в журнал инстанции импорта
diff --git a/mirzaev/skillparts/system/models/traits/SearchByEdge.php b/mirzaev/skillparts/system/models/traits/SearchByEdge.php
index 1909c55..fd7601e 100644
--- a/mirzaev/skillparts/system/models/traits/SearchByEdge.php
+++ b/mirzaev/skillparts/system/models/traits/SearchByEdge.php
@@ -32,6 +32,7 @@ trait SearchByEdge
         array $params = [],
         bool $asArray = true,
         bool $debug = false,
+        bool $aql = false,
         bool $count = false
     ): mixed {
         $subquery = static::find()
@@ -71,11 +72,20 @@ trait SearchByEdge
             ->limit($limit)
             ->select($select ?? $to);
 
+        // Режим вывода строки запроса
+        if ($aql) {
+            // Запрошена проверка
+
+            return (string) $request->createCommand();
+        }
+
         // Режим проверки
         if ($debug) {
             // Запрошена проверка
 
-            return (string) $request->createCommand();
+            var_dump((string) $request->createCommand());
+
+            return null;
         }
 
         // Запрос
diff --git a/mirzaev/skillparts/system/views/cart/index.php b/mirzaev/skillparts/system/views/cart/index.php
index a28fa97..2c1cd06 100644
--- a/mirzaev/skillparts/system/views/cart/index.php
+++ b/mirzaev/skillparts/system/views/cart/index.php
@@ -6,7 +6,7 @@ use yii;
 use yii\bootstrap\ActiveForm;
 
 use app\models\connection\Dellin;
-
+use app\models\Supply;
 use DateTime;
 
 ?>
@@ -21,10 +21,14 @@ use DateTime;
                 
+
+ Производитель +
Артикул
-
+
+ Поставщик
Количество @@ -37,168 +41,100 @@ use DateTime;
$list) { + // Перебор поставщиков - // Инициализация комментария - $comment = $order_edge_supply[0]['comm'] ?? 'Комментарий к заказу'; + foreach ($list as $catn => $deliveries) { + // Перебор поставок - if ($amount['auto'] > 0) { - // Найдены поставки с автоматической доставкой + foreach ($deliveries as $delivery => $supply) { + // Перебор типов доставки - // Инициализация цены - $price_auto = $price_raw['auto'] . ' ' . $currency; + // Инициализация комментария + $comment = $supply['edge']['comm'] ?? 'Комментарий к заказу'; - // Инициализация доставки - if (!isset($delivery) || (isset($delivery['auto'], $delivery['auto']['error']) || $delivery === '?')) { - // Не удалось рассчитать доставку + // Инициализация доставки + if (empty($supply['delivery'])) { + // Не удалось рассчитать доставку - // Инициализация времени - $delivery_auto = '?'; - } else { - // Удалось рассчитать доставку + // Инициализация времени + $days = '?'; + } else { + // Удалось рассчитать доставку - // Инициализация даты отправки - try { - // Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель) + // Инициализация даты отправки + try { + // Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель) - $delivery_auto_send_date = DateTime::createFromFormat('Y-m-d', $delivery['auto']['orderDates']['arrivalToOspSender'])->getTimestamp(); - } catch (Throwable $e) { - // Взять данные из "pickup" (Дата передачи груза на адресе отправителя) + $delivery_send_date = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['arrivalToOspSender'])->getTimestamp(); + } catch (Throwable $e) { + // Взять данные из "pickup" (Дата передачи груза на адресе отправителя) - $delivery_auto_send_date = DateTime::createFromFormat('Y-m-d', $delivery['auto']['orderDates']['pickup'])->getTimestamp(); + $delivery_send_date = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['pickup'])->getTimestamp(); + } + + // Инициализация времени доставки + try { + // Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале) + + // Оставлено на всякий случай для дальнейших разбирательств + + $delivery_converted = DateTime::createFromFormat('Y-m-d H:i:s', $supply['delivery']['orderDates']['giveoutFromOspReceiver'])->getTimestamp(); + } catch (Throwable $e) { + // Инициализация даты отправки + + // Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель) + $delivery_converted = DateTime::createFromFormat('Y-m-d', $supply['delivery']['orderDates']['arrivalToOspReceiver'])->getTimestamp(); + } + $days = ceil(($delivery_converted - ($delivery_send_date ?? 0)) / 60 / 60 / 24) + 1; } - // Инициализация времени доставки - try { - // Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале) + // Инициализация иконки + $icon = $delivery === 'avia' ? 'fa-plane' : 'fa-truck'; - // Оставлено на всякий случай для дальнейших разбирательств - - $delivery_auto_converted = DateTime::createFromFormat('Y-m-d H:i:s', $delivery['auto']['orderDates']['giveoutFromOspReceiver'])->getTimestamp(); - } catch (Throwable $e) { - // Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель) - - $delivery_auto_converted = DateTime::createFromFormat('Y-m-d', $delivery['auto']['orderDates']['arrivalToOspReceiver'])->getTimestamp(); - } - $delivery_auto = ceil(($delivery_auto_converted - ($delivery_auto_send_date ?? 0)) / 60 / 60 / 24) + 1; - } - - // Генерация HTML - echo << -
-
-
- + // Генерация HTML + echo << +
+
+
+ +
+
+ $prod +
+
+ $catn +
+
+ {$supply['account']['indx']} +
+
+ +
+
+

~$days дн

+
+
+ {$supply['cost']} {$supply['currency']} +
-
- {$supply['catn']} -
-
-
-
- -
-
-

~$delivery_auto дн

-
-
- $price_auto -
-
- -
-
-

$comment

+ +
+
+

$comment

+
-
- HTML; - } - - if ($amount['avia'] > 0) { - // Найдены поставки с доставкой по воздуху - - // Инициализация цены - $price_avia = $price_raw['avia'] . ' ' . $currency; - - // Инициализация доставки - if (!isset($delivery) || (isset($delivery, $delivery['auto'], $delivery['avia']['error']) || $delivery === '?')) { - // Не удалось рассчитать доставку - - // Инициализация времени - $delivery_avia = '?'; - } else { - // Удалось рассчитать доставку - - // Инициализация даты отправки - try { - // Взять данные из "arrivalToOspSender" (Дата прибытия на терминал-отправитель) - - $delivery_avia_send_date = DateTime::createFromFormat('Y-m-d', $delivery['avia']['orderDates']['arrivalToOspSender'])->getTimestamp(); - } catch (Throwable $e) { - // Взять данные из "pickup" (Дата передачи груза на адресе отправителя) - - $delivery_avia_send_date = DateTime::createFromFormat('Y-m-d', $delivery['avia']['orderDates']['pickup'])->getTimestamp(); - } - - // Инициализация времени доставки - try { - // Доставка по воздуху (подразумевается), данные из "giveoutFromOspReceiver" (Дата и время, с которого груз готов к выдаче на терминале) - - $delivery_avia_converted = DateTime::createFromFormat('Y-m-d H:i:s', $delivery['avia']['orderDates']['giveoutFromOspReceiver'])->getTimestamp(); - } catch (Throwable $e) { - // Автоматическая доставка (подразумевается), данные из "arrivalToOspReceiver" (Дата прибытия натерминал-получатель) - - // Оставлено на всякий случай для дальнейших разбирательств - - $delivery_avia_converted = DateTime::createFromFormat('Y-m-d', $delivery['avia']['orderDates']['arrivalToOspReceiver'])->getTimestamp(); - } - $delivery_avia = ceil(($delivery_avia_converted - ($delivery_avia_send_date ?? 0)) / 60 / 60 / 24) + 1; + HTML; } - - // Генерация HTML - echo << -
-
-
- -
-
- {$supply['catn']} -
-
- {$supply['dscr']} -
-
- -
-
-

~$delivery_avia дн

-
-
- $price_avia -
-
- -
-
-

$comment

-
-
-
-
- HTML; } } } else { diff --git a/mirzaev/skillparts/system/views/invoice/order/pattern.php b/mirzaev/skillparts/system/views/invoice/order/pattern.php index 1b88f16..4f61725 100644 --- a/mirzaev/skillparts/system/views/invoice/order/pattern.php +++ b/mirzaev/skillparts/system/views/invoice/order/pattern.php @@ -8,7 +8,7 @@ use app\models\Settings; - Счёт №<?= $order['id'] ?> + Счёт №<?= $data['order']->_key ?> @@ -89,7 +89,7 @@ use app\models\Settings; preg_match_all('/UTC([\+\-0-9:]*)/', $account->zone ?? Settings::searchActive()['timezone_default'] ?? 'UTC+3', $timezone); $timezone = $timezone[1][0]; ?> - Счет на оплату № от setTimestamp($order['date'])->setTimezone(new DateTimeZone($timezone))->format('d.m.Y') ?> + Счет на оплату №_key ?> от setTimestamp($date)->setTimezone(new DateTimeZone($timezone))->format('d.m.Y') ?> @@ -133,7 +133,9 @@ use app\models\Settings; - Товары (работы, услуги) + Производитель + Товар + Поставщик Количество Цена Сумма @@ -151,16 +153,21 @@ use app\models\Settings; $cost = 0; ?> - - - - - - - - - - + $supplies) : ?> + $deliveries) : ?> + $supply) : ?> + + + + + + + + + + + + diff --git a/mirzaev/skillparts/system/views/notification/system/orders/new.php b/mirzaev/skillparts/system/views/notification/system/orders/new.php index c2ff066..464a228 100644 --- a/mirzaev/skillparts/system/views/notification/system/orders/new.php +++ b/mirzaev/skillparts/system/views/notification/system/orders/new.php @@ -1,3 +1,3 @@
-

Новый заказ: #

+

Заказ запрошен: #

diff --git a/mirzaev/skillparts/system/views/orders/index.php b/mirzaev/skillparts/system/views/orders/index.php index fd54dff..3b37966 100644 --- a/mirzaev/skillparts/system/views/orders/index.php +++ b/mirzaev/skillparts/system/views/orders/index.php @@ -47,16 +47,15 @@ if (empty($window)) {
- - - -
+ + +
jrnl as $entry) { // Перебор записей в журнале if ($entry['action'] === 'requested') { @@ -81,7 +80,7 @@ if (empty($window)) { ]; ?> -

#

+

#_key ?>

@@ -89,96 +88,73 @@ if (empty($window)) {

-
- +
+ - + $list) : ?> + $deliveries) : ?> + $supply) : ?> + imgs ?? [] as $img) { + // Перебор изображений для обложки - // Инициализация обложки - $covr = null; + if ($img['covr'] ?? false) { + // Обложка найдена - foreach ($imgs ?? [] as $img) { - // Перебор изображений для обложки + $covr = $img['h150']; - if ($img['covr'] ?? false) { - // Обложка найдена + break; + } + } - $covr = $img['h150']; + if (is_null($covr)) { + // Обложка не инициализирована - break; - } - } + if (!$covr = $supply['product']->imgs[0]['h150'] ?? false) { + // Не удалось использовать первое изображение как обложку - if (is_null($covr)) { - // Обложка не инициализирована + // Запись обложки по умолчанию + $covr = '/img/covers/h150/product.png'; + } + } - if (!$covr = $imgs[0]['h150'] ?? false) { - // Не удалось использовать первое изображение как обложку + if ($supply['amount'] > 0) { + // Пройдена проверка на количество поставок в заказе - // Запись обложки по умолчанию - $covr = '/img/covers/h150/product.png'; - } - } + // if (Order::checkSuppliesStts($order_edge_supply)) { + // $status = ''; + // } else { + $status = ''; + // } - if ($amount['auto'] > 0) { - // Найдены поставки с автоматической доставкой + // Инициализация иконки + $icon = $delivery === 'avia' ? 'fa-plane' : 'fa-truck'; - if (Order::checkSuppliesStts($order_edge_supply)) { - $status = ''; - } else { - $status = ''; - } - - // Генерация HTML - echo << - -

- {$product['catn']} x{$amount['auto']} - - $status -

- - HTML; - } - - if ($amount['avia'] > 0) { - // Найдены поставки с автоматической доставкой - - if (Order::checkSuppliesStts($order_edge_supply)) { - $status = ''; - } else { - $status = ''; - } - - // Генерация HTML - echo << - -

- {$product['catn']} x{$amount['avia']} - - $status -

- - HTML; - } - - ?> - - - - + // Генерация HTML + echo << + +

+ {$catn} x{$supply['amount']} + + $status +

+ + HTML; + } + ?> + + + + +
@@ -186,30 +162,19 @@ if (empty($window)) {
-
+

Выберите поставку

-
- - 'Запрошен', - 'handled' => 'Обрабатывается', - 'completed' => 'Завершен', - } - ?> - -

Статус:

- Подтвердить +
+

Статус: readId())['stts'] ?? '') ?>

+ Подтвердить