diff --git a/deploy/fix-internal-ean13.sql b/deploy/fix-internal-ean13.sql new file mode 100644 index 0000000..ab07039 --- /dev/null +++ b/deploy/fix-internal-ean13.sql @@ -0,0 +1,64 @@ +-- deploy/fix-internal-ean13.sql +-- Исправление контрольной цифры у внутренних EAN-13 (префикс «2»), которые +-- веб-генератор выдавал с НЕверной контрольной цифрой (баг digitsChecksum: +-- веса считались слева, а для 12-значного тела чётность переворачивается). +-- Фикс генератора — в src/food-market.web/src/lib/barcode.ts; этот скрипт +-- чинит УЖЕ ВЫДАННЫЕ коды, не теряя связь с товаром (та же строка product_barcodes, +-- тот же ProductId — меняется только последняя цифра Code). +-- +-- Идемпотентно. Пропускает коды, у которых исправленный вариант уже занят +-- в этой организации (во избежание нарушения unique(OrganizationId,Code)) — +-- такие выводятся отдельно для ручного разбора. +-- +-- Запуск: ssh nns@192.168.1.190 "docker exec -i food-market-stage-postgres \ +-- psql -U food_market -d food_market" < deploy/fix-internal-ean13.sql + +CREATE OR REPLACE FUNCTION pg_temp.ean13_check(body text) RETURNS int AS $f$ +DECLARE s int := 0; w int := 3; i int; +BEGIN + IF body !~ '^[0-9]{12}$' THEN RETURN -1; END IF; + FOR i IN REVERSE 12..1 LOOP + s := s + substr(body,i,1)::int * w; + w := CASE WHEN w=3 THEN 1 ELSE 3 END; + END LOOP; + RETURN (10 - s % 10) % 10; +END $f$ LANGUAGE plpgsql IMMUTABLE; + +-- Отчёт: что будет исправлено (старый → новый), с товаром и организацией. +SELECT pb."OrganizationId", pb."ProductId", p."Name" AS product, + pb."Code" AS old_code, + substr(pb."Code",1,12) || pg_temp.ean13_check(substr(pb."Code",1,12))::text AS new_code +FROM product_barcodes pb +JOIN products p ON p."Id" = pb."ProductId" +WHERE pb."Code" ~ '^2[0-9]{12}$' + AND substr(pb."Code",13,1)::int <> pg_temp.ean13_check(substr(pb."Code",1,12)) +ORDER BY pb."OrganizationId", p."Name"; + +-- Конфликты: исправленный код уже занят в этой орге — НЕ трогаем. +SELECT pb."Code" AS old_code, + substr(pb."Code",1,12) || pg_temp.ean13_check(substr(pb."Code",1,12))::text AS new_code, + pb."OrganizationId" +FROM product_barcodes pb +WHERE pb."Code" ~ '^2[0-9]{12}$' + AND substr(pb."Code",13,1)::int <> pg_temp.ean13_check(substr(pb."Code",1,12)) + AND EXISTS ( + SELECT 1 FROM product_barcodes x + WHERE x."OrganizationId" = pb."OrganizationId" + AND x."Code" = substr(pb."Code",1,12) || pg_temp.ean13_check(substr(pb."Code",1,12))::text + AND x."Id" <> pb."Id"); + +-- Собственно исправление (без конфликтных). +UPDATE product_barcodes pb +SET "Code" = substr(pb."Code",1,12) || pg_temp.ean13_check(substr(pb."Code",1,12))::text +WHERE pb."Code" ~ '^2[0-9]{12}$' + AND substr(pb."Code",13,1)::int <> pg_temp.ean13_check(substr(pb."Code",1,12)) + AND NOT EXISTS ( + SELECT 1 FROM product_barcodes x + WHERE x."OrganizationId" = pb."OrganizationId" + AND x."Code" = substr(pb."Code",1,12) || pg_temp.ean13_check(substr(pb."Code",1,12))::text + AND x."Id" <> pb."Id"); + +-- Контроль: сколько осталось неверных (должно стать = числу конфликтных). +SELECT count(*) AS remaining_wrong FROM product_barcodes +WHERE "Code" ~ '^2[0-9]{12}$' + AND substr("Code",13,1)::int <> pg_temp.ean13_check(substr("Code",1,12)); diff --git a/deploy/stage-reset-demo.sql b/deploy/stage-reset-demo.sql new file mode 100644 index 0000000..2781bb0 --- /dev/null +++ b/deploy/stage-reset-demo.sql @@ -0,0 +1,103 @@ +-- deploy/stage-reset-demo.sql +-- Очистка организации «Демо-магазин» на стенде от ВСЕХ транзакционных данных, +-- КРОМЕ каталога (товары/группы/штрихкоды/цены/ед.изм./картинки), сотрудников/ +-- пользователей/ролей и магазина/точек продаж. По поправке владельца 07.10.2026. +-- +-- Запуск (с обязательной предварительной резервной копией!): +-- ssh nns@192.168.1.190 "docker exec -i food-market-stage-postgres \ +-- psql -U food_market -d food_market -v demo_org=" < deploy/stage-reset-demo.sql +-- +-- Идемпотентно. KEEP-таблицы НЕ трогаются. Удаление — только по OrganizationId +-- целевой орги; FK-порядок разрешается retry-loop'ом (как в stage-prune-autotest.sql). + +\set ON_ERROR_STOP on + +-- psql не подставляет :var внутри dollar-quoted DO-блока, поэтому пробрасываем +-- org через GUC и читаем current_setting() внутри блока. +SELECT set_config('fm.demo_org', :'demo_org', false); + +DO $do$ +DECLARE + org uuid := current_setting('fm.demo_org')::uuid; + keep text[] := ARRAY[ + -- каталог + 'products','product_prices','product_barcodes','product_groups','product_images', + 'org_units_of_measure','price_types', + -- сотрудники / пользователи / роли + 'employees','employee_roles','users','roles', + -- магазин и точки продаж + 'stores','retail_points','employee_retail_point_assignments', + -- прочий org-scoped reference, который владелец хочет сохранить + 'expense_items','user_presets' + ]; + tbl text; + pass int; + remaining int; + total_deleted bigint := 0; + n bigint; + prod_cnt bigint; +BEGIN + IF org IS NULL THEN + RAISE EXCEPTION 'demo_org не задан (psql -v demo_org=...)'; + END IF; + PERFORM 1 FROM organizations WHERE "Id" = org; + IF NOT FOUND THEN + RAISE EXCEPTION 'Орг % не найдена', org; + END IF; + + RAISE NOTICE 'Очистка Демо-орг % — KEEP % таблиц', org, array_length(keep,1); + + FOR pass IN 1..12 LOOP + remaining := 0; + FOR tbl IN + SELECT c.table_name + FROM information_schema.columns c + JOIN information_schema.tables t + ON t.table_schema=c.table_schema AND t.table_name=c.table_name + WHERE c.table_schema='public' + AND c.column_name='OrganizationId' + AND t.table_type='BASE TABLE' + AND c.table_name <> ALL(keep) + LOOP + BEGIN + EXECUTE format('DELETE FROM public.%I WHERE "OrganizationId"=$1', tbl) USING org; + GET DIAGNOSTICS n = ROW_COUNT; + total_deleted := total_deleted + n; + EXCEPTION WHEN foreign_key_violation THEN + remaining := remaining + 1; + END; + END LOOP; + EXIT WHEN remaining = 0; + END LOOP; + + IF remaining <> 0 THEN + RAISE EXCEPTION 'Остались неудалённые таблицы из-за FK после 12 проходов (remaining=%)', remaining; + END IF; + + SELECT count(*) INTO prod_cnt FROM products WHERE "OrganizationId"=org; + RAISE NOTICE 'Удалено строк: %, товаров осталось: %', total_deleted, prod_cnt; +END $do$; + +-- Контроль: документы и остатки должны быть 0, каталог/сотрудники/точки целы. +SELECT 'products' m, count(*) v FROM products WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'product_prices', count(*) FROM product_prices WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'product_barcodes', count(*) FROM product_barcodes WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'product_groups', count(*) FROM product_groups WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'employees', count(*) FROM employees WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'users', count(*) FROM users WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'stores', count(*) FROM stores WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'retail_points', count(*) FROM retail_points WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'supplies', count(*) FROM supplies WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'retail_sales', count(*) FROM retail_sales WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'stocks', count(*) FROM stocks WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'stock_movements', count(*) FROM stock_movements WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'money_accounts', count(*) FROM money_accounts WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'money_transactions', count(*) FROM money_transactions WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'cash_shifts', count(*) FROM cash_shifts WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'enters', count(*) FROM enters WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'losses', count(*) FROM losses WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'inventories', count(*) FROM inventories WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'invoice_uploads', count(*) FROM invoice_uploads WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'counterparties', count(*) FROM counterparties WHERE "OrganizationId"=:'demo_org' +UNION ALL SELECT 'org_audit_log', count(*) FROM org_audit_log WHERE "OrganizationId"=:'demo_org' +ORDER BY m; diff --git a/docs/ean13-fixed-20261008.txt b/docs/ean13-fixed-20261008.txt new file mode 100644 index 0000000..c890026 --- /dev/null +++ b/docs/ean13-fixed-20261008.txt @@ -0,0 +1,43 @@ + OrganizationId | ProductId | product | old_code | new_code +--------------------------------------+--------------------------------------+--------------------------------------+---------------+--------------- + 02a0ba90-0efb-4856-8922-bc90db46032c | 989779be-8c27-4787-8140-4f94dcdb156b | Товар 1 | 2774151174816 | 2774151174818 + 2030055a-1c1e-414e-ac0e-591ebfb27134 | 3edb0b3d-1992-4544-92b6-f5d4a4acd3c3 | Товар 1 | 2322682989291 | 2322682989298 + 241c01d6-319e-42f7-b678-a3d257040d8d | b4d60653-355e-4f52-8b89-9528b9883a52 | Товар 1 | 2989387437553 | 2989387437556 + 2b8ad716-844a-4ccc-8634-1f7054aff4f2 | cb3fec1b-94bf-46a6-9ef3-6a060279f9dd | Товар 1 | 2573011570966 | 2573011570964 + 2d6f1727-649c-4517-8750-4c1055fcd1b8 | ccd8f9f8-551a-4d5a-b500-45d6fe81fec6 | Товар 1 | 2777942088706 | 2777942088707 + 2df1e315-111d-4fb5-8416-d540400ee96e | 3c6c724b-38bd-4bfe-990c-d899b55ffef0 | Товар 1 | 2221311034961 | 2221311034968 + 3d88118f-12ca-491b-ae8b-54889d656217 | 6f0e1fa2-5bdc-4d1e-a4b8-4ea7cd42b1eb | Товар 1 | 2319153165810 | 2319153165817 + 40f09e9d-fce5-4e9b-a25c-1c9c9422b4c3 | de430ffe-b939-4478-8cdb-cf618507f6f2 | Товар 1 | 2778342543970 | 2778342543971 + 43eaa2ec-cdf5-4599-9ea9-de02bd7b7f02 | 57fee67e-1907-4ea3-ab63-efc19966d93f | Товар 1 | 2641783270397 | 2641783270396 + 565819c6-6821-4367-89cf-ac08fd5b8bce | 164f9409-5bb0-4f1d-ae2d-14b57d87cd5e | Товар 1 | 2104944436773 | 2104944436777 + 5744895d-8f8f-4521-a171-8d01a45c9716 | 8207ad35-7321-41ba-bd83-42fe869a57f9 | Товар 1 | 2454147839196 | 2454147839197 + 5ae2d7aa-1e3c-4ab9-8f37-e432c37059bd | 4644bda1-e8c1-49f1-aa4d-e587d8417822 | Товар 1 | 2773341942620 | 2773341942626 + 5c739483-4cf0-4a84-abc9-e5abc2d9498c | a3de6b46-2583-42b9-a149-5c5c1cde44bb | Товар 1 | 2141394451683 | 2141394451684 + 68d55389-9240-48f1-924b-5fcaef582662 | dfe0af38-8273-44e4-af2b-1863373ac7a8 | Товар 1 | 2802175321220 | 2802175321229 + 6da240a9-ed19-422e-a8ff-3e88319411bc | 6005300f-a165-460c-97f5-d0bd5dd82744 | Товар 1 | 2172174489121 | 2172174489122 + 7353187f-af3d-4218-b0b3-b48af77efedc | 3c7f28cb-edef-4ed8-be7f-b5b9acd54217 | Товар 1 | 2975569597001 | 2975569597002 + 82f94765-c378-4eec-80e8-15e1f1216315 | e2cec8e4-9be1-4314-ad61-782dad5135a0 | Товар 1 | 2908501111892 | 2908501111899 + 8404e55e-d325-4c11-9576-9c733c107afb | d38a0cde-db35-468f-8e65-3c038a980e15 | Товар 1 | 2561410176365 | 2561410176368 + 88a1b10b-9006-4dbf-8010-c86833e3226e | 833451f0-eb3e-445d-9672-f501ed98d73e | Товар 1 | 2971275425507 | 2971275425509 + 89262cea-977b-4097-8e78-fc34e25244de | a981266c-d936-4af6-ab29-fbce3313ec00 | Товар 1 | 2022774230099 | 2022774230092 + 8bbb43a0-711f-46d9-a8bb-0f4f0d633ed5 | 681c6c8e-a808-47d7-8d50-9115d6a78395 | Товар 1 | 2166137596314 | 2166137596316 + 929b00b1-cdef-4c72-a1d2-ec48a79c0e50 | 2648291a-9181-497d-b9d5-57852f2e1806 | Товар 1 | 2650860255151 | 2650860255157 + 9aea0e38-cb74-4983-b6f6-b1389620cdfd | e7c5c371-a4a2-40db-9f90-6ff413d9ffa0 | Товар 3 | 2800124503582 | 2800124503580 + 9b22f3cf-46cd-4388-8d39-62da0bec7377 | 2f4441dc-2a94-4a2c-ade6-5c270ad48a86 | Товар 1 | 2769454863444 | 2769454863446 + 9b5593c0-7415-4347-aaf6-7908af63e075 | f4b50e3c-e63a-4c4d-9c3f-2c32a552102a | Товар 2 | 2732219120138 | 2732219120139 + a70c0d6f-b99b-41bf-b12e-56c733c55202 | 5a857b3b-57c8-4460-8ad7-9f6eaee0ce97 | Товар 1 | 2630344841029 | 2630344841021 + ab3ba17b-e1c7-4779-b4da-7a6f74fab877 | 5952aac2-0aaa-4e6b-8f73-88c3980f5a76 | Товар 1 | 2479429060400 | 2479429060403 + b72b5ccf-8b86-41d5-87a5-c69b0ead6c6d | 8e721005-feaf-432e-8ebb-34efac5a7503 | Товар 1 | 2181329206711 | 2181329206712 + b7c403fe-7fc8-48af-9490-4a9a2eafe2c2 | d93afc1e-ea27-461f-afea-16d409e88468 | Тунец натуральный 240 гр Fish master | 2000000399788 | 2000000399782 + bf4466f7-2c50-4ca9-8be7-7db2a1dc96d2 | f290ea9b-41f8-485f-8f06-024e2dd67d04 | Товар 1 | 2526982373876 | 2526982373874 + d1fe3292-a167-4c63-9944-eed29c7494a7 | ea6a9db6-581c-4b5e-a011-9d92c5d903f7 | Товар 1 | 2400138194244 | 2400138194240 + db7ff78b-1755-4388-bc73-8dde9546279c | 001f8390-fade-4e50-8942-cbe8700e8647 | Товар 1 | 2121043372681 | 2121043372683 + dcd787cd-3209-4c57-a88e-e17d8bd567da | 4d2f3b56-c73b-49e4-a6f2-1402af486e54 | Товар 1 | 2091076032522 | 2091076032529 + e1057e0f-493b-458a-ac63-2b0d3b005ce2 | 7a74794c-d672-4e63-afc8-a59e9cafb50b | Товар 1 | 2422506372305 | 2422506372302 + e166d264-2b56-4fee-8aeb-2c650f0b9e32 | ca3f24ec-ce67-4402-bda0-108205f1e6aa | Товар 1 | 2191353131442 | 2191353131447 + f158b0b8-6266-4574-bbb0-6cfe2333962b | 2aec0165-42a9-4088-a48d-0b786a367d8d | Товар 3 | 2893012813027 | 2893012813021 + f4018fd4-041a-4e72-8204-e091ecbfb9b8 | e5ace923-0c5f-4b35-bf8d-559275bd6247 | Товар 2 | 2544714677075 | 2544714677076 + f44b79c5-e617-4aa4-aeab-b4b2f7d05a7a | ab0ca323-51e9-4242-a0b5-b2833bb7782b | Товар 1 | 2965763672860 | 2965763672865 + f80269c4-5702-4d67-a56f-5f9e871094a7 | e939a60b-30bb-44d7-9fc6-f7208e318c55 | Товар 1 | 2276824292069 | 2276824292060 + fbc3505d-585e-47ac-ae91-9339907233a3 | f7ca3aab-10ec-4fb0-8130-f2b29234a225 | Товар 2 | 2021646806852 | 2021646806854 + fc966697-a6c5-4a74-b5a2-54bdac02f5e6 | 0fbacfc8-dea8-46dc-8927-3e455c1438a0 | Товар 1 | 2718687983533 | 2718687983537