mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-17 08:15:16 +03:00
Compare commits
573 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afdfc97fb1 | |||
| 82e70d2c30 | |||
| aedf6df674 | |||
| 0d31d17204 | |||
| 2206c4cf3e | |||
| 8a9f285da9 | |||
| e5e3f199f2 | |||
| 3ee715939b | |||
| b8ab791456 | |||
| 97a2ef91d5 | |||
| a41897a966 | |||
| 076bbcb9d5 | |||
| 3c48451704 | |||
| 801ceea2c0 | |||
| 63de8bd27c | |||
| d1e770c843 | |||
| e3069ceb36 | |||
| 0b2c09f8d9 | |||
| 0d5d3451ff | |||
| 7a8f437e77 | |||
| dc4ffa1fc8 | |||
| 6805b9da02 | |||
| aa93031a3f | |||
| 037062c728 | |||
| 9cbda1989a | |||
| d34ddd41ff | |||
| 96d1dea552 | |||
| 83b289de32 | |||
| 384e68cab4 | |||
| 46e065adb9 | |||
| fdfe4ac2f4 | |||
| afacaa5ade | |||
| 73242ee60e | |||
| e2924c4ae0 | |||
| 123fc41b06 | |||
| 338faa253d | |||
| d3520a8945 | |||
| e731bde7f0 | |||
| 321e426f98 | |||
| 54c04cfe76 | |||
| 9727635e5d | |||
| 6404016afb | |||
| 17afd807d2 | |||
| 9bfc431a6a | |||
| d755c41233 | |||
| 87e636f644 | |||
| ae1dcaac07 | |||
| d155086444 | |||
| 940eafb8e4 | |||
| e4dcef8c90 | |||
| 08688f5cab | |||
| 1bc5e8f07a | |||
| 358830c65b | |||
| 3ba6072660 | |||
| 20adf4f463 | |||
| d5aacc9e60 | |||
| 3bd69baae6 | |||
| b0fdc32053 | |||
| 731a652cae | |||
| dd88f78969 | |||
| ff068202e3 | |||
| 91798e3838 | |||
| 9620c3846a | |||
| ffc34904c5 | |||
| b008c820f9 | |||
| e5697d6f5c | |||
| 29ee294de6 | |||
| d72cbacfd6 | |||
| c8a7ba4683 | |||
| ea8b28280d | |||
| eebc2a0679 | |||
| 974e8ff5e4 | |||
| 47c2e5d48f | |||
| e56a932ee2 | |||
| 69f5e0b7ae | |||
| 84462e3dd9 | |||
| d574320849 | |||
| 90ff1d53b6 | |||
| 09bc0d00b8 | |||
| 66c6dc7aa1 | |||
| 1d89b3ec69 | |||
| 43d1d09ee8 | |||
| 404e6c6851 | |||
| 009022d04b | |||
| 70e684d189 | |||
| 72bad1aaaa | |||
| 29a5cbae4c | |||
| be1bbb2d26 | |||
| 5281fd54f2 | |||
| f697ba3ff3 | |||
| 0778cc89ee | |||
| 28b2765f68 | |||
| 0d116863ec | |||
| bc6bc01047 | |||
| 972cd5061d | |||
| 4565808b77 | |||
| 250978c2fd | |||
| 4551525718 | |||
| 9422c66df4 | |||
| a1b3fdd4e2 | |||
| d6d44b8b35 | |||
| b480fa0b49 | |||
| 44a84f8ce7 | |||
| dcacecff7f | |||
| 8b38f8f78d | |||
| 74b3f76a32 | |||
| 671be7c6b5 | |||
| 4a697db47a | |||
| 34f582ddef | |||
| eec626eb6f | |||
| 312d058412 | |||
| 95776a4248 | |||
| 347722ef0d | |||
| 9f6793abae | |||
| 0ddb675502 | |||
| d2dfcfd160 | |||
| 6c01f3a261 | |||
| 506f0b84df | |||
| f5c02144cb | |||
| d982c5082a | |||
| cce00a4def | |||
| bc4ee63986 | |||
| a314ec32fc | |||
| baefeaa05b | |||
| 2d88cdc864 | |||
| f7695a9534 | |||
| 252105396b | |||
| 42df4cd6b1 | |||
| b9a04b235f | |||
| 9bc0240e95 | |||
| 4cfcaaaa1c | |||
| f9c3792528 | |||
| 9090a81e43 | |||
| f037324ee9 | |||
| 6f36e36166 | |||
| ca0dac2693 | |||
| ab7d82ba20 | |||
| 669b5f42f2 | |||
| 5b6fcc7c74 | |||
| df47128994 | |||
| ea453e5c97 | |||
| 14b0782904 | |||
| b7e3bd876d | |||
| b68f4145ce | |||
| dc2056a5d5 | |||
| bef4a13ba9 | |||
| c280e52932 | |||
| c86bf8779f | |||
| a4e0faaeb3 | |||
| 9d6ceae4f1 | |||
| 18ad662378 | |||
| 05fc7eba27 | |||
| ffa3189442 | |||
| bb02c1b0bd | |||
| 46f0e6be8c | |||
| 38ccded7d9 | |||
| 4e0ce5ba0f | |||
| e948d39adb | |||
| d9e7d9c107 | |||
| 1da43109fc | |||
| 21a0e360ef | |||
| 9f5e244f68 | |||
| 18a198d12b | |||
| 8fca42193a | |||
| b603c7e3fa | |||
| b00289e62c | |||
| 97820111d5 | |||
| 0a2a36dbc6 | |||
| b36232ae19 | |||
| 1479f944f5 | |||
| 9cffa81bcc | |||
| 6667ab38ee | |||
| 26c5e849a5 | |||
| ca681676b4 | |||
| e3a9be0036 | |||
| 361354cdd4 | |||
| 2ce7b12c4c | |||
| f244d5e507 | |||
| 6cb54a8f96 | |||
| 75f56603b6 | |||
| 55ab172ba4 | |||
| e9e00c906d | |||
| 31792a8a2d | |||
| fa2e3dd3e7 | |||
| 3e8a0a792f | |||
| d5e4bc3f21 | |||
| 7a6e63078d | |||
| 55b0ffa4fd | |||
| d06ecb940f | |||
| 21de2a4749 | |||
| 07be2bcafd | |||
| 24a48b4a9f | |||
| 91b5204ab2 | |||
| f2bd42c54c | |||
| 7e56cd79db | |||
| 1abc44334c | |||
| 0ca2faa6a6 | |||
| 767b9fcaf0 | |||
| 598e223288 | |||
| f7a27e750d | |||
| 15840ed1ca | |||
| 733418e630 | |||
| b15fb999cf | |||
| 5414ac373e | |||
| 0c4addaedc | |||
| cc0d577fc5 | |||
| 160a845bd8 | |||
| 184e4773a4 | |||
| 97105486b5 | |||
| 23f4cc0bbd | |||
| c5c67600b3 | |||
| 85191dc759 | |||
| 064e42842e | |||
| 4507d9b59c | |||
| 17be0ca009 | |||
| 8f59d3bc66 | |||
| f93a1560a5 | |||
| 298589d7cc | |||
| c6a84e2f91 | |||
| de7e943de1 | |||
| 4855cd1e01 | |||
| 7abe26afee | |||
| 512562ca50 | |||
| 84d078bd05 | |||
| 8584021446 | |||
| e7e6c885c7 | |||
| b5a779bd5d | |||
| 6df64ae1c1 | |||
| 007b4ec69c | |||
| cf5eae6428 | |||
| c72f2210b5 | |||
| 06584c29b2 | |||
| d94ffdea99 | |||
| 9f370dd499 | |||
| 903f1f2750 | |||
| 29b124f3fd | |||
| ffdee04a95 | |||
| b2a2534b5a | |||
| 422e397381 | |||
| 191706b984 | |||
| 93e4130ff2 | |||
| 3f091b209c | |||
| 5d4ee6c58c | |||
| 86c8440531 | |||
| 42cc7acdbe | |||
| 72a4015a8d | |||
| 843916642c | |||
| 84f3662c02 | |||
| 4cdd3377ae | |||
| 730daf1089 | |||
| c0255c71d0 | |||
| cf1cd1a711 | |||
| 5a1974be4e | |||
| 589091510b | |||
| 19667caccb | |||
| 97fc6dbd7f | |||
| 49108f72dc | |||
| dbb39b7a3e | |||
| 87dc18b120 | |||
| ca328e3e8f | |||
| c2b90a97ef | |||
| e717d4a57f | |||
| 6051972391 | |||
| 24751a59de | |||
| b824759a05 | |||
| fb23cbda56 | |||
| 69c89d3d70 | |||
| dee85e19d5 | |||
| 87348ec73c | |||
| dd81c85366 | |||
| c6f499a099 | |||
| b5826a61c4 | |||
| 3097811dde | |||
| f01f9d6ae8 | |||
| 0a1f8888dc | |||
| 66eb0b19ec | |||
| 872675652f | |||
| fde6d346d7 | |||
| 4490aeb533 | |||
| 6958cc558b | |||
| 8a286d7191 | |||
| a5fa730747 | |||
| 53219cae6c | |||
| b1187865ec | |||
| 22bf17ee00 | |||
| 2e0dcb773b | |||
| c958f3f818 | |||
| 684e97e518 | |||
| ce5651a5d7 | |||
| e706aca958 | |||
| 90b3d5d2e9 | |||
| 1b37bad331 | |||
| 8b0664b18d | |||
| 2af73d25bf | |||
| 449e2f667e | |||
| e032143157 | |||
| 09fc3a7f43 | |||
| 01315839fb | |||
| 6505fb1986 | |||
| 9e022c60f9 | |||
| e23a235ffa | |||
| 5bccca2475 | |||
| a8d8007846 | |||
| 37d2a5ee15 | |||
| 050d42a457 | |||
| 5acb586bbf | |||
| 7f6ea32533 | |||
| e9b53505ac | |||
| 6ce36f7d9b | |||
| a13f16e49d | |||
| 8f130f2e83 | |||
| 26ea09e2ff | |||
| 98c416b5a7 | |||
| 98ece6206e | |||
| 0c0a1aea49 | |||
| 5870fa200f | |||
| 7c10e5e4b1 | |||
| 665d5579d2 | |||
| 0fec18bbc4 | |||
| cc751a344e | |||
| f956749e1e | |||
| 2a6f4b1d5f | |||
| 9318d05535 | |||
| bb07bfae14 | |||
| 38c82f4a2f | |||
| d5fa5349d4 | |||
| ebc63c0264 | |||
| 99a5c7168a | |||
| 751a6a8f12 | |||
| 43a2691d6a | |||
| e6da514b67 | |||
| 1e5c42fa56 | |||
| 59627b0699 | |||
| 7b69228d23 | |||
| da0cedf256 | |||
| cc6b170488 | |||
| f9439dce6d | |||
| 336466ae82 | |||
| 59d223d72b | |||
| f56f5dc849 | |||
| 1fd3d8ffb8 | |||
| d6befb0dc8 | |||
| 5120b135f5 | |||
| 5d18bf4501 | |||
| 366378c4b5 | |||
| 56203e2b71 | |||
| ff14880871 | |||
| a17a81fe98 | |||
| e58f5c1f82 | |||
| 0fbdd298ca | |||
| 68fd9bb4a7 | |||
| e18e8cd284 | |||
| 5ea6414585 | |||
| 75558fe46c | |||
| 90ca2a7c4a | |||
| 500e05703e | |||
| ebda3e6608 | |||
| 328cb60f18 | |||
| 5061d83282 | |||
| b10802d4d7 | |||
| ae0c3e3278 | |||
| 00ec14aed3 | |||
| 102a7b8849 | |||
| 68e346d645 | |||
| c8f58b5461 | |||
| ba963739d8 | |||
| afe760af4b | |||
| c6eae770f5 | |||
| d28bea8f0a | |||
| 475b6d9544 | |||
| 2d69ecf709 | |||
| 88f74e96f0 | |||
| bce2d7c85c | |||
| 8f3bde5cfc | |||
| 85cdea925e | |||
| 679f6bae94 | |||
| c5abfc3a4f | |||
| d9ce7b03cc | |||
| d6abb2b651 | |||
| 86a959a354 | |||
| 8e7125d850 | |||
| fa9fae49c2 | |||
| c0e18ed428 | |||
| 64453ebe08 | |||
| c91ad0a158 | |||
| 6b5992de34 | |||
| 9d5cbaab9f | |||
| fe8fa4bc3f | |||
| 028c5292d7 | |||
| 31debfb61b | |||
| a110c17066 | |||
| b535dff609 | |||
| 96b400ddb9 | |||
| b68013b2f2 | |||
| e2f765fcc0 | |||
| a3e9e3c907 | |||
| bc4778d6cc | |||
| 2a0b896f5d | |||
| bc9087957f | |||
| 78e252af1e | |||
| 4c5e10a958 | |||
| 7d0d2dbf3d | |||
| 89d1ad3a12 | |||
| 9c75e9392e | |||
| 5f7ee6fcae | |||
| 6f32e18e37 | |||
| 3565e1c97f | |||
| 28b967f591 | |||
| 9c1985b710 | |||
| 99d37fb494 | |||
| 48de4cdc2a | |||
| 544893b781 | |||
| 8ecc5764ed | |||
| 329d94dc55 | |||
| 7268b169d8 | |||
| 2a4d2bf8df | |||
| 6e7284fc79 | |||
| c9e991e1e8 | |||
| 2a0fff1416 | |||
| 1364f22df3 | |||
| 4b0304d0c8 | |||
| cfcc633257 | |||
| d7f012b307 | |||
| 27bb08d127 | |||
| 151ded9bae | |||
| d84a2fb4cf | |||
| 05362aeb96 | |||
| 01d02065a3 | |||
| a711f05acc | |||
| 7003a46ad0 | |||
| e29c184f8e | |||
| 38c0ce11a9 | |||
| fcc85c9c6e | |||
| 9331aa7825 | |||
| d6ad1eacdb | |||
| 52875b4dea | |||
| 2698a2b80b | |||
| 8e8660c52b | |||
| 5d3eccc8eb | |||
| b8e3107d14 | |||
| aae69664c5 | |||
| bd947f2c0b | |||
| 1b6ab2f144 | |||
| a6a708d14d | |||
| 3ec2d2594c | |||
| 08d29c0341 | |||
| 37ecb77256 | |||
| 9cfb62c5b1 | |||
| fff77d97ca | |||
| 484ee44387 | |||
| 47174ee236 | |||
| d02ca602a4 | |||
| f3b3f26af9 | |||
| be47935973 | |||
| 1312cd7db4 | |||
| a81c26afda | |||
| 7044f125ed | |||
| 989f4e49d9 | |||
| cb4b5ec935 | |||
| 7c5700e40f | |||
| a6bf6520ce | |||
| ed45f54594 | |||
| 099de2d850 | |||
| 2d79c0b0ee | |||
| acee4bd188 | |||
| 01b20426fa | |||
| 04e5360e52 | |||
| 33fa6dbe9a | |||
| 11152d2443 | |||
| daea0a72d1 | |||
| b4905fc3e3 | |||
| 23555b513f | |||
| 8aef749471 | |||
| bd30e48dae | |||
| 92dc00e152 | |||
| 45c2587e4a | |||
| 08722ff285 | |||
| 89c0775c65 | |||
| 5c0a55e090 | |||
| 2f2ecca88b | |||
| acb7a4eadc | |||
| 28b8061d64 | |||
| 703c3e6f80 | |||
| 2d0835ba17 | |||
| a9b7a90672 | |||
| 69213930b3 | |||
| 151d1d77cb | |||
| 70bb5074c8 | |||
| 91d254b718 | |||
| 1382194201 | |||
| 7e587b9a3e | |||
| 4594c74d21 | |||
| 2650b1a063 | |||
| f23a3e5536 | |||
| eb6f8379e6 | |||
| 91e4e1948f | |||
| f4d60d67bf | |||
| b67f4b21e6 | |||
| d573f84c9d | |||
| c306d18648 | |||
| d72dd1678e | |||
| 0ca6d3f089 | |||
| dddb9e3dac | |||
| f211ffa2f0 | |||
| 21dded4d1c | |||
| 941fa73803 | |||
| 24358f212f | |||
| d210d21079 | |||
| 2949682ce8 | |||
| e86620dbd7 | |||
| 9922f118d1 | |||
| 8c833f042c | |||
| 1d593af63a | |||
| be216cd064 | |||
| 55b1ef4e06 | |||
| e229fc4873 | |||
| 3a4c1905f4 | |||
| 7aaa9fc070 | |||
| 8b8bc1225a | |||
| dc10422f10 | |||
| 65afdf2e55 | |||
| f87c94e832 | |||
| 6681758121 | |||
| 89f0788585 | |||
| d27dfbf8e1 | |||
| fc24524c75 | |||
| 2c96c90d45 | |||
| 8fc30c9b9a | |||
| ecfb473827 | |||
| dc9e1fe3db | |||
| 5c07dec82c | |||
| c5fa794cf3 | |||
| 7e0e30730c | |||
| ad412dffa2 | |||
| c665800916 | |||
| d05199d048 | |||
| a9e59265dd | |||
| 99f57a5ff2 | |||
| a813c7fd46 | |||
| cc2595b57a | |||
| 580124a971 | |||
| 044bc18974 | |||
| 3819a5f7af | |||
| 56b1458545 | |||
| bca022fba8 | |||
| 0e680d151d | |||
| 8092015807 | |||
| 03a6820f69 | |||
| bfbef3c361 | |||
| 298d503c64 | |||
| 1ed5d783e8 | |||
| f0bb8c860f | |||
| 2f260ddb7f | |||
| 2771424c71 | |||
| 96890598f9 | |||
| 50dc9ceec9 | |||
| 455c12e6b6 | |||
| 1a79e84598 | |||
| 76a0dd80be | |||
| fdf661192d | |||
| be3a31ea94 | |||
| 061c73b23d | |||
| 9f9a022659 | |||
| 215319382a | |||
| d6660a4d00 | |||
| 4bc86decf8 | |||
| 34e88d2914 | |||
| b3dac25729 | |||
| c2348b6b68 | |||
| aaa4f65be1 | |||
| d34cbf7272 | |||
| e3daaf233d | |||
| 43333f1481 |
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
|
|
||||||
"name": "1c-skills-py",
|
|
||||||
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
|
|
||||||
"author": {
|
|
||||||
"name": "Nikolay Shirokov"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"repository": "https://github.com/Nikolay-Shirokov/cc-1c-skills",
|
|
||||||
"license": "MIT",
|
|
||||||
"keywords": [
|
|
||||||
"1c",
|
|
||||||
"1c-dev",
|
|
||||||
"cf",
|
|
||||||
"cfe",
|
|
||||||
"epf",
|
|
||||||
"erf",
|
|
||||||
"metadata",
|
|
||||||
"configuration",
|
|
||||||
"extension",
|
|
||||||
"form",
|
|
||||||
"report",
|
|
||||||
"skd",
|
|
||||||
"data-processor",
|
|
||||||
"mxl",
|
|
||||||
"web-client",
|
|
||||||
"testing",
|
|
||||||
"test-automation"
|
|
||||||
],
|
|
||||||
"skills": "./.claude/skills/"
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: cf-edit
|
name: cf-edit
|
||||||
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
|
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию
|
||||||
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `NoValidate` | Пропустить авто-валидацию |
|
| `NoValidate` | Пропустить авто-валидацию |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
powershell.exe -NoProfile -File .claude/skills/cf-edit/scripts/cf-edit.ps1 -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Операции
|
## Операции
|
||||||
@@ -37,8 +37,6 @@ python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation
|
|||||||
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
|
||||||
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
|
||||||
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
|
||||||
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
|
|
||||||
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
|
|
||||||
|
|
||||||
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
|
||||||
|
|
||||||
@@ -46,15 +44,15 @@ python "${CLAUDE_SKILL_DIR}/scripts/cf-edit.py" -ConfigPath '<path>' -Operation
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Изменить версию и поставщика
|
# Изменить версию и поставщика
|
||||||
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
... -ConfigPath test-tmp/cf -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
|
||||||
|
|
||||||
# Добавить объекты
|
# Добавить объекты
|
||||||
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
... -ConfigPath test-tmp/cf -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
|
||||||
|
|
||||||
# Удалить объект
|
# Удалить объект
|
||||||
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
|
... -ConfigPath test-tmp/cf -Operation remove-childObject -Value "Catalog.Устаревший"
|
||||||
|
|
||||||
# Роли по умолчанию
|
# Роли по умолчанию
|
||||||
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
|
... -ConfigPath test-tmp/cf -Operation add-defaultRole -Value "ПолныеПрава"
|
||||||
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
... -ConfigPath test-tmp/cf -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
### Enum
|
### Enum
|
||||||
| Свойство | Допустимые значения |
|
| Свойство | Допустимые значения |
|
||||||
|----------|---------------------|
|
|----------|---------------------|
|
||||||
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_27`, `DontUse` |
|
||||||
| `ConfigurationExtensionCompatibilityMode` | то же |
|
| `ConfigurationExtensionCompatibilityMode` | то же |
|
||||||
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
|
||||||
| `ScriptVariant` | `Russian`, `English` |
|
| `ScriptVariant` | `Russian`, `English` |
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
|
||||||
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||||
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
|
||||||
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
|
| `InterfaceCompatibilityMode` | `Taxi`, `TaxiEnableVersion8_2`, `Version8_2` |
|
||||||
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
|
||||||
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
|
||||||
|
|
||||||
@@ -35,7 +35,14 @@
|
|||||||
|
|
||||||
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
Формат: `Type.Name` — XML-тип и имя объекта через точку.
|
||||||
|
|
||||||
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
|
**Важно про `add-childObject`**: операция регистрирует в `<ChildObjects>` Configuration.xml только объект, **файл которого уже существует на диске** (например `Catalogs/Товары.xml`). Если файла нет — скрипт падает с exit 1 и подсказкой. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его в Configuration.xml за один вызов.
|
||||||
|
|
||||||
|
Когда `add-childObject` всё-таки нужен: откатили Configuration.xml (или перезаписали из выгрузки БД), а файлы объектов остались — нужно восстановить ссылки в `<ChildObjects>`.
|
||||||
|
|
||||||
|
При добавлении объект вставляется в каноническую позицию:
|
||||||
|
1. Находит последний элемент того же типа → вставляет после
|
||||||
|
2. Если тип отсутствует → находит последний элемент предшествующего типа → вставляет после
|
||||||
|
3. Внутри одного типа — алфавитный порядок
|
||||||
|
|
||||||
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
|
||||||
|
|
||||||
@@ -45,99 +52,6 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
|
|||||||
|
|
||||||
`set-defaultRoles` полностью заменяет список ролей.
|
`set-defaultRoles` полностью заменяет список ролей.
|
||||||
|
|
||||||
## set-panels
|
|
||||||
|
|
||||||
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
|
|
||||||
|
|
||||||
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
|
|
||||||
|
|
||||||
**Запись** — одна из:
|
|
||||||
- Строка-алиас (одна панель в этом слоте)
|
|
||||||
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
|
|
||||||
|
|
||||||
**Алиасы панелей:**
|
|
||||||
|
|
||||||
| Алиас | Панель |
|
|
||||||
|-------|--------|
|
|
||||||
| `sections` | Панель разделов |
|
|
||||||
| `open` | Панель открытых |
|
|
||||||
| `favorites` | Панель избранного |
|
|
||||||
| `history` | Панель истории |
|
|
||||||
| `functions` | Панель функций текущего раздела |
|
|
||||||
|
|
||||||
**Семантика:**
|
|
||||||
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
|
|
||||||
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
|
|
||||||
|
|
||||||
**Пример** (DefinitionFile):
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"operation": "set-panels",
|
|
||||||
"value": {
|
|
||||||
"top": ["open"],
|
|
||||||
"left": ["sections"],
|
|
||||||
"right": [{ "group": ["favorites", "history"] }],
|
|
||||||
"bottom": ["functions"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
|
|
||||||
|
|
||||||
## set-home-page
|
|
||||||
|
|
||||||
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
|
|
||||||
|
|
||||||
`value` — объект:
|
|
||||||
|
|
||||||
| Ключ | Канонич. (XML) | Описание |
|
|
||||||
|------|----------------|----------|
|
|
||||||
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
|
|
||||||
| `left` | `LeftColumn` | массив записей форм |
|
|
||||||
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
|
|
||||||
|
|
||||||
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
|
|
||||||
|
|
||||||
**Запись формы** — одна из:
|
|
||||||
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
|
|
||||||
- Объект `{form, height?, visibility?, roles?}`
|
|
||||||
|
|
||||||
| Поле | Канонич. | Дефолт | Описание |
|
|
||||||
|------|----------|--------|----------|
|
|
||||||
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
|
|
||||||
| `height` | `Height` | `10` | Высота |
|
|
||||||
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
|
|
||||||
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
|
|
||||||
|
|
||||||
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"operation": "set-home-page",
|
|
||||||
"value": {
|
|
||||||
"template": "TwoColumnsVariableWidth",
|
|
||||||
"left": [
|
|
||||||
"CommonForm.НачалоРаботы",
|
|
||||||
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
|
|
||||||
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
|
|
||||||
{
|
|
||||||
"form": "CommonForm.РабочийСтолОператора",
|
|
||||||
"visibility": false,
|
|
||||||
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"right": [
|
|
||||||
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## DefinitionFile (JSON)
|
## DefinitionFile (JSON)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -148,3 +62,6 @@ Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Авто-валидация
|
||||||
|
|
||||||
|
После сохранения автоматически запускается `cf-validate` (если не указан `-NoValidate`).
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory)][string]$ConfigPath,
|
||||||
[string]$DefinitionFile,
|
[string]$DefinitionFile,
|
||||||
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
|
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles")]
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[string]$Value,
|
[string]$Value,
|
||||||
[switch]$NoValidate
|
[switch]$NoValidate
|
||||||
@@ -29,126 +29,6 @@ if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; e
|
|||||||
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
$resolvedPath = (Resolve-Path $ConfigPath).Path
|
||||||
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
|
||||||
|
|
||||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
|
||||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
|
||||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
|
||||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
|
||||||
# throws — guard errors degrade to allow.
|
|
||||||
function Get-RootUuid([string]$xmlPath) {
|
|
||||||
if (-not (Test-Path $xmlPath)) { return $null }
|
|
||||||
try {
|
|
||||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
|
||||||
$el = $mx.DocumentElement.FirstChild
|
|
||||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
|
||||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Find-V8Project([string]$startDir) {
|
|
||||||
$d = $startDir
|
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
|
||||||
$pj = Join-Path $d ".v8-project.json"
|
|
||||||
if (Test-Path $pj) { return $pj }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Get-EditMode([string]$cfgDir) {
|
|
||||||
try {
|
|
||||||
$pj = Find-V8Project (Get-Location).Path
|
|
||||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
|
||||||
if (-not $pj) { return 'deny' }
|
|
||||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
|
||||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
|
||||||
if ($proj.databases) {
|
|
||||||
foreach ($db in $proj.databases) {
|
|
||||||
if ($db.configSrc) {
|
|
||||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
|
||||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
|
||||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
|
||||||
return 'deny'
|
|
||||||
} catch { return 'deny' }
|
|
||||||
}
|
|
||||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|
||||||
try {
|
|
||||||
$rp = $targetPath
|
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
|
||||||
$elemUuid = Get-RootUuid $rp
|
|
||||||
$cfgDir = $null; $binPath = $null
|
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
|
||||||
if (-not $cfgDir) {
|
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
|
||||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
|
||||||
}
|
|
||||||
if ($elemUuid -and $cfgDir) { break }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
# New object (no element file): fall back to config root uuid.
|
|
||||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
|
||||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
|
||||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
|
||||||
if ($bytes.Length -le 32) { return }
|
|
||||||
$start = 0
|
|
||||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
|
||||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
|
||||||
if (-not $hm.Success) { return }
|
|
||||||
$G = [int]$hm.Groups[1].Value
|
|
||||||
$K = [int]$hm.Groups[2].Value
|
|
||||||
if ($K -eq 0) { return }
|
|
||||||
$best = $null
|
|
||||||
if ($elemUuid) {
|
|
||||||
$u = [regex]::Escape($elemUuid.ToLower())
|
|
||||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
|
||||||
$f1 = [int]$m.Groups[1].Value
|
|
||||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$blocked = $false; $code = ""; $reason = ""
|
|
||||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
|
||||||
elseif ($require -eq 'removed') {
|
|
||||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
|
||||||
}
|
|
||||||
if (-not $blocked) { return }
|
|
||||||
$mode = Get-EditMode $cfgDir
|
|
||||||
if ($mode -eq 'off') { return }
|
|
||||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
|
||||||
# latter throws and would be swallowed by this function's own catch.
|
|
||||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
|
||||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if ($code -eq "capability-off") {
|
|
||||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
|
||||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
} elseif ($code -eq "not-removed") {
|
|
||||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
|
||||||
} else {
|
|
||||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
|
||||||
}
|
|
||||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
|
||||||
exit 1
|
|
||||||
} catch { return }
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert-EditAllowed $resolvedPath 'editable'
|
|
||||||
|
|
||||||
# --- Load XML with PreserveWhitespace ---
|
# --- Load XML with PreserveWhitespace ---
|
||||||
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
$script:xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
$script:xmlDoc.PreserveWhitespace = $true
|
$script:xmlDoc.PreserveWhitespace = $true
|
||||||
@@ -196,7 +76,7 @@ Info "Configuration: $($script:objName)"
|
|||||||
$script:typeOrder = @(
|
$script:typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -212,7 +92,7 @@ $script:typeOrder = @(
|
|||||||
$script:typeToDir = @{
|
$script:typeToDir = @{
|
||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
|
||||||
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
|
||||||
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
|
||||||
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
|
||||||
@@ -564,309 +444,6 @@ function Do-RemoveDefaultRole([string]$batchVal) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Operation: set-panels ---
|
|
||||||
# Canonical English aliases — preferred form, used in docs and error messages.
|
|
||||||
$script:panelUuids = @{
|
|
||||||
"sections" = "b553047f-c9aa-4157-978d-448ecad24248"
|
|
||||||
"open" = "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"
|
|
||||||
"favorites" = "13322b22-3960-4d68-93a6-fe2dd7f28ca3"
|
|
||||||
"history" = "c933ac92-92cd-459d-81cc-e0c8a83ced99"
|
|
||||||
"functions" = "b2735bd3-d822-4430-ba59-c9e869693b24"
|
|
||||||
}
|
|
||||||
# Russian synonyms — silently accepted (cf-info displays Russian names; users
|
|
||||||
# may copy them straight into cf-edit value).
|
|
||||||
$script:panelSynonyms = @{
|
|
||||||
"разделов" = "sections"; "разделы" = "sections"
|
|
||||||
"открытых" = "open"; "открытые" = "open"
|
|
||||||
"избранного" = "favorites";"избранное" = "favorites"
|
|
||||||
"истории" = "history"; "история" = "history"
|
|
||||||
"функций" = "functions";"функции" = "functions"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-PanelEntryXml($entry, [string]$indent) {
|
|
||||||
# String alias -> <panel><uuid>...</uuid></panel>
|
|
||||||
if ($entry -is [string]) {
|
|
||||||
$key = $entry.ToLowerInvariant()
|
|
||||||
if ($script:panelSynonyms.ContainsKey($key)) { $key = $script:panelSynonyms[$key] }
|
|
||||||
if (-not $script:panelUuids.ContainsKey($key)) {
|
|
||||||
Write-Error "Unknown panel alias '$entry'. Allowed: $(($script:panelUuids.Keys | Sort-Object) -join ', ')"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$u = $script:panelUuids[$key]
|
|
||||||
$instId = [guid]::NewGuid().ToString()
|
|
||||||
return "$indent<panel id=`"$instId`">`r`n$indent`t<uuid>$u</uuid>`r`n$indent</panel>"
|
|
||||||
}
|
|
||||||
# Object {group: [...]} -> <group id=""><group><panel/></group>...</group> (stack)
|
|
||||||
if ($entry.PSObject.Properties['group']) {
|
|
||||||
$children = $entry.group
|
|
||||||
if (-not $children -or $children.Count -eq 0) {
|
|
||||||
Write-Error "group must contain at least one entry"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$gid = [guid]::NewGuid().ToString()
|
|
||||||
$inner = ""
|
|
||||||
foreach ($child in $children) {
|
|
||||||
$childXml = Build-PanelEntryXml $child "$indent`t`t"
|
|
||||||
$inner += "$indent`t<group>`r`n$childXml`r`n$indent`t</group>`r`n"
|
|
||||||
}
|
|
||||||
return "$indent<group id=`"$gid`">`r`n$inner$indent</group>"
|
|
||||||
}
|
|
||||||
Write-Error "Panel entry must be a string alias or object {group:[...]}, got: $($entry | ConvertTo-Json -Compress)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-SetPanels($valArg) {
|
|
||||||
# Accept string (JSON), PSCustomObject, or hashtable
|
|
||||||
$layout = $valArg
|
|
||||||
if ($layout -is [string]) {
|
|
||||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
|
||||||
Write-Error "set-panels value must be valid JSON object, got: $valArg"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $layout) {
|
|
||||||
Write-Error "set-panels value is empty"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$sides = @("top","left","right","bottom")
|
|
||||||
$bodyParts = @()
|
|
||||||
foreach ($side in $sides) {
|
|
||||||
$entries = $null
|
|
||||||
if ($layout.PSObject.Properties[$side]) { $entries = $layout.$side }
|
|
||||||
if ($null -eq $entries) { continue }
|
|
||||||
# Normalize to array
|
|
||||||
if ($entries -isnot [System.Array] -and $entries -isnot [System.Collections.IList]) {
|
|
||||||
$entries = @($entries)
|
|
||||||
}
|
|
||||||
foreach ($entry in $entries) {
|
|
||||||
$entryXml = Build-PanelEntryXml $entry "`t`t"
|
|
||||||
$bodyParts += "`t<$side>`r`n$entryXml`r`n`t</$side>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reject unknown side keys (catches typos like "Top" vs "top")
|
|
||||||
foreach ($prop in $layout.PSObject.Properties) {
|
|
||||||
if ($sides -notcontains $prop.Name) {
|
|
||||||
Write-Error "Unknown side '$($prop.Name)'. Allowed: $($sides -join ', ')"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = $bodyParts -join "`r`n"
|
|
||||||
$declarations = @"
|
|
||||||
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
|
||||||
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
|
||||||
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
|
||||||
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
|
||||||
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
|
||||||
"@
|
|
||||||
$bodyBlock = if ($body) { "$body`r`n" } else { "" }
|
|
||||||
$caiXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
|
||||||
$bodyBlock$declarations
|
|
||||||
</ClientApplicationInterface>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$extDir = Join-Path $script:configDir "Ext"
|
|
||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
|
||||||
$caiPath = Join-Path $extDir "ClientApplicationInterface.xml"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($caiPath, $caiXml, $utf8Bom)
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Wrote panel layout: $caiPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Operation: set-home-page ---
|
|
||||||
# Russian → English type aliases for form-ref normalization
|
|
||||||
$script:ruTypeMap = @{
|
|
||||||
"справочник" = "Catalog"
|
|
||||||
"документ" = "Document"
|
|
||||||
"перечисление" = "Enum"
|
|
||||||
"отчёт" = "Report"
|
|
||||||
"отчет" = "Report"
|
|
||||||
"обработка" = "DataProcessor"
|
|
||||||
"общаяформа" = "CommonForm"
|
|
||||||
"журналдокументов" = "DocumentJournal"
|
|
||||||
"планвидовхарактеристик" = "ChartOfCharacteristicTypes"
|
|
||||||
"плансчетов" = "ChartOfAccounts"
|
|
||||||
"планвидоврасчета" = "ChartOfCalculationTypes"
|
|
||||||
"планвидоврасчёта" = "ChartOfCalculationTypes"
|
|
||||||
"регистрсведений" = "InformationRegister"
|
|
||||||
"регистрнакопления" = "AccumulationRegister"
|
|
||||||
"регистрбухгалтерии" = "AccountingRegister"
|
|
||||||
"регистррасчета" = "CalculationRegister"
|
|
||||||
"регистррасчёта" = "CalculationRegister"
|
|
||||||
"бизнеспроцесс" = "BusinessProcess"
|
|
||||||
"задача" = "Task"
|
|
||||||
"бот" = "Bot"
|
|
||||||
"планобмена" = "ExchangePlan"
|
|
||||||
"хранилищенастроек" = "SettingsStorage"
|
|
||||||
}
|
|
||||||
# plural folder → singular type
|
|
||||||
$script:dirToType = @{}
|
|
||||||
foreach ($k in $script:typeToDir.Keys) { $script:dirToType[$script:typeToDir[$k].ToLowerInvariant()] = $k }
|
|
||||||
|
|
||||||
function Normalize-FormRef([string]$s) {
|
|
||||||
$s = $s.Trim()
|
|
||||||
if (-not $s) { return $s }
|
|
||||||
# UUID — leave as-is
|
|
||||||
if ($s -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { return $s }
|
|
||||||
# Path form?
|
|
||||||
if ($s.Contains("/") -or $s.Contains("\")) {
|
|
||||||
$parts = $s.Replace("\","/").Split("/") | Where-Object { $_ -ne "" -and $_.ToLowerInvariant() -ne "ext" }
|
|
||||||
# Strip trailing Form.xml
|
|
||||||
if ($parts.Count -gt 0 -and $parts[-1].ToLowerInvariant() -eq "form.xml") {
|
|
||||||
$parts = @($parts[0..($parts.Count - 2)])
|
|
||||||
}
|
|
||||||
if ($parts.Count -ge 2) {
|
|
||||||
$typeDir = $parts[0]
|
|
||||||
$typeSingular = $script:dirToType[$typeDir.ToLowerInvariant()]
|
|
||||||
if ($typeSingular) {
|
|
||||||
if ($typeSingular -eq "CommonForm" -and $parts.Count -ge 2) {
|
|
||||||
return "CommonForm.$($parts[1])"
|
|
||||||
}
|
|
||||||
if ($parts.Count -ge 4 -and $parts[2].ToLowerInvariant() -eq "forms") {
|
|
||||||
return "$typeSingular.$($parts[1]).Form.$($parts[3])"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $s
|
|
||||||
}
|
|
||||||
# Dot form — translate Russian head and 'Форма' segment, auto-insert 'Form'
|
|
||||||
$segs = $s.Split(".")
|
|
||||||
if ($segs.Count -ge 1) {
|
|
||||||
$head = $segs[0].ToLowerInvariant()
|
|
||||||
if ($script:ruTypeMap.ContainsKey($head)) { $segs[0] = $script:ruTypeMap[$head] }
|
|
||||||
for ($i = 1; $i -lt $segs.Count; $i++) {
|
|
||||||
if ($segs[$i] -eq "Форма") { $segs[$i] = "Form" }
|
|
||||||
}
|
|
||||||
# Auto-insert Form: for object types with 3 segments (Type.Object.FormName)
|
|
||||||
if ($segs.Count -eq 3 -and $script:typeOrder -contains $segs[0] -and $segs[0] -ne "CommonForm") {
|
|
||||||
$segs = @($segs[0], $segs[1], "Form", $segs[2])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ($segs -join ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
# Accept short DSL or canonical XML keys (silently)
|
|
||||||
function Get-FieldValue($obj, [string[]]$keys) {
|
|
||||||
foreach ($k in $keys) {
|
|
||||||
if ($obj.PSObject.Properties[$k]) { return $obj.PSObject.Properties[$k].Value }
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-HomePageItemXml($entry, [string]$indent) {
|
|
||||||
# Resolve fields
|
|
||||||
if ($entry -is [string]) {
|
|
||||||
$formRef = Normalize-FormRef $entry
|
|
||||||
$height = 10
|
|
||||||
$common = $true
|
|
||||||
$roles = $null
|
|
||||||
} else {
|
|
||||||
$formRaw = Get-FieldValue $entry @("form","Form")
|
|
||||||
if (-not $formRaw) { Write-Error "Home page item: 'form' is required, got: $($entry | ConvertTo-Json -Compress)"; exit 1 }
|
|
||||||
$formRef = Normalize-FormRef ([string]$formRaw)
|
|
||||||
$h = Get-FieldValue $entry @("height","Height")
|
|
||||||
$height = if ($null -ne $h) { [int]$h } else { 10 }
|
|
||||||
$vis = Get-FieldValue $entry @("visibility","Visibility")
|
|
||||||
$common = if ($null -ne $vis) { [bool]$vis } else { $true }
|
|
||||||
$roles = Get-FieldValue $entry @("roles")
|
|
||||||
}
|
|
||||||
|
|
||||||
$visParts = @()
|
|
||||||
$visParts += "$indent`t`t<xr:Common>$($common.ToString().ToLower())</xr:Common>"
|
|
||||||
if ($roles) {
|
|
||||||
# roles is PSCustomObject {Role.X: bool, ...}
|
|
||||||
foreach ($prop in $roles.PSObject.Properties) {
|
|
||||||
$rname = $prop.Name
|
|
||||||
if (-not $rname.StartsWith("Role.") -and -not ($rname -match '^[0-9a-fA-F]{8}-')) { $rname = "Role.$rname" }
|
|
||||||
$rval = ([bool]$prop.Value).ToString().ToLower()
|
|
||||||
$escName = [System.Security.SecurityElement]::Escape($rname)
|
|
||||||
$visParts += "$indent`t`t<xr:Value name=`"$escName`">$rval</xr:Value>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$visBlock = $visParts -join "`r`n"
|
|
||||||
$escForm = [System.Security.SecurityElement]::Escape($formRef)
|
|
||||||
return @"
|
|
||||||
$indent<Item>
|
|
||||||
$indent`t<Form>$escForm</Form>
|
|
||||||
$indent`t<Height>$height</Height>
|
|
||||||
$indent`t<Visibility>
|
|
||||||
$visBlock
|
|
||||||
$indent`t</Visibility>
|
|
||||||
$indent</Item>
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
function Do-SetHomePage($valArg) {
|
|
||||||
$layout = $valArg
|
|
||||||
if ($layout -is [string]) {
|
|
||||||
try { $layout = $layout | ConvertFrom-Json } catch {
|
|
||||||
Write-Error "set-home-page value must be valid JSON object"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
|
|
||||||
|
|
||||||
$allowedTemplates = @("OneColumn","TwoColumnsEqualWidth","TwoColumnsVariableWidth")
|
|
||||||
$tmpl = Get-FieldValue $layout @("template","WorkingAreaTemplate")
|
|
||||||
if (-not $tmpl) { $tmpl = "TwoColumnsEqualWidth" }
|
|
||||||
if ($allowedTemplates -notcontains $tmpl) {
|
|
||||||
Write-Error "Unknown template '$tmpl'. Allowed: $($allowedTemplates -join ', ')"; exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
$leftItems = Get-FieldValue $layout @("left","LeftColumn")
|
|
||||||
$rightItems = Get-FieldValue $layout @("right","RightColumn")
|
|
||||||
|
|
||||||
# Reject unknown keys
|
|
||||||
$known = @("template","WorkingAreaTemplate","left","LeftColumn","right","RightColumn")
|
|
||||||
foreach ($prop in $layout.PSObject.Properties) {
|
|
||||||
if ($known -notcontains $prop.Name) {
|
|
||||||
Write-Error "Unknown key '$($prop.Name)'. Allowed: template, left, right"; exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($tmpl -eq "OneColumn" -and $rightItems) {
|
|
||||||
Write-Error "Template 'OneColumn' cannot have items in 'right' column"; exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
function Build-Column([string]$tag, $items) {
|
|
||||||
if (-not $items) { return "`t<$tag/>" }
|
|
||||||
if ($items -isnot [System.Array] -and $items -isnot [System.Collections.IList]) {
|
|
||||||
$items = @($items)
|
|
||||||
}
|
|
||||||
if ($items.Count -eq 0) { return "`t<$tag/>" }
|
|
||||||
$itemBlocks = @()
|
|
||||||
foreach ($it in $items) {
|
|
||||||
$itemBlocks += Build-HomePageItemXml $it "`t`t"
|
|
||||||
}
|
|
||||||
$body = $itemBlocks -join "`r`n"
|
|
||||||
return "`t<$tag>`r`n$body`r`n`t</$tag>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$leftXml = Build-Column "LeftColumn" $leftItems
|
|
||||||
$rightXml = Build-Column "RightColumn" $rightItems
|
|
||||||
|
|
||||||
$hpXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
|
||||||
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
|
|
||||||
$leftXml
|
|
||||||
$rightXml
|
|
||||||
</HomePageWorkArea>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$extDir = Join-Path $script:configDir "Ext"
|
|
||||||
if (-not (Test-Path $extDir)) { New-Item -ItemType Directory -Path $extDir -Force | Out-Null }
|
|
||||||
$hpPath = Join-Path $extDir "HomePageWorkArea.xml"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllText($hpPath, $hpXml, $utf8Bom)
|
|
||||||
$script:modifyCount++
|
|
||||||
Info "Wrote home page layout: $hpPath"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Operation: set-defaultRoles ---
|
# --- Operation: set-defaultRoles ---
|
||||||
function Do-SetDefaultRoles([string]$batchVal) {
|
function Do-SetDefaultRoles([string]$batchVal) {
|
||||||
$items = Parse-BatchValue $batchVal
|
$items = Parse-BatchValue $batchVal
|
||||||
@@ -931,19 +508,15 @@ if ($DefinitionFile) {
|
|||||||
|
|
||||||
foreach ($op in $operations) {
|
foreach ($op in $operations) {
|
||||||
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
$opName = if ($op.operation) { "$($op.operation)" } else { "$Operation" }
|
||||||
# Pass value through as-is (object or string); set-panels needs object form
|
$opValue = if ($op.value) { "$($op.value)" } else { "$Value" }
|
||||||
$opValue = if ($null -ne $op.value) { $op.value } else { $Value }
|
|
||||||
$opValueStr = if ($opValue -is [string]) { $opValue } else { "$opValue" }
|
|
||||||
|
|
||||||
switch ($opName) {
|
switch ($opName) {
|
||||||
"modify-property" { Do-ModifyProperty $opValueStr }
|
"modify-property" { Do-ModifyProperty $opValue }
|
||||||
"add-childObject" { Do-AddChildObject $opValueStr }
|
"add-childObject" { Do-AddChildObject $opValue }
|
||||||
"remove-childObject" { Do-RemoveChildObject $opValueStr }
|
"remove-childObject" { Do-RemoveChildObject $opValue }
|
||||||
"add-defaultRole" { Do-AddDefaultRole $opValueStr }
|
"add-defaultRole" { Do-AddDefaultRole $opValue }
|
||||||
"remove-defaultRole" { Do-RemoveDefaultRole $opValueStr }
|
"remove-defaultRole" { Do-RemoveDefaultRole $opValue }
|
||||||
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
|
"set-defaultRoles" { Do-SetDefaultRoles $opValue }
|
||||||
"set-panels" { Do-SetPanels $opValue }
|
|
||||||
"set-home-page" { Do-SetHomePage $opValue }
|
|
||||||
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
default { Write-Error "Unknown operation: $opName"; exit 1 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-edit v1.8 — Edit 1C configuration root (Configuration.xml)
|
# cf-edit v1.1 — Edit 1C configuration root (Configuration.xml)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import uuid as _uuid
|
|
||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
|
||||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
|
||||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def _sg_root_uuid(xml_path):
|
|
||||||
if not os.path.isfile(xml_path):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
mx = etree.parse(xml_path).getroot()
|
|
||||||
for child in mx:
|
|
||||||
if isinstance(child.tag, str) and child.get("uuid"):
|
|
||||||
return child.get("uuid")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
|
||||||
d = start_dir
|
|
||||||
for _ in range(20):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
pj = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pj):
|
|
||||||
return pj
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_get_edit_mode(cfg_dir):
|
|
||||||
try:
|
|
||||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
|
||||||
if not pj:
|
|
||||||
return "deny"
|
|
||||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
|
||||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
|
||||||
for db in proj.get("databases", []):
|
|
||||||
src = db.get("configSrc")
|
|
||||||
if src:
|
|
||||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
|
||||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
|
||||||
if db.get("editingAllowedCheck"):
|
|
||||||
return db["editingAllowedCheck"]
|
|
||||||
if proj.get("editingAllowedCheck"):
|
|
||||||
return proj["editingAllowedCheck"]
|
|
||||||
return "deny"
|
|
||||||
except Exception:
|
|
||||||
return "deny"
|
|
||||||
|
|
||||||
|
|
||||||
def assert_edit_allowed(target_path, require):
|
|
||||||
try:
|
|
||||||
rp = os.path.abspath(target_path)
|
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
|
||||||
cfg_dir = None
|
|
||||||
bin_path = None
|
|
||||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
|
||||||
for _ in range(12):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
if not elem_uuid:
|
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
|
||||||
if not cfg_dir:
|
|
||||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
|
||||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
|
||||||
cfg_dir = d
|
|
||||||
bin_path = cand
|
|
||||||
if elem_uuid and cfg_dir:
|
|
||||||
break
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
if not elem_uuid and cfg_dir:
|
|
||||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
|
||||||
if not bin_path or not os.path.exists(bin_path):
|
|
||||||
return
|
|
||||||
data = open(bin_path, "rb").read()
|
|
||||||
if len(data) <= 32:
|
|
||||||
return
|
|
||||||
if data[:3] == b"\xef\xbb\xbf":
|
|
||||||
data = data[3:]
|
|
||||||
text = data.decode("utf-8", "replace")
|
|
||||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
|
||||||
if not h:
|
|
||||||
return
|
|
||||||
g = int(h.group(1))
|
|
||||||
k = int(h.group(2))
|
|
||||||
if k == 0:
|
|
||||||
return
|
|
||||||
best = None
|
|
||||||
if elem_uuid:
|
|
||||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
|
||||||
f1 = int(m.group(1))
|
|
||||||
if best is None or f1 < best:
|
|
||||||
best = f1
|
|
||||||
blocked = False
|
|
||||||
code = ""
|
|
||||||
reason = ""
|
|
||||||
if g == 1:
|
|
||||||
blocked = True
|
|
||||||
code = "capability-off"
|
|
||||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
|
||||||
elif require == "removed":
|
|
||||||
if best is not None and best != 2:
|
|
||||||
blocked = True
|
|
||||||
code = "not-removed"
|
|
||||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
|
||||||
else:
|
|
||||||
if best is not None and best == 0:
|
|
||||||
blocked = True
|
|
||||||
code = "locked"
|
|
||||||
reason = "объект на замке — редактирование сломает обновления"
|
|
||||||
if not blocked:
|
|
||||||
return
|
|
||||||
mode = _sg_get_edit_mode(cfg_dir)
|
|
||||||
if mode == "off":
|
|
||||||
return
|
|
||||||
if mode == "warn":
|
|
||||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
|
||||||
return
|
|
||||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if code == "capability-off":
|
|
||||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
|
||||||
fix = (
|
|
||||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
|
||||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
|
||||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
|
||||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
)
|
|
||||||
elif code == "not-removed":
|
|
||||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
fix = (
|
|
||||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
fix = (
|
|
||||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
|
||||||
)
|
|
||||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
@@ -182,7 +20,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|||||||
TYPE_ORDER = [
|
TYPE_ORDER = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -198,7 +36,7 @@ TYPE_ORDER = [
|
|||||||
TYPE_TO_DIR = {
|
TYPE_TO_DIR = {
|
||||||
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
|
||||||
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
|
||||||
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
|
||||||
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
|
||||||
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
|
||||||
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
|
||||||
@@ -321,9 +159,9 @@ def main():
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True)
|
parser.add_argument("-ConfigPath", required=True)
|
||||||
parser.add_argument("-DefinitionFile", default=None)
|
parser.add_argument("-DefinitionFile", default=None)
|
||||||
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
|
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles"])
|
||||||
parser.add_argument("-Value", default=None)
|
parser.add_argument("-Value", default=None)
|
||||||
parser.add_argument("-NoValidate", action="store_true")
|
parser.add_argument("-NoValidate", action="store_true")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -351,8 +189,6 @@ def main():
|
|||||||
resolved_path = os.path.abspath(config_path)
|
resolved_path = os.path.abspath(config_path)
|
||||||
config_dir = os.path.dirname(resolved_path)
|
config_dir = os.path.dirname(resolved_path)
|
||||||
|
|
||||||
assert_edit_allowed(resolved_path, "editable")
|
|
||||||
|
|
||||||
xml_parser = etree.XMLParser(remove_blank_text=False)
|
xml_parser = etree.XMLParser(remove_blank_text=False)
|
||||||
tree = etree.parse(resolved_path, xml_parser)
|
tree = etree.parse(resolved_path, xml_parser)
|
||||||
xml_root = tree.getroot()
|
xml_root = tree.getroot()
|
||||||
@@ -657,270 +493,6 @@ def main():
|
|||||||
modify_count += 1
|
modify_count += 1
|
||||||
info(f"Set DefaultRoles: {len(items)} roles")
|
info(f"Set DefaultRoles: {len(items)} roles")
|
||||||
|
|
||||||
# --- set-panels (writes Ext/ClientApplicationInterface.xml from scratch) ---
|
|
||||||
# Canonical English aliases — preferred form, used in docs and error messages.
|
|
||||||
PANEL_UUIDS = {
|
|
||||||
"sections": "b553047f-c9aa-4157-978d-448ecad24248",
|
|
||||||
"open": "cbab57f2-a0f3-4f0a-89ea-4cb19570ab75",
|
|
||||||
"favorites": "13322b22-3960-4d68-93a6-fe2dd7f28ca3",
|
|
||||||
"history": "c933ac92-92cd-459d-81cc-e0c8a83ced99",
|
|
||||||
"functions": "b2735bd3-d822-4430-ba59-c9e869693b24",
|
|
||||||
}
|
|
||||||
# Russian synonyms — silently accepted (cf-info displays Russian names;
|
|
||||||
# users may copy them straight into cf-edit value).
|
|
||||||
PANEL_SYNONYMS = {
|
|
||||||
"разделов": "sections", "разделы": "sections",
|
|
||||||
"открытых": "open", "открытые": "open",
|
|
||||||
"избранного": "favorites","избранное": "favorites",
|
|
||||||
"истории": "history", "история": "history",
|
|
||||||
"функций": "functions", "функции": "functions",
|
|
||||||
}
|
|
||||||
|
|
||||||
def build_panel_entry_xml(entry, indent):
|
|
||||||
if isinstance(entry, str):
|
|
||||||
key = entry.lower()
|
|
||||||
key = PANEL_SYNONYMS.get(key, key)
|
|
||||||
if key not in PANEL_UUIDS:
|
|
||||||
allowed = ", ".join(sorted(PANEL_UUIDS.keys()))
|
|
||||||
print(f"Unknown panel alias '{entry}'. Allowed: {allowed}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
inst = str(_uuid.uuid4())
|
|
||||||
return f'{indent}<panel id="{inst}">\r\n{indent}\t<uuid>{PANEL_UUIDS[key]}</uuid>\r\n{indent}</panel>'
|
|
||||||
if isinstance(entry, dict) and "group" in entry:
|
|
||||||
children = entry["group"]
|
|
||||||
if not children:
|
|
||||||
print("group must contain at least one entry", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
gid = str(_uuid.uuid4())
|
|
||||||
inner = ""
|
|
||||||
for child in children:
|
|
||||||
child_xml = build_panel_entry_xml(child, indent + "\t\t")
|
|
||||||
inner += f"{indent}\t<group>\r\n{child_xml}\r\n{indent}\t</group>\r\n"
|
|
||||||
return f'{indent}<group id="{gid}">\r\n{inner}{indent}</group>'
|
|
||||||
print(f"Panel entry must be string alias or {{group:[...]}}, got: {entry!r}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def do_set_panels(value):
|
|
||||||
nonlocal modify_count
|
|
||||||
layout = value
|
|
||||||
if isinstance(layout, str):
|
|
||||||
try:
|
|
||||||
layout = json.loads(layout)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print(f"set-panels value must be valid JSON object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not isinstance(layout, dict) or not layout:
|
|
||||||
print("set-panels value must be non-empty object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
sides = ("top", "left", "right", "bottom")
|
|
||||||
# Reject unknown side keys
|
|
||||||
for k in layout.keys():
|
|
||||||
if k not in sides:
|
|
||||||
print(f"Unknown side '{k}'. Allowed: {', '.join(sides)}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
body_parts = []
|
|
||||||
for side in sides:
|
|
||||||
entries = layout.get(side)
|
|
||||||
if entries is None:
|
|
||||||
continue
|
|
||||||
if not isinstance(entries, list):
|
|
||||||
entries = [entries]
|
|
||||||
for entry in entries:
|
|
||||||
entry_xml = build_panel_entry_xml(entry, "\t\t")
|
|
||||||
body_parts.append(f"\t<{side}>\r\n{entry_xml}\r\n\t</{side}>")
|
|
||||||
body = "\r\n".join(body_parts)
|
|
||||||
body_block = body + "\r\n" if body else ""
|
|
||||||
declarations = (
|
|
||||||
'\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>\r\n'
|
|
||||||
'\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>\r\n'
|
|
||||||
'\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>\r\n'
|
|
||||||
'\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>\r\n'
|
|
||||||
'\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>'
|
|
||||||
)
|
|
||||||
cai_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
|
||||||
'<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" '
|
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
|
||||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
|
|
||||||
'xsi:type="InterfaceLayouter">\r\n'
|
|
||||||
f'{body_block}{declarations}\r\n'
|
|
||||||
'</ClientApplicationInterface>'
|
|
||||||
)
|
|
||||||
ext_dir = os.path.join(config_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
cai_path = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
|
||||||
with open(cai_path, "w", encoding="utf-8-sig", newline="") as fh:
|
|
||||||
fh.write(cai_xml)
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Wrote panel layout: {cai_path}")
|
|
||||||
|
|
||||||
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
|
|
||||||
RU_TYPE_MAP = {
|
|
||||||
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
|
|
||||||
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
|
|
||||||
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
|
|
||||||
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
|
|
||||||
"плансчетов": "ChartOfAccounts",
|
|
||||||
"планвидоврасчета": "ChartOfCalculationTypes",
|
|
||||||
"планвидоврасчёта": "ChartOfCalculationTypes",
|
|
||||||
"регистрсведений": "InformationRegister",
|
|
||||||
"регистрнакопления": "AccumulationRegister",
|
|
||||||
"регистрбухгалтерии": "AccountingRegister",
|
|
||||||
"регистррасчета": "CalculationRegister",
|
|
||||||
"регистррасчёта": "CalculationRegister",
|
|
||||||
"бизнеспроцесс": "BusinessProcess",
|
|
||||||
"бот": "Bot",
|
|
||||||
"задача": "Task", "планобмена": "ExchangePlan",
|
|
||||||
"хранилищенастроек": "SettingsStorage",
|
|
||||||
}
|
|
||||||
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
|
|
||||||
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
||||||
|
|
||||||
def normalize_form_ref(s):
|
|
||||||
s = (s or "").strip()
|
|
||||||
if not s:
|
|
||||||
return s
|
|
||||||
if UUID_RE.match(s):
|
|
||||||
return s
|
|
||||||
if "/" in s or "\\" in s:
|
|
||||||
parts = [p for p in s.replace("\\", "/").split("/") if p and p.lower() != "ext"]
|
|
||||||
if parts and parts[-1].lower() == "form.xml":
|
|
||||||
parts = parts[:-1]
|
|
||||||
if len(parts) >= 2:
|
|
||||||
type_dir = parts[0]
|
|
||||||
type_singular = DIR_TO_TYPE.get(type_dir.lower())
|
|
||||||
if type_singular:
|
|
||||||
if type_singular == "CommonForm" and len(parts) >= 2:
|
|
||||||
return f"CommonForm.{parts[1]}"
|
|
||||||
if len(parts) >= 4 and parts[2].lower() == "forms":
|
|
||||||
return f"{type_singular}.{parts[1]}.Form.{parts[3]}"
|
|
||||||
return s
|
|
||||||
segs = s.split(".")
|
|
||||||
if segs:
|
|
||||||
head = segs[0].lower()
|
|
||||||
if head in RU_TYPE_MAP:
|
|
||||||
segs[0] = RU_TYPE_MAP[head]
|
|
||||||
for i in range(1, len(segs)):
|
|
||||||
if segs[i] == "Форма":
|
|
||||||
segs[i] = "Form"
|
|
||||||
if len(segs) == 3 and segs[0] in TYPE_ORDER and segs[0] != "CommonForm":
|
|
||||||
segs = [segs[0], segs[1], "Form", segs[2]]
|
|
||||||
return ".".join(segs)
|
|
||||||
|
|
||||||
def get_field(obj, keys):
|
|
||||||
for k in keys:
|
|
||||||
if isinstance(obj, dict) and k in obj:
|
|
||||||
return obj[k]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def build_home_page_item_xml(entry, indent):
|
|
||||||
if isinstance(entry, str):
|
|
||||||
form_ref = normalize_form_ref(entry)
|
|
||||||
height = 10
|
|
||||||
common = True
|
|
||||||
roles = None
|
|
||||||
elif isinstance(entry, dict):
|
|
||||||
form_raw = get_field(entry, ["form", "Form"])
|
|
||||||
if not form_raw:
|
|
||||||
print(f"Home page item: 'form' is required, got: {entry!r}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
form_ref = normalize_form_ref(str(form_raw))
|
|
||||||
h = get_field(entry, ["height", "Height"])
|
|
||||||
height = int(h) if h is not None else 10
|
|
||||||
vis = get_field(entry, ["visibility", "Visibility"])
|
|
||||||
common = bool(vis) if vis is not None else True
|
|
||||||
roles = get_field(entry, ["roles"])
|
|
||||||
else:
|
|
||||||
print(f"Home page item must be string or object, got: {entry!r}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
vis_parts = [f"{indent}\t\t<xr:Common>{str(common).lower()}</xr:Common>"]
|
|
||||||
if roles and isinstance(roles, dict):
|
|
||||||
for rname, rval in roles.items():
|
|
||||||
if not rname.startswith("Role.") and not UUID_RE.match(rname):
|
|
||||||
rname = f"Role.{rname}"
|
|
||||||
rval_s = str(bool(rval)).lower()
|
|
||||||
vis_parts.append(f'{indent}\t\t<xr:Value name="{html_escape(rname, quote=True)}">{rval_s}</xr:Value>')
|
|
||||||
vis_block = "\r\n".join(vis_parts)
|
|
||||||
esc_form = html_escape(form_ref, quote=True)
|
|
||||||
return (
|
|
||||||
f"{indent}<Item>\r\n"
|
|
||||||
f"{indent}\t<Form>{esc_form}</Form>\r\n"
|
|
||||||
f"{indent}\t<Height>{height}</Height>\r\n"
|
|
||||||
f"{indent}\t<Visibility>\r\n"
|
|
||||||
f"{vis_block}\r\n"
|
|
||||||
f"{indent}\t</Visibility>\r\n"
|
|
||||||
f"{indent}</Item>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_set_home_page(value):
|
|
||||||
nonlocal modify_count
|
|
||||||
layout = value
|
|
||||||
if isinstance(layout, str):
|
|
||||||
try:
|
|
||||||
layout = json.loads(layout)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("set-home-page value must be valid JSON object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if not isinstance(layout, dict) or not layout:
|
|
||||||
print("set-home-page value must be non-empty object", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
allowed_templates = ("OneColumn", "TwoColumnsEqualWidth", "TwoColumnsVariableWidth")
|
|
||||||
tmpl = get_field(layout, ["template", "WorkingAreaTemplate"]) or "TwoColumnsEqualWidth"
|
|
||||||
if tmpl not in allowed_templates:
|
|
||||||
print(f"Unknown template '{tmpl}'. Allowed: {', '.join(allowed_templates)}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
left_items = get_field(layout, ["left", "LeftColumn"])
|
|
||||||
right_items = get_field(layout, ["right", "RightColumn"])
|
|
||||||
|
|
||||||
known = {"template", "WorkingAreaTemplate", "left", "LeftColumn", "right", "RightColumn"}
|
|
||||||
for k in layout.keys():
|
|
||||||
if k not in known:
|
|
||||||
print(f"Unknown key '{k}'. Allowed: template, left, right", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if tmpl == "OneColumn" and right_items:
|
|
||||||
print("Template 'OneColumn' cannot have items in 'right' column", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def build_column(tag, items):
|
|
||||||
if not items:
|
|
||||||
return f"\t<{tag}/>"
|
|
||||||
if not isinstance(items, list):
|
|
||||||
items = [items]
|
|
||||||
if not items:
|
|
||||||
return f"\t<{tag}/>"
|
|
||||||
blocks = [build_home_page_item_xml(it, "\t\t") for it in items]
|
|
||||||
body = "\r\n".join(blocks)
|
|
||||||
return f"\t<{tag}>\r\n{body}\r\n\t</{tag}>"
|
|
||||||
|
|
||||||
left_xml = build_column("LeftColumn", left_items)
|
|
||||||
right_xml = build_column("RightColumn", right_items)
|
|
||||||
|
|
||||||
hp_xml = (
|
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
|
||||||
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
|
|
||||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
|
|
||||||
'xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
|
||||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">\r\n'
|
|
||||||
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
|
|
||||||
f'{left_xml}\r\n'
|
|
||||||
f'{right_xml}\r\n'
|
|
||||||
'</HomePageWorkArea>'
|
|
||||||
)
|
|
||||||
|
|
||||||
ext_dir = os.path.join(config_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
hp_path = os.path.join(ext_dir, "HomePageWorkArea.xml")
|
|
||||||
with open(hp_path, "w", encoding="utf-8-sig", newline="") as fh:
|
|
||||||
fh.write(hp_xml)
|
|
||||||
modify_count += 1
|
|
||||||
info(f"Wrote home page layout: {hp_path}")
|
|
||||||
|
|
||||||
# --- Execute operations ---
|
# --- Execute operations ---
|
||||||
operations = []
|
operations = []
|
||||||
if args.DefinitionFile:
|
if args.DefinitionFile:
|
||||||
@@ -941,21 +513,17 @@ def main():
|
|||||||
op_value = op.get("value", args.Value or "")
|
op_value = op.get("value", args.Value or "")
|
||||||
|
|
||||||
if op_name == "modify-property":
|
if op_name == "modify-property":
|
||||||
do_modify_property(op_value if isinstance(op_value, str) else str(op_value))
|
do_modify_property(op_value)
|
||||||
elif op_name == "add-childObject":
|
elif op_name == "add-childObject":
|
||||||
do_add_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_child_object(op_value)
|
||||||
elif op_name == "remove-childObject":
|
elif op_name == "remove-childObject":
|
||||||
do_remove_child_object(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_child_object(op_value)
|
||||||
elif op_name == "add-defaultRole":
|
elif op_name == "add-defaultRole":
|
||||||
do_add_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_add_default_role(op_value)
|
||||||
elif op_name == "remove-defaultRole":
|
elif op_name == "remove-defaultRole":
|
||||||
do_remove_default_role(op_value if isinstance(op_value, str) else str(op_value))
|
do_remove_default_role(op_value)
|
||||||
elif op_name == "set-defaultRoles":
|
elif op_name == "set-defaultRoles":
|
||||||
do_set_default_roles(op_value if isinstance(op_value, str) else str(op_value))
|
do_set_default_roles(op_value)
|
||||||
elif op_name == "set-panels":
|
|
||||||
do_set_panels(op_value)
|
|
||||||
elif op_name == "set-home-page":
|
|
||||||
do_set_home_page(op_value)
|
|
||||||
else:
|
else:
|
||||||
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
print(f"Unknown operation: {op_name}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: cf-info
|
name: cf-info
|
||||||
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
|
||||||
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
|
argument-hint: <ConfigPath> [-Mode overview|brief|full]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
- Read
|
- Read
|
||||||
@@ -18,12 +18,11 @@ allowed-tools:
|
|||||||
|----------|----------|
|
|----------|----------|
|
||||||
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
|
||||||
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
| `Mode` | Режим: `overview` (default), `brief`, `full` |
|
||||||
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
|
|
||||||
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
|
||||||
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
|
powershell.exe -NoProfile -File .claude/skills/cf-info/scripts/cf-info.ps1 -ConfigPath "<путь>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима
|
## Три режима
|
||||||
@@ -38,17 +37,14 @@ python "${CLAUDE_SKILL_DIR}/scripts/cf-info.py" -ConfigPath "<путь>"
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обзор пустой конфигурации
|
# Обзор пустой конфигурации
|
||||||
... -ConfigPath src
|
... -ConfigPath upload/cfempty
|
||||||
|
|
||||||
# Краткая сводка реальной конфигурации
|
# Краткая сводка реальной конфигурации
|
||||||
... -ConfigPath src -Mode brief
|
... -ConfigPath upload/acc_8.3.24 -Mode brief
|
||||||
|
|
||||||
# Полная информация
|
# Полная информация
|
||||||
... -ConfigPath src -Mode full
|
... -ConfigPath upload/acc_8.3.24 -Mode full
|
||||||
|
|
||||||
# С пагинацией
|
# С пагинацией
|
||||||
... -ConfigPath src -Mode full -Limit 50 -Offset 100
|
... -ConfigPath upload/acc_8.3.24 -Mode full -Limit 50 -Offset 100
|
||||||
|
|
||||||
# Drill-down: только начальная страница (раскладка форм с ролями)
|
|
||||||
... -ConfigPath src -Section home-page
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.0 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
|
[Parameter(Mandatory=$true)][string]$ConfigPath,
|
||||||
[ValidateSet("overview","brief","full")]
|
[ValidateSet("overview","brief","full")]
|
||||||
[string]$Mode = "overview",
|
[string]$Mode = "overview",
|
||||||
[Alias('Name')]
|
|
||||||
[ValidateSet("home-page")]
|
|
||||||
[string]$Section,
|
|
||||||
[int]$Limit = 150,
|
[int]$Limit = 150,
|
||||||
[int]$Offset = 0,
|
[int]$Offset = 0,
|
||||||
[string]$OutFile
|
[string]$OutFile
|
||||||
@@ -89,7 +86,7 @@ function Get-PropML([string]$propName) {
|
|||||||
$typeOrder = @(
|
$typeOrder = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -105,7 +102,6 @@ $typeRuNames = @{
|
|||||||
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
|
||||||
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
|
||||||
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
|
||||||
"Bot"="Боты"
|
|
||||||
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
|
||||||
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
|
||||||
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
|
||||||
@@ -122,187 +118,6 @@ $typeRuNames = @{
|
|||||||
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
|
||||||
$script:panelNames = @{
|
|
||||||
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75" = "Открытых"
|
|
||||||
"b553047f-c9aa-4157-978d-448ecad24248" = "Разделов"
|
|
||||||
"13322b22-3960-4d68-93a6-fe2dd7f28ca3" = "Избранного"
|
|
||||||
"c933ac92-92cd-459d-81cc-e0c8a83ced99" = "История"
|
|
||||||
"b2735bd3-d822-4430-ba59-c9e869693b24" = "Функций"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-PanelsLayout {
|
|
||||||
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
|
||||||
$caiPath = Join-Path (Join-Path $configDir "Ext") "ClientApplicationInterface.xml"
|
|
||||||
if (-not (Test-Path $caiPath)) { return $null }
|
|
||||||
try { [xml]$caiDoc = Get-Content -Path $caiPath -Encoding UTF8 } catch { return $null }
|
|
||||||
if (-not $caiDoc.DocumentElement) { return $null }
|
|
||||||
$caiNs = New-Object System.Xml.XmlNamespaceManager($caiDoc.NameTable)
|
|
||||||
$caiNs.AddNamespace("ca", "http://v8.1c.ru/8.2/managed-application/core")
|
|
||||||
$layout = [ordered]@{ top=@(); left=@(); right=@(); bottom=@(); declared=@() }
|
|
||||||
foreach ($side in @("top","left","right","bottom")) {
|
|
||||||
foreach ($sideEl in $caiDoc.DocumentElement.SelectNodes("ca:$side", $caiNs)) {
|
|
||||||
$slot = @()
|
|
||||||
foreach ($u in $sideEl.SelectNodes(".//ca:panel/ca:uuid", $caiNs)) {
|
|
||||||
$key = $u.InnerText.Trim()
|
|
||||||
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
|
||||||
$slot += $nm
|
|
||||||
}
|
|
||||||
if ($slot.Count -gt 0) { $layout[$side] += ,$slot }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($pd in $caiDoc.DocumentElement.SelectNodes("ca:panelDef", $caiNs)) {
|
|
||||||
$key = $pd.GetAttribute("id")
|
|
||||||
$nm = if ($script:panelNames.Contains($key)) { $script:panelNames[$key] } else { "?$key" }
|
|
||||||
$layout.declared += $nm
|
|
||||||
}
|
|
||||||
return $layout
|
|
||||||
}
|
|
||||||
|
|
||||||
function Format-LayoutSlots($slots) {
|
|
||||||
# slots is array of arrays (each inner array = one side-tag's panels, may be 1+)
|
|
||||||
# Single inner array, single panel -> just name
|
|
||||||
# Single inner array, multiple panels -> "Стек(a, b)"
|
|
||||||
# Multiple inner arrays -> separate entries joined by " | "
|
|
||||||
if (-not $slots -or $slots.Count -eq 0) { return "" }
|
|
||||||
$parts = @()
|
|
||||||
foreach ($slot in $slots) {
|
|
||||||
if ($slot.Count -eq 1) { $parts += $slot[0] }
|
|
||||||
else { $parts += ("Стек(" + ($slot -join ", ") + ")") }
|
|
||||||
}
|
|
||||||
return ($parts -join " | ")
|
|
||||||
}
|
|
||||||
|
|
||||||
$script:panelLayout = Get-PanelsLayout
|
|
||||||
|
|
||||||
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
|
||||||
function Get-HomePageLayout {
|
|
||||||
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
|
||||||
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
|
||||||
if (-not (Test-Path $hpPath)) { return $null }
|
|
||||||
try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null }
|
|
||||||
if (-not $hpDoc.DocumentElement) { return $null }
|
|
||||||
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
|
||||||
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
|
||||||
$hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
|
|
||||||
$result = [ordered]@{ template = ""; left = @(); right = @() }
|
|
||||||
$tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs)
|
|
||||||
if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() }
|
|
||||||
foreach ($colName in @("LeftColumn","RightColumn")) {
|
|
||||||
$colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs)
|
|
||||||
if (-not $colNode) { continue }
|
|
||||||
$items = @()
|
|
||||||
foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) {
|
|
||||||
$f = $item.SelectSingleNode("hp:Form", $hpNs)
|
|
||||||
$h = $item.SelectSingleNode("hp:Height", $hpNs)
|
|
||||||
$visNode = $item.SelectSingleNode("hp:Visibility", $hpNs)
|
|
||||||
$common = $true
|
|
||||||
$roles = @()
|
|
||||||
if ($visNode) {
|
|
||||||
$cn = $visNode.SelectSingleNode("xr:Common", $hpNs)
|
|
||||||
if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") }
|
|
||||||
foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) {
|
|
||||||
$roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$items += [ordered]@{
|
|
||||||
form = if ($f) { $f.InnerText.Trim() } else { "" }
|
|
||||||
height = if ($h) { [int]$h.InnerText.Trim() } else { 10 }
|
|
||||||
common = $common
|
|
||||||
roles = $roles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items }
|
|
||||||
}
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
|
|
||||||
$script:homePage = Get-HomePageLayout
|
|
||||||
|
|
||||||
# --- Support state (Ext/ParentConfigurations.bin) ---
|
|
||||||
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
|
||||||
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
|
|
||||||
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
|
|
||||||
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
|
|
||||||
# vendor block separately); only computed when G=0.
|
|
||||||
function Read-SupportState([string]$binPath) {
|
|
||||||
try {
|
|
||||||
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
|
|
||||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
|
||||||
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
|
|
||||||
$startIdx = 0
|
|
||||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
|
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
|
|
||||||
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
|
||||||
if (-not $h.Success) { return $null }
|
|
||||||
$G = [int]$h.Groups[1].Value
|
|
||||||
$K = [int]$h.Groups[2].Value
|
|
||||||
if ($K -eq 0) { return @{ State = 'removed' } }
|
|
||||||
# Vendor descriptors: ...,"ver","vendor","name",count,
|
|
||||||
$vendors = @()
|
|
||||||
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
|
|
||||||
foreach ($m in $vRe.Matches($text)) {
|
|
||||||
$vendors += @{
|
|
||||||
Version = ($m.Groups[1].Value -replace '""','"')
|
|
||||||
Vendor = ($m.Groups[2].Value -replace '""','"')
|
|
||||||
Name = ($m.Groups[3].Value -replace '""','"')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
|
|
||||||
# whole config is read-only and stored f1 values are the inactive default.
|
|
||||||
$counts = $null
|
|
||||||
if ($G -eq 0) {
|
|
||||||
$counts = @(0, 0, 0)
|
|
||||||
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
|
|
||||||
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
|
||||||
foreach ($m in $rRe.Matches($text)) {
|
|
||||||
$counts[[int]$m.Groups[1].Value]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
|
|
||||||
} catch { return $null }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SupportLines {
|
|
||||||
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
|
|
||||||
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
|
|
||||||
$st = Read-SupportState $binPath
|
|
||||||
$out = @()
|
|
||||||
if (-not $st -or $st.State -eq 'absent') {
|
|
||||||
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
|
|
||||||
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
|
|
||||||
return $out
|
|
||||||
}
|
|
||||||
if ($st.State -eq 'removed') {
|
|
||||||
$out += "Поддержка: снята с поддержки полностью"
|
|
||||||
return $out
|
|
||||||
}
|
|
||||||
$out += "Поддержка: на поддержке"
|
|
||||||
if ($st.G -eq 0) {
|
|
||||||
$out += " Возможность изменения: включена"
|
|
||||||
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
|
|
||||||
} else {
|
|
||||||
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
|
|
||||||
}
|
|
||||||
$out += " Конфигураций поставщика: $($st.K)"
|
|
||||||
if ($st.K -gt 1) {
|
|
||||||
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" }
|
|
||||||
}
|
|
||||||
return $out
|
|
||||||
}
|
|
||||||
|
|
||||||
function Format-HomePageItem($it, [bool]$detailed) {
|
|
||||||
$badges = @()
|
|
||||||
$badges += "h=$($it.height)"
|
|
||||||
if (-not $it.common) { $badges += "скрыта" }
|
|
||||||
if ($it.roles.Count -gt 0) {
|
|
||||||
if ($detailed) { $badges += "роли: $($it.roles.Count)" }
|
|
||||||
else { $badges += "+$($it.roles.Count) ролей" }
|
|
||||||
}
|
|
||||||
$tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" }
|
|
||||||
return " $($it.form)$tail"
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
$objectCounts = [ordered]@{}
|
$objectCounts = [ordered]@{}
|
||||||
$totalObjects = 0
|
$totalObjects = 0
|
||||||
@@ -326,7 +141,6 @@ $cfgVersion = Get-PropText "Version"
|
|||||||
$cfgVendor = Get-PropText "Vendor"
|
$cfgVendor = Get-PropText "Vendor"
|
||||||
$cfgCompat = Get-PropText "CompatibilityMode"
|
$cfgCompat = Get-PropText "CompatibilityMode"
|
||||||
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
|
||||||
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
|
|
||||||
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
$cfgDefaultRun = Get-PropText "DefaultRunMode"
|
||||||
$cfgScript = Get-PropText "ScriptVariant"
|
$cfgScript = Get-PropText "ScriptVariant"
|
||||||
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
$cfgDefaultLang = Get-PropText "DefaultLanguage"
|
||||||
@@ -340,7 +154,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
|
|||||||
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if ($Mode -eq "brief" -and -not $Section) {
|
if ($Mode -eq "brief") {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
|
||||||
@@ -348,7 +162,7 @@ if ($Mode -eq "brief" -and -not $Section) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if ($Mode -eq "overview" -and -not $Section) {
|
if ($Mode -eq "overview") {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -358,7 +172,6 @@ if ($Mode -eq "overview" -and -not $Section) {
|
|||||||
Out "Формат: $version"
|
Out "Формат: $version"
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
foreach ($l in (Get-SupportLines)) { Out $l }
|
|
||||||
Out "Совместимость: $cfgCompat"
|
Out "Совместимость: $cfgCompat"
|
||||||
Out "Режим запуска: $cfgDefaultRun"
|
Out "Режим запуска: $cfgDefaultRun"
|
||||||
Out "Язык скриптов: $cfgScript"
|
Out "Язык скриптов: $cfgScript"
|
||||||
@@ -368,33 +181,6 @@ if ($Mode -eq "overview" -and -not $Section) {
|
|||||||
Out "Интерфейс: $cfgIntfCompat"
|
Out "Интерфейс: $cfgIntfCompat"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
# Panel layout (if file exists)
|
|
||||||
if ($script:panelLayout) {
|
|
||||||
$hasPlaced = $false
|
|
||||||
foreach ($s in @("top","left","right","bottom")) {
|
|
||||||
if ($script:panelLayout[$s].Count -gt 0) { $hasPlaced = $true; break }
|
|
||||||
}
|
|
||||||
if ($hasPlaced) {
|
|
||||||
Out "--- Раскладка панелей ---"
|
|
||||||
foreach ($s in @("top","left","right","bottom")) {
|
|
||||||
if ($script:panelLayout[$s].Count -gt 0) {
|
|
||||||
Out " $($s.PadRight(7)) $(Format-LayoutSlots $script:panelLayout[$s])"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Out ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Home page layout (brief summary)
|
|
||||||
if ($script:homePage) {
|
|
||||||
$ln = $script:homePage.left.Count
|
|
||||||
$rn = $script:homePage.right.Count
|
|
||||||
Out "--- Начальная страница ---"
|
|
||||||
Out " Шаблон: $($script:homePage.template)"
|
|
||||||
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
|
||||||
Out ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
Out "--- Состав ($totalObjects объектов) ---"
|
Out "--- Состав ($totalObjects объектов) ---"
|
||||||
Out ""
|
Out ""
|
||||||
@@ -417,34 +203,8 @@ if ($Mode -eq "overview" -and -not $Section) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Drill-down: -Section home-page ---
|
|
||||||
if ($Section -eq "home-page") {
|
|
||||||
if (-not $script:homePage) {
|
|
||||||
Out "Файл Ext/HomePageWorkArea.xml не найден"
|
|
||||||
} else {
|
|
||||||
Out "=== Начальная страница: $cfgName ==="
|
|
||||||
Out ""
|
|
||||||
Out "Шаблон: $($script:homePage.template)"
|
|
||||||
Out ""
|
|
||||||
foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) {
|
|
||||||
$items = $script:homePage[$side[1]]
|
|
||||||
$lbl = $side[0]
|
|
||||||
if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue }
|
|
||||||
Out "${lbl} ($($items.Count)):"
|
|
||||||
foreach ($it in $items) {
|
|
||||||
Out (Format-HomePageItem $it $true)
|
|
||||||
foreach ($r in $it.roles) {
|
|
||||||
$rval = if ($r.value) { "true" } else { "false" }
|
|
||||||
Out " $($r.name): $rval"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Out ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
if ($Mode -eq "full" -and -not $Section) {
|
if ($Mode -eq "full") {
|
||||||
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
|
||||||
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
|
||||||
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
|
||||||
@@ -461,7 +221,6 @@ if ($Mode -eq "full" -and -not $Section) {
|
|||||||
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
|
||||||
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
|
||||||
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
if ($cfgVersion) { Out "Версия: $cfgVersion" }
|
||||||
foreach ($l in (Get-SupportLines)) { Out $l }
|
|
||||||
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
|
||||||
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
|
||||||
Out ""
|
Out ""
|
||||||
@@ -516,33 +275,6 @@ if ($Mode -eq "full" -and -not $Section) {
|
|||||||
Out "Обычн.формы в управл.: $useOF"
|
Out "Обычн.формы в управл.: $useOF"
|
||||||
Out ""
|
Out ""
|
||||||
|
|
||||||
# --- Section: Panel layout ---
|
|
||||||
if ($script:panelLayout) {
|
|
||||||
Out "--- Раскладка панелей ---"
|
|
||||||
foreach ($s in @("top","left","right","bottom")) {
|
|
||||||
$slots = $script:panelLayout[$s]
|
|
||||||
if ($slots.Count -gt 0) {
|
|
||||||
Out " $($s.PadRight(7)) $(Format-LayoutSlots $slots)"
|
|
||||||
} else {
|
|
||||||
Out " $($s.PadRight(7)) —"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($script:panelLayout.declared.Count -gt 0) {
|
|
||||||
Out " объявлено: $($script:panelLayout.declared -join ', ')"
|
|
||||||
}
|
|
||||||
Out ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Section: Home page (brief summary) ---
|
|
||||||
if ($script:homePage) {
|
|
||||||
$ln = $script:homePage.left.Count
|
|
||||||
$rn = $script:homePage.right.Count
|
|
||||||
Out "--- Начальная страница ---"
|
|
||||||
Out " Шаблон: $($script:homePage.template)"
|
|
||||||
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
|
|
||||||
Out ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
Out "--- Хранилища и формы по умолчанию ---"
|
Out "--- Хранилища и формы по умолчанию ---"
|
||||||
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-info v1.4 — Compact summary of 1C configuration root
|
# cf-info v1.0 — Compact summary of 1C configuration root
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
@@ -14,9 +13,8 @@ sys.stderr.reconfigure(encoding="utf-8")
|
|||||||
|
|
||||||
# --- Argument parsing ---
|
# --- Argument parsing ---
|
||||||
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
|
parser.add_argument("-ConfigPath", required=True, help="Path to Configuration.xml or directory")
|
||||||
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
|
||||||
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
|
|
||||||
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
|
||||||
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
|
||||||
parser.add_argument("-OutFile", default="", help="Write output to file")
|
parser.add_argument("-OutFile", default="", help="Write output to file")
|
||||||
@@ -95,7 +93,7 @@ def get_prop_ml(prop_name):
|
|||||||
type_order = [
|
type_order = [
|
||||||
"Language", "Subsystem", "StyleItem", "Style",
|
"Language", "Subsystem", "StyleItem", "Style",
|
||||||
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
|
||||||
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
|
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
|
||||||
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
"XDTOPackage", "WebService", "HTTPService", "WSReference",
|
||||||
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
|
||||||
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
|
||||||
@@ -111,7 +109,6 @@ type_ru_names = {
|
|||||||
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
|
||||||
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
|
||||||
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
|
||||||
"Bot": "Боты",
|
|
||||||
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
|
||||||
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
|
||||||
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
|
||||||
@@ -128,173 +125,6 @@ type_ru_names = {
|
|||||||
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
|
|
||||||
PANEL_NAMES = {
|
|
||||||
"cbab57f2-a0f3-4f0a-89ea-4cb19570ab75": "Открытых",
|
|
||||||
"b553047f-c9aa-4157-978d-448ecad24248": "Разделов",
|
|
||||||
"13322b22-3960-4d68-93a6-fe2dd7f28ca3": "Избранного",
|
|
||||||
"c933ac92-92cd-459d-81cc-e0c8a83ced99": "История",
|
|
||||||
"b2735bd3-d822-4430-ba59-c9e869693b24": "Функций",
|
|
||||||
}
|
|
||||||
CAI_NS = "http://v8.1c.ru/8.2/managed-application/core"
|
|
||||||
|
|
||||||
def get_panels_layout():
|
|
||||||
cfg_dir = os.path.dirname(config_path)
|
|
||||||
cai_path = os.path.join(cfg_dir, "Ext", "ClientApplicationInterface.xml")
|
|
||||||
if not os.path.isfile(cai_path):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
cai_tree = etree.parse(cai_path)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
cai_root = cai_tree.getroot()
|
|
||||||
layout = {"top": [], "left": [], "right": [], "bottom": [], "declared": []}
|
|
||||||
for side in ("top", "left", "right", "bottom"):
|
|
||||||
for side_el in cai_root.findall(f"{{{CAI_NS}}}{side}"):
|
|
||||||
slot = []
|
|
||||||
for u in side_el.iter(f"{{{CAI_NS}}}uuid"):
|
|
||||||
key = (u.text or "").strip()
|
|
||||||
slot.append(PANEL_NAMES.get(key, f"?{key}"))
|
|
||||||
if slot:
|
|
||||||
layout[side].append(slot)
|
|
||||||
for pd in cai_root.findall(f"{{{CAI_NS}}}panelDef"):
|
|
||||||
key = pd.get("id", "")
|
|
||||||
layout["declared"].append(PANEL_NAMES.get(key, f"?{key}"))
|
|
||||||
return layout
|
|
||||||
|
|
||||||
def format_layout_slots(slots):
|
|
||||||
if not slots:
|
|
||||||
return ""
|
|
||||||
parts = []
|
|
||||||
for slot in slots:
|
|
||||||
if len(slot) == 1:
|
|
||||||
parts.append(slot[0])
|
|
||||||
else:
|
|
||||||
parts.append("Стек(" + ", ".join(slot) + ")")
|
|
||||||
return " | ".join(parts)
|
|
||||||
|
|
||||||
panel_layout = get_panels_layout()
|
|
||||||
|
|
||||||
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
|
|
||||||
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
|
|
||||||
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
|
|
||||||
|
|
||||||
def get_home_page_layout():
|
|
||||||
cfg_dir = os.path.dirname(config_path)
|
|
||||||
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
|
|
||||||
if not os.path.isfile(hp_path):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
hp_tree = etree.parse(hp_path)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
hp_root = hp_tree.getroot()
|
|
||||||
result = {"template": "", "left": [], "right": []}
|
|
||||||
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
|
|
||||||
if tn is not None and tn.text:
|
|
||||||
result["template"] = tn.text.strip()
|
|
||||||
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
|
||||||
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
|
|
||||||
if col is None:
|
|
||||||
continue
|
|
||||||
items = []
|
|
||||||
for it in col.findall(f"{{{HP_NS}}}Item"):
|
|
||||||
f = it.find(f"{{{HP_NS}}}Form")
|
|
||||||
h = it.find(f"{{{HP_NS}}}Height")
|
|
||||||
vis = it.find(f"{{{HP_NS}}}Visibility")
|
|
||||||
common = True
|
|
||||||
roles = []
|
|
||||||
if vis is not None:
|
|
||||||
cn = vis.find(f"{{{XR_NS_HP}}}Common")
|
|
||||||
if cn is not None and cn.text:
|
|
||||||
common = cn.text.strip() == "true"
|
|
||||||
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
|
|
||||||
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
|
|
||||||
items.append({
|
|
||||||
"form": (f.text or "").strip() if f is not None else "",
|
|
||||||
"height": int((h.text or "10").strip()) if h is not None else 10,
|
|
||||||
"common": common,
|
|
||||||
"roles": roles,
|
|
||||||
})
|
|
||||||
result[key] = items
|
|
||||||
return result
|
|
||||||
|
|
||||||
home_page = get_home_page_layout()
|
|
||||||
|
|
||||||
# --- Support state (Ext/ParentConfigurations.bin) ---
|
|
||||||
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
|
|
||||||
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
|
|
||||||
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
|
|
||||||
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
|
|
||||||
# vendor block separately); only computed when g==0.
|
|
||||||
def read_support_state(bin_path):
|
|
||||||
try:
|
|
||||||
if not os.path.isfile(bin_path):
|
|
||||||
return {"state": "absent"}
|
|
||||||
data = open(bin_path, "rb").read()
|
|
||||||
if len(data) <= 32:
|
|
||||||
return {"state": "removed"}
|
|
||||||
if data[:3] == b"\xef\xbb\xbf":
|
|
||||||
data = data[3:]
|
|
||||||
text = data.decode("utf-8", "replace")
|
|
||||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
|
||||||
if not h:
|
|
||||||
return None
|
|
||||||
g = int(h.group(1))
|
|
||||||
k = int(h.group(2))
|
|
||||||
if k == 0:
|
|
||||||
return {"state": "removed"}
|
|
||||||
vendors = []
|
|
||||||
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
|
|
||||||
vendors.append({
|
|
||||||
"version": m.group(1).replace('""', '"'),
|
|
||||||
"vendor": m.group(2).replace('""', '"'),
|
|
||||||
"name": m.group(3).replace('""', '"'),
|
|
||||||
})
|
|
||||||
counts = None
|
|
||||||
if g == 0:
|
|
||||||
counts = [0, 0, 0]
|
|
||||||
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
|
|
||||||
counts[int(m.group(1))] += 1
|
|
||||||
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_support_lines():
|
|
||||||
config_dir = os.path.dirname(config_path)
|
|
||||||
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
|
|
||||||
st = read_support_state(bin_path)
|
|
||||||
res = []
|
|
||||||
if not st or st["state"] == "absent":
|
|
||||||
if cfg_ext_purpose:
|
|
||||||
res.append("Поддержка: расширение (CFE), правки свободны")
|
|
||||||
else:
|
|
||||||
res.append("Поддержка: не на поддержке (своя конфигурация)")
|
|
||||||
return res
|
|
||||||
if st["state"] == "removed":
|
|
||||||
res.append("Поддержка: снята с поддержки полностью")
|
|
||||||
return res
|
|
||||||
res.append("Поддержка: на поддержке")
|
|
||||||
if st["g"] == 0:
|
|
||||||
res.append(" Возможность изменения: включена")
|
|
||||||
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
|
|
||||||
else:
|
|
||||||
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
|
|
||||||
res.append(f" Конфигураций поставщика: {st['k']}")
|
|
||||||
if st["k"] > 1:
|
|
||||||
for v in st["vendors"]:
|
|
||||||
res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}")
|
|
||||||
return res
|
|
||||||
|
|
||||||
def format_home_page_item(it, detailed):
|
|
||||||
badges = [f"h={it['height']}"]
|
|
||||||
if not it["common"]:
|
|
||||||
badges.append("скрыта")
|
|
||||||
if it["roles"]:
|
|
||||||
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
|
|
||||||
tail = f" ({', '.join(badges)})" if badges else ""
|
|
||||||
return f" {it['form']}{tail}"
|
|
||||||
|
|
||||||
# --- Count objects in ChildObjects ---
|
# --- Count objects in ChildObjects ---
|
||||||
object_counts = OrderedDict()
|
object_counts = OrderedDict()
|
||||||
total_objects = 0
|
total_objects = 0
|
||||||
@@ -316,7 +146,6 @@ cfg_version = get_prop_text("Version")
|
|||||||
cfg_vendor = get_prop_text("Vendor")
|
cfg_vendor = get_prop_text("Vendor")
|
||||||
cfg_compat = get_prop_text("CompatibilityMode")
|
cfg_compat = get_prop_text("CompatibilityMode")
|
||||||
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
|
||||||
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
|
|
||||||
cfg_default_run = get_prop_text("DefaultRunMode")
|
cfg_default_run = get_prop_text("DefaultRunMode")
|
||||||
cfg_script = get_prop_text("ScriptVariant")
|
cfg_script = get_prop_text("ScriptVariant")
|
||||||
cfg_default_lang = get_prop_text("DefaultLanguage")
|
cfg_default_lang = get_prop_text("DefaultLanguage")
|
||||||
@@ -330,14 +159,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
|
|||||||
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
|
||||||
|
|
||||||
# --- BRIEF mode ---
|
# --- BRIEF mode ---
|
||||||
if args.Mode == "brief" and not args.Section:
|
if args.Mode == "brief":
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
compat_part = f" | {cfg_compat}" if cfg_compat else ""
|
||||||
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
|
||||||
|
|
||||||
# --- OVERVIEW mode ---
|
# --- OVERVIEW mode ---
|
||||||
if args.Mode == "overview" and not args.Section:
|
if args.Mode == "overview":
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -349,8 +178,6 @@ if args.Mode == "overview" and not args.Section:
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
for ln in get_support_lines():
|
|
||||||
out(ln)
|
|
||||||
out(f"Совместимость: {cfg_compat}")
|
out(f"Совместимость: {cfg_compat}")
|
||||||
out(f"Режим запуска: {cfg_default_run}")
|
out(f"Режим запуска: {cfg_default_run}")
|
||||||
out(f"Язык скриптов: {cfg_script}")
|
out(f"Язык скриптов: {cfg_script}")
|
||||||
@@ -360,20 +187,6 @@ if args.Mode == "overview" and not args.Section:
|
|||||||
out(f"Интерфейс: {cfg_intf_compat}")
|
out(f"Интерфейс: {cfg_intf_compat}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
if panel_layout and any(panel_layout[s] for s in ("top", "left", "right", "bottom")):
|
|
||||||
out("--- Раскладка панелей ---")
|
|
||||||
for s in ("top", "left", "right", "bottom"):
|
|
||||||
if panel_layout[s]:
|
|
||||||
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
|
|
||||||
out()
|
|
||||||
|
|
||||||
# Home page (brief summary)
|
|
||||||
if home_page:
|
|
||||||
out("--- Начальная страница ---")
|
|
||||||
out(f" Шаблон: {home_page['template']}")
|
|
||||||
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
|
||||||
out()
|
|
||||||
|
|
||||||
# Object counts table
|
# Object counts table
|
||||||
out(f"--- Состав ({total_objects} объектов) ---")
|
out(f"--- Состав ({total_objects} объектов) ---")
|
||||||
out()
|
out()
|
||||||
@@ -394,30 +207,7 @@ if args.Mode == "overview" and not args.Section:
|
|||||||
out(f" {padded} {count}")
|
out(f" {padded} {count}")
|
||||||
|
|
||||||
# --- FULL mode ---
|
# --- FULL mode ---
|
||||||
# --- Drill-down: -Section home-page ---
|
if args.Mode == "full":
|
||||||
if args.Section == "home-page":
|
|
||||||
if not home_page:
|
|
||||||
out("Файл Ext/HomePageWorkArea.xml не найден")
|
|
||||||
else:
|
|
||||||
out(f"=== Начальная страница: {cfg_name} ===")
|
|
||||||
out()
|
|
||||||
out(f"Шаблон: {home_page['template']}")
|
|
||||||
out()
|
|
||||||
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
|
|
||||||
items = home_page[col_key]
|
|
||||||
if not items:
|
|
||||||
out(f"{col_lbl}: —")
|
|
||||||
out()
|
|
||||||
continue
|
|
||||||
out(f"{col_lbl} ({len(items)}):")
|
|
||||||
for it in items:
|
|
||||||
out(format_home_page_item(it, True))
|
|
||||||
for r in it["roles"]:
|
|
||||||
rval = "true" if r["value"] else "false"
|
|
||||||
out(f" {r['name']}: {rval}")
|
|
||||||
out()
|
|
||||||
|
|
||||||
if args.Mode == "full" and not args.Section:
|
|
||||||
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
|
||||||
ver_part = f" v{cfg_version}" if cfg_version else ""
|
ver_part = f" v{cfg_version}" if cfg_version else ""
|
||||||
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
|
||||||
@@ -439,8 +229,6 @@ if args.Mode == "full" and not args.Section:
|
|||||||
out(f"Поставщик: {cfg_vendor}")
|
out(f"Поставщик: {cfg_vendor}")
|
||||||
if cfg_version:
|
if cfg_version:
|
||||||
out(f"Версия: {cfg_version}")
|
out(f"Версия: {cfg_version}")
|
||||||
for ln in get_support_lines():
|
|
||||||
out(ln)
|
|
||||||
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
|
||||||
if cfg_update_addr:
|
if cfg_update_addr:
|
||||||
out(f"Каталог обн.: {cfg_update_addr}")
|
out(f"Каталог обн.: {cfg_update_addr}")
|
||||||
@@ -495,26 +283,6 @@ if args.Mode == "full" and not args.Section:
|
|||||||
out(f"Обычн.формы в управл.: {use_of}")
|
out(f"Обычн.формы в управл.: {use_of}")
|
||||||
out()
|
out()
|
||||||
|
|
||||||
# --- Section: Panel layout ---
|
|
||||||
if panel_layout:
|
|
||||||
out("--- Раскладка панелей ---")
|
|
||||||
for s in ("top", "left", "right", "bottom"):
|
|
||||||
slots = panel_layout[s]
|
|
||||||
if slots:
|
|
||||||
out(f" {s.ljust(7)} {format_layout_slots(slots)}")
|
|
||||||
else:
|
|
||||||
out(f" {s.ljust(7)} —")
|
|
||||||
if panel_layout["declared"]:
|
|
||||||
out(f" объявлено: {', '.join(panel_layout['declared'])}")
|
|
||||||
out()
|
|
||||||
|
|
||||||
# --- Section: Home page (brief summary) ---
|
|
||||||
if home_page:
|
|
||||||
out("--- Начальная страница ---")
|
|
||||||
out(f" Шаблон: {home_page['template']}")
|
|
||||||
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
|
|
||||||
out()
|
|
||||||
|
|
||||||
# --- Section: Storages & default forms ---
|
# --- Section: Storages & default forms ---
|
||||||
out("--- Хранилища и формы по умолчанию ---")
|
out("--- Хранилища и формы по умолчанию ---")
|
||||||
storage_props = [
|
storage_props = [
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-init.py" -Name "МояКонфигурация"
|
powershell.exe -NoProfile -File .claude/skills/cf-init/scripts/cf-init.ps1 -Name "МояКонфигурация"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
# cf-init v1.1 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -192,33 +192,6 @@ $langXml = @"
|
|||||||
</MetaDataObject>
|
</MetaDataObject>
|
||||||
"@
|
"@
|
||||||
|
|
||||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
|
||||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
|
||||||
# via panelDef but not placed by default. Without this file the web client renders
|
|
||||||
# section icons without labels (icon-only mode).
|
|
||||||
$openPanelInst = [guid]::NewGuid().ToString()
|
|
||||||
$sectionsPanelInst = [guid]::NewGuid().ToString()
|
|
||||||
$caiXml = @"
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
|
||||||
<top>
|
|
||||||
<panel id="$openPanelInst">
|
|
||||||
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
|
||||||
</panel>
|
|
||||||
</top>
|
|
||||||
<left>
|
|
||||||
<panel id="$sectionsPanelInst">
|
|
||||||
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
|
||||||
</panel>
|
|
||||||
</left>
|
|
||||||
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
|
||||||
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
|
||||||
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
|
||||||
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
|
||||||
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
|
||||||
</ClientApplicationInterface>
|
|
||||||
"@
|
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
if (-not (Test-Path $OutputDir)) {
|
if (-not (Test-Path $OutputDir)) {
|
||||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
@@ -227,10 +200,6 @@ $langDir = Join-Path $OutputDir "Languages"
|
|||||||
if (-not (Test-Path $langDir)) {
|
if (-not (Test-Path $langDir)) {
|
||||||
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$extDir = Join-Path $OutputDir "Ext"
|
|
||||||
if (-not (Test-Path $extDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Write files with UTF-8 BOM ---
|
# --- Write files with UTF-8 BOM ---
|
||||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||||
@@ -238,12 +207,9 @@ $enc = New-Object System.Text.UTF8Encoding($true)
|
|||||||
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
|
||||||
$langFile = Join-Path $langDir "Русский.xml"
|
$langFile = Join-Path $langDir "Русский.xml"
|
||||||
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
|
||||||
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
|
|
||||||
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
|
|
||||||
|
|
||||||
# --- Output ---
|
# --- Output ---
|
||||||
Write-Host "[OK] Создана конфигурация: $Name"
|
Write-Host "[OK] Создана конфигурация: $Name"
|
||||||
Write-Host " Каталог: $OutputDir"
|
Write-Host " Каталог: $OutputDir"
|
||||||
Write-Host " Configuration.xml: $cfgFile"
|
Write-Host " Configuration.xml: $cfgFile"
|
||||||
Write-Host " Languages: $langFile"
|
Write-Host " Languages: $langFile"
|
||||||
Write-Host " Ext/CAI: $caiFile"
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-init v1.2 — Create empty 1C configuration scaffold
|
# cf-init v1.1 — Create empty 1C configuration scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration."""
|
"""Generates minimal XML source files for a 1C configuration."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -184,50 +184,20 @@ def main():
|
|||||||
\t</Language>
|
\t</Language>
|
||||||
</MetaDataObject>'''
|
</MetaDataObject>'''
|
||||||
|
|
||||||
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
|
|
||||||
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
|
|
||||||
# via panelDef but not placed by default. Without this file the web client renders
|
|
||||||
# section icons without labels (icon-only mode).
|
|
||||||
open_panel_inst = new_uuid()
|
|
||||||
sections_panel_inst = new_uuid()
|
|
||||||
cai_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
|
|
||||||
\t<top>
|
|
||||||
\t\t<panel id="{open_panel_inst}">
|
|
||||||
\t\t\t<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
|
|
||||||
\t\t</panel>
|
|
||||||
\t</top>
|
|
||||||
\t<left>
|
|
||||||
\t\t<panel id="{sections_panel_inst}">
|
|
||||||
\t\t\t<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
|
|
||||||
\t\t</panel>
|
|
||||||
\t</left>
|
|
||||||
\t<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
|
|
||||||
\t<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
|
|
||||||
\t<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
|
|
||||||
\t<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
|
|
||||||
\t<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
|
|
||||||
</ClientApplicationInterface>'''
|
|
||||||
|
|
||||||
# --- Create directories ---
|
# --- Create directories ---
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
lang_dir = os.path.join(output_dir, "Languages")
|
lang_dir = os.path.join(output_dir, "Languages")
|
||||||
os.makedirs(lang_dir, exist_ok=True)
|
os.makedirs(lang_dir, exist_ok=True)
|
||||||
ext_dir = os.path.join(output_dir, "Ext")
|
|
||||||
os.makedirs(ext_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- Write files ---
|
# --- Write files ---
|
||||||
write_utf8_bom(cfg_file, cfg_xml)
|
write_utf8_bom(cfg_file, cfg_xml)
|
||||||
lang_file = os.path.join(lang_dir, "Русский.xml")
|
lang_file = os.path.join(lang_dir, "Русский.xml")
|
||||||
write_utf8_bom(lang_file, lang_xml)
|
write_utf8_bom(lang_file, lang_xml)
|
||||||
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
|
|
||||||
write_utf8_bom(cai_file, cai_xml)
|
|
||||||
|
|
||||||
print(f"[OK] Создана конфигурация: {name}")
|
print(f"[OK] Создана конфигурация: {name}")
|
||||||
print(f" Каталог: {output_dir}")
|
print(f" Каталог: {output_dir}")
|
||||||
print(f" Configuration.xml: {cfg_file}")
|
print(f" Configuration.xml: {cfg_file}")
|
||||||
print(f" Languages: {lang_file}")
|
print(f" Languages: {lang_file}")
|
||||||
print(f" Ext/CAI: {cai_file}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty"
|
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty"
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cf-validate.py" -ConfigPath "upload/cfempty/Configuration.xml"
|
powershell.exe -NoProfile -File .claude/skills/cf-validate/scripts/cf-validate.ps1 -ConfigPath "upload/cfempty/Configuration.xml"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# cf-validate v1.4 — Validate 1C configuration root structure
|
# cf-validate v1.1 — Validate 1C configuration root structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[Alias('Path')]
|
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -104,11 +103,11 @@ $validClassIds = @(
|
|||||||
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
|
||||||
)
|
)
|
||||||
|
|
||||||
# 45 types in canonical order
|
# 44 types in canonical order
|
||||||
$childObjectTypes = @(
|
$childObjectTypes = @(
|
||||||
"Language","Subsystem","StyleItem","Style",
|
"Language","Subsystem","StyleItem","Style",
|
||||||
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
"CommonPicture","SessionParameter","Role","CommonTemplate",
|
||||||
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
|
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
|
||||||
"XDTOPackage","WebService","HTTPService","WSReference",
|
"XDTOPackage","WebService","HTTPService","WSReference",
|
||||||
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
|
||||||
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
|
||||||
@@ -125,7 +124,6 @@ $childTypeDirMap = @{
|
|||||||
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
|
||||||
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
|
||||||
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
|
||||||
"Bot"="Bots"
|
|
||||||
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
|
||||||
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
|
||||||
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
|
||||||
@@ -147,17 +145,17 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# Valid enum values for Configuration properties
|
# Valid enum values for Configuration properties
|
||||||
$validEnumValues = @{
|
$validEnumValues = @{
|
||||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
|
||||||
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
"ObjectAutonumerationMode" = @("NotAutoFree","AutoFree")
|
||||||
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
"ModalityUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
"SynchronousPlatformExtensionAndAddInCallUseMode" = @("DontUse","Use","UseWithWarnings")
|
||||||
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
||||||
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
"DatabaseTablespacesUseMode" = @("DontUse","Use")
|
||||||
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
|
"MainClientApplicationWindowMode" = @("Normal","Fullscreen","Kiosk")
|
||||||
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"CompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -205,8 +203,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
@@ -537,72 +535,6 @@ if ($childObjNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
|
||||||
function Test-FormRef([string]$ref) {
|
|
||||||
if (-not $ref) { return $true }
|
|
||||||
# UUID — cannot verify without scanning all forms; skip
|
|
||||||
if ($ref -match $guidPattern) { return $true }
|
|
||||||
$parts = $ref.Split(".")
|
|
||||||
if ($parts.Count -eq 2 -and $parts[0] -eq "CommonForm") {
|
|
||||||
$p = Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Form.xml"
|
|
||||||
$pExt = Join-Path (Join-Path (Join-Path (Join-Path $configDir "CommonForms") $parts[1]) "Ext") "Form.xml"
|
|
||||||
return (Test-Path $p) -or (Test-Path $pExt)
|
|
||||||
}
|
|
||||||
if ($parts.Count -eq 4 -and $parts[2] -eq "Form" -and $childTypeDirMap.ContainsKey($parts[0])) {
|
|
||||||
$dir = $childTypeDirMap[$parts[0]]
|
|
||||||
$p = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Form.xml"
|
|
||||||
$pExt = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $configDir $dir) $parts[1]) "Forms") $parts[3]) "Ext") "Form.xml"
|
|
||||||
return (Test-Path $p) -or (Test-Path $pExt)
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
$formRefsChecked = 0
|
|
||||||
$formRefErrors = @()
|
|
||||||
|
|
||||||
# HomePageWorkArea
|
|
||||||
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
|
|
||||||
if (Test-Path $hpPath) {
|
|
||||||
try {
|
|
||||||
[xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8
|
|
||||||
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
|
|
||||||
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
|
|
||||||
foreach ($f in $hpDoc.DocumentElement.SelectNodes("//hp:Item/hp:Form", $hpNs)) {
|
|
||||||
$ref = $f.InnerText.Trim()
|
|
||||||
if (-not $ref) { continue }
|
|
||||||
$formRefsChecked++
|
|
||||||
if (-not (Test-FormRef $ref)) {
|
|
||||||
$formRefErrors += "HomePageWorkArea.Form '$ref' — file not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
$formRefErrors += "HomePageWorkArea.xml: parse error — $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Properties: DefaultXxxForm refs
|
|
||||||
if ($propsNode) {
|
|
||||||
$formProps = @("DefaultReportForm","DefaultReportVariantForm","DefaultReportSettingsForm","DefaultDynamicListSettingsForm","DefaultSearchForm","DefaultDataHistoryChangeHistoryForm","DefaultDataHistoryVersionDataForm","DefaultDataHistoryVersionDifferencesForm","DefaultCollaborationSystemUsersChoiceForm","DefaultConstantsForm")
|
|
||||||
foreach ($pn in $formProps) {
|
|
||||||
$node = $propsNode.SelectSingleNode("md:$pn", $ns)
|
|
||||||
if ($node -and $node.InnerText.Trim()) {
|
|
||||||
$ref = $node.InnerText.Trim()
|
|
||||||
$formRefsChecked++
|
|
||||||
if (-not (Test-FormRef $ref)) {
|
|
||||||
$formRefErrors += "Properties.$pn '$ref' — form not found"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($formRefsChecked -eq 0) {
|
|
||||||
Report-OK "9. Form references: none to check"
|
|
||||||
} elseif ($formRefErrors.Count -eq 0) {
|
|
||||||
Report-OK "9. Form references: $formRefsChecked verified"
|
|
||||||
} else {
|
|
||||||
foreach ($err in $formRefErrors) { Report-Error "9. $err" }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
& $finalize
|
& $finalize
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cf-validate v1.4 — Validate 1C configuration XML structure
|
# cf-validate v1.1 — Validate 1C configuration XML structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
|
|||||||
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
|
||||||
]
|
]
|
||||||
|
|
||||||
# 45 types in canonical order
|
# 44 types in canonical order
|
||||||
CHILD_OBJECT_TYPES = [
|
CHILD_OBJECT_TYPES = [
|
||||||
'Language', 'Subsystem', 'StyleItem', 'Style',
|
'Language', 'Subsystem', 'StyleItem', 'Style',
|
||||||
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
|
||||||
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
|
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
|
||||||
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
|
||||||
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
|
||||||
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
|
||||||
@@ -54,7 +54,6 @@ CHILD_TYPE_DIR_MAP = {
|
|||||||
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
|
||||||
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
|
||||||
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
|
||||||
'Bot': 'Bots',
|
|
||||||
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
|
||||||
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
|
||||||
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
|
||||||
@@ -83,7 +82,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
@@ -91,10 +90,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
'ObjectAutonumerationMode': ['NotAutoFree', 'AutoFree'],
|
||||||
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'ModalityUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
'SynchronousPlatformExtensionAndAddInCallUseMode': ['DontUse', 'Use', 'UseWithWarnings'],
|
||||||
'InterfaceCompatibilityMode': [
|
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
||||||
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
|
||||||
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
|
||||||
],
|
|
||||||
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
'DatabaseTablespacesUseMode': ['DontUse', 'Use'],
|
||||||
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
'MainClientApplicationWindowMode': ['Normal', 'Fullscreen', 'Kiosk'],
|
||||||
'CompatibilityMode': [
|
'CompatibilityMode': [
|
||||||
@@ -104,7 +100,7 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +162,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration XML structure', allow_abbrev=False
|
description='Validate 1C configuration XML structure', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ConfigPath', '-Path', dest='ConfigPath', required=True)
|
parser.add_argument('-ConfigPath', dest='ConfigPath', required=True)
|
||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
@@ -232,8 +228,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.20'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
@@ -538,60 +534,6 @@ def main():
|
|||||||
else:
|
else:
|
||||||
pass # no ChildObjects
|
pass # no ChildObjects
|
||||||
|
|
||||||
# --- Check 9: Form references (HomePageWorkArea + Properties) ---
|
|
||||||
def test_form_ref(ref):
|
|
||||||
if not ref:
|
|
||||||
return True
|
|
||||||
if GUID_PATTERN.match(ref):
|
|
||||||
return True
|
|
||||||
parts = ref.split('.')
|
|
||||||
if len(parts) == 2 and parts[0] == 'CommonForm':
|
|
||||||
p = os.path.join(config_dir, 'CommonForms', parts[1], 'Form.xml')
|
|
||||||
p_ext = os.path.join(config_dir, 'CommonForms', parts[1], 'Ext', 'Form.xml')
|
|
||||||
return os.path.isfile(p) or os.path.isfile(p_ext)
|
|
||||||
if len(parts) == 4 and parts[2] == 'Form' and parts[0] in CHILD_TYPE_DIR_MAP:
|
|
||||||
d = CHILD_TYPE_DIR_MAP[parts[0]]
|
|
||||||
p = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Form.xml')
|
|
||||||
p_ext = os.path.join(config_dir, d, parts[1], 'Forms', parts[3], 'Ext', 'Form.xml')
|
|
||||||
return os.path.isfile(p) or os.path.isfile(p_ext)
|
|
||||||
return False
|
|
||||||
|
|
||||||
form_refs_checked = 0
|
|
||||||
form_ref_errors = []
|
|
||||||
|
|
||||||
hp_path = os.path.join(config_dir, 'Ext', 'HomePageWorkArea.xml')
|
|
||||||
if os.path.isfile(hp_path):
|
|
||||||
try:
|
|
||||||
hp_tree = etree.parse(hp_path)
|
|
||||||
HP_NS = 'http://v8.1c.ru/8.3/xcf/extrnprops'
|
|
||||||
for f in hp_tree.getroot().iter(f'{{{HP_NS}}}Form'):
|
|
||||||
ref = (f.text or '').strip()
|
|
||||||
if not ref:
|
|
||||||
continue
|
|
||||||
form_refs_checked += 1
|
|
||||||
if not test_form_ref(ref):
|
|
||||||
form_ref_errors.append(f"HomePageWorkArea.Form '{ref}' — file not found")
|
|
||||||
except Exception as e:
|
|
||||||
form_ref_errors.append(f'HomePageWorkArea.xml: parse error — {e}')
|
|
||||||
|
|
||||||
if props_node is not None:
|
|
||||||
form_props = ['DefaultReportForm','DefaultReportVariantForm','DefaultReportSettingsForm','DefaultDynamicListSettingsForm','DefaultSearchForm','DefaultDataHistoryChangeHistoryForm','DefaultDataHistoryVersionDataForm','DefaultDataHistoryVersionDifferencesForm','DefaultCollaborationSystemUsersChoiceForm','DefaultConstantsForm']
|
|
||||||
for pn in form_props:
|
|
||||||
node = props_node.find(f'md:{pn}', NS)
|
|
||||||
if node is not None and node.text and node.text.strip():
|
|
||||||
ref = node.text.strip()
|
|
||||||
form_refs_checked += 1
|
|
||||||
if not test_form_ref(ref):
|
|
||||||
form_ref_errors.append(f"Properties.{pn} '{ref}' — form not found")
|
|
||||||
|
|
||||||
if form_refs_checked == 0:
|
|
||||||
r.ok('9. Form references: none to check')
|
|
||||||
elif not form_ref_errors:
|
|
||||||
r.ok(f'9. Form references: {form_refs_checked} verified')
|
|
||||||
else:
|
|
||||||
for err in form_ref_errors:
|
|
||||||
r.error(f'9. {err}')
|
|
||||||
|
|
||||||
# --- Final output ---
|
# --- Final output ---
|
||||||
r.finalize(out_file)
|
r.finalize(out_file)
|
||||||
sys.exit(1 if r.errors > 0 else 0)
|
sys.exit(1 if r.errors > 0 else 0)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
powershell.exe -NoProfile -File .claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)][string]$ExtensionPath,
|
[Parameter(Mandatory)][string]$ExtensionPath,
|
||||||
@@ -13,31 +13,6 @@ $ErrorActionPreference = "Stop"
|
|||||||
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
function Info([string]$msg) { Write-Host "[INFO] $msg" }
|
||||||
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
function Warn([string]$msg) { Write-Host "[WARN] $msg" }
|
||||||
|
|
||||||
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
|
||||||
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
|
||||||
# platform rejects the form with "Неверный путь к данным" on load.
|
|
||||||
$script:formBindingDataTags = @('DataPath','TitleDataPath','FooterDataPath','HeaderDataPath','MultipleValueDataPath','MultipleValuePresentDataPath')
|
|
||||||
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
|
||||||
$script:formBindingPictureTags = @('RowPictureDataPath','MultipleValuePictureDataPath')
|
|
||||||
|
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed.
|
|
||||||
# $keepObjekt=$true (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
|
||||||
# $keepObjekt=$false (default skeleton): strip all bindings. Picture-path tags are always stripped.
|
|
||||||
function Strip-FormBindings {
|
|
||||||
param([string]$xml, [bool]$keepObjekt)
|
|
||||||
foreach ($tag in $script:formBindingDataTags) {
|
|
||||||
if ($keepObjekt) {
|
|
||||||
$xml = [regex]::Replace($xml, "\s*<$tag>(?!Объект\.)[^<]*</$tag>", '')
|
|
||||||
} else {
|
|
||||||
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($tag in $script:formBindingPictureTags) {
|
|
||||||
$xml = [regex]::Replace($xml, "\s*<$tag>[^<]*</$tag>", '')
|
|
||||||
}
|
|
||||||
return $xml
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1. Resolve paths ---
|
# --- 1. Resolve paths ---
|
||||||
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
|
||||||
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
|
||||||
@@ -444,14 +419,6 @@ function Read-SourceObject {
|
|||||||
$srcProps[$propName] = $propNode.InnerText.Trim()
|
$srcProps[$propName] = $propNode.InnerText.Trim()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
|
||||||
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
|
||||||
if ($typeName -eq "DefinedType") {
|
|
||||||
$typeNode = $propsNode.SelectSingleNode("md:Type", $srcNs)
|
|
||||||
if ($typeNode) {
|
|
||||||
$srcProps["__TypeXml"] = [regex]::Replace($typeNode.OuterXml, '\s+xmlns(?::\w+)?="[^"]*"', '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return @{
|
return @{
|
||||||
@@ -514,23 +481,8 @@ function Borrow-Form {
|
|||||||
}
|
}
|
||||||
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
$srcFormContent = [System.IO.File]::ReadAllText($srcFormXmlPath, $enc)
|
||||||
|
|
||||||
# 3. Generate form metadata XML (ФормаЭлемента.xml).
|
# 3. Generate form metadata XML (ФормаЭлемента.xml)
|
||||||
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
$newFormUuid = [guid]::NewGuid().ToString()
|
||||||
# (regenerating it would churn the form's identity on every rerun).
|
|
||||||
$formMetaFileExisting = Join-Path (Join-Path (Join-Path (Join-Path $extDir $dirName) $objName) "Forms") "${formName}.xml"
|
|
||||||
$newFormUuid = ""
|
|
||||||
if (Test-Path $formMetaFileExisting) {
|
|
||||||
try {
|
|
||||||
$existingDoc = New-Object System.Xml.XmlDocument
|
|
||||||
$existingDoc.Load($formMetaFileExisting)
|
|
||||||
$existingFormNode = $existingDoc.DocumentElement.SelectSingleNode("*[local-name()='Form']")
|
|
||||||
if ($existingFormNode) {
|
|
||||||
$existingUuid = $existingFormNode.GetAttribute("uuid")
|
|
||||||
if ($existingUuid) { $newFormUuid = $existingUuid }
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
if (-not $newFormUuid) { $newFormUuid = [guid]::NewGuid().ToString() }
|
|
||||||
$formMetaSb = New-Object System.Text.StringBuilder
|
$formMetaSb = New-Object System.Text.StringBuilder
|
||||||
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
$formMetaSb.AppendLine("<?xml version=`"1.0`" encoding=`"UTF-8`"?>") | Out-Null
|
||||||
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
$formMetaSb.AppendLine("<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">") | Out-Null
|
||||||
@@ -564,10 +516,8 @@ function Borrow-Form {
|
|||||||
$srcFormDoc.Load($srcFormXmlPath)
|
$srcFormDoc.Load($srcFormXmlPath)
|
||||||
$srcFormEl = $srcFormDoc.DocumentElement
|
$srcFormEl = $srcFormDoc.DocumentElement
|
||||||
|
|
||||||
# Borrowed form must use the extension's format version (not the source form's), so the whole
|
$formVersion = $srcFormEl.GetAttribute("version")
|
||||||
# extension stays uniform — otherwise the platform rejects the import on a version mismatch
|
if (-not $formVersion) { $formVersion = $script:formatVersion }
|
||||||
# (e.g. a 2.13 form inside a 2.17 extension). The platform itself upgrades the form to the root version.
|
|
||||||
$formVersion = $script:formatVersion
|
|
||||||
|
|
||||||
# Find direct children: form properties, AutoCommandBar, ChildItems
|
# Find direct children: form properties, AutoCommandBar, ChildItems
|
||||||
$srcAutoCmd = $null
|
$srcAutoCmd = $null
|
||||||
@@ -602,8 +552,13 @@ function Borrow-Form {
|
|||||||
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
$autoCmdXml = $autoCmdXml -replace '<Autofill>true</Autofill>', '<Autofill>false</Autofill>'
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip DataPath in AutoCommandBar buttons
|
||||||
$autoCmdXml = Strip-FormBindings $autoCmdXml ([bool]$BorrowMainAttr)
|
if ($BorrowMainAttr) {
|
||||||
|
# Keep only Объект.* DataPaths
|
||||||
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
||||||
|
} else {
|
||||||
|
$autoCmdXml = [regex]::Replace($autoCmdXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
@@ -613,9 +568,17 @@ function Borrow-Form {
|
|||||||
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
$childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '')
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
$childItemsXml = [regex]::Replace($childItemsXml, '<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>')
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip DataPath, TitleDataPath, RowPictureDataPath
|
||||||
# (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*/RowPicture*)
|
if ($BorrowMainAttr) {
|
||||||
$childItemsXml = Strip-FormBindings $childItemsXml ([bool]$BorrowMainAttr)
|
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>(?!Объект\.)[^<]*</DataPath>', '')
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>(?!Объект\.)[^<]*</TitleDataPath>', '')
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
||||||
|
} else {
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<DataPath>[^<]*</DataPath>', '')
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<TitleDataPath>[^<]*</TitleDataPath>', '')
|
||||||
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '')
|
||||||
|
}
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
$childItemsXml = [regex]::Replace($childItemsXml, '\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '')
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID)
|
||||||
@@ -892,19 +855,14 @@ function Borrow-Form {
|
|||||||
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
[System.IO.File]::WriteAllText($formXmlFile, $formXmlSb.ToString(), $enc)
|
||||||
Info " Created: $formXmlFile"
|
Info " Created: $formXmlFile"
|
||||||
|
|
||||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
# 6. Create empty Module.bsl
|
||||||
# not clobber user code added to the form module).
|
|
||||||
$moduleDir = Join-Path $formXmlDir "Form"
|
$moduleDir = Join-Path $formXmlDir "Form"
|
||||||
if (-not (Test-Path $moduleDir)) {
|
if (-not (Test-Path $moduleDir)) {
|
||||||
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
$moduleBslFile = Join-Path $moduleDir "Module.bsl"
|
||||||
if (Test-Path $moduleBslFile) {
|
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
||||||
Info " Preserved existing Module.bsl"
|
Info " Created: $moduleBslFile"
|
||||||
} else {
|
|
||||||
[System.IO.File]::WriteAllText($moduleBslFile, "", $enc)
|
|
||||||
Info " Created: $moduleBslFile"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
Register-FormInObject $typeName $objName $formName
|
Register-FormInObject $typeName $objName $formName
|
||||||
@@ -1052,29 +1010,8 @@ function Collect-FormDataPaths {
|
|||||||
$firstLevel = @{}
|
$firstLevel = @{}
|
||||||
$deepPaths = @()
|
$deepPaths = @()
|
||||||
|
|
||||||
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
$matches2 = [regex]::Matches($content, '<DataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</DataPath>')
|
||||||
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
foreach ($m in $matches2) {
|
||||||
foreach ($tag in $script:formBindingDataTags) {
|
|
||||||
$bms = [regex]::Matches($content, "<$tag>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</$tag>")
|
|
||||||
foreach ($m in $bms) {
|
|
||||||
$path = $m.Groups[1].Value
|
|
||||||
$segments = $path.Split(".")
|
|
||||||
$seg0 = $segments[0]
|
|
||||||
if ($script:standardFields -contains $seg0) { continue }
|
|
||||||
$firstLevel[$seg0] = $true
|
|
||||||
if ($segments.Count -ge 2) {
|
|
||||||
$seg1 = $segments[1]
|
|
||||||
if ($script:standardFields -contains $seg1) { continue }
|
|
||||||
$seg2 = if ($segments.Count -ge 3) { $segments[2] } else { $null }
|
|
||||||
$deepPaths += @{ ObjectAttr = $seg0; SubAttr = $seg1; SubSubAttr = $seg2 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
|
||||||
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
|
||||||
$fieldMatches = [regex]::Matches($content, "<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>")
|
|
||||||
foreach ($m in $fieldMatches) {
|
|
||||||
$path = $m.Groups[1].Value
|
$path = $m.Groups[1].Value
|
||||||
$segments = $path.Split(".")
|
$segments = $path.Split(".")
|
||||||
$seg0 = $segments[0]
|
$seg0 = $segments[0]
|
||||||
@@ -1087,11 +1024,21 @@ function Collect-FormDataPaths {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Also collect from TitleDataPath
|
||||||
|
$matches3 = [regex]::Matches($content, '<TitleDataPath>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</TitleDataPath>')
|
||||||
|
foreach ($m in $matches3) {
|
||||||
|
$path = $m.Groups[1].Value
|
||||||
|
$segments = $path.Split(".")
|
||||||
|
$seg0 = $segments[0]
|
||||||
|
if ($script:standardFields -contains $seg0) { continue }
|
||||||
|
$firstLevel[$seg0] = $true
|
||||||
|
}
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
$seen = @{}
|
$seen = @{}
|
||||||
$uniqueDeep = @()
|
$uniqueDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
$key = "$($dp.ObjectAttr).$($dp.SubAttr).$($dp.SubSubAttr)"
|
$key = "$($dp.ObjectAttr).$($dp.SubAttr)"
|
||||||
if (-not $seen.ContainsKey($key)) {
|
if (-not $seen.ContainsKey($key)) {
|
||||||
$seen[$key] = $true
|
$seen[$key] = $true
|
||||||
$uniqueDeep += $dp
|
$uniqueDeep += $dp
|
||||||
@@ -1195,8 +1142,7 @@ function Resolve-SourceAttributes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
# Extract extra Properties for main object enrichment (Hierarchical, CodeLength, etc.)
|
||||||
# Ordered so PS emits the same property order as the Python port (dict preserves insertion order).
|
$extraProps = @{}
|
||||||
$extraProps = [ordered]@{}
|
|
||||||
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
$propsNode = $srcEl.SelectSingleNode("md:Properties", $srcNs)
|
||||||
if ($propsNode) {
|
if ($propsNode) {
|
||||||
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
$propsToExtract = @("Hierarchical","FoldersOnTop","CodeLength","DescriptionLength","CodeType","CodeAllowedLength",
|
||||||
@@ -1429,45 +1375,28 @@ function Borrow-MainAttribute {
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
$objFile = Join-Path (Join-Path $extDir $dirName) "${objName}.xml"
|
||||||
|
|
||||||
# Read existing object XML (needed for dedup + enrichment)
|
|
||||||
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
|
||||||
|
|
||||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
|
||||||
$existingChildNames = @{}
|
|
||||||
if ($objContent -match '(?s)<ChildObjects>(.*?)</ChildObjects>') {
|
|
||||||
foreach ($nm in [regex]::Matches($Matches[1], '<Name>(\w+)</Name>')) {
|
|
||||||
$existingChildNames[$nm.Groups[1].Value] = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$insertAttrs = @($srcAttrs | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
|
||||||
$insertTS = @($srcTS | Where-Object { -not $existingChildNames.ContainsKey($_.Name) })
|
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
# Generate full object XML with attributes and TS
|
||||||
$contentSb = New-Object System.Text.StringBuilder
|
$contentSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($attr in $insertAttrs) {
|
foreach ($attr in $srcAttrs) {
|
||||||
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
$attrXml = Build-AdoptedAttributeXml $attr.Name $attr.Uuid $attr.TypeXml "`t`t`t"
|
||||||
$contentSb.AppendLine($attrXml) | Out-Null
|
$contentSb.AppendLine($attrXml) | Out-Null
|
||||||
}
|
}
|
||||||
foreach ($ts in $insertTS) {
|
foreach ($ts in $srcTS) {
|
||||||
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
$tsXml = Build-AdoptedTabularSectionXml $ts.Name $ts.Uuid $ts.GeneratedTypes $ts.Attributes "`t`t`t"
|
||||||
$contentSb.AppendLine($tsXml) | Out-Null
|
$contentSb.AppendLine($tsXml) | Out-Null
|
||||||
}
|
}
|
||||||
$adoptedContent = $contentSb.ToString().TrimEnd()
|
$adoptedContent = $contentSb.ToString().TrimEnd()
|
||||||
|
|
||||||
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
# Read existing object XML and inject
|
||||||
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
$objContent = [System.IO.File]::ReadAllText($objFile, (New-Object System.Text.UTF8Encoding($true)))
|
||||||
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
|
||||||
|
# Inject extra properties after ExtendedConfigurationObject
|
||||||
if ($extraProps.Count -gt 0) {
|
if ($extraProps.Count -gt 0) {
|
||||||
$objPropsBlock = ""
|
|
||||||
if ($objContent -match '(?s)<Properties>(.*?)</Properties>') { $objPropsBlock = $Matches[1] }
|
|
||||||
$propsSb = New-Object System.Text.StringBuilder
|
$propsSb = New-Object System.Text.StringBuilder
|
||||||
foreach ($pName in $extraProps.Keys) {
|
foreach ($pName in $extraProps.Keys) {
|
||||||
if ($objPropsBlock -match "<$pName>") { continue }
|
|
||||||
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
$propsSb.Append("`r`n`t`t`t<${pName}>$($extraProps[$pName])</${pName}>") | Out-Null
|
||||||
}
|
}
|
||||||
if ($propsSb.Length -gt 0) {
|
$objContent = $objContent -replace '(</ExtendedConfigurationObject>)', "`$1$($propsSb.ToString())"
|
||||||
$objContent = ([regex]'</ExtendedConfigurationObject>').Replace($objContent, "</ExtendedConfigurationObject>$($propsSb.ToString())", 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
@@ -1525,46 +1454,79 @@ function Borrow-MainAttribute {
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
if ($mode -eq "Form" -and $deepPaths.Count -gt 0) {
|
||||||
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
# Filter out deep paths where ObjectAttr is a TabularSection (those are TS column refs, not deep attribute refs)
|
||||||
$deepByAttr = @{}
|
$realDeep = @()
|
||||||
foreach ($dp in $deepPaths) {
|
foreach ($dp in $deepPaths) {
|
||||||
if ($tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { $realDeep += $dp }
|
||||||
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
|
||||||
if ($deepByAttr[$dp.ObjectAttr] -notcontains $dp.SubAttr) { $deepByAttr[$dp.ObjectAttr] += $dp.SubAttr }
|
|
||||||
}
|
|
||||||
if ($deepByAttr.Count -gt 0) {
|
|
||||||
Info " Processing $($deepByAttr.Count) deep path attribute(s)..."
|
|
||||||
foreach ($attrName in $deepByAttr.Keys) {
|
|
||||||
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
|
||||||
if (-not $attrInfo) { continue }
|
|
||||||
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
|
||||||
if (-not $catMatch.Success) { continue }
|
|
||||||
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $deepByAttr[$attrName]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
if ($realDeep.Count -gt 0) {
|
||||||
$tsDeepByCol = @{}
|
Info " Processing $($realDeep.Count) deep path(s)..."
|
||||||
foreach ($dp in $deepPaths) {
|
|
||||||
if (-not $tsNames.ContainsKey($dp.ObjectAttr)) { continue }
|
# Group by ObjectAttr → target catalog
|
||||||
if (-not $dp.SubSubAttr) { continue }
|
$deepByAttr = @{}
|
||||||
if ($script:standardFields -contains $dp.SubSubAttr) { continue }
|
foreach ($dp in $realDeep) {
|
||||||
$k = "$($dp.ObjectAttr)|$($dp.SubAttr)"
|
if (-not $deepByAttr.ContainsKey($dp.ObjectAttr)) { $deepByAttr[$dp.ObjectAttr] = @() }
|
||||||
if (-not $tsDeepByCol.ContainsKey($k)) { $tsDeepByCol[$k] = @() }
|
$deepByAttr[$dp.ObjectAttr] += $dp.SubAttr
|
||||||
if ($tsDeepByCol[$k] -notcontains $dp.SubSubAttr) { $tsDeepByCol[$k] += $dp.SubSubAttr }
|
}
|
||||||
}
|
|
||||||
if ($tsDeepByCol.Count -gt 0) {
|
foreach ($attrName in $deepByAttr.Keys) {
|
||||||
Info " Processing $($tsDeepByCol.Count) tabular-section deep path(s)..."
|
# Find the attribute's type to determine target catalog
|
||||||
foreach ($k in $tsDeepByCol.Keys) {
|
$attrInfo = $srcAttrs | Where-Object { $_.Name -eq $attrName } | Select-Object -First 1
|
||||||
$parts = $k.Split("|")
|
if (-not $attrInfo) { continue }
|
||||||
$tsName = $parts[0]; $colName = $parts[1]
|
|
||||||
$tsInfo = $srcTS | Where-Object { $_.Name -eq $tsName } | Select-Object -First 1
|
# Extract catalog name from type: cfg:CatalogRef.XXX
|
||||||
if (-not $tsInfo) { continue }
|
$catMatch = [regex]::Match($attrInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
||||||
$colInfo = $tsInfo.Attributes | Where-Object { $_.Name -eq $colName } | Select-Object -First 1
|
|
||||||
if (-not $colInfo) { continue }
|
|
||||||
$catMatch = [regex]::Match($colInfo.TypeXml, 'cfg:(\w+)Ref\.(\w+)')
|
|
||||||
if (-not $catMatch.Success) { continue }
|
if (-not $catMatch.Success) { continue }
|
||||||
Borrow-DeepTargetAttrs $catMatch.Groups[1].Value $catMatch.Groups[2].Value $tsDeepByCol[$k]
|
|
||||||
|
$targetTypeName = $catMatch.Groups[1].Value
|
||||||
|
$targetObjName = $catMatch.Groups[2].Value
|
||||||
|
|
||||||
|
# Ensure target is borrowed
|
||||||
|
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
||||||
|
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
||||||
|
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
||||||
|
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
||||||
|
if (-not (Test-Path $tTargetDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
||||||
|
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBom)
|
||||||
|
Add-ToChildObjects $targetTypeName $targetObjName
|
||||||
|
$script:borrowedFiles += $tTargetFile
|
||||||
|
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve sub-attributes in target catalog
|
||||||
|
$subNames = @{}
|
||||||
|
foreach ($sn in $deepByAttr[$attrName]) { $subNames[$sn] = $true }
|
||||||
|
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
||||||
|
|
||||||
|
if ($subResolved.Attributes.Count -gt 0) {
|
||||||
|
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
||||||
|
|
||||||
|
# Collect and borrow ref types from deep attributes
|
||||||
|
$subTypeXmls = @()
|
||||||
|
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
||||||
|
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
||||||
|
foreach ($srt in $subRefTypes) {
|
||||||
|
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
||||||
|
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
||||||
|
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
||||||
|
if (-not (Test-Path $sSrcFile)) { continue }
|
||||||
|
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
||||||
|
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
||||||
|
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
||||||
|
if (-not (Test-Path $sTargetDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
||||||
|
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBom)
|
||||||
|
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
||||||
|
$script:borrowedFiles += $sTargetFile
|
||||||
|
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1572,57 +1534,6 @@ function Borrow-MainAttribute {
|
|||||||
Info " Main attribute borrowing complete"
|
Info " Main attribute borrowing complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 11i. Helper: borrow a deep-path target catalog together with the referenced sub-attributes ---
|
|
||||||
# Used for both Объект.<Ref>.<Sub> (top-level ref attr) and Объект.<ТЧ>.<Колонка>.<Sub> (tabular-section
|
|
||||||
# column ref). Mirrors Designer: the referenced catalog is adopted WITH the sub-attributes the form shows,
|
|
||||||
# otherwise the platform rejects the deep DataPath ("Неверный путь к данным").
|
|
||||||
function Borrow-DeepTargetAttrs {
|
|
||||||
param([string]$targetTypeName, [string]$targetObjName, $subAttrNames)
|
|
||||||
|
|
||||||
$encBomLocal = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
|
|
||||||
# Ensure target is borrowed (shell)
|
|
||||||
if (-not (Test-ObjectBorrowed $targetTypeName $targetObjName)) {
|
|
||||||
$tSrc = Read-SourceObject $targetTypeName $targetObjName
|
|
||||||
$tBorrowedXml = Build-BorrowedObjectXml $targetTypeName $targetObjName $tSrc.Uuid $tSrc.Properties
|
|
||||||
$tTargetDir = Join-Path $extDir $childTypeDirMap[$targetTypeName]
|
|
||||||
if (-not (Test-Path $tTargetDir)) { New-Item -ItemType Directory -Path $tTargetDir -Force | Out-Null }
|
|
||||||
$tTargetFile = Join-Path $tTargetDir "${targetObjName}.xml"
|
|
||||||
[System.IO.File]::WriteAllText($tTargetFile, $tBorrowedXml, $encBomLocal)
|
|
||||||
Add-ToChildObjects $targetTypeName $targetObjName
|
|
||||||
$script:borrowedFiles += $tTargetFile
|
|
||||||
Info " Auto-borrowed for deep path: ${targetTypeName}.${targetObjName}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Resolve sub-attributes in target catalog and merge them in
|
|
||||||
$subNames = @{}
|
|
||||||
foreach ($sn in $subAttrNames) { $subNames[$sn] = $true }
|
|
||||||
$subResolved = Resolve-SourceAttributes $targetTypeName $targetObjName $subNames
|
|
||||||
if ($subResolved.Attributes.Count -gt 0) {
|
|
||||||
Merge-AttributesIntoObject $targetTypeName $targetObjName $subResolved.Attributes
|
|
||||||
|
|
||||||
# Borrow ref types referenced by the sub-attributes
|
|
||||||
$subTypeXmls = @()
|
|
||||||
foreach ($sa in $subResolved.Attributes) { $subTypeXmls += $sa.TypeXml }
|
|
||||||
$subRefTypes = Collect-ReferenceTypes $subTypeXmls
|
|
||||||
foreach ($srt in $subRefTypes) {
|
|
||||||
if (-not $childTypeDirMap.ContainsKey($srt.TypeName)) { continue }
|
|
||||||
if (Test-ObjectBorrowed $srt.TypeName $srt.ObjName) { continue }
|
|
||||||
$sSrcFile = Join-Path (Join-Path $cfgDir $childTypeDirMap[$srt.TypeName]) "$($srt.ObjName).xml"
|
|
||||||
if (-not (Test-Path $sSrcFile)) { continue }
|
|
||||||
$sSrc = Read-SourceObject $srt.TypeName $srt.ObjName
|
|
||||||
$sBorrowedXml = Build-BorrowedObjectXml $srt.TypeName $srt.ObjName $sSrc.Uuid $sSrc.Properties
|
|
||||||
$sTargetDir = Join-Path $extDir $childTypeDirMap[$srt.TypeName]
|
|
||||||
if (-not (Test-Path $sTargetDir)) { New-Item -ItemType Directory -Path $sTargetDir -Force | Out-Null }
|
|
||||||
$sTargetFile = Join-Path $sTargetDir "$($srt.ObjName).xml"
|
|
||||||
[System.IO.File]::WriteAllText($sTargetFile, $sBorrowedXml, $encBomLocal)
|
|
||||||
Add-ToChildObjects $srt.TypeName $srt.ObjName
|
|
||||||
$script:borrowedFiles += $sTargetFile
|
|
||||||
Info " Auto-borrowed (deep): $($srt.TypeName).$($srt.ObjName)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 12. Helper: build borrowed object XML ---
|
# --- 12. Helper: build borrowed object XML ---
|
||||||
function Build-BorrowedObjectXml {
|
function Build-BorrowedObjectXml {
|
||||||
param(
|
param(
|
||||||
@@ -1661,11 +1572,6 @@ function Build-BorrowedObjectXml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
|
||||||
if ($typeName -eq "DefinedType" -and $sourceProps.ContainsKey("__TypeXml")) {
|
|
||||||
$sb.AppendLine("`t`t`t$($sourceProps['__TypeXml'])") | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
$sb.AppendLine("`t`t</Properties>") | Out-Null
|
||||||
|
|
||||||
# ChildObjects (for types that need it)
|
# ChildObjects (for types that need it)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
|
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -14,36 +14,6 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
|
|||||||
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
||||||
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
V8_NS = "http://v8.1c.ru/8.1/data/core"
|
||||||
|
|
||||||
# Form data-binding tags (value = attribute path). A binding survives only if its root
|
|
||||||
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
|
|
||||||
# platform rejects the form with "Неверный путь к данным" on load.
|
|
||||||
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
|
|
||||||
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
|
|
||||||
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
|
|
||||||
|
|
||||||
|
|
||||||
def strip_form_bindings(xml, keep_objekt):
|
|
||||||
"""Strip data-binding tags whose root attribute isn't borrowed.
|
|
||||||
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
|
|
||||||
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
|
|
||||||
for tag in FORM_BINDING_DATA_TAGS:
|
|
||||||
if keep_objekt:
|
|
||||||
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
|
|
||||||
else:
|
|
||||||
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
|
||||||
for tag in FORM_BINDING_PICTURE_TAGS:
|
|
||||||
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
|
|
||||||
return xml
|
|
||||||
|
|
||||||
|
|
||||||
def decode_numeric_entities(s):
|
|
||||||
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
|
|
||||||
elements where the PowerShell port writes literal characters. Normalize numeric refs
|
|
||||||
back to literal so PS↔PY output matches. Named entities (& < ...) are left intact."""
|
|
||||||
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
|
|
||||||
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
def localname(el):
|
def localname(el):
|
||||||
return etree.QName(el.tag).localname
|
return etree.QName(el.tag).localname
|
||||||
@@ -492,13 +462,6 @@ def main():
|
|||||||
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
|
||||||
if prop_node is not None:
|
if prop_node is not None:
|
||||||
src_props[prop_name] = (prop_node.text or "").strip()
|
src_props[prop_name] = (prop_node.text or "").strip()
|
||||||
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
|
|
||||||
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
|
|
||||||
if type_name == "DefinedType":
|
|
||||||
type_node = props_node.find(f"{{{MD_NS}}}Type")
|
|
||||||
if type_node is not None:
|
|
||||||
type_xml = etree.tostring(type_node, encoding="unicode")
|
|
||||||
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
|
|
||||||
|
|
||||||
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
|
||||||
|
|
||||||
@@ -570,10 +533,6 @@ def main():
|
|||||||
prop_val = source_props.get(prop_name, "false")
|
prop_val = source_props.get(prop_name, "false")
|
||||||
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
|
||||||
|
|
||||||
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
|
|
||||||
if type_name == "DefinedType" and "__TypeXml" in source_props:
|
|
||||||
lines.append(f"\t\t\t{source_props['__TypeXml']}")
|
|
||||||
|
|
||||||
lines.append("\t\t</Properties>")
|
lines.append("\t\t</Properties>")
|
||||||
|
|
||||||
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
if type_name in TYPES_WITH_CHILD_OBJECTS:
|
||||||
@@ -685,26 +644,7 @@ def main():
|
|||||||
first_level = {}
|
first_level = {}
|
||||||
deep_paths = []
|
deep_paths = []
|
||||||
|
|
||||||
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
|
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
|
||||||
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
|
|
||||||
for tag in FORM_BINDING_DATA_TAGS:
|
|
||||||
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
|
|
||||||
path = m.group(1)
|
|
||||||
segments = path.split(".")
|
|
||||||
seg0 = segments[0]
|
|
||||||
if seg0 in STANDARD_FIELDS:
|
|
||||||
continue
|
|
||||||
first_level[seg0] = True
|
|
||||||
if len(segments) >= 2:
|
|
||||||
seg1 = segments[1]
|
|
||||||
if seg1 in STANDARD_FIELDS:
|
|
||||||
continue
|
|
||||||
seg2 = segments[2] if len(segments) >= 3 else None
|
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
|
||||||
|
|
||||||
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
|
|
||||||
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
|
|
||||||
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
|
|
||||||
path = m.group(1)
|
path = m.group(1)
|
||||||
segments = path.split(".")
|
segments = path.split(".")
|
||||||
seg0 = segments[0]
|
seg0 = segments[0]
|
||||||
@@ -715,14 +655,22 @@ def main():
|
|||||||
seg1 = segments[1]
|
seg1 = segments[1]
|
||||||
if seg1 in STANDARD_FIELDS:
|
if seg1 in STANDARD_FIELDS:
|
||||||
continue
|
continue
|
||||||
seg2 = segments[2] if len(segments) >= 3 else None
|
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
|
||||||
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
|
|
||||||
|
# Also collect from TitleDataPath
|
||||||
|
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
|
||||||
|
path = m.group(1)
|
||||||
|
segments = path.split(".")
|
||||||
|
seg0 = segments[0]
|
||||||
|
if seg0 in STANDARD_FIELDS:
|
||||||
|
continue
|
||||||
|
first_level[seg0] = True
|
||||||
|
|
||||||
# Deduplicate deep paths
|
# Deduplicate deep paths
|
||||||
seen = set()
|
seen = set()
|
||||||
unique_deep = []
|
unique_deep = []
|
||||||
for dp in deep_paths:
|
for dp in deep_paths:
|
||||||
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
|
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
|
||||||
if key not in seen:
|
if key not in seen:
|
||||||
seen.add(key)
|
seen.add(key)
|
||||||
unique_deep.append(dp)
|
unique_deep.append(dp)
|
||||||
@@ -993,40 +941,26 @@ def main():
|
|||||||
# Step 3: Build the adopted content and insert into main object XML
|
# Step 3: Build the adopted content and insert into main object XML
|
||||||
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
|
||||||
|
|
||||||
# Read existing object XML (needed for dedup + enrichment)
|
# Generate full object XML with attributes and TS
|
||||||
|
content_parts = []
|
||||||
|
for attr in src_attrs:
|
||||||
|
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
|
||||||
|
content_parts.append(attr_xml)
|
||||||
|
for ts in src_ts:
|
||||||
|
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
|
||||||
|
content_parts.append(ts_xml)
|
||||||
|
adopted_content = "\n".join(content_parts).rstrip()
|
||||||
|
|
||||||
|
# Read existing object XML and inject
|
||||||
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
with open(obj_file, "r", encoding="utf-8-sig") as fh:
|
||||||
obj_content = fh.read()
|
obj_content = fh.read()
|
||||||
|
|
||||||
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
|
# Inject extra properties after ExtendedConfigurationObject
|
||||||
existing_child_names = set()
|
|
||||||
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
|
|
||||||
if m_co:
|
|
||||||
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
|
|
||||||
existing_child_names.add(nm)
|
|
||||||
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
|
|
||||||
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
|
|
||||||
|
|
||||||
# Generate full object XML with attributes and TS
|
|
||||||
content_parts = []
|
|
||||||
for attr in insert_attrs:
|
|
||||||
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
|
|
||||||
for ts in insert_ts:
|
|
||||||
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
|
|
||||||
adopted_content = "\n".join(content_parts).rstrip()
|
|
||||||
|
|
||||||
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
|
|
||||||
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
|
|
||||||
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
|
|
||||||
if extra_props:
|
if extra_props:
|
||||||
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
|
|
||||||
obj_props_block = m_props.group(1) if m_props else ""
|
|
||||||
props_xml = ""
|
props_xml = ""
|
||||||
for p_name, p_val in extra_props.items():
|
for p_name, p_val in extra_props.items():
|
||||||
if f"<{p_name}>" in obj_props_block:
|
|
||||||
continue
|
|
||||||
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
|
||||||
if props_xml:
|
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
|
||||||
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
|
|
||||||
|
|
||||||
# Replace empty ChildObjects with adopted content
|
# Replace empty ChildObjects with adopted content
|
||||||
if adopted_content:
|
if adopted_content:
|
||||||
@@ -1078,93 +1012,79 @@ def main():
|
|||||||
|
|
||||||
# Step 5: Handle deep paths (Form mode only)
|
# Step 5: Handle deep paths (Form mode only)
|
||||||
if mode == "Form" and deep_paths:
|
if mode == "Form" and deep_paths:
|
||||||
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
|
# Filter out deep paths where ObjectAttr is a TabularSection
|
||||||
deep_by_attr = {}
|
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
|
||||||
for dp in deep_paths:
|
|
||||||
if dp["ObjectAttr"] in ts_names:
|
if real_deep:
|
||||||
continue
|
info(f" Processing {len(real_deep)} deep path(s)...")
|
||||||
deep_by_attr.setdefault(dp["ObjectAttr"], [])
|
|
||||||
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
|
# Group by ObjectAttr -> target catalog
|
||||||
|
deep_by_attr = {}
|
||||||
|
for dp in real_deep:
|
||||||
|
if dp["ObjectAttr"] not in deep_by_attr:
|
||||||
|
deep_by_attr[dp["ObjectAttr"]] = []
|
||||||
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
|
||||||
if deep_by_attr:
|
|
||||||
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
|
|
||||||
for attr_name, sub_attr_names in deep_by_attr.items():
|
for attr_name, sub_attr_names in deep_by_attr.items():
|
||||||
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
|
# Find the attribute's type to determine target catalog
|
||||||
|
attr_info = None
|
||||||
|
for a in src_attrs:
|
||||||
|
if a["Name"] == attr_name:
|
||||||
|
attr_info = a
|
||||||
|
break
|
||||||
if not attr_info:
|
if not attr_info:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Extract catalog name from type: cfg:CatalogRef.XXX
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
|
||||||
if not cat_match:
|
if not cat_match:
|
||||||
continue
|
continue
|
||||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
|
||||||
|
|
||||||
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
|
target_type_name = cat_match.group(1)
|
||||||
ts_deep_by_col = {}
|
target_obj_name = cat_match.group(2)
|
||||||
for dp in deep_paths:
|
|
||||||
if dp["ObjectAttr"] not in ts_names:
|
# Ensure target is borrowed
|
||||||
continue
|
if not test_object_borrowed(target_type_name, target_obj_name):
|
||||||
if not dp.get("SubSubAttr"):
|
t_src = read_source_object(target_type_name, target_obj_name)
|
||||||
continue
|
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
||||||
if dp["SubSubAttr"] in STANDARD_FIELDS:
|
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
||||||
continue
|
os.makedirs(t_target_dir, exist_ok=True)
|
||||||
k = (dp["ObjectAttr"], dp["SubAttr"])
|
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
||||||
ts_deep_by_col.setdefault(k, [])
|
save_text_bom(t_target_file, t_borrowed_xml)
|
||||||
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
|
add_to_child_objects(target_type_name, target_obj_name)
|
||||||
ts_deep_by_col[k].append(dp["SubSubAttr"])
|
borrowed_files.append(t_target_file)
|
||||||
if ts_deep_by_col:
|
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
||||||
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
|
|
||||||
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
|
# Resolve sub-attributes in target catalog
|
||||||
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
|
sub_names = {sn: True for sn in sub_attr_names}
|
||||||
if not ts_info:
|
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
||||||
continue
|
|
||||||
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
|
if sub_resolved["Attributes"]:
|
||||||
if not col_info:
|
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
||||||
continue
|
|
||||||
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
|
# Collect and borrow ref types from deep attributes
|
||||||
if not cat_match:
|
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
||||||
continue
|
sub_ref_types = collect_reference_types(sub_type_xmls)
|
||||||
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
|
for srt in sub_ref_types:
|
||||||
|
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
||||||
|
continue
|
||||||
|
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
||||||
|
continue
|
||||||
|
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
||||||
|
if not os.path.isfile(s_src_file):
|
||||||
|
continue
|
||||||
|
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
||||||
|
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
||||||
|
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
||||||
|
os.makedirs(s_target_dir, exist_ok=True)
|
||||||
|
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
||||||
|
save_text_bom(s_target_file, s_borrowed_xml)
|
||||||
|
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
||||||
|
borrowed_files.append(s_target_file)
|
||||||
|
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
||||||
|
|
||||||
info(" Main attribute borrowing complete")
|
info(" Main attribute borrowing complete")
|
||||||
|
|
||||||
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
|
|
||||||
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
|
|
||||||
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
|
|
||||||
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
|
|
||||||
if not test_object_borrowed(target_type_name, target_obj_name):
|
|
||||||
t_src = read_source_object(target_type_name, target_obj_name)
|
|
||||||
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
|
|
||||||
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
|
|
||||||
os.makedirs(t_target_dir, exist_ok=True)
|
|
||||||
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
|
|
||||||
save_text_bom(t_target_file, t_borrowed_xml)
|
|
||||||
add_to_child_objects(target_type_name, target_obj_name)
|
|
||||||
borrowed_files.append(t_target_file)
|
|
||||||
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
|
|
||||||
|
|
||||||
sub_names = {sn: True for sn in sub_attr_names}
|
|
||||||
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
|
|
||||||
if sub_resolved["Attributes"]:
|
|
||||||
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
|
|
||||||
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
|
|
||||||
sub_ref_types = collect_reference_types(sub_type_xmls)
|
|
||||||
for srt in sub_ref_types:
|
|
||||||
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
|
|
||||||
continue
|
|
||||||
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
|
|
||||||
continue
|
|
||||||
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
|
|
||||||
if not os.path.isfile(s_src_file):
|
|
||||||
continue
|
|
||||||
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
|
|
||||||
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
|
|
||||||
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
|
|
||||||
os.makedirs(s_target_dir, exist_ok=True)
|
|
||||||
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
|
|
||||||
save_text_bom(s_target_file, s_borrowed_xml)
|
|
||||||
add_to_child_objects(srt["TypeName"], srt["ObjName"])
|
|
||||||
borrowed_files.append(s_target_file)
|
|
||||||
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
|
|
||||||
|
|
||||||
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
|
||||||
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
dir_name = CHILD_TYPE_DIR_MAP[type_name]
|
||||||
|
|
||||||
@@ -1180,22 +1100,8 @@ def main():
|
|||||||
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
|
||||||
src_form_content = fh.read()
|
src_form_content = fh.read()
|
||||||
|
|
||||||
# 3. Generate form metadata XML.
|
# 3. Generate form metadata XML
|
||||||
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
|
new_form_uuid = new_guid()
|
||||||
# (regenerating it would churn the form's identity on every rerun).
|
|
||||||
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
|
|
||||||
new_form_uuid = ""
|
|
||||||
if os.path.isfile(existing_wrapper):
|
|
||||||
try:
|
|
||||||
existing_root = etree.parse(existing_wrapper).getroot()
|
|
||||||
for c in existing_root:
|
|
||||||
if isinstance(c.tag, str) and localname(c) == "Form":
|
|
||||||
new_form_uuid = c.get("uuid", "") or ""
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
new_form_uuid = ""
|
|
||||||
if not new_form_uuid:
|
|
||||||
new_form_uuid = new_guid()
|
|
||||||
form_meta_lines = [
|
form_meta_lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
|
||||||
@@ -1225,10 +1131,7 @@ def main():
|
|||||||
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
|
||||||
src_form_el = src_form_tree.getroot()
|
src_form_el = src_form_tree.getroot()
|
||||||
|
|
||||||
# Borrowed form uses the extension's format version (not the source form's) — keeps the
|
form_version = src_form_el.get("version", format_version)
|
||||||
# extension uniform; otherwise the platform rejects the import on a version mismatch
|
|
||||||
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
|
|
||||||
form_version = format_version
|
|
||||||
|
|
||||||
src_auto_cmd = None
|
src_auto_cmd = None
|
||||||
form_props = []
|
form_props = []
|
||||||
@@ -1246,21 +1149,25 @@ def main():
|
|||||||
continue
|
continue
|
||||||
if not reached_visual:
|
if not reached_visual:
|
||||||
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
|
||||||
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
|
form_props.append(etree.tostring(fc, encoding="unicode"))
|
||||||
|
|
||||||
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
|
||||||
|
|
||||||
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
|
||||||
auto_cmd_xml = ""
|
auto_cmd_xml = ""
|
||||||
if src_auto_cmd is not None:
|
if src_auto_cmd is not None:
|
||||||
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
|
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
|
||||||
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
|
||||||
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
|
||||||
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
|
||||||
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
# Strip ExcludedCommand (references to standard commands invalid in extension)
|
||||||
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip DataPath in AutoCommandBar buttons
|
||||||
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
|
if borrow_main_attr:
|
||||||
|
# Keep only Объект.* DataPaths
|
||||||
|
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
|
||||||
|
else:
|
||||||
|
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
|
||||||
|
|
||||||
# ChildItems: copy full tree, clean up base-config references
|
# ChildItems: copy full tree, clean up base-config references
|
||||||
child_items_xml = ""
|
child_items_xml = ""
|
||||||
@@ -1271,12 +1178,20 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if src_child_items is not None:
|
if src_child_items is not None:
|
||||||
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
|
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
|
||||||
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
|
||||||
# Replace all CommandName values with 0
|
# Replace all CommandName values with 0
|
||||||
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
|
||||||
# Strip data-binding tags whose root attribute isn't borrowed
|
# Strip DataPath / TitleDataPath / RowPictureDataPath
|
||||||
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
|
if borrow_main_attr:
|
||||||
|
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
|
||||||
|
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
|
||||||
|
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
|
||||||
|
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
||||||
|
else:
|
||||||
|
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
|
||||||
|
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
|
||||||
|
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
|
||||||
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
|
||||||
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
|
||||||
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
|
||||||
@@ -1513,16 +1428,12 @@ def main():
|
|||||||
save_text_bom(form_xml_file, "".join(parts))
|
save_text_bom(form_xml_file, "".join(parts))
|
||||||
info(f" Created: {form_xml_file}")
|
info(f" Created: {form_xml_file}")
|
||||||
|
|
||||||
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
|
# 6. Create empty Module.bsl
|
||||||
# not clobber user code added to the form module).
|
|
||||||
module_dir = os.path.join(form_xml_dir, "Form")
|
module_dir = os.path.join(form_xml_dir, "Form")
|
||||||
os.makedirs(module_dir, exist_ok=True)
|
os.makedirs(module_dir, exist_ok=True)
|
||||||
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
module_bsl_file = os.path.join(module_dir, "Module.bsl")
|
||||||
if os.path.isfile(module_bsl_file):
|
save_text_bom(module_bsl_file, "")
|
||||||
info(" Preserved existing Module.bsl")
|
info(f" Created: {module_bsl_file}")
|
||||||
else:
|
|
||||||
save_text_bom(module_bsl_file, "")
|
|
||||||
info(f" Created: {module_bsl_file}")
|
|
||||||
|
|
||||||
# 7. Register form in parent object ChildObjects
|
# 7. Register form in parent object ChildObjects
|
||||||
register_form_in_object(type_name, obj_name, form_name)
|
register_form_in_object(type_name, obj_name, form_name)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.py" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
powershell.exe -NoProfile -File .claude/skills/cfe-diff/scripts/cfe-diff.ps1 -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mode A — обзор расширения
|
## Mode A — обзор расширения
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-init.py" -Name "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/cfe-init/scripts/cfe-init.ps1 -Name "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -35,10 +35,6 @@ if (Test-Path $cfgFile) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# MDClasses format version — inherited from the base config so the extension stays uniform
|
|
||||||
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
|
||||||
$formatVersion = "2.17"
|
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if ($ConfigPath) {
|
if ($ConfigPath) {
|
||||||
@@ -79,11 +75,6 @@ if ($ConfigPath) {
|
|||||||
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
|
||||||
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
|
||||||
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
|
|
||||||
if ($fmtVer) {
|
|
||||||
$formatVersion = $fmtVer
|
|
||||||
Write-Host "[INFO] Base config format version: $formatVersion"
|
|
||||||
}
|
|
||||||
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
|
||||||
if ($compatNode -and $compatNode.InnerText) {
|
if ($compatNode -and $compatNode.InnerText) {
|
||||||
$CompatibilityMode = $compatNode.InnerText.Trim()
|
$CompatibilityMode = $compatNode.InnerText.Trim()
|
||||||
@@ -147,7 +138,7 @@ $childObjectsXml += "`r`n`t`t"
|
|||||||
# --- Configuration.xml ---
|
# --- Configuration.xml ---
|
||||||
$cfgXml = @"
|
$cfgXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
<Configuration uuid="$uuidCfg">
|
<Configuration uuid="$uuidCfg">
|
||||||
<InternalInfo>
|
<InternalInfo>
|
||||||
<xr:ContainedObject>
|
<xr:ContainedObject>
|
||||||
@@ -212,7 +203,7 @@ $cfgXml = @"
|
|||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
$langXml = @"
|
$langXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
<Language uuid="$uuidLang">
|
<Language uuid="$uuidLang">
|
||||||
<InternalInfo/>
|
<InternalInfo/>
|
||||||
<Properties>
|
<Properties>
|
||||||
@@ -229,7 +220,7 @@ $langXml = @"
|
|||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
$roleXml = @"
|
$roleXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
<Role uuid="$uuidRole">
|
<Role uuid="$uuidRole">
|
||||||
<Properties>
|
<Properties>
|
||||||
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
|
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C configuration extension."""
|
"""Generates minimal XML source files for a 1C configuration extension."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
@@ -50,10 +50,6 @@ def main():
|
|||||||
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# MDClasses format version — inherited from the base config so the extension stays uniform
|
|
||||||
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
|
|
||||||
format_version = "2.17"
|
|
||||||
|
|
||||||
# --- Resolve ConfigPath ---
|
# --- Resolve ConfigPath ---
|
||||||
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
|
||||||
if args.ConfigPath:
|
if args.ConfigPath:
|
||||||
@@ -92,10 +88,6 @@ def main():
|
|||||||
try:
|
try:
|
||||||
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
base_cfg_tree = ET.parse(os.path.abspath(config_path))
|
||||||
base_cfg_root = base_cfg_tree.getroot()
|
base_cfg_root = base_cfg_tree.getroot()
|
||||||
fmt_ver = base_cfg_root.get("version")
|
|
||||||
if fmt_ver:
|
|
||||||
format_version = fmt_ver
|
|
||||||
print(f"[INFO] Base config format version: {format_version}")
|
|
||||||
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
|
||||||
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
|
||||||
if compat_node is not None and compat_node.text:
|
if compat_node is not None and compat_node.text:
|
||||||
@@ -163,7 +155,7 @@ def main():
|
|||||||
\t\t\t</xr:ContainedObject>\n"""
|
\t\t\t</xr:ContainedObject>\n"""
|
||||||
|
|
||||||
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
\t<Configuration uuid="{uuid_cfg}">
|
\t<Configuration uuid="{uuid_cfg}">
|
||||||
\t\t<InternalInfo>
|
\t\t<InternalInfo>
|
||||||
{contained_objects}\t\t</InternalInfo>
|
{contained_objects}\t\t</InternalInfo>
|
||||||
@@ -198,7 +190,7 @@ def main():
|
|||||||
|
|
||||||
# --- Languages/Русский.xml (adopted format) ---
|
# --- Languages/Русский.xml (adopted format) ---
|
||||||
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
\t<Language uuid="{uuid_lang}">
|
\t<Language uuid="{uuid_lang}">
|
||||||
\t\t<InternalInfo/>
|
\t\t<InternalInfo/>
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
@@ -213,7 +205,7 @@ def main():
|
|||||||
|
|
||||||
# --- Role XML ---
|
# --- Role XML ---
|
||||||
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
|
||||||
\t<Role uuid="{uuid_role}">
|
\t<Role uuid="{uuid_role}">
|
||||||
\t\t<Properties>
|
\t\t<Properties>
|
||||||
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
\t\t\t<Name>{esc_xml(role_name)}</Name>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.py" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
powershell.exe -NoProfile -File .claude/skills/cfe-patch-method/scripts/cfe-patch-method.ps1 -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
|
||||||
```
|
```
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|||||||
@@ -24,6 +24,6 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src"
|
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src"
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.py" -ExtensionPath "src/Configuration.xml"
|
powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
|
# cfe-validate v1.3 — Validate 1C configuration extension structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[Alias('Path')]
|
|
||||||
[string]$ExtensionPath,
|
[string]$ExtensionPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -146,10 +145,10 @@ $childTypeDirMap = @{
|
|||||||
|
|
||||||
# Valid enum values for extension properties
|
# Valid enum values for extension properties
|
||||||
$validEnumValues = @{
|
$validEnumValues = @{
|
||||||
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28","Version8_5_1")
|
"ConfigurationExtensionCompatibilityMode" = @("DontUse","Version8_1","Version8_2_13","Version8_2_16","Version8_3_1","Version8_3_2","Version8_3_3","Version8_3_4","Version8_3_5","Version8_3_6","Version8_3_7","Version8_3_8","Version8_3_9","Version8_3_10","Version8_3_11","Version8_3_12","Version8_3_13","Version8_3_14","Version8_3_15","Version8_3_16","Version8_3_17","Version8_3_18","Version8_3_19","Version8_3_20","Version8_3_21","Version8_3_22","Version8_3_23","Version8_3_24","Version8_3_25","Version8_3_26","Version8_3_27","Version8_3_28")
|
||||||
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
"DefaultRunMode" = @("ManagedApplication","OrdinaryApplication","Auto")
|
||||||
"ScriptVariant" = @("Russian","English")
|
"ScriptVariant" = @("Russian","English")
|
||||||
"InterfaceCompatibilityMode" = @("Version8_2","Version8_2EnableTaxi","Taxi","TaxiEnableVersion8_2","TaxiEnableVersion8_5","Version8_5EnableTaxi","Version8_5")
|
"InterfaceCompatibilityMode" = @("Taxi","TaxiEnableVersion8_2","Version8_2")
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 1. Parse XML ---
|
# --- 1. Parse XML ---
|
||||||
@@ -197,8 +196,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
|
# cfe-validate v1.3 — Validate 1C configuration extension XML structure (CFE)
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
|
||||||
import sys, os, argparse, re
|
import sys, os, argparse, re
|
||||||
@@ -82,14 +82,11 @@ VALID_ENUM_VALUES = {
|
|||||||
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
'Version8_3_11', 'Version8_3_12', 'Version8_3_13', 'Version8_3_14', 'Version8_3_15',
|
||||||
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
'Version8_3_16', 'Version8_3_17', 'Version8_3_18', 'Version8_3_19', 'Version8_3_20',
|
||||||
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
'Version8_3_21', 'Version8_3_22', 'Version8_3_23', 'Version8_3_24', 'Version8_3_25',
|
||||||
'Version8_3_26', 'Version8_3_27', 'Version8_3_28', 'Version8_5_1',
|
'Version8_3_26', 'Version8_3_27', 'Version8_3_28',
|
||||||
],
|
],
|
||||||
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
'DefaultRunMode': ['ManagedApplication', 'OrdinaryApplication', 'Auto'],
|
||||||
'ScriptVariant': ['Russian', 'English'],
|
'ScriptVariant': ['Russian', 'English'],
|
||||||
'InterfaceCompatibilityMode': [
|
'InterfaceCompatibilityMode': ['Taxi', 'TaxiEnableVersion8_2', 'Version8_2'],
|
||||||
'Version8_2', 'Version8_2EnableTaxi', 'Taxi', 'TaxiEnableVersion8_2',
|
|
||||||
'TaxiEnableVersion8_5', 'Version8_5EnableTaxi', 'Version8_5',
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
EXPECTED_NS = 'http://v8.1c.ru/8.3/MDClasses'
|
||||||
@@ -150,7 +147,7 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
description='Validate 1C configuration extension XML structure (CFE)', allow_abbrev=False
|
||||||
)
|
)
|
||||||
parser.add_argument('-ExtensionPath', '-Path', dest='ExtensionPath', required=True)
|
parser.add_argument('-ExtensionPath', dest='ExtensionPath', required=True)
|
||||||
parser.add_argument('-Detailed', action='store_true')
|
parser.add_argument('-Detailed', action='store_true')
|
||||||
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
parser.add_argument('-MaxErrors', dest='MaxErrors', type=int, default=30)
|
||||||
parser.add_argument('-OutFile', dest='OutFile', default='')
|
parser.add_argument('-OutFile', dest='OutFile', default='')
|
||||||
@@ -216,8 +213,8 @@ def main():
|
|||||||
version = root.get('version', '')
|
version = root.get('version', '')
|
||||||
if not version:
|
if not version:
|
||||||
r.warn('1. Missing version attribute on MetaDataObject')
|
r.warn('1. Missing version attribute on MetaDataObject')
|
||||||
elif version not in ('2.17', '2.20', '2.21'):
|
elif version not in ('2.17', '2.20'):
|
||||||
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
r.warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||||
|
|
||||||
# Must have Configuration child
|
# Must have Configuration child
|
||||||
cfg_node = None
|
cfg_node = None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-create
|
name: db-create
|
||||||
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
|
description: Создание информационной базы 1С. Используй когда пользователь просит создать базу, новую ИБ, пустую базу
|
||||||
argument-hint: <path|name>
|
argument-hint: <path|name>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -25,20 +25,20 @@ allowed-tools:
|
|||||||
## Параметры подключения
|
## Параметры подключения
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
После создания базы предложи зарегистрировать через `/db-list add`.
|
После создания базы предложи зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -48,23 +48,31 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" <параметры>
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
## После создания
|
## После создания
|
||||||
|
|
||||||
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
1. Прочитай лог-файл и покажи результат
|
||||||
|
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
|
||||||
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Создать файловую базу
|
# Создать файловую базу
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB"
|
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
|
||||||
|
|
||||||
# Создать серверную базу
|
# Создать серверную базу
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
|
||||||
|
|
||||||
# Создать из шаблона CF
|
# Создать из шаблона CF
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
|
||||||
|
|
||||||
# Создать и добавить в список баз
|
# Создать и добавить в список баз
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-create.py" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
powershell.exe -NoProfile -File .claude/skills/db-create/scripts/db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-create v1.6 — Create 1C information base
|
# db-create v1.0 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Создание информационной базы 1С
|
Создание информационной базы 1С
|
||||||
@@ -68,85 +67,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -162,31 +101,6 @@ $tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
|
|
||||||
if ($UseTemplate) {
|
|
||||||
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
|
|
||||||
$arguments += "--restore=$UseTemplate"
|
|
||||||
} else {
|
|
||||||
$arguments += "--load=$UseTemplate", "--apply"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("CREATEINFOBASE")
|
$arguments = @("CREATEINFOBASE")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-create v1.6 — Create 1C information base
|
# db-create v1.0 — Create 1C information base
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||||
if not v8path:
|
if found:
|
||||||
if os.name == "nt":
|
return found[-1]
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -116,14 +47,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -132,29 +58,6 @@ def main():
|
|||||||
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
|
|
||||||
if args.UseTemplate:
|
|
||||||
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
|
|
||||||
arguments.append(f"--restore={args.UseTemplate}")
|
|
||||||
else:
|
|
||||||
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Information base created successfully: {args.InfoBasePath}")
|
|
||||||
else:
|
|
||||||
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -164,11 +67,9 @@ def main():
|
|||||||
arguments = ["CREATEINFOBASE"]
|
arguments = ["CREATEINFOBASE"]
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
if args.InfoBaseServer and args.InfoBaseRef:
|
||||||
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
|
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
|
||||||
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
|
|
||||||
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
|
|
||||||
else:
|
else:
|
||||||
arguments.append(f'File={args.InfoBasePath}')
|
arguments.append(f'File="{args.InfoBasePath}"')
|
||||||
|
|
||||||
# --- Template ---
|
# --- Template ---
|
||||||
if args.UseTemplate:
|
if args.UseTemplate:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-cf
|
name: db-dump-cf
|
||||||
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
description: Выгрузка конфигурации 1С в CF-файл. Используй когда пользователь просит выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
|
||||||
argument-hint: "[database] [output.cf]"
|
argument-hint: "[database] [output.cf]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -54,15 +54,26 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" <параметры>
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
|
## После выполнения
|
||||||
|
|
||||||
|
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Выгрузка конфигурации (файловая база)
|
# Выгрузка конфигурации (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/db-dump-cf/scripts/db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в CF-файл
|
Выгрузка конфигурации 1С в CF-файл
|
||||||
@@ -77,85 +76,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -171,32 +110,6 @@ $tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if ($AllExtensions) {
|
|
||||||
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
$arguments += "$OutputFile"
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-cf v1.6 — Dump 1C configuration to CF file
|
# db-dump-cf v1.0 — Dump 1C configuration to CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||||
if not v8path:
|
if found:
|
||||||
if os.name == "nt":
|
return found[-1]
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -118,14 +49,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -134,34 +60,6 @@ def main():
|
|||||||
if out_dir and not os.path.isdir(out_dir):
|
if out_dir and not os.path.isdir(out_dir):
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if args.AllExtensions:
|
|
||||||
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
arguments.append(args.OutputFile)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Configuration dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
---
|
|
||||||
name: db-dump-dt
|
|
||||||
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
|
|
||||||
argument-hint: "[database] [output.dt]"
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- AskUserQuestion
|
|
||||||
---
|
|
||||||
|
|
||||||
# /db-dump-dt — Выгрузка информационной базы в DT-файл
|
|
||||||
|
|
||||||
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
|
|
||||||
|
|
||||||
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
|
|
||||||
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/db-dump-dt [database] [output.dt]
|
|
||||||
/db-dump-dt dev backup.dt
|
|
||||||
/db-dump-dt — база по умолчанию, имя файла по базе и дате
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры подключения
|
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
|
||||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
|
||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
|
||||||
4. Если ветка не совпала — используй `default`
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
|
||||||
Если файла нет — предложи `/db-list add`.
|
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" <параметры>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры скрипта
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|----------|:------------:|----------|
|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
|
||||||
| `-UserName <имя>` | нет | Имя пользователя |
|
|
||||||
| `-Password <пароль>` | нет | Пароль |
|
|
||||||
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
|
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Выгрузка ИБ (файловая база)
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
|
|
||||||
|
|
||||||
# Серверная база
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Связанные навыки
|
|
||||||
|
|
||||||
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
|
|
||||||
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
|
|
||||||
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Выгрузка информационной базы 1С в DT-файл
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER OutputFile
|
|
||||||
Путь к выходному DT-файлу
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$OutputFile
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
$outDir = Split-Path $OutputFile -Parent
|
|
||||||
if ($outDir -and -not (Test-Path $outDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "$OutputFile"
|
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
|
|
||||||
$arguments += "/DumpIB", "`"$OutputFile`""
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "dump_dt_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-dump-dt v1.5 — Dump 1C information base to DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import atexit
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Dump 1C information base to DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-OutputFile", required=True)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Ensure output directory exists ---
|
|
||||||
out_dir = os.path.dirname(args.OutputFile)
|
|
||||||
if out_dir and not os.path.isdir(out_dir):
|
|
||||||
os.makedirs(out_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(args.OutputFile)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
|
|
||||||
arguments.extend(["/DumpIB", args.OutputFile])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base dumped successfully to: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-dump-xml
|
name: db-dump-xml
|
||||||
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда пользователь просит выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
|
||||||
argument-hint: "[database] [outputDir]"
|
argument-hint: "[database] [outputDir]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,7 +29,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
|
||||||
@@ -37,14 +37,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -68,23 +68,30 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" <параметры>
|
|||||||
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
|
||||||
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная выгрузка (файловая база)
|
# Полная выгрузка (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Инкрементальная выгрузка
|
# Инкрементальная выгрузка
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
|
||||||
|
|
||||||
# Частичная выгрузка
|
# Частичная выгрузка
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Выгрузка расширения
|
# Выгрузка расширения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/db-dump-xml/scripts/db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-dump-xml v1.8 — Dump 1C configuration to XML files
|
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Выгрузка конфигурации 1С в XML-файлы
|
Выгрузка конфигурации 1С в XML-файлы
|
||||||
@@ -100,85 +99,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -200,44 +139,6 @@ $tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
|
||||||
if ($Format -eq "Plain") {
|
|
||||||
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($AllExtensions) {
|
|
||||||
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
|
||||||
} elseif ($Mode -eq "UpdateInfo") {
|
|
||||||
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
} elseif ($Mode -eq "Partial") {
|
|
||||||
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
||||||
$arguments = @("infobase", "config", "export", "objects") + $objList
|
|
||||||
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
} else {
|
|
||||||
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
$arguments += "$ConfigDir"
|
|
||||||
}
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-dump-xml v1.8 — Dump 1C configuration to XML files
|
# db-dump-xml v1.0 — Dump 1C configuration to XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
candidates.sort()
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
return candidates[-1]
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -132,14 +65,9 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -153,46 +81,6 @@ def main():
|
|||||||
os.makedirs(args.ConfigDir, exist_ok=True)
|
os.makedirs(args.ConfigDir, exist_ok=True)
|
||||||
print(f"Created output directory: {args.ConfigDir}")
|
print(f"Created output directory: {args.ConfigDir}")
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if args.Format == "Plain":
|
|
||||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if args.AllExtensions:
|
|
||||||
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
|
||||||
elif args.Mode == "UpdateInfo":
|
|
||||||
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif args.Mode == "Partial":
|
|
||||||
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
|
|
||||||
arguments = ["infobase", "config", "export", "objects"] + obj_list
|
|
||||||
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
else:
|
|
||||||
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
arguments.append(args.ConfigDir)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Configuration exported successfully to: {args.ConfigDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-list
|
name: db-list
|
||||||
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
|
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда пользователь говорит про базы данных, список баз, "добавь базу", "какие базы есть"
|
||||||
argument-hint: "[add|remove|show]"
|
argument-hint: "[add|remove|show]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-cf
|
name: db-load-cf
|
||||||
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
|
description: Загрузка конфигурации 1С из CF-файла. Используй когда пользователь просит загрузить конфигурацию из CF, восстановить из бэкапа CF
|
||||||
argument-hint: <input.cf> [database]
|
argument-hint: <input.cf> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,21 +29,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -55,19 +55,27 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" <параметры>
|
|||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
1. Прочитай лог-файл и покажи результат
|
||||||
|
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Файловая база
|
# Файловая база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/db-load-cf/scripts/db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-load-cf v1.6 — Load 1C configuration from CF file
|
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из CF-файла
|
Загрузка конфигурации 1С из CF-файла
|
||||||
@@ -77,85 +76,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -171,32 +110,6 @@ $tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if ($AllExtensions) {
|
|
||||||
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
$arguments += "$InputFile"
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-cf v1.6 — Load 1C configuration from CF file
|
# db-load-cf v1.0 — Load 1C configuration from CF file
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||||
if not v8path:
|
if found:
|
||||||
if os.name == "nt":
|
return found[-1]
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -118,14 +49,9 @@ def main():
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -134,34 +60,6 @@ def main():
|
|||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if args.AllExtensions:
|
|
||||||
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
arguments.append(args.InputFile)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Configuration loaded successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
---
|
|
||||||
name: db-load-dt
|
|
||||||
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
|
|
||||||
disable-model-invocation: true
|
|
||||||
argument-hint: <input.dt> [database]
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Glob
|
|
||||||
- AskUserQuestion
|
|
||||||
---
|
|
||||||
|
|
||||||
# /db-load-dt — Загрузка информационной базы из DT-файла
|
|
||||||
|
|
||||||
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
|
|
||||||
|
|
||||||
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
|
|
||||||
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
|
|
||||||
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
|
|
||||||
|
|
||||||
## Когда НЕ использовать
|
|
||||||
|
|
||||||
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
|
|
||||||
в существующую.
|
|
||||||
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```
|
|
||||||
/db-load-dt <input.dt> [database]
|
|
||||||
/db-load-dt backup.dt dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Порядок действий перед загрузкой
|
|
||||||
|
|
||||||
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
|
|
||||||
отката (восстановиться будет нечем, если не сохранить).
|
|
||||||
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
|
|
||||||
3. Только после подтверждения выполняй загрузку.
|
|
||||||
|
|
||||||
## Параметры подключения
|
|
||||||
|
|
||||||
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
|
|
||||||
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
|
|
||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
|
||||||
4. Если ветка не совпала — используй `default`
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
|
||||||
Если файла нет — предложи `/db-list add`.
|
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
|
||||||
|
|
||||||
## Команда
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" <параметры>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Параметры скрипта
|
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
|
||||||
|----------|:------------:|----------|
|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
|
||||||
| `-UserName <имя>` | нет | Имя пользователя |
|
|
||||||
| `-Password <пароль>` | нет | Пароль |
|
|
||||||
| `-InputFile <путь>` | да | Путь к DT-файлу |
|
|
||||||
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
|
|
||||||
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
|
|
||||||
|
|
||||||
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
|
|
||||||
|
|
||||||
## После выполнения
|
|
||||||
|
|
||||||
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
|
|
||||||
передать `-UnlockCode`; иначе освободи базу и повтори.
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# Файловая база
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
|
|
||||||
|
|
||||||
# Серверная база с ускорением загрузки
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.py" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Связанные навыки
|
|
||||||
|
|
||||||
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
|
|
||||||
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
# db-load-dt v1.5 — Load 1C information base from DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Загрузка информационной базы 1С из DT-файла
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
|
|
||||||
ВНИМАНИЕ: операция полностью перезаписывает базу.
|
|
||||||
|
|
||||||
.PARAMETER V8Path
|
|
||||||
Путь к каталогу bin платформы или к 1cv8.exe
|
|
||||||
|
|
||||||
.PARAMETER InfoBasePath
|
|
||||||
Путь к файловой информационной базе
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseServer
|
|
||||||
Сервер 1С (для серверной базы)
|
|
||||||
|
|
||||||
.PARAMETER InfoBaseRef
|
|
||||||
Имя базы на сервере
|
|
||||||
|
|
||||||
.PARAMETER UserName
|
|
||||||
Имя пользователя 1С
|
|
||||||
|
|
||||||
.PARAMETER Password
|
|
||||||
Пароль пользователя
|
|
||||||
|
|
||||||
.PARAMETER InputFile
|
|
||||||
Путь к DT-файлу для загрузки
|
|
||||||
|
|
||||||
.PARAMETER JobsCount
|
|
||||||
Количество фоновых заданий для загрузки (0 = по числу процессоров)
|
|
||||||
|
|
||||||
.PARAMETER UnlockCode
|
|
||||||
Код разблокировки базы (/UC) — если заблокировано начало сеансов
|
|
||||||
|
|
||||||
.EXAMPLE
|
|
||||||
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
|
|
||||||
#>
|
|
||||||
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$V8Path,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBasePath,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseServer,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$InfoBaseRef,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UserName,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$Password,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$true)]
|
|
||||||
[string]$InputFile,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[int]$JobsCount = 0,
|
|
||||||
|
|
||||||
[Parameter(Mandatory=$false)]
|
|
||||||
[string]$UnlockCode
|
|
||||||
)
|
|
||||||
|
|
||||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$V8Path = Find-ProjectV8Path
|
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
|
||||||
$V8Path = $found.FullName
|
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if (-not (Test-Path $InputFile)) {
|
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
|
|
||||||
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "$InputFile"
|
|
||||||
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
|
||||||
$arguments = @("DESIGNER")
|
|
||||||
|
|
||||||
if ($InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
|
|
||||||
} else {
|
|
||||||
$arguments += "/F", "`"$InfoBasePath`""
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($UserName) { $arguments += "/N`"$UserName`"" }
|
|
||||||
if ($Password) { $arguments += "/P`"$Password`"" }
|
|
||||||
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
|
|
||||||
|
|
||||||
$arguments += "/RestoreIB", "`"$InputFile`""
|
|
||||||
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
$outFile = Join-Path $tempDir "load_dt_log.txt"
|
|
||||||
$arguments += "/Out", "`"$outFile`""
|
|
||||||
$arguments += "/DisableStartupDialogs"
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
|
|
||||||
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
|
|
||||||
$exitCode = $process.ExitCode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path $outFile) {
|
|
||||||
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
|
|
||||||
if ($logContent) {
|
|
||||||
Write-Host "--- Log ---"
|
|
||||||
Write-Host $logContent
|
|
||||||
Write-Host "--- End ---"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exit $exitCode
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
if (Test-Path $tempDir) {
|
|
||||||
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# db-load-dt v1.5 — Load 1C information base from DT file
|
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import atexit
|
|
||||||
import glob
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
|
||||||
if not v8path:
|
|
||||||
v8path = _find_project_v8path()
|
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if os.path.isdir(v8path):
|
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
return v8path
|
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Load 1C information base from DT file",
|
|
||||||
allow_abbrev=False,
|
|
||||||
)
|
|
||||||
parser.add_argument("-V8Path", default="")
|
|
||||||
parser.add_argument("-InfoBasePath", default="")
|
|
||||||
parser.add_argument("-InfoBaseServer", default="")
|
|
||||||
parser.add_argument("-InfoBaseRef", default="")
|
|
||||||
parser.add_argument("-UserName", default="")
|
|
||||||
parser.add_argument("-Password", default="")
|
|
||||||
parser.add_argument("-InputFile", required=True)
|
|
||||||
parser.add_argument("-JobsCount", type=int, default=0)
|
|
||||||
parser.add_argument("-UnlockCode", default="")
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
|
||||||
if not os.path.isfile(args.InputFile):
|
|
||||||
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
|
|
||||||
arguments.append("--create-database")
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(args.InputFile)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
|
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# --- Build arguments ---
|
|
||||||
arguments = ["DESIGNER"]
|
|
||||||
|
|
||||||
if args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
|
|
||||||
else:
|
|
||||||
arguments.extend(["/F", args.InfoBasePath])
|
|
||||||
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"/N{args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"/P{args.Password}")
|
|
||||||
if args.UnlockCode:
|
|
||||||
arguments.append(f"/UC{args.UnlockCode}")
|
|
||||||
|
|
||||||
arguments.extend(["/RestoreIB", args.InputFile])
|
|
||||||
if args.JobsCount > 0:
|
|
||||||
arguments.extend(["-JobsCount", str(args.JobsCount)])
|
|
||||||
|
|
||||||
# --- Output ---
|
|
||||||
out_file = os.path.join(temp_dir, "load_dt_log.txt")
|
|
||||||
arguments.extend(["/Out", out_file])
|
|
||||||
arguments.append("/DisableStartupDialogs")
|
|
||||||
|
|
||||||
# --- Execute ---
|
|
||||||
print(f"Running: 1cv8.exe {' '.join(arguments)}")
|
|
||||||
result = subprocess.run(
|
|
||||||
[v8path] + arguments,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
exit_code = result.returncode
|
|
||||||
|
|
||||||
# --- Result ---
|
|
||||||
if exit_code == 0:
|
|
||||||
print(f"Information base restored successfully from: {args.InputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
|
|
||||||
|
|
||||||
if os.path.isfile(out_file):
|
|
||||||
try:
|
|
||||||
with open(out_file, "r", encoding="utf-8-sig") as f:
|
|
||||||
log_content = f.read()
|
|
||||||
if log_content:
|
|
||||||
print("--- Log ---")
|
|
||||||
print(log_content)
|
|
||||||
print("--- End ---")
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
finally:
|
|
||||||
if os.path.isdir(temp_dir):
|
|
||||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-git
|
name: db-load-git
|
||||||
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
description: Загрузка изменений из Git в базу 1С. Используй когда пользователь просит загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
|
||||||
argument-hint: "[database] [source]"
|
argument-hint: "[database] [source]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -64,14 +64,15 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" <параметры>
|
|||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
1. Показать список загруженных файлов и результат из лога
|
||||||
|
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Все незафиксированные изменения
|
# Все незафиксированные изменения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
|
||||||
|
|
||||||
# Из диапазона коммитов
|
# Из диапазона коммитов
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-git.py" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
powershell.exe -NoProfile -File .claude/skills/db-load-git/scripts/db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-load-git v1.11 — Load Git changes into 1C database
|
# db-load-git v1.3 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка изменений из Git в базу 1С
|
Загрузка изменений из Git в базу 1С
|
||||||
@@ -121,86 +120,27 @@ function Get-ObjectXmlFromSubFile {
|
|||||||
|
|
||||||
# --- Resolve V8Path (skip if DryRun) ---
|
# --- Resolve V8Path (skip if DryRun) ---
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine + validate connection (skip if DryRun) ---
|
# --- Validate connection (skip if DryRun) ---
|
||||||
$engine = "1cv8"
|
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun) {
|
||||||
function Invoke-IbcmdProcess {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -227,15 +167,6 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# --- Get changed files from Git ---
|
# --- Get changed files from Git ---
|
||||||
# Все git-вызовы для сбора путей идут через один хелпер с -c core.quotePath=false,
|
|
||||||
# иначе кириллические пути возвращаются в octal-виде и не распознаются (зеркало run_git в .py).
|
|
||||||
function Invoke-GitLines {
|
|
||||||
param([string[]]$GitArgs)
|
|
||||||
$out = git -c core.quotePath=false @GitArgs 2>&1
|
|
||||||
if ($LASTEXITCODE -eq 0) { return $out }
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
$changedFiles = @()
|
$changedFiles = @()
|
||||||
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
|
||||||
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
$configDirNormalized = $ConfigDir.Replace('\', '/')
|
||||||
@@ -245,22 +176,29 @@ try {
|
|||||||
switch ($Source) {
|
switch ($Source) {
|
||||||
"Staged" {
|
"Staged" {
|
||||||
Write-Host "Getting staged changes..."
|
Write-Host "Getting staged changes..."
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
$raw = git diff --cached --name-only --relative 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
}
|
}
|
||||||
"Unstaged" {
|
"Unstaged" {
|
||||||
Write-Host "Getting unstaged changes..."
|
Write-Host "Getting unstaged changes..."
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
$raw = git diff --name-only --relative 2>&1
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
|
$raw = git ls-files --others --exclude-standard 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
}
|
}
|
||||||
"Commit" {
|
"Commit" {
|
||||||
Write-Host "Getting changes from $CommitRange..."
|
Write-Host "Getting changes from $CommitRange..."
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative', $CommitRange)
|
$raw = git diff --name-only --relative $CommitRange 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
}
|
}
|
||||||
"All" {
|
"All" {
|
||||||
Write-Host "Getting all uncommitted changes..."
|
Write-Host "Getting all uncommitted changes..."
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--cached', '--name-only', '--relative')
|
$raw = git diff --cached --name-only --relative 2>&1
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('diff', '--name-only', '--relative')
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
$changedFiles += Invoke-GitLines -GitArgs @('ls-files', '--others', '--exclude-standard')
|
$raw = git diff --name-only --relative 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
|
$raw = git ls-files --others --exclude-standard 2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -278,16 +216,13 @@ Write-Host "Git changes detected: $($changedFiles.Count) files"
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
$configFiles = @()
|
$configFiles = @()
|
||||||
$supportSkipped = @()
|
|
||||||
|
|
||||||
foreach ($file in $changedFiles) {
|
foreach ($file in $changedFiles) {
|
||||||
$file = $file.Trim().Replace('\', '/')
|
$file = $file.Trim().Replace('\', '/')
|
||||||
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
if ([string]::IsNullOrWhiteSpace($file)) { continue }
|
||||||
|
|
||||||
# Skip service files (not partially loadable). Support-state files are tracked
|
# Skip service files
|
||||||
# to warn the user: support changes apply only via a full load.
|
if ($file -eq "ConfigDumpInfo.xml") { continue }
|
||||||
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
|
|
||||||
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
|
|
||||||
|
|
||||||
$fullPath = Join-Path $ConfigDir $file
|
$fullPath = Join-Path $ConfigDir $file
|
||||||
|
|
||||||
@@ -330,12 +265,6 @@ foreach ($file in $changedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($supportSkipped.Count -gt 0) {
|
|
||||||
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
|
|
||||||
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
|
|
||||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($configFiles.Count -eq 0) {
|
if ($configFiles.Count -eq 0) {
|
||||||
Write-Host "No configuration files found in changes"
|
Write-Host "No configuration files found in changes"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -356,53 +285,6 @@ $tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only; import specific files) ---
|
|
||||||
if ($Format -eq "Plain") {
|
|
||||||
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($AllExtensions) {
|
|
||||||
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$arguments = @("infobase", "config", "import", "files") + $configFiles
|
|
||||||
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -ne 0) {
|
|
||||||
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
|
||||||
$applyArgs += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
|
||||||
$applyOut = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
|
||||||
}
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
$listFile = Join-Path $tempDir "load_list.txt"
|
$listFile = Join-Path $tempDir "load_list.txt"
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-git v1.11 — Load Git changes into 1C database
|
# db-load-git v1.3 — Load Git changes into 1C database
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
@@ -15,90 +13,26 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
candidates.sort()
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
return candidates[-1]
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def get_object_xml_from_subfile(relative_path):
|
def get_object_xml_from_subfile(relative_path):
|
||||||
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
"""Map sub-file path (BSL, HTML, etc.) to object XML path."""
|
||||||
parts = re.split(r"[\\/]", relative_path)
|
parts = re.split(r"[\\/]", relative_path)
|
||||||
@@ -110,7 +44,7 @@ def get_object_xml_from_subfile(relative_path):
|
|||||||
def run_git(config_dir, git_args):
|
def run_git(config_dir, git_args):
|
||||||
"""Run a git command in config_dir and return output lines on success."""
|
"""Run a git command in config_dir and return output lines on success."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git", "-c", "core.quotePath=false"] + git_args,
|
["git"] + git_args,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
@@ -159,15 +93,9 @@ def main():
|
|||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
# --- Detect engine + validate connection (skip if DryRun) ---
|
# --- Validate connection (skip if DryRun) ---
|
||||||
engine = "1cv8"
|
|
||||||
if not args.DryRun:
|
if not args.DryRun:
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if engine == "ibcmd":
|
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -218,19 +146,14 @@ def main():
|
|||||||
|
|
||||||
# --- Filter and map to config files ---
|
# --- Filter and map to config files ---
|
||||||
config_files = []
|
config_files = []
|
||||||
support_skipped = []
|
|
||||||
|
|
||||||
for file in changed_files:
|
for file in changed_files:
|
||||||
file = file.strip().replace("\\", "/")
|
file = file.strip().replace("\\", "/")
|
||||||
if not file:
|
if not file:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip service files (not partially loadable). Support-state files are
|
# Skip service files
|
||||||
# tracked to warn: support changes apply only via a full load.
|
if file == "ConfigDumpInfo.xml":
|
||||||
if file.endswith("ParentConfigurations.bin"):
|
|
||||||
support_skipped.append(file)
|
|
||||||
continue
|
|
||||||
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
full_path = os.path.join(args.ConfigDir, file)
|
full_path = os.path.join(args.ConfigDir, file)
|
||||||
@@ -263,12 +186,6 @@ def main():
|
|||||||
if rel_path not in config_files:
|
if rel_path not in config_files:
|
||||||
config_files.append(rel_path)
|
config_files.append(rel_path)
|
||||||
|
|
||||||
if support_skipped:
|
|
||||||
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
|
|
||||||
for sf in support_skipped:
|
|
||||||
print(f" - {sf}", file=sys.stderr)
|
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
|
|
||||||
|
|
||||||
if len(config_files) == 0:
|
if len(config_files) == 0:
|
||||||
print("No configuration files found in changes")
|
print("No configuration files found in changes")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -288,58 +205,6 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if engine == "ibcmd":
|
|
||||||
# --- ibcmd branch (file infobase only; import specific files) ---
|
|
||||||
if args.Format == "Plain":
|
|
||||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if args.AllExtensions:
|
|
||||||
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
arguments = ["infobase", "config", "import", "files"] + config_files
|
|
||||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
print(f"Changes loaded successfully ({len(config_files)} files)")
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
|
||||||
if args.UpdateDB:
|
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
|
||||||
if args.UserName:
|
|
||||||
apply_args.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
apply_args.append(f"--password={args.Password}")
|
|
||||||
apply_args.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
|
||||||
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
|
||||||
exit_code = ar.returncode
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
if ar.stdout:
|
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
# --- Write list file (UTF-8 with BOM) ---
|
# --- Write list file (UTF-8 with BOM) ---
|
||||||
list_file = os.path.join(temp_dir, "load_list.txt")
|
list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
with open(list_file, "w", encoding="utf-8-sig") as f:
|
with open(list_file, "w", encoding="utf-8-sig") as f:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-load-xml
|
name: db-load-xml
|
||||||
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
description: Загрузка конфигурации 1С из XML-файлов. Используй когда пользователь просит загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
|
||||||
argument-hint: <configDir> [database]
|
argument-hint: <configDir> [database]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,7 +30,7 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
|
||||||
@@ -38,14 +38,14 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -80,22 +80,30 @@ Documents/Заказ.xml
|
|||||||
Documents/Заказ/Forms/ФормаДокумента.xml
|
Documents/Заказ/Forms/ФормаДокумента.xml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
## После выполнения
|
## После выполнения
|
||||||
|
|
||||||
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
1. Прочитай лог и покажи результат
|
||||||
|
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
|
||||||
|
|
||||||
## Примеры
|
## Примеры
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Полная загрузка
|
# Полная загрузка
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
|
||||||
|
|
||||||
# Частичная загрузка конкретных файлов
|
# Частичная загрузка конкретных файлов
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
|
||||||
|
|
||||||
# Загрузка расширения
|
# Загрузка расширения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
|
||||||
|
|
||||||
# Загрузка + обновление БД в одном запуске
|
# Загрузка + обновление БД в одном запуске
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
powershell.exe -NoProfile -File .claude/skills/db-load-xml/scripts/db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-load-xml v1.12 — Load 1C configuration from XML files
|
# db-load-xml v1.3 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Загрузка конфигурации 1С из XML-файлов
|
Загрузка конфигурации 1С из XML-файлов
|
||||||
@@ -109,85 +108,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -209,73 +148,6 @@ $tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
|
||||||
if ($Format -eq "Plain") {
|
|
||||||
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($AllExtensions) {
|
|
||||||
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
|
|
||||||
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
|
|
||||||
# partial: import specific files (relative to ConfigDir)
|
|
||||||
$fileList = @()
|
|
||||||
if ($ListFile) {
|
|
||||||
if (-not (Test-Path $ListFile)) {
|
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
||||||
} elseif ($Files) {
|
|
||||||
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
|
||||||
}
|
|
||||||
if ($fileList.Count -eq 0) {
|
|
||||||
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$arguments = @("infobase", "config", "import", "files") + $fileList
|
|
||||||
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
} else {
|
|
||||||
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
$arguments += "$ConfigDir"
|
|
||||||
}
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -ne 0) {
|
|
||||||
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
|
|
||||||
if ($UpdateDB) {
|
|
||||||
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
|
||||||
if ($UserName) { $applyArgs += "--user=$UserName" }
|
|
||||||
if ($Password) { $applyArgs += "--password=$Password" }
|
|
||||||
$applyArgs += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
|
|
||||||
$applyOut = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($applyOut) { Write-Host ($applyOut | Out-String) }
|
|
||||||
}
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
@@ -296,35 +168,24 @@ try {
|
|||||||
Write-Host "Executing partial configuration load..."
|
Write-Host "Executing partial configuration load..."
|
||||||
|
|
||||||
# Build list file
|
# Build list file
|
||||||
$rawList = @()
|
$generatedListFile = $null
|
||||||
if ($ListFile) {
|
if ($ListFile) {
|
||||||
|
# Use provided list file
|
||||||
if (-not (Test-Path $ListFile)) {
|
if (-not (Test-Path $ListFile)) {
|
||||||
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
$generatedListFile = $ListFile
|
||||||
} else {
|
} else {
|
||||||
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
|
# Generate from -Files parameter
|
||||||
}
|
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
|
||||||
|
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
||||||
|
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
||||||
|
|
||||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
Write-Host "Files to load: $($fileList.Count)"
|
||||||
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
|
foreach ($f in $fileList) { Write-Host " $f" }
|
||||||
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
|
|
||||||
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
|
|
||||||
if ($supportFiles.Count -gt 0) {
|
|
||||||
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
|
|
||||||
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
|
|
||||||
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
|
|
||||||
}
|
}
|
||||||
if ($fileList.Count -eq 0) {
|
|
||||||
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$generatedListFile = Join-Path $tempDir "load_list.txt"
|
|
||||||
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
|
|
||||||
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
|
|
||||||
Write-Host "Files to load: $($fileList.Count)"
|
|
||||||
foreach ($f in $fileList) { Write-Host " $f" }
|
|
||||||
|
|
||||||
$arguments += "-listFile", "`"$generatedListFile`""
|
$arguments += "-listFile", "`"$generatedListFile`""
|
||||||
$arguments += "-partial"
|
$arguments += "-partial"
|
||||||
|
|||||||
@@ -1,104 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-load-xml v1.12 — Load 1C configuration from XML files
|
# db-load-xml v1.3 — Load 1C configuration from XML files
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
candidates.sort()
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
return candidates[-1]
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -140,14 +73,8 @@ def main():
|
|||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
@@ -161,77 +88,6 @@ def main():
|
|||||||
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if args.Format == "Plain":
|
|
||||||
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if args.AllExtensions:
|
|
||||||
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
|
|
||||||
elif args.Mode == "Partial" or args.Files or args.ListFile:
|
|
||||||
# partial: import specific files (relative to ConfigDir)
|
|
||||||
if args.ListFile:
|
|
||||||
if not os.path.isfile(args.ListFile):
|
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
|
||||||
file_list = [ln.strip() for ln in f if ln.strip()]
|
|
||||||
elif args.Files:
|
|
||||||
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
|
|
||||||
else:
|
|
||||||
file_list = []
|
|
||||||
if not file_list:
|
|
||||||
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
arguments = ["infobase", "config", "import", "files"] + file_list
|
|
||||||
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
else:
|
|
||||||
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
arguments.append(args.ConfigDir)
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode != 0:
|
|
||||||
print(f"Error loading configuration from files (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
print(f"Configuration loaded successfully from: {args.ConfigDir}")
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
exit_code = 0
|
|
||||||
if args.UpdateDB:
|
|
||||||
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
|
||||||
if args.UserName:
|
|
||||||
apply_args.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
apply_args.append(f"--password={args.Password}")
|
|
||||||
apply_args.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(apply_args)}")
|
|
||||||
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
|
|
||||||
exit_code = ar.returncode
|
|
||||||
if exit_code == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
|
|
||||||
if ar.stdout:
|
|
||||||
print(ar.stdout)
|
|
||||||
if ar.stderr:
|
|
||||||
print(ar.stderr, file=sys.stderr)
|
|
||||||
sys.exit(exit_code)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
@@ -258,33 +114,23 @@ def main():
|
|||||||
print("Executing partial configuration load...")
|
print("Executing partial configuration load...")
|
||||||
|
|
||||||
# Build list file
|
# Build list file
|
||||||
|
generated_list_file = None
|
||||||
if args.ListFile:
|
if args.ListFile:
|
||||||
|
# Use provided list file
|
||||||
if not os.path.isfile(args.ListFile):
|
if not os.path.isfile(args.ListFile):
|
||||||
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
with open(args.ListFile, encoding="utf-8-sig") as f:
|
generated_list_file = args.ListFile
|
||||||
raw_list = [ln.strip() for ln in f if ln.strip()]
|
|
||||||
else:
|
else:
|
||||||
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
# Generate from -Files parameter
|
||||||
|
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
|
||||||
|
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
||||||
|
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write("\n".join(file_list))
|
||||||
|
|
||||||
# Support-state service files are NOT partially loadable — exclude with a hint.
|
print(f"Files to load: {len(file_list)}")
|
||||||
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
|
for fl in file_list:
|
||||||
support_files = [x for x in raw_list if support_re.search(x)]
|
print(f" {fl}")
|
||||||
file_list = [x for x in raw_list if not support_re.search(x)]
|
|
||||||
if support_files:
|
|
||||||
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
|
|
||||||
for sf in support_files:
|
|
||||||
print(f" - {sf}", file=sys.stderr)
|
|
||||||
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
|
|
||||||
if not file_list:
|
|
||||||
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
generated_list_file = os.path.join(temp_dir, "load_list.txt")
|
|
||||||
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
|
|
||||||
f.write("\n".join(file_list))
|
|
||||||
print(f"Files to load: {len(file_list)}")
|
|
||||||
for fl in file_list:
|
|
||||||
print(f" {fl}")
|
|
||||||
|
|
||||||
arguments += ["-listFile", generated_list_file]
|
arguments += ["-listFile", generated_list_file]
|
||||||
arguments.append("-partial")
|
arguments.append("-partial")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-run
|
name: db-run
|
||||||
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
|
description: Запуск 1С:Предприятие. Используй когда пользователь просит запустить 1С, открыть базу, запустить предприятие
|
||||||
argument-hint: "[database]"
|
argument-hint: "[database]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -29,14 +29,14 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -63,14 +63,14 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" <параметры>
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Простой запуск
|
# Простой запуск
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Запуск с обработкой
|
# Запуск с обработкой
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
|
||||||
|
|
||||||
# Открыть по навигационной ссылке
|
# Открыть по навигационной ссылке
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
|
||||||
|
|
||||||
# Серверная база с параметром запуска
|
# Серверная база с параметром запуска
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-run.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
powershell.exe -NoProfile -File .claude/skills/db-run/scripts/db-run.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-run v1.2 — Launch 1C:Enterprise
|
# db-run v1.0 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Запуск 1С:Предприятие
|
Запуск 1С:Предприятие
|
||||||
@@ -80,45 +79,20 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +1,28 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-run v1.2 — Launch 1C:Enterprise
|
# db-run v1.0 — Launch 1C:Enterprise
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||||
if not v8path:
|
if found:
|
||||||
if os.name == "nt":
|
return found[-1]
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: db-update
|
name: db-update
|
||||||
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
|
description: Обновление конфигурации базы данных 1С. Используй когда пользователь просит обновить БД, применить конфигурацию, UpdateDBCfg
|
||||||
argument-hint: "[database]"
|
argument-hint: "[database]"
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -28,21 +28,21 @@ allowed-tools:
|
|||||||
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
|
||||||
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
|
||||||
4. Если ветка не совпала — используй `default`
|
4. Если ветка не совпала — используй `default`
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если файла нет — предложи `/db-list add`.
|
Если файла нет — предложи `/db-list add`.
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -66,6 +66,13 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
|
|||||||
| `-BackgroundSuspend` | Приостановить |
|
| `-BackgroundSuspend` | Приостановить |
|
||||||
| `-BackgroundResume` | Возобновить |
|
| `-BackgroundResume` | Возобновить |
|
||||||
|
|
||||||
|
## Коды возврата
|
||||||
|
|
||||||
|
| Код | Описание |
|
||||||
|
|-----|----------|
|
||||||
|
| 0 | Успешно |
|
||||||
|
| 1 | Ошибка (см. лог) |
|
||||||
|
|
||||||
## Предупреждения
|
## Предупреждения
|
||||||
|
|
||||||
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
|
||||||
@@ -76,11 +83,11 @@ python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" <параметры>
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Обычное обновление (файловая база)
|
# Обычное обновление (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
|
||||||
|
|
||||||
# Динамическое обновление (серверная база)
|
# Динамическое обновление (серверная база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
|
||||||
|
|
||||||
# Обновление расширения
|
# Обновление расширения
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/db-update.py" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
powershell.exe -NoProfile -File .claude/skills/db-update/scripts/db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# db-update v1.6 — Update 1C database configuration
|
# db-update v1.0 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Обновление конфигурации базы данных 1С
|
Обновление конфигурации базы данных 1С
|
||||||
@@ -90,85 +89,25 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if ($engine -eq "ibcmd") {
|
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|
||||||
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -178,33 +117,6 @@ $tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if ($AllExtensions) {
|
|
||||||
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
|
|
||||||
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
|
|
||||||
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
|
|
||||||
if ($Extension) { $arguments += "--extension=$Extension" }
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "Database configuration updated successfully" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,35 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# db-update v1.6 — Update 1C database configuration
|
# db-update v1.0 — Update 1C database configuration
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
|
||||||
if not v8path:
|
if found:
|
||||||
if os.name == "nt":
|
return found[-1]
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
|
||||||
v8path = max(candidates, key=_version_key)
|
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
|
||||||
else:
|
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -121,48 +52,11 @@ def main():
|
|||||||
|
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
|
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate connection ---
|
# --- Validate connection ---
|
||||||
if engine == "ibcmd":
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
|
||||||
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# --- ibcmd branch (file infobase only) ---
|
|
||||||
if engine == "ibcmd":
|
|
||||||
if args.AllExtensions:
|
|
||||||
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
|
|
||||||
if args.Dynamic == "+":
|
|
||||||
arguments.append("--dynamic=auto")
|
|
||||||
elif args.Dynamic == "-":
|
|
||||||
arguments.append("--dynamic=disable")
|
|
||||||
if args.Extension:
|
|
||||||
arguments.append(f"--extension={args.Extension}")
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
|
|
||||||
if result.returncode == 0:
|
|
||||||
print("Database configuration updated successfully")
|
|
||||||
else:
|
|
||||||
print(f"Error updating database configuration (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Temp dir ---
|
# --- Temp dir ---
|
||||||
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
|
||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
name: epf-add-form
|
||||||
|
description: Добавить управляемую форму к внешней обработке 1С
|
||||||
|
argument-hint: <ProcessorName> <FormName> [Synonym]
|
||||||
|
allowed-tools:
|
||||||
|
- Bash
|
||||||
|
- Read
|
||||||
|
- Write
|
||||||
|
- Edit
|
||||||
|
- Glob
|
||||||
|
- Grep
|
||||||
|
---
|
||||||
|
|
||||||
|
# /epf-add-form — Добавление формы
|
||||||
|
|
||||||
|
Создаёт управляемую форму и регистрирует её в корневом XML обработки.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/epf-add-form <ProcessorName> <FormName> [Synonym] [--main]
|
||||||
|
```
|
||||||
|
|
||||||
|
| Параметр | Обязательный | По умолчанию | Описание |
|
||||||
|
|---------------|:------------:|--------------|-------------------------------------------|
|
||||||
|
| ProcessorName | да | — | Имя обработки (должна существовать) |
|
||||||
|
| FormName | да | — | Имя формы |
|
||||||
|
| Synonym | нет | = FormName | Синоним формы |
|
||||||
|
| --main | нет | авто | Установить как форму по умолчанию (автоматически для первой формы) |
|
||||||
|
| SrcDir | нет | `src` | Каталог исходников |
|
||||||
|
|
||||||
|
## Команда
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -File .claude/skills/epf-add-form/scripts/add-form.ps1 -ProcessorName "<ProcessorName>" -FormName "<FormName>" [-Synonym "<Synonym>"] [-Main] [-SrcDir "<SrcDir>"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что создаётся
|
||||||
|
|
||||||
|
```
|
||||||
|
<SrcDir>/<ProcessorName>/Forms/
|
||||||
|
├── <FormName>.xml # Метаданные формы (1 UUID)
|
||||||
|
└── <FormName>/
|
||||||
|
└── Ext/
|
||||||
|
├── Form.xml # Описание формы (logform namespace)
|
||||||
|
└── Form/
|
||||||
|
└── Module.bsl # BSL-модуль с 4 регионами
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что модифицируется
|
||||||
|
|
||||||
|
- `<SrcDir>/<ProcessorName>.xml` — добавляется `<Form>` в `ChildObjects`, обновляется `DefaultForm` (автоматически если это первая форма, или явно при `--main`)
|
||||||
|
|
||||||
|
## Детали
|
||||||
|
|
||||||
|
- FormType: Managed
|
||||||
|
- UsePurposes: PlatformApplication, MobilePlatformApplication
|
||||||
|
- AutoCommandBar с id=-1
|
||||||
|
- Реквизит "Объект" с MainAttribute=true
|
||||||
|
- BSL-модуль содержит 5 регионов: ОбработчикиСобытийФормы, ОбработчикиСобытийЭлементовФормы, ОбработчикиКомандФормы, ОбработчикиОповещений, СлужебныеПроцедурыИФункции
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
# epf-add-form v1.1 — Add managed form to 1C processor
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$ProcessorName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]$FormName,
|
||||||
|
|
||||||
|
[string]$Synonym = $FormName,
|
||||||
|
|
||||||
|
[switch]$Main,
|
||||||
|
|
||||||
|
[string]$SrcDir = "src"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- Detect format version ---
|
||||||
|
|
||||||
|
function Detect-FormatVersion([string]$dir) {
|
||||||
|
$d = $dir
|
||||||
|
while ($d) {
|
||||||
|
$cfgPath = Join-Path $d "Configuration.xml"
|
||||||
|
if (Test-Path $cfgPath) {
|
||||||
|
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
|
||||||
|
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
|
||||||
|
}
|
||||||
|
$parent = Split-Path $d -Parent
|
||||||
|
if ($parent -eq $d) { break }
|
||||||
|
$d = $parent
|
||||||
|
}
|
||||||
|
return "2.17"
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatVersion = Detect-FormatVersion (Resolve-Path $SrcDir).Path
|
||||||
|
|
||||||
|
# --- Проверки ---
|
||||||
|
|
||||||
|
$rootXmlPath = Join-Path $SrcDir "$ProcessorName.xml"
|
||||||
|
if (-not (Test-Path $rootXmlPath)) {
|
||||||
|
Write-Error "Корневой файл обработки не найден: $rootXmlPath. Сначала выполните epf-init."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$processorDir = Join-Path $SrcDir $ProcessorName
|
||||||
|
$formsDir = Join-Path $processorDir "Forms"
|
||||||
|
$formMetaPath = Join-Path $formsDir "$FormName.xml"
|
||||||
|
|
||||||
|
if (Test-Path $formMetaPath) {
|
||||||
|
Write-Error "Форма уже существует: $formMetaPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Создание каталогов ---
|
||||||
|
|
||||||
|
$formDir = Join-Path $formsDir $FormName
|
||||||
|
$formExtDir = Join-Path $formDir "Ext"
|
||||||
|
$formModuleDir = Join-Path $formExtDir "Form"
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $formModuleDir -Force | Out-Null
|
||||||
|
|
||||||
|
# --- Кодировка ---
|
||||||
|
|
||||||
|
$encBom = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
$encNoBom = New-Object System.Text.UTF8Encoding($false)
|
||||||
|
|
||||||
|
# --- 1. Метаданные формы (Forms/<FormName>.xml) ---
|
||||||
|
|
||||||
|
$formUuid = [guid]::NewGuid().ToString()
|
||||||
|
|
||||||
|
$formMetaXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
|
<Form uuid="$formUuid">
|
||||||
|
<Properties>
|
||||||
|
<Name>$FormName</Name>
|
||||||
|
<Synonym>
|
||||||
|
<v8:item>
|
||||||
|
<v8:lang>ru</v8:lang>
|
||||||
|
<v8:content>$Synonym</v8:content>
|
||||||
|
</v8:item>
|
||||||
|
</Synonym>
|
||||||
|
<Comment/>
|
||||||
|
<FormType>Managed</FormType>
|
||||||
|
<IncludeHelpInContents>false</IncludeHelpInContents>
|
||||||
|
<UsePurposes>
|
||||||
|
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
|
||||||
|
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
|
||||||
|
</UsePurposes>
|
||||||
|
<ExtendedPresentation/>
|
||||||
|
</Properties>
|
||||||
|
</Form>
|
||||||
|
</MetaDataObject>
|
||||||
|
"@
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom)
|
||||||
|
|
||||||
|
# --- 2. Описание формы (Forms/<FormName>/Ext/Form.xml) ---
|
||||||
|
|
||||||
|
$formXmlPath = Join-Path $formExtDir "Form.xml"
|
||||||
|
|
||||||
|
$formXml = @"
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Form xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
|
||||||
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
|
<Autofill>true</Autofill>
|
||||||
|
</AutoCommandBar>
|
||||||
|
<ChildItems/>
|
||||||
|
<Attributes>
|
||||||
|
<Attribute name="Объект" id="1">
|
||||||
|
<Type>
|
||||||
|
<v8:Type>cfg:ExternalDataProcessorObject.$ProcessorName</v8:Type>
|
||||||
|
</Type>
|
||||||
|
<MainAttribute>true</MainAttribute>
|
||||||
|
</Attribute>
|
||||||
|
</Attributes>
|
||||||
|
</Form>
|
||||||
|
"@
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom)
|
||||||
|
|
||||||
|
# --- 3. BSL-модуль (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
||||||
|
|
||||||
|
$modulePath = Join-Path $formModuleDir "Module.bsl"
|
||||||
|
|
||||||
|
$moduleBsl = @"
|
||||||
|
#Область ОбработчикиСобытийФормы
|
||||||
|
|
||||||
|
#КонецОбласти
|
||||||
|
|
||||||
|
#Область ОбработчикиСобытийЭлементовФормы
|
||||||
|
|
||||||
|
#КонецОбласти
|
||||||
|
|
||||||
|
#Область ОбработчикиКомандФормы
|
||||||
|
|
||||||
|
#КонецОбласти
|
||||||
|
|
||||||
|
#Область ОбработчикиОповещений
|
||||||
|
|
||||||
|
#КонецОбласти
|
||||||
|
|
||||||
|
#Область СлужебныеПроцедурыИФункции
|
||||||
|
|
||||||
|
#КонецОбласти
|
||||||
|
"@
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
|
||||||
|
|
||||||
|
# --- 4. Модификация корневого XML ---
|
||||||
|
|
||||||
|
$rootXmlFull = Resolve-Path $rootXmlPath
|
||||||
|
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
|
$xmlDoc.PreserveWhitespace = $true
|
||||||
|
$xmlDoc.Load($rootXmlFull.Path)
|
||||||
|
|
||||||
|
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||||
|
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
|
||||||
|
$childObjects = $xmlDoc.SelectSingleNode("//md:ChildObjects", $nsMgr)
|
||||||
|
if (-not $childObjects) {
|
||||||
|
Write-Error "Не найден элемент ChildObjects в $rootXmlPath"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добавить <Form> перед первым <Template>, или в конец
|
||||||
|
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
|
||||||
|
$formElem.InnerText = $FormName
|
||||||
|
|
||||||
|
$firstTemplate = $childObjects.SelectSingleNode("md:Template", $nsMgr)
|
||||||
|
if ($firstTemplate) {
|
||||||
|
# Вставить перед Template, добавив перенос строки + табуляцию
|
||||||
|
$whitespace = $xmlDoc.CreateWhitespace("`n`t`t`t")
|
||||||
|
$childObjects.InsertBefore($whitespace, $firstTemplate) | Out-Null
|
||||||
|
$childObjects.InsertBefore($formElem, $whitespace) | Out-Null
|
||||||
|
} else {
|
||||||
|
# Добавить в конец ChildObjects
|
||||||
|
# Если ChildObjects пустой (самозакрывающийся), нужно добавить форматирование
|
||||||
|
if ($childObjects.ChildNodes.Count -eq 0) {
|
||||||
|
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||||
|
$childObjects.AppendChild($formElem) | Out-Null
|
||||||
|
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||||
|
} else {
|
||||||
|
$lastChild = $childObjects.LastChild
|
||||||
|
# Вставить перед закрывающим whitespace (если есть), или в конец
|
||||||
|
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
|
||||||
|
$childObjects.InsertBefore($xmlDoc.CreateWhitespace("`n`t`t`t"), $lastChild) | Out-Null
|
||||||
|
$childObjects.InsertBefore($formElem, $lastChild) | Out-Null
|
||||||
|
} else {
|
||||||
|
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
|
||||||
|
$childObjects.AppendChild($formElem) | Out-Null
|
||||||
|
$childObjects.AppendChild($xmlDoc.CreateWhitespace("`n`t`t")) | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Обновить DefaultForm: явно при -Main, или автоматически если это первая форма
|
||||||
|
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
|
||||||
|
$isFirstForm = ($existingForms.Count -eq 1)
|
||||||
|
|
||||||
|
if ($Main -or $isFirstForm) {
|
||||||
|
$defaultForm = $xmlDoc.SelectSingleNode("//md:DefaultForm", $nsMgr)
|
||||||
|
if ($defaultForm) {
|
||||||
|
$defaultForm.InnerText = "ExternalDataProcessor.$ProcessorName.Form.$FormName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Сохранить с BOM
|
||||||
|
$settings = New-Object System.Xml.XmlWriterSettings
|
||||||
|
$settings.Encoding = $encBom
|
||||||
|
$settings.Indent = $false # Preserve original whitespace
|
||||||
|
|
||||||
|
$stream = New-Object System.IO.FileStream($rootXmlFull.Path, [System.IO.FileMode]::Create)
|
||||||
|
$writer = [System.Xml.XmlWriter]::Create($stream, $settings)
|
||||||
|
$xmlDoc.Save($writer)
|
||||||
|
$writer.Close()
|
||||||
|
$stream.Close()
|
||||||
|
|
||||||
|
Write-Host "[OK] Создана форма: $FormName"
|
||||||
|
Write-Host " Метаданные: $formMetaPath"
|
||||||
|
Write-Host " Описание: $formXmlPath"
|
||||||
|
Write-Host " Модуль: $modulePath"
|
||||||
|
if ($Main -or $isFirstForm) {
|
||||||
|
Write-Host " DefaultForm обновлён"
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# add-form v1.1 — Add managed form to 1C external data processor
|
||||||
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from lxml import etree
|
||||||
|
|
||||||
|
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_format_version(d):
|
||||||
|
while d:
|
||||||
|
cfg_path = os.path.join(d, "Configuration.xml")
|
||||||
|
if os.path.isfile(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8-sig") as f:
|
||||||
|
head = f.read(2000)
|
||||||
|
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
parent = os.path.dirname(d)
|
||||||
|
if parent == d:
|
||||||
|
break
|
||||||
|
d = parent
|
||||||
|
return "2.17"
|
||||||
|
|
||||||
|
|
||||||
|
def save_xml_with_bom(tree, path):
|
||||||
|
"""Save XML tree to file with UTF-8 BOM."""
|
||||||
|
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
|
||||||
|
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
|
||||||
|
if not xml_bytes.endswith(b"\n"):
|
||||||
|
xml_bytes += b"\n"
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(b"\xef\xbb\xbf")
|
||||||
|
f.write(xml_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def write_text_with_bom(path, text):
|
||||||
|
"""Write text to file with UTF-8 BOM."""
|
||||||
|
with open(path, "w", encoding="utf-8-sig") as f:
|
||||||
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
parser = argparse.ArgumentParser(description="Add managed form to 1C processor", allow_abbrev=False)
|
||||||
|
parser.add_argument("-ProcessorName", required=True)
|
||||||
|
parser.add_argument("-FormName", required=True)
|
||||||
|
parser.add_argument("-Synonym", default=None)
|
||||||
|
parser.add_argument("-Main", action="store_true")
|
||||||
|
parser.add_argument("-SrcDir", default="src")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
processor_name = args.ProcessorName
|
||||||
|
form_name = args.FormName
|
||||||
|
synonym = args.Synonym if args.Synonym is not None else form_name
|
||||||
|
is_main = args.Main
|
||||||
|
src_dir = args.SrcDir
|
||||||
|
|
||||||
|
format_version = detect_format_version(os.path.abspath(src_dir))
|
||||||
|
|
||||||
|
# --- Checks ---
|
||||||
|
|
||||||
|
root_xml_path = os.path.join(src_dir, f"{processor_name}.xml")
|
||||||
|
if not os.path.exists(root_xml_path):
|
||||||
|
print(f"Корневой файл обработки не найден: {root_xml_path}. Сначала выполните epf-init.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
processor_dir = os.path.join(src_dir, processor_name)
|
||||||
|
forms_dir = os.path.join(processor_dir, "Forms")
|
||||||
|
form_meta_path = os.path.join(forms_dir, f"{form_name}.xml")
|
||||||
|
|
||||||
|
if os.path.exists(form_meta_path):
|
||||||
|
print(f"Форма уже существует: {form_meta_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# --- Create directories ---
|
||||||
|
|
||||||
|
form_dir = os.path.join(forms_dir, form_name)
|
||||||
|
form_ext_dir = os.path.join(form_dir, "Ext")
|
||||||
|
form_module_dir = os.path.join(form_ext_dir, "Form")
|
||||||
|
|
||||||
|
os.makedirs(form_module_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# --- 1. Form metadata (Forms/<FormName>.xml) ---
|
||||||
|
|
||||||
|
form_uuid = str(uuid.uuid4())
|
||||||
|
|
||||||
|
form_meta_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xen="http://v8.1c.ru/8.3/xcf/enums"'
|
||||||
|
' xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
f' version="{format_version}">\n'
|
||||||
|
f'\t<Form uuid="{form_uuid}">\n'
|
||||||
|
'\t\t<Properties>\n'
|
||||||
|
f'\t\t\t<Name>{form_name}</Name>\n'
|
||||||
|
'\t\t\t<Synonym>\n'
|
||||||
|
'\t\t\t\t<v8:item>\n'
|
||||||
|
'\t\t\t\t\t<v8:lang>ru</v8:lang>\n'
|
||||||
|
f'\t\t\t\t\t<v8:content>{synonym}</v8:content>\n'
|
||||||
|
'\t\t\t\t</v8:item>\n'
|
||||||
|
'\t\t\t</Synonym>\n'
|
||||||
|
'\t\t\t<Comment/>\n'
|
||||||
|
'\t\t\t<FormType>Managed</FormType>\n'
|
||||||
|
'\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>\n'
|
||||||
|
'\t\t\t<UsePurposes>\n'
|
||||||
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
|
||||||
|
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
|
||||||
|
'\t\t\t</UsePurposes>\n'
|
||||||
|
'\t\t\t<ExtendedPresentation/>\n'
|
||||||
|
'\t\t</Properties>\n'
|
||||||
|
'\t</Form>\n'
|
||||||
|
'</MetaDataObject>'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_text_with_bom(form_meta_path, form_meta_xml)
|
||||||
|
|
||||||
|
# --- 2. Form description (Forms/<FormName>/Ext/Form.xml) ---
|
||||||
|
|
||||||
|
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
|
||||||
|
|
||||||
|
form_xml = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
|
'<Form xmlns="http://v8.1c.ru/8.3/xcf/logform"'
|
||||||
|
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
|
||||||
|
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
|
||||||
|
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
|
||||||
|
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
|
||||||
|
' xmlns:ent="http://v8.1c.ru/8.1/data/enterprise"'
|
||||||
|
' xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform"'
|
||||||
|
' xmlns:style="http://v8.1c.ru/8.1/data/ui/style"'
|
||||||
|
' xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
|
||||||
|
' xmlns:v8="http://v8.1c.ru/8.1/data/core"'
|
||||||
|
' xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"'
|
||||||
|
' xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web"'
|
||||||
|
' xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows"'
|
||||||
|
' xmlns:xr="http://v8.1c.ru/8.3/xcf/readable"'
|
||||||
|
' xmlns:xs="http://www.w3.org/2001/XMLSchema"'
|
||||||
|
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
|
||||||
|
f' version="{format_version}">\n'
|
||||||
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
|
'\t</AutoCommandBar>\n'
|
||||||
|
'\t<ChildItems/>\n'
|
||||||
|
'\t<Attributes>\n'
|
||||||
|
f'\t\t<Attribute name="\u041e\u0431\u044a\u0435\u043a\u0442" id="1">\n'
|
||||||
|
'\t\t\t<Type>\n'
|
||||||
|
f'\t\t\t\t<v8:Type>cfg:ExternalDataProcessorObject.{processor_name}</v8:Type>\n'
|
||||||
|
'\t\t\t</Type>\n'
|
||||||
|
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||||
|
'\t\t</Attribute>\n'
|
||||||
|
'\t</Attributes>\n'
|
||||||
|
'</Form>'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_text_with_bom(form_xml_path, form_xml)
|
||||||
|
|
||||||
|
# --- 3. BSL module (Forms/<FormName>/Ext/Form/Module.bsl) ---
|
||||||
|
|
||||||
|
module_path = os.path.join(form_module_dir, "Module.bsl")
|
||||||
|
|
||||||
|
module_bsl = (
|
||||||
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041a\u043e\u043c\u0430\u043d\u0434\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u041e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0439\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0421\u043b\u0443\u0436\u0435\u0431\u043d\u044b\u0435\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\u0418\u0424\u0443\u043d\u043a\u0446\u0438\u0438\n'
|
||||||
|
'\n'
|
||||||
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438'
|
||||||
|
)
|
||||||
|
|
||||||
|
write_text_with_bom(module_path, module_bsl)
|
||||||
|
|
||||||
|
# --- 4. Modify root XML ---
|
||||||
|
|
||||||
|
root_xml_full = os.path.abspath(root_xml_path)
|
||||||
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||||
|
tree = etree.parse(root_xml_full, parser_xml)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
ns = "http://v8.1c.ru/8.3/MDClasses"
|
||||||
|
child_objects = root.find(".//md:ChildObjects", NSMAP)
|
||||||
|
if child_objects is None:
|
||||||
|
print(f"Не найден элемент ChildObjects в {root_xml_path}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Add <Form> before first <Template>, or at end
|
||||||
|
form_elem = etree.Element(f"{{{ns}}}Form")
|
||||||
|
form_elem.text = form_name
|
||||||
|
|
||||||
|
first_template = child_objects.find("md:Template", NSMAP)
|
||||||
|
if first_template is not None:
|
||||||
|
# Insert before Template, adding newline + indent
|
||||||
|
idx = list(child_objects).index(first_template)
|
||||||
|
child_objects.insert(idx, form_elem)
|
||||||
|
# Set whitespace: form_elem gets same tail pattern
|
||||||
|
form_elem.tail = "\n\t\t\t"
|
||||||
|
else:
|
||||||
|
# Add to end of ChildObjects
|
||||||
|
children = list(child_objects)
|
||||||
|
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
|
||||||
|
# Empty ChildObjects (self-closing)
|
||||||
|
child_objects.text = "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = "\n\t\t"
|
||||||
|
else:
|
||||||
|
if len(children) > 0:
|
||||||
|
last_child = children[-1]
|
||||||
|
old_tail = last_child.tail
|
||||||
|
last_child.tail = "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = old_tail if old_tail else "\n\t\t"
|
||||||
|
else:
|
||||||
|
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
|
||||||
|
child_objects.append(form_elem)
|
||||||
|
form_elem.tail = "\n\t\t"
|
||||||
|
|
||||||
|
# Update DefaultForm: explicitly with -Main, or automatically if this is the first form
|
||||||
|
existing_forms = child_objects.findall("md:Form", NSMAP)
|
||||||
|
is_first_form = len(existing_forms) == 1
|
||||||
|
|
||||||
|
if is_main or is_first_form:
|
||||||
|
default_form = root.find(".//md:DefaultForm", NSMAP)
|
||||||
|
if default_form is not None:
|
||||||
|
default_form.text = f"ExternalDataProcessor.{processor_name}.Form.{form_name}"
|
||||||
|
|
||||||
|
# Save with BOM
|
||||||
|
save_xml_with_bom(tree, root_xml_full)
|
||||||
|
|
||||||
|
print(f"[OK] Создана форма: {form_name}")
|
||||||
|
print(f" Метаданные: {form_meta_path}")
|
||||||
|
print(f" Описание: {form_xml_path}")
|
||||||
|
print(f" Модуль: {module_path}")
|
||||||
|
if is_main or is_first_form:
|
||||||
|
print(" DefaultForm обновлён")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-bsp-add-command
|
name: epf-bsp-add-command
|
||||||
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
|
description: Добавить команду в дополнительную обработку БСП
|
||||||
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-bsp-init
|
name: epf-bsp-init
|
||||||
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
|
description: Добавить функцию регистрации БСП (СведенияОВнешнейОбработке) в модуль объекта обработки
|
||||||
argument-hint: <ProcessorName> <Вид>
|
argument-hint: <ProcessorName> <Вид>
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Read
|
- Read
|
||||||
@@ -203,6 +203,6 @@ allowed-tools:
|
|||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|
||||||
- Добавить ещё команду: `/epf-bsp-add-command`
|
- Добавить ещё команду: `/epf-bsp-add-command`
|
||||||
- Добавить форму: `/form-add`
|
- Добавить форму: `/epf-add-form`
|
||||||
- Добавить макет: `/template-add`
|
- Добавить макет: `/template-add`
|
||||||
- Собрать EPF: `/epf-build`
|
- Собрать EPF: `/epf-build`
|
||||||
|
|||||||
@@ -34,20 +34,20 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -62,8 +62,8 @@ python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" <параметры>
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка обработки (файловая база)
|
# Сборка обработки (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Сборка внешней обработки/отчёта 1С из XML-исходников
|
Сборка внешней обработки/отчёта 1С из XML-исходников
|
||||||
@@ -71,79 +70,20 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,27 +121,6 @@ $tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
|
||||||
$srcDir = Split-Path $SourceFile -Parent
|
|
||||||
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
|
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
candidates.sort()
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
return candidates[-1]
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -118,10 +51,6 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Auto-create stub database if no connection specified ---
|
# --- Auto-create stub database if no connection specified ---
|
||||||
auto_created_base = None
|
auto_created_base = None
|
||||||
@@ -155,29 +84,6 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if engine == "ibcmd":
|
|
||||||
# --- ibcmd branch: build EPF/ERF via config import --out ---
|
|
||||||
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
|
|
||||||
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"External data processor/report built successfully: {args.OutputFile}")
|
|
||||||
else:
|
|
||||||
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -1252,57 +1252,6 @@ $propsXml </Properties>$childObjLine
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
if ($stubEngine -eq "ibcmd") {
|
|
||||||
Write-Host "Creating infobase (ibcmd): $TempBasePath"
|
|
||||||
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
|
|
||||||
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
|
|
||||||
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
|
|
||||||
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
|
|
||||||
$ibArgs += "--data=$ibData"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
|
|
||||||
$ibOut = $__ib.Output
|
|
||||||
$ibRc = $__ib.ExitCode
|
|
||||||
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
|
|
||||||
if ($ibRc -ne 0) {
|
|
||||||
if ($ibOut) { Write-Host ($ibOut | Out-String) }
|
|
||||||
Write-Error "Failed to create stub infobase (code: $ibRc)"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
|
|
||||||
Write-Host "[OK] Stub database created: $TempBasePath"
|
|
||||||
Write-Host $TempBasePath
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 5. Create infobase ---
|
# --- 5. Create infobase ---
|
||||||
Write-Host "Creating infobase: $TempBasePath"
|
Write-Host "Creating infobase: $TempBasePath"
|
||||||
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -12,27 +12,6 @@ import tempfile
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def new_uuid():
|
def new_uuid():
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
@@ -1055,32 +1034,6 @@ def main():
|
|||||||
if register_columns:
|
if register_columns:
|
||||||
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
|
||||||
|
|
||||||
# Stub via ibcmd (one call: create [--import --apply])
|
|
||||||
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
if stub_engine == "ibcmd":
|
|
||||||
import shutil
|
|
||||||
print(f'Creating infobase (ibcmd): {temp_base}')
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="stub_data_")
|
|
||||||
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
|
|
||||||
if has_ref_types:
|
|
||||||
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
|
|
||||||
ib_args.append(f'--data={ib_data}')
|
|
||||||
result = run_ibcmd(ib_args, warn_no_user=False)
|
|
||||||
shutil.rmtree(ib_data, ignore_errors=True)
|
|
||||||
if result.returncode != 0:
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if has_ref_types:
|
|
||||||
import shutil
|
|
||||||
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
|
|
||||||
print(f'[OK] Stub database created: {temp_base}')
|
|
||||||
print(temp_base)
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Create infobase
|
# Create infobase
|
||||||
print(f'Creating infobase: {temp_base}')
|
print(f'Creating infobase: {temp_base}')
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
|
|||||||
@@ -33,20 +33,20 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
|
|
||||||
| Параметр | Обязательный | Описание |
|
| Параметр | Обязательный | Описание |
|
||||||
|----------|:------------:|----------|
|
|----------|:------------:|----------|
|
||||||
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
|
||||||
| `-InfoBasePath <путь>` | * | Файловая база |
|
| `-InfoBasePath <путь>` | * | Файловая база |
|
||||||
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
|
||||||
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
|
||||||
@@ -62,8 +62,8 @@ python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" <параметры>
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка обработки (файловая база)
|
# Разборка обработки (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
|
|
||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Разборка внешней обработки/отчёта 1С в XML-исходники
|
Разборка внешней обработки/отчёта 1С в XML-исходники
|
||||||
@@ -78,45 +77,20 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
|
|||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
function Find-ProjectV8Path {
|
|
||||||
$dir = (Get-Location).Path
|
|
||||||
while ($dir) {
|
|
||||||
$pf = Join-Path $dir ".v8-project.json"
|
|
||||||
if (Test-Path $pf) {
|
|
||||||
try {
|
|
||||||
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
if ($j.v8path) { return [string]$j.v8path }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$parent = Split-Path $dir -Parent
|
|
||||||
if (-not $parent -or $parent -eq $dir) { break }
|
|
||||||
$dir = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $V8Path) {
|
if (-not $V8Path) {
|
||||||
$V8Path = Find-ProjectV8Path
|
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
|
||||||
}
|
|
||||||
if (-not $V8Path) {
|
|
||||||
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
if ($found) {
|
if ($found) {
|
||||||
$V8Path = $found.FullName
|
$V8Path = $found.FullName
|
||||||
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
|
|
||||||
} else {
|
} else {
|
||||||
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
} elseif (Test-Path $V8Path -PathType Container) {
|
||||||
if (Test-Path $V8Path -PathType Container) {
|
|
||||||
$V8Path = Join-Path $V8Path "1cv8.exe"
|
$V8Path = Join-Path $V8Path "1cv8.exe"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-Path $V8Path)) {
|
if (-not (Test-Path $V8Path)) {
|
||||||
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
|
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,46 +101,6 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
|
|
||||||
function Invoke-IbcmdProcess {
|
|
||||||
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
|
|
||||||
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
|
|
||||||
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
|
|
||||||
param([string]$Exe, [string[]]$IbArgs)
|
|
||||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
||||||
$psi.FileName = $Exe
|
|
||||||
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
|
|
||||||
$psi.UseShellExecute = $false
|
|
||||||
$psi.CreateNoWindow = $true
|
|
||||||
$psi.RedirectStandardInput = $true
|
|
||||||
$psi.RedirectStandardOutput = $true
|
|
||||||
$psi.RedirectStandardError = $true
|
|
||||||
try {
|
|
||||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
|
|
||||||
} catch {}
|
|
||||||
$p = [System.Diagnostics.Process]::Start($psi)
|
|
||||||
$p.StandardInput.Close()
|
|
||||||
$out = $p.StandardOutput.ReadToEnd()
|
|
||||||
$err = $p.StandardError.ReadToEnd()
|
|
||||||
$p.WaitForExit()
|
|
||||||
if ($err) { $out += $err }
|
|
||||||
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
|
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
if (-not $InfoBasePath) {
|
|
||||||
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
if ($Format -eq "Plain") {
|
|
||||||
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if (-not (Test-Path $InputFile)) {
|
if (-not (Test-Path $InputFile)) {
|
||||||
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
|
||||||
@@ -183,26 +117,6 @@ $tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
|
|||||||
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if ($engine -eq "ibcmd") {
|
|
||||||
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
|
||||||
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
|
|
||||||
if ($UserName) { $arguments += "--user=$UserName" }
|
|
||||||
if ($Password) { $arguments += "--password=$Password" }
|
|
||||||
$arguments += "--data=$tempDir"
|
|
||||||
Write-Host "Running: ibcmd $($arguments -join ' ')"
|
|
||||||
$__ib = Invoke-IbcmdProcess $V8Path $arguments
|
|
||||||
$output = $__ib.Output
|
|
||||||
$exitCode = $__ib.ExitCode
|
|
||||||
if ($exitCode -eq 0) {
|
|
||||||
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
|
|
||||||
} else {
|
|
||||||
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
if ($output) { Write-Host ($output | Out-String) }
|
|
||||||
exit $exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- 1cv8 branch ---
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
$arguments = @("DESIGNER")
|
$arguments = @("DESIGNER")
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +1,37 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
|
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import atexit
|
|
||||||
import glob
|
import glob
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
def _find_project_v8path():
|
|
||||||
"""Walk up from CWD to find .v8-project.json and read its v8path."""
|
|
||||||
d = os.getcwd()
|
|
||||||
while True:
|
|
||||||
pf = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pf):
|
|
||||||
try:
|
|
||||||
with open(pf, encoding="utf-8-sig") as f:
|
|
||||||
data = json.load(f)
|
|
||||||
v = data.get("v8path")
|
|
||||||
if v:
|
|
||||||
return v
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
return None
|
|
||||||
d = parent
|
|
||||||
|
|
||||||
|
|
||||||
def _version_dir(p):
|
|
||||||
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
|
|
||||||
parent = os.path.dirname(p)
|
|
||||||
if os.path.basename(parent).lower() == "bin":
|
|
||||||
parent = os.path.dirname(parent)
|
|
||||||
return os.path.basename(parent)
|
|
||||||
|
|
||||||
|
|
||||||
def _version_key(p):
|
|
||||||
"""Numeric sort key from version dir name."""
|
|
||||||
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_v8path(v8path):
|
def resolve_v8path(v8path):
|
||||||
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
|
"""Resolve path to 1cv8.exe."""
|
||||||
if not v8path:
|
if not v8path:
|
||||||
v8path = _find_project_v8path()
|
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
||||||
if not v8path:
|
|
||||||
if os.name == "nt":
|
|
||||||
candidates = (
|
|
||||||
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
|
|
||||||
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
|
|
||||||
candidates = glob.glob("/opt/1cv8/*/1cv8")
|
|
||||||
if candidates:
|
if candidates:
|
||||||
v8path = max(candidates, key=_version_key)
|
candidates.sort()
|
||||||
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
|
return candidates[-1]
|
||||||
else:
|
else:
|
||||||
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
|
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if os.path.isdir(v8path):
|
elif os.path.isdir(v8path):
|
||||||
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
|
v8path = os.path.join(v8path, "1cv8.exe")
|
||||||
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
|
|
||||||
v8path = os.path.join(v8path, exe)
|
|
||||||
if not os.path.isfile(v8path):
|
if not os.path.isfile(v8path):
|
||||||
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
|
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return v8path
|
return v8path
|
||||||
|
|
||||||
|
|
||||||
IBCMD_NOUSER_HINT = (
|
|
||||||
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
|
|
||||||
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
|
|
||||||
"call may block instead of failing. If it does not return promptly, abort and "
|
|
||||||
"re-run with -UserName and -Password.\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
|
|
||||||
"""Run an ibcmd command non-interactively.
|
|
||||||
|
|
||||||
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
|
|
||||||
On Windows without -UserName ibcmd reads the console directly and may still block —
|
|
||||||
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
|
|
||||||
"""
|
|
||||||
if warn_no_user and os.name == "nt" and not has_username:
|
|
||||||
sys.stderr.write(IBCMD_NOUSER_HINT)
|
|
||||||
sys.stderr.flush()
|
|
||||||
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
@@ -124,20 +57,12 @@ def main():
|
|||||||
|
|
||||||
# --- Resolve V8Path ---
|
# --- Resolve V8Path ---
|
||||||
v8path = resolve_v8path(args.V8Path)
|
v8path = resolve_v8path(args.V8Path)
|
||||||
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
|
|
||||||
|
|
||||||
# --- Validate database connection ---
|
# --- Validate database connection ---
|
||||||
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
|
||||||
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
|
||||||
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if engine == "ibcmd":
|
|
||||||
if not args.InfoBasePath:
|
|
||||||
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
if args.Format == "Plain":
|
|
||||||
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# --- Validate input file ---
|
# --- Validate input file ---
|
||||||
if not os.path.isfile(args.InputFile):
|
if not os.path.isfile(args.InputFile):
|
||||||
@@ -153,28 +78,6 @@ def main():
|
|||||||
os.makedirs(temp_dir, exist_ok=True)
|
os.makedirs(temp_dir, exist_ok=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if engine == "ibcmd":
|
|
||||||
# --- ibcmd branch: dump EPF/ERF via config export --file ---
|
|
||||||
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
|
|
||||||
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
|
|
||||||
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
|
|
||||||
if args.UserName:
|
|
||||||
arguments.append(f"--user={args.UserName}")
|
|
||||||
if args.Password:
|
|
||||||
arguments.append(f"--password={args.Password}")
|
|
||||||
arguments.append(f"--data={ib_data}")
|
|
||||||
print(f"Running: ibcmd {' '.join(arguments)}")
|
|
||||||
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
|
|
||||||
else:
|
|
||||||
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
|
|
||||||
if result.stdout:
|
|
||||||
print(result.stdout)
|
|
||||||
if result.stderr:
|
|
||||||
print(result.stderr, file=sys.stderr)
|
|
||||||
sys.exit(result.returncode)
|
|
||||||
|
|
||||||
# --- Build arguments ---
|
# --- Build arguments ---
|
||||||
arguments = ["DESIGNER"]
|
arguments = ["DESIGNER"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: epf-init
|
name: epf-init
|
||||||
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
|
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников)
|
||||||
argument-hint: <Name> [Synonym]
|
argument-hint: <Name> [Synonym]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -30,12 +30,12 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
powershell.exe -NoProfile -File .claude/skills/epf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|
||||||
- Добавить форму: `/form-add`
|
- Добавить форму: `/epf-add-form`
|
||||||
- Добавить макет: `/template-add`
|
- Добавить макет: `/template-add`
|
||||||
- Добавить справку: `/help-add`
|
- Добавить справку: `/help-add`
|
||||||
- Собрать EPF: `/epf-build`
|
- Собрать EPF: `/epf-build`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.0 — Init 1C external data processor scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -10,8 +10,6 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-init v1.1 — Init 1C external data processor scaffold
|
# epf-init v1.0 — Init 1C external data processor scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external data processor."""
|
"""Generates minimal XML source files for a 1C external data processor."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка"
|
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка"
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/epf-validate.py" -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МояОбработка/МояОбработка.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.1 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
[Alias('Path')]
|
|
||||||
[string]$ObjectPath,
|
[string]$ObjectPath,
|
||||||
|
|
||||||
[switch]$Detailed,
|
[switch]$Detailed,
|
||||||
@@ -185,8 +184,8 @@ if ($root.NamespaceURI -ne $expectedNs) {
|
|||||||
$version = $root.GetAttribute("version")
|
$version = $root.GetAttribute("version")
|
||||||
if (-not $version) {
|
if (-not $version) {
|
||||||
Report-Warn "1. Missing version attribute on MetaDataObject"
|
Report-Warn "1. Missing version attribute on MetaDataObject"
|
||||||
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
|
} elseif ($version -ne "2.17" -and $version -ne "2.20") {
|
||||||
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
|
Report-Warn "1. Unusual version '$version' (expected 2.17 or 2.20)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Detect type: ExternalDataProcessor or ExternalReport
|
# Detect type: ExternalDataProcessor or ExternalReport
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# epf-validate v1.2 — Validate 1C external data processor / report structure
|
# epf-validate v1.1 — Validate 1C external data processor / report structure
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ def main():
|
|||||||
sys.stdout.reconfigure(encoding="utf-8")
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
sys.stderr.reconfigure(encoding="utf-8")
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
|
parser = argparse.ArgumentParser(description="Validate 1C external data processor/report structure", allow_abbrev=False)
|
||||||
parser.add_argument("-ObjectPath", "-Path", required=True)
|
parser.add_argument("-ObjectPath", required=True)
|
||||||
parser.add_argument("-Detailed", action="store_true")
|
parser.add_argument("-Detailed", action="store_true")
|
||||||
parser.add_argument("-MaxErrors", type=int, default=30)
|
parser.add_argument("-MaxErrors", type=int, default=30)
|
||||||
parser.add_argument("-OutFile", default=None)
|
parser.add_argument("-OutFile", default=None)
|
||||||
@@ -165,8 +165,8 @@ def main():
|
|||||||
version = root.get("version", "")
|
version = root.get("version", "")
|
||||||
if not version:
|
if not version:
|
||||||
report_warn("1. Missing version attribute on MetaDataObject")
|
report_warn("1. Missing version attribute on MetaDataObject")
|
||||||
elif version not in ("2.17", "2.20", "2.21"):
|
elif version not in ("2.17", "2.20"):
|
||||||
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
|
report_warn(f"1. Unusual version '{version}' (expected 2.17 or 2.20)")
|
||||||
|
|
||||||
# Detect type
|
# Detect type
|
||||||
child_elements = []
|
child_elements = []
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
|
||||||
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -42,7 +42,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-build:
|
Используй общий скрипт из epf-build:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" <параметр
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Сборка отчёта (файловая база)
|
# Сборка отчёта (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
powershell.exe -NoProfile -File .claude/skills/epf-build/scripts/epf-build.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ allowed-tools:
|
|||||||
5. Если ветка не совпала — используй `default`
|
5. Если ветка не совпала — используй `default`
|
||||||
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
|
||||||
|
|
||||||
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
|
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
|
||||||
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
|
||||||
|
|
||||||
## Команда
|
## Команда
|
||||||
@@ -41,7 +41,7 @@ allowed-tools:
|
|||||||
Используй общий скрипт из epf-dump:
|
Используй общий скрипт из epf-dump:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры>
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 <параметры>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Параметры скрипта
|
### Параметры скрипта
|
||||||
@@ -64,8 +64,8 @@ python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" <параметры
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Разборка отчёта (файловая база)
|
# Разборка отчёта (файловая база)
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
|
|
||||||
# Серверная база
|
# Серверная база
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.py" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
powershell.exe -NoProfile -File .claude/skills/epf-dump/scripts/epf-dump.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: erf-init
|
name: erf-init
|
||||||
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
|
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников)
|
||||||
argument-hint: <Name> [Synonym] [--with-skd]
|
argument-hint: <Name> [Synonym] [--with-skd]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -31,7 +31,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/init.py" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
powershell.exe -NoProfile -File .claude/skills/erf-init/scripts/init.ps1 -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Дальнейшие шаги
|
## Дальнейшие шаги
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.0 — Init 1C external report scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -12,8 +12,6 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
$uuid1 = [guid]::NewGuid().ToString()
|
$uuid1 = [guid]::NewGuid().ToString()
|
||||||
$uuid2 = [guid]::NewGuid().ToString()
|
$uuid2 = [guid]::NewGuid().ToString()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# erf-init v1.1 — Init 1C external report scaffold
|
# erf-init v1.0 — Init 1C external report scaffold
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
"""Generates minimal XML source files for a 1C external report."""
|
"""Generates minimal XML source files for a 1C external report."""
|
||||||
import sys, os, argparse, uuid
|
import sys, os, argparse, uuid
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт"
|
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт"
|
||||||
python "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.py" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
powershell.exe -NoProfile -File .claude/skills/epf-validate/scripts/epf-validate.ps1 -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: form-add
|
name: form-add
|
||||||
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
|
description: Добавить управляемую форму к объекту конфигурации 1С
|
||||||
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- Bash
|
- Bash
|
||||||
@@ -32,7 +32,7 @@ allowed-tools:
|
|||||||
## Команда
|
## Команда
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/form-add.py" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
powershell.exe -NoProfile -File .claude/skills/form-add/scripts/form-add.ps1 -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Purpose — назначение формы
|
## Purpose — назначение формы
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# form-add v1.8 — Add managed form to 1C config object
|
# form-add v1.3 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory)]
|
[Parameter(Mandatory)]
|
||||||
@@ -15,126 +15,6 @@ param(
|
|||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
||||||
|
|
||||||
# --- Support guard (Ext/ParentConfigurations.bin) ---
|
|
||||||
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
|
|
||||||
# read-only configs unless allowed. Trigger = bin present; reaction from
|
|
||||||
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
|
|
||||||
# throws — guard errors degrade to allow.
|
|
||||||
function Get-RootUuid([string]$xmlPath) {
|
|
||||||
if (-not (Test-Path $xmlPath)) { return $null }
|
|
||||||
try {
|
|
||||||
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
|
|
||||||
$el = $mx.DocumentElement.FirstChild
|
|
||||||
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
|
|
||||||
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
|
|
||||||
} catch {}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Find-V8Project([string]$startDir) {
|
|
||||||
$d = $startDir
|
|
||||||
for ($i = 0; $i -lt 20 -and $d; $i++) {
|
|
||||||
$pj = Join-Path $d ".v8-project.json"
|
|
||||||
if (Test-Path $pj) { return $pj }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
function Get-EditMode([string]$cfgDir) {
|
|
||||||
try {
|
|
||||||
$pj = Find-V8Project (Get-Location).Path
|
|
||||||
if (-not $pj) { $pj = Find-V8Project $cfgDir }
|
|
||||||
if (-not $pj) { return 'deny' }
|
|
||||||
$proj = Get-Content -Raw $pj | ConvertFrom-Json
|
|
||||||
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
|
|
||||||
if ($proj.databases) {
|
|
||||||
foreach ($db in $proj.databases) {
|
|
||||||
if ($db.configSrc) {
|
|
||||||
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
|
|
||||||
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
|
|
||||||
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
|
|
||||||
return 'deny'
|
|
||||||
} catch { return 'deny' }
|
|
||||||
}
|
|
||||||
function Assert-EditAllowed([string]$targetPath, [string]$require) {
|
|
||||||
try {
|
|
||||||
$rp = $targetPath
|
|
||||||
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
|
|
||||||
$elemUuid = Get-RootUuid $rp
|
|
||||||
$cfgDir = $null; $binPath = $null
|
|
||||||
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
|
|
||||||
for ($i = 0; $i -lt 12 -and $d; $i++) {
|
|
||||||
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
|
|
||||||
if (-not $cfgDir) {
|
|
||||||
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
|
|
||||||
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
|
|
||||||
}
|
|
||||||
if ($elemUuid -and $cfgDir) { break }
|
|
||||||
$parent = [System.IO.Path]::GetDirectoryName($d)
|
|
||||||
if ($parent -eq $d) { break }
|
|
||||||
$d = $parent
|
|
||||||
}
|
|
||||||
# New object (no element file): fall back to config root uuid.
|
|
||||||
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
|
|
||||||
if (-not $binPath -or -not (Test-Path $binPath)) { return }
|
|
||||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
|
||||||
if ($bytes.Length -le 32) { return }
|
|
||||||
$start = 0
|
|
||||||
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
|
|
||||||
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
|
|
||||||
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
|
|
||||||
if (-not $hm.Success) { return }
|
|
||||||
$G = [int]$hm.Groups[1].Value
|
|
||||||
$K = [int]$hm.Groups[2].Value
|
|
||||||
if ($K -eq 0) { return }
|
|
||||||
$best = $null
|
|
||||||
if ($elemUuid) {
|
|
||||||
$u = [regex]::Escape($elemUuid.ToLower())
|
|
||||||
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
|
|
||||||
$f1 = [int]$m.Groups[1].Value
|
|
||||||
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$blocked = $false; $code = ""; $reason = ""
|
|
||||||
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
|
|
||||||
elseif ($require -eq 'removed') {
|
|
||||||
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
|
|
||||||
}
|
|
||||||
if (-not $blocked) { return }
|
|
||||||
$mode = Get-EditMode $cfgDir
|
|
||||||
if ($mode -eq 'off') { return }
|
|
||||||
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
|
|
||||||
# latter throws and would be swallowed by this function's own catch.
|
|
||||||
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
|
|
||||||
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if ($code -eq "capability-off") {
|
|
||||||
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
|
|
||||||
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
} elseif ($code -eq "not-removed") {
|
|
||||||
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
|
|
||||||
} else {
|
|
||||||
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
|
|
||||||
}
|
|
||||||
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
|
|
||||||
exit 1
|
|
||||||
} catch { return }
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Detect XML format version ---
|
# --- Detect XML format version ---
|
||||||
|
|
||||||
@@ -173,7 +53,6 @@ if (-not (Test-Path $ObjectPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$objectXmlFull = Resolve-Path $ObjectPath
|
$objectXmlFull = Resolve-Path $ObjectPath
|
||||||
Assert-EditAllowed $objectXmlFull.Path 'editable'
|
|
||||||
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
|
||||||
|
|
||||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||||
@@ -195,7 +74,7 @@ $supportedTypes = @(
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
|
"ExchangePlan", "BusinessProcess", "Task"
|
||||||
)
|
)
|
||||||
|
|
||||||
$objectType = $null
|
$objectType = $null
|
||||||
@@ -340,6 +219,9 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
|
<Events>
|
||||||
|
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
||||||
|
</Events>
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="Список" id="1">
|
<Attribute name="Список" id="1">
|
||||||
@@ -365,6 +247,9 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
|
<Events>
|
||||||
|
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
||||||
|
</Events>
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="$mainAttrName" id="1">
|
<Attribute name="$mainAttrName" id="1">
|
||||||
@@ -400,25 +285,23 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
|
|||||||
|
|
||||||
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
$savedDataLine = ""
|
|
||||||
if ($objectType -notin $processorLikeTypes) {
|
|
||||||
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
|
|
||||||
}
|
|
||||||
|
|
||||||
$formXml = @"
|
$formXml = @"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Form $formNsDecl version="$($script:formatVersion)">
|
<Form $formNsDecl version="$($script:formatVersion)">
|
||||||
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
|
||||||
<Autofill>true</Autofill>
|
<Autofill>true</Autofill>
|
||||||
</AutoCommandBar>
|
</AutoCommandBar>
|
||||||
|
<Events>
|
||||||
|
<Event name="OnCreateAtServer">ПриСозданииНаСервере</Event>
|
||||||
|
</Events>
|
||||||
<ChildItems/>
|
<ChildItems/>
|
||||||
<Attributes>
|
<Attributes>
|
||||||
<Attribute name="$mainAttrName" id="1">
|
<Attribute name="$mainAttrName" id="1">
|
||||||
<Type>
|
<Type>
|
||||||
<v8:Type>cfg:$mainAttrType</v8:Type>
|
<v8:Type>cfg:$mainAttrType</v8:Type>
|
||||||
</Type>
|
</Type>
|
||||||
<MainAttribute>true</MainAttribute>$savedDataLine
|
<MainAttribute>true</MainAttribute>
|
||||||
|
<SavedData>true</SavedData>
|
||||||
</Attribute>
|
</Attribute>
|
||||||
</Attributes>
|
</Attributes>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -438,6 +321,11 @@ $modulePath = Join-Path $formModuleDir "Module.bsl"
|
|||||||
$moduleBsl = @"
|
$moduleBsl = @"
|
||||||
#Область ОбработчикиСобытийФормы
|
#Область ОбработчикиСобытийФормы
|
||||||
|
|
||||||
|
&НаСервере
|
||||||
|
Процедура ПриСозданииНаСервере(Отказ, СтандартнаяОбработка)
|
||||||
|
|
||||||
|
КонецПроцедуры
|
||||||
|
|
||||||
#КонецОбласти
|
#КонецОбласти
|
||||||
|
|
||||||
#Область ОбработчикиСобытийЭлементовФормы
|
#Область ОбработчикиСобытийЭлементовФормы
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# form-add v1.8 — Add managed form to 1C config object
|
# form-add v1.3 — Add managed form to 1C config object
|
||||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@@ -11,166 +10,6 @@ import uuid
|
|||||||
|
|
||||||
from lxml import etree
|
from lxml import etree
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
|
|
||||||
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
|
|
||||||
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
|
|
||||||
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
def _sg_root_uuid(xml_path):
|
|
||||||
if not os.path.isfile(xml_path):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
mx = etree.parse(xml_path).getroot()
|
|
||||||
for child in mx:
|
|
||||||
if isinstance(child.tag, str) and child.get("uuid"):
|
|
||||||
return child.get("uuid")
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_find_v8project(start_dir):
|
|
||||||
d = start_dir
|
|
||||||
for _ in range(20):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
pj = os.path.join(d, ".v8-project.json")
|
|
||||||
if os.path.isfile(pj):
|
|
||||||
return pj
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _sg_get_edit_mode(cfg_dir):
|
|
||||||
try:
|
|
||||||
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
|
|
||||||
if not pj:
|
|
||||||
return "deny"
|
|
||||||
proj = json.loads(open(pj, encoding="utf-8-sig").read())
|
|
||||||
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
|
|
||||||
for db in proj.get("databases", []):
|
|
||||||
src = db.get("configSrc")
|
|
||||||
if src:
|
|
||||||
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
|
|
||||||
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
|
|
||||||
if db.get("editingAllowedCheck"):
|
|
||||||
return db["editingAllowedCheck"]
|
|
||||||
if proj.get("editingAllowedCheck"):
|
|
||||||
return proj["editingAllowedCheck"]
|
|
||||||
return "deny"
|
|
||||||
except Exception:
|
|
||||||
return "deny"
|
|
||||||
|
|
||||||
|
|
||||||
def assert_edit_allowed(target_path, require):
|
|
||||||
try:
|
|
||||||
rp = os.path.abspath(target_path)
|
|
||||||
elem_uuid = _sg_root_uuid(rp)
|
|
||||||
cfg_dir = None
|
|
||||||
bin_path = None
|
|
||||||
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
|
|
||||||
for _ in range(12):
|
|
||||||
if not d:
|
|
||||||
break
|
|
||||||
if not elem_uuid:
|
|
||||||
elem_uuid = _sg_root_uuid(d + ".xml")
|
|
||||||
if not cfg_dir:
|
|
||||||
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
|
||||||
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
|
|
||||||
cfg_dir = d
|
|
||||||
bin_path = cand
|
|
||||||
if elem_uuid and cfg_dir:
|
|
||||||
break
|
|
||||||
parent = os.path.dirname(d)
|
|
||||||
if parent == d:
|
|
||||||
break
|
|
||||||
d = parent
|
|
||||||
if not elem_uuid and cfg_dir:
|
|
||||||
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
|
|
||||||
if not bin_path or not os.path.exists(bin_path):
|
|
||||||
return
|
|
||||||
data = open(bin_path, "rb").read()
|
|
||||||
if len(data) <= 32:
|
|
||||||
return
|
|
||||||
if data[:3] == b"\xef\xbb\xbf":
|
|
||||||
data = data[3:]
|
|
||||||
text = data.decode("utf-8", "replace")
|
|
||||||
h = re.match(r"\{6,(\d+),(\d+),", text)
|
|
||||||
if not h:
|
|
||||||
return
|
|
||||||
g = int(h.group(1))
|
|
||||||
k = int(h.group(2))
|
|
||||||
if k == 0:
|
|
||||||
return
|
|
||||||
best = None
|
|
||||||
if elem_uuid:
|
|
||||||
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
|
|
||||||
f1 = int(m.group(1))
|
|
||||||
if best is None or f1 < best:
|
|
||||||
best = f1
|
|
||||||
blocked = False
|
|
||||||
code = ""
|
|
||||||
reason = ""
|
|
||||||
if g == 1:
|
|
||||||
blocked = True
|
|
||||||
code = "capability-off"
|
|
||||||
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
|
|
||||||
elif require == "removed":
|
|
||||||
if best is not None and best != 2:
|
|
||||||
blocked = True
|
|
||||||
code = "not-removed"
|
|
||||||
reason = "объект не снят с поддержки — удаление сломает обновления"
|
|
||||||
else:
|
|
||||||
if best is not None and best == 0:
|
|
||||||
blocked = True
|
|
||||||
code = "locked"
|
|
||||||
reason = "объект на замке — редактирование сломает обновления"
|
|
||||||
if not blocked:
|
|
||||||
return
|
|
||||||
mode = _sg_get_edit_mode(cfg_dir)
|
|
||||||
if mode == "off":
|
|
||||||
return
|
|
||||||
if mode == "warn":
|
|
||||||
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
|
|
||||||
return
|
|
||||||
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
|
|
||||||
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
|
|
||||||
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
|
|
||||||
if code == "capability-off":
|
|
||||||
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
|
|
||||||
fix = (
|
|
||||||
"Либо снять защиту явно (навык support-edit, два шага):\n"
|
|
||||||
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
|
|
||||||
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
|
|
||||||
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
|
|
||||||
)
|
|
||||||
elif code == "not-removed":
|
|
||||||
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
|
|
||||||
fix = (
|
|
||||||
"Либо сначала снять объект с поддержки, затем удалять:\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
|
|
||||||
fix = (
|
|
||||||
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
|
|
||||||
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
|
|
||||||
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
|
|
||||||
)
|
|
||||||
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
|
|
||||||
sys.exit(1)
|
|
||||||
except SystemExit:
|
|
||||||
raise
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
NSMAP = {
|
NSMAP = {
|
||||||
"md": "http://v8.1c.ru/8.3/MDClasses",
|
"md": "http://v8.1c.ru/8.3/MDClasses",
|
||||||
"v8": "http://v8.1c.ru/8.1/data/core",
|
"v8": "http://v8.1c.ru/8.1/data/core",
|
||||||
@@ -245,7 +84,6 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
object_xml_full = os.path.abspath(object_path)
|
object_xml_full = os.path.abspath(object_path)
|
||||||
assert_edit_allowed(object_xml_full, "editable")
|
|
||||||
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
format_version = detect_format_version(os.path.dirname(object_xml_full))
|
||||||
|
|
||||||
parser_xml = etree.XMLParser(remove_blank_text=False)
|
parser_xml = etree.XMLParser(remove_blank_text=False)
|
||||||
@@ -256,7 +94,7 @@ def main():
|
|||||||
"Document", "Catalog", "DataProcessor", "Report",
|
"Document", "Catalog", "DataProcessor", "Report",
|
||||||
"ExternalDataProcessor", "ExternalReport",
|
"ExternalDataProcessor", "ExternalReport",
|
||||||
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
|
||||||
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
|
"ExchangePlan", "BusinessProcess", "Task",
|
||||||
]
|
]
|
||||||
|
|
||||||
object_type = None
|
object_type = None
|
||||||
@@ -410,6 +248,9 @@ def main():
|
|||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
|
'\t<Events>\n'
|
||||||
|
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
||||||
|
'\t</Events>\n'
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
|
||||||
@@ -436,6 +277,9 @@ def main():
|
|||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
|
'\t<Events>\n'
|
||||||
|
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
||||||
|
'\t</Events>\n'
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||||
@@ -471,17 +315,15 @@ def main():
|
|||||||
|
|
||||||
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
|
||||||
|
|
||||||
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
|
|
||||||
saved_data_line = ''
|
|
||||||
if object_type not in processor_like_types:
|
|
||||||
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
|
|
||||||
|
|
||||||
form_xml = (
|
form_xml = (
|
||||||
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
f'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||||
f'<Form {form_ns_decl} version="{format_version}">\n'
|
f'<Form {form_ns_decl} version="{format_version}">\n'
|
||||||
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
|
||||||
'\t\t<Autofill>true</Autofill>\n'
|
'\t\t<Autofill>true</Autofill>\n'
|
||||||
'\t</AutoCommandBar>\n'
|
'\t</AutoCommandBar>\n'
|
||||||
|
'\t<Events>\n'
|
||||||
|
'\t\t<Event name="OnCreateAtServer">\u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435</Event>\n'
|
||||||
|
'\t</Events>\n'
|
||||||
'\t<ChildItems/>\n'
|
'\t<ChildItems/>\n'
|
||||||
'\t<Attributes>\n'
|
'\t<Attributes>\n'
|
||||||
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
|
||||||
@@ -489,7 +331,7 @@ def main():
|
|||||||
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
|
||||||
'\t\t\t</Type>\n'
|
'\t\t\t</Type>\n'
|
||||||
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
'\t\t\t<MainAttribute>true</MainAttribute>\n'
|
||||||
f'{saved_data_line}'
|
'\t\t\t<SavedData>true</SavedData>\n'
|
||||||
'\t\t</Attribute>\n'
|
'\t\t</Attribute>\n'
|
||||||
'\t</Attributes>\n'
|
'\t</Attributes>\n'
|
||||||
'</Form>'
|
'</Form>'
|
||||||
@@ -507,6 +349,11 @@ def main():
|
|||||||
module_bsl = (
|
module_bsl = (
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
'\n'
|
'\n'
|
||||||
|
'&\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435\n'
|
||||||
|
'\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u0430 \u041f\u0440\u0438\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0438\u041d\u0430\u0421\u0435\u0440\u0432\u0435\u0440\u0435(\u041e\u0442\u043a\u0430\u0437, \u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u0430\u044f\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430)\n'
|
||||||
|
'\n'
|
||||||
|
'\u041a\u043e\u043d\u0435\u0446\u041f\u0440\u043e\u0446\u0435\u0434\u0443\u0440\u044b\n'
|
||||||
|
'\n'
|
||||||
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
'#\u041a\u043e\u043d\u0435\u0446\u041e\u0431\u043b\u0430\u0441\u0442\u0438\n'
|
||||||
'\n'
|
'\n'
|
||||||
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
'#\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a\u0438\u0421\u043e\u0431\u044b\u0442\u0438\u0439\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u043e\u0432\u0424\u043e\u0440\u043c\u044b\n'
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ allowed-tools:
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Режим JSON DSL
|
# Режим JSON DSL
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -JsonPath "<json>" -OutputPath "<Form.xml>"
|
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -JsonPath "<json>" -OutputPath "<Form.xml>"
|
||||||
|
|
||||||
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
|
# Режим from-object (объект и purpose выводятся из OutputPath; Document и Catalog)
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
|
powershell.exe -NoProfile -File .claude/skills/form-compile/scripts/form-compile.ps1 -FromObject -OutputPath "<.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml>"
|
||||||
```
|
```
|
||||||
|
|
||||||
## JSON DSL — справка
|
## JSON DSL — справка
|
||||||
@@ -61,11 +61,9 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
|
|
||||||
| DSL ключ | XML элемент | Значение ключа |
|
| DSL ключ | XML элемент | Значение ключа |
|
||||||
|--------------|-------------------|---------------------------------------------------|
|
|--------------|-------------------|---------------------------------------------------|
|
||||||
| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
|
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
|
||||||
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
|
|
||||||
| `"input"` | InputField | имя элемента |
|
| `"input"` | InputField | имя элемента |
|
||||||
| `"check"` | CheckBoxField | имя |
|
| `"check"` | CheckBoxField | имя |
|
||||||
| `"radio"` | RadioButtonField | имя |
|
|
||||||
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
| `"label"` | LabelDecoration | имя (текст задаётся через `title`) |
|
||||||
| `"labelField"` | LabelField | имя |
|
| `"labelField"` | LabelField | имя |
|
||||||
| `"table"` | Table | имя |
|
| `"table"` | Table | имя |
|
||||||
@@ -76,22 +74,21 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| `"picField"` | PictureField | имя |
|
| `"picField"` | PictureField | имя |
|
||||||
| `"calendar"` | CalendarField | имя |
|
| `"calendar"` | CalendarField | имя |
|
||||||
| `"cmdBar"` | CommandBar | имя |
|
| `"cmdBar"` | CommandBar | имя |
|
||||||
| `"autoCmdBar"` | AutoCommandBar формы | имя — наполняет главную АКП формы (id=-1), не попадает в `<ChildItems>` |
|
|
||||||
| `"popup"` | Popup | имя |
|
| `"popup"` | Popup | имя |
|
||||||
|
|
||||||
### Общие свойства (все типы элементов)
|
### Общие свойства (все типы элементов)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
|
| `name` | Переопределить имя (по умолчанию = значение ключа типа) |
|
||||||
| `title` | Заголовок элемента |
|
| `title` | Заголовок элемента |
|
||||||
| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
|
|
||||||
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
| `visible: false` | Скрыть (синоним: `hidden: true`) |
|
||||||
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
|
||||||
| `readOnly: true` | Только чтение |
|
| `readOnly: true` | Только чтение |
|
||||||
| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
|
| `on: [...]` | События с автоименованием обработчиков |
|
||||||
|
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` |
|
||||||
|
|
||||||
### Допустимые имена событий (`events`)
|
### Допустимые имена событий (`on`)
|
||||||
|
|
||||||
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
|
||||||
|
|
||||||
@@ -99,7 +96,7 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
|
|
||||||
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
**input / picField**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `AutoComplete`, `TextEditEnd`, `Clearing`, `Creating`, `EditTextChange`
|
||||||
|
|
||||||
**check / radio**: `OnChange`
|
**check**: `OnChange`
|
||||||
|
|
||||||
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
**table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `ValueChoice`, `BeforeAddRow`, `BeforeDeleteRow`, `AfterDeleteRow`, `BeforeRowChange`, `BeforeEditEnd`, `OnActivateRow`, `OnActivateCell`, `Drag`, `DragStart`, `DragCheck`, `DragEnd`
|
||||||
|
|
||||||
@@ -116,7 +113,7 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| Ключ | Описание | Пример |
|
| Ключ | Описание | Пример |
|
||||||
|------|----------|--------|
|
|------|----------|--------|
|
||||||
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
|
||||||
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
|
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
|
||||||
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
|
||||||
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
|
||||||
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
|
||||||
@@ -127,8 +124,7 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
| `skipOnInput: true` | Пропускать при обходе Tab | |
|
||||||
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
| `inputHint` | Подсказка в пустом поле | `"Введите наименование..."` |
|
||||||
| `width` / `height` | Размер | числа |
|
| `width` / `height` | Размер | числа |
|
||||||
| `autoMaxWidth: false` | Снять авто-ограничение ширины (поле растянется) | |
|
| `autoMaxWidth: false` | Отключить авто-ширину | для фиксированных полей |
|
||||||
| `maxWidth` / `maxHeight` | Жёсткое ограничение размера | числа; обычно вместе с `autoMaxWidth: false` |
|
|
||||||
| `horizontalStretch: true` | Растягивать по ширине | |
|
| `horizontalStretch: true` | Растягивать по ширине | |
|
||||||
|
|
||||||
### Чекбокс (check)
|
### Чекбокс (check)
|
||||||
@@ -138,37 +134,6 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| `path` | DataPath |
|
| `path` | DataPath |
|
||||||
| `titleLocation` | Размещение заголовка |
|
| `titleLocation` | Размещение заголовка |
|
||||||
|
|
||||||
### Поле переключателя (radio)
|
|
||||||
|
|
||||||
Радиокнопки или тумблер для выбора одного значения из списка.
|
|
||||||
|
|
||||||
| Ключ | Описание | Пример |
|
|
||||||
|------|----------|--------|
|
|
||||||
| `path` | DataPath — привязка к реквизиту | `"СпособКурса"` |
|
|
||||||
| `radioButtonType` | Вид переключателя | `"Auto"` (по умолчанию), `"RadioButtons"`, `"Tumbler"` |
|
|
||||||
| `columnsCount` | Число колонок раскладки | `1`, `2`, ... |
|
|
||||||
| `titleLocation` | Размещение заголовка | по умолчанию `"none"` |
|
|
||||||
| `choiceList` | Список вариантов: массив `{value, presentation}` | см. ниже |
|
|
||||||
|
|
||||||
`choiceList[*]`:
|
|
||||||
|
|
||||||
| Ключ | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `value` | Значение варианта. Строка/число/булево; для перечисления — `"Enum.ИмяТипа.EnumValue.ИмяЗначения"` |
|
|
||||||
| `presentation` | Текст рядом с переключателем. Строка (русский) либо объект `{ru, en, ...}` для мультиязычности |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"radio": "СпособКурса",
|
|
||||||
"path": "Объект.СпособУстановкиКурса",
|
|
||||||
"radioButtonType": "Auto",
|
|
||||||
"choiceList": [
|
|
||||||
{ "value": "Enum.СпособыКурса.EnumValue.Авто", "presentation": { "ru": "Автоматически", "en": "Automatic" } },
|
|
||||||
{ "value": "Enum.СпособыКурса.EnumValue.Ручной", "presentation": "вручную" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Надпись-декорация (label)
|
### Надпись-декорация (label)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
@@ -179,14 +144,12 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
|
|
||||||
### Группа (group)
|
### Группа (group)
|
||||||
|
|
||||||
Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
|
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`.
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
|
|
||||||
| `showTitle: true` | Показывать заголовок группы |
|
| `showTitle: true` | Показывать заголовок группы |
|
||||||
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
|
| `united: false` | Не объединять рамку |
|
||||||
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
|
|
||||||
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
|
||||||
| `children: [...]` | Вложенные элементы |
|
| `children: [...]` | Вложенные элементы |
|
||||||
|
|
||||||
@@ -203,8 +166,8 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| `height` | Высота в строках таблицы |
|
| `height` | Высота в строках таблицы |
|
||||||
| `header: false` | Скрыть шапку |
|
| `header: false` | Скрыть шапку |
|
||||||
| `footer: true` | Показать подвал |
|
| `footer: true` | Показать подвал |
|
||||||
| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` |
|
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||||
| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` |
|
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
|
||||||
| `choiceMode: true` | Режим выбора (для форм выбора) |
|
| `choiceMode: true` | Режим выбора (для форм выбора) |
|
||||||
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
|
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
|
||||||
| `enableDrag: true` | Разрешить перетаскивание |
|
| `enableDrag: true` | Разрешить перетаскивание |
|
||||||
@@ -212,44 +175,6 @@ python "${CLAUDE_SKILL_DIR}/scripts/form-compile.py" -FromObject -OutputPath "<.
|
|||||||
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
|
| `rowPictureDataPath` | Путь к картинке строки (напр. `"Список.DefaultPicture"`) |
|
||||||
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
|
| `tableAutofill: false` | Управление Autofill внутреннего AutoCommandBar |
|
||||||
|
|
||||||
Колонки можно группировать через `columnGroup` (см. ниже).
|
|
||||||
|
|
||||||
### Группа колонок (columnGroup)
|
|
||||||
|
|
||||||
Используется только внутри `columns` таблицы. Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"inCell"` (склеивает колонки в одну ячейку шапки). Допускается вложение `columnGroup` в `columnGroup`.
|
|
||||||
|
|
||||||
| Ключ | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `name` | Имя элемента (рекомендуется задавать явно) |
|
|
||||||
| `title` | Заголовок группы |
|
|
||||||
| `showTitle: false` | Скрыть заголовок |
|
|
||||||
| `showInHeader: true/false` | Показывать ли группу в шапке таблицы |
|
|
||||||
| `width` | Ширина |
|
|
||||||
| `horizontalStretch: false` | Растягивание |
|
|
||||||
| `children: [...]` | Колонки внутри группы (`input`, `labelField`, `picField`, вложенный `columnGroup` …) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список", "columns": [
|
|
||||||
{ "columnGroup": "horizontal", "name": "ГруппаДата", "title": "Срок", "children": [
|
|
||||||
{ "input": "СрокИсполнения", "path": "Список.СрокИсполнения" },
|
|
||||||
{ "labelField": "Просрочено", "path": "Список.Просрочено" }
|
|
||||||
]},
|
|
||||||
{ "columnGroup": "inCell", "name": "ГруппаИсполнитель", "showInHeader": true, "children": [
|
|
||||||
{ "input": "Исполнитель", "path": "Список.Исполнитель" }
|
|
||||||
]},
|
|
||||||
{ "input": "Комментарий", "path": "Список.Комментарий" }
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Картинка-поле (picField)
|
|
||||||
|
|
||||||
PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`:
|
|
||||||
|
|
||||||
| Ключ | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` |
|
|
||||||
| `loadTransparent: true` | Скрыть кадр «нет значения» |
|
|
||||||
|
|
||||||
### Страницы (pages + page)
|
### Страницы (pages + page)
|
||||||
|
|
||||||
| Ключ (pages) | Описание |
|
| Ключ (pages) | Описание |
|
||||||
@@ -270,39 +195,18 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
| `command` | Имя команды формы → `Form.Command.Имя` |
|
| `command` | Имя команды формы → `Form.Command.Имя` |
|
||||||
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
| `stdCommand` | Стандартная команда: `"Close"` → `Form.StandardCommand.Close`; с точкой: `"Товары.Add"` → `Form.Item.Товары.StandardCommand.Add` |
|
||||||
| `defaultButton: true` | Кнопка по умолчанию |
|
| `defaultButton: true` | Кнопка по умолчанию |
|
||||||
| `type` | `"usual"`, `"hyperlink"`. По умолчанию `"usual"`. Конкретный XML-вид (UsualButton/Hyperlink/CommandBarButton/CommandBarHyperlink) подставляется автоматически по контексту |
|
| `type` | `"usual"`, `"hyperlink"`, `"commandBar"` |
|
||||||
| `picture` | Картинка кнопки |
|
| `picture` | Картинка кнопки |
|
||||||
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
| `representation` | `"Auto"`, `"Text"`, `"Picture"`, `"PictureAndText"` |
|
||||||
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
| `locationInCommandBar` | `"Auto"`, `"InCommandBar"`, `"InAdditionalSubmenu"` |
|
||||||
|
|
||||||
### Командная панель (cmdBar)
|
### Командная панель (cmdBar)
|
||||||
|
|
||||||
Дополнительная пользовательская панель команд, размещается как обычный элемент в layout формы.
|
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `autofill: true` | Автозаполнение стандартными командами |
|
| `autofill: true` | Автозаполнение стандартными командами |
|
||||||
| `children: [...]` | Кнопки панели |
|
| `children: [...]` | Кнопки панели |
|
||||||
|
|
||||||
### Главная автокомандная панель формы (autoCmdBar)
|
|
||||||
|
|
||||||
Наполняет встроенную AutoCommandBar формы (id=-1) кастомными кнопками. Указывать только если нужно добавить свои кнопки на главную панель или явно управлять автозаполнением.
|
|
||||||
|
|
||||||
| Ключ | Описание |
|
|
||||||
|------|----------|
|
|
||||||
| `autofill: true/false` | Автозаполнение стандартными командами |
|
|
||||||
| `horizontalAlign` | `"Left"` / `"Center"` / `"Right"` |
|
|
||||||
| `children: [...]` | Кнопки/popup |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "autoCmdBar": "ФормаКоманднаяПанель", "autofill": true, "children": [
|
|
||||||
{ "button": "ИзменитьВыделенные", "command": "ИзменитьВыделенные",
|
|
||||||
"locationInCommandBar": "InAdditionalSubmenu" }
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
Кнопки основных действий формы и подменю размещают здесь, а не в отдельной группе на форме. Отдельной кнопкой в layout — только если она логически привязана к конкретному полю или группе.
|
|
||||||
|
|
||||||
### Выпадающее меню (popup)
|
### Выпадающее меню (popup)
|
||||||
|
|
||||||
| Ключ | Описание |
|
| Ключ | Описание |
|
||||||
@@ -335,7 +239,6 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `savedData: true` — сохраняемые данные
|
- `savedData: true` — сохраняемые данные
|
||||||
- `main: true` — главный реквизит формы (например, основной `*Object.*`, `DynamicList`, `*RecordSet.*`)
|
|
||||||
|
|
||||||
### Команды (commands)
|
### Команды (commands)
|
||||||
|
|
||||||
@@ -441,13 +344,13 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
|
||||||
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
|
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] },
|
||||||
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
|
||||||
]},
|
]},
|
||||||
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
|
||||||
{ "autoCmdBar": "ФормаКоманднаяПанель", "children": [
|
{ "group": "horizontal", "name": "ГруппаКнопок", "children": [
|
||||||
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
{ "button": "Загрузить", "command": "Загрузить", "defaultButton": true },
|
||||||
{ "button": "Закрыть", "stdCommand": "Close" }
|
{ "button": "Закрыть", "stdCommand": "Close" }
|
||||||
]}
|
]}
|
||||||
],
|
],
|
||||||
"attributes": [
|
"attributes": [
|
||||||
@@ -501,8 +404,8 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
"title": "Просмотр данных",
|
"title": "Просмотр данных",
|
||||||
"elements": [
|
"elements": [
|
||||||
{ "group": "horizontal", "name": "Фильтр", "children": [
|
{ "group": "horizontal", "name": "Фильтр", "children": [
|
||||||
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
|
{ "input": "Период", "path": "Период", "on": ["OnChange"] },
|
||||||
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
|
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] }
|
||||||
]},
|
]},
|
||||||
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
|
||||||
{ "input": "Дата", "path": "Данные.Дата" },
|
{ "input": "Дата", "path": "Данные.Дата" },
|
||||||
@@ -523,26 +426,10 @@ PictureField, привязанный к булеву/числу, рисует и
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Продвинутые конструкции (по необходимости)
|
|
||||||
|
|
||||||
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`:
|
|
||||||
|
|
||||||
- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса
|
|
||||||
- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки)
|
|
||||||
- `choice-params.md` — параметры и связи выбора у полей ввода
|
|
||||||
- `command-interface.md` — командный интерфейс формы
|
|
||||||
- `roles-access.md` — пользовательская видимость и доступ по ролям
|
|
||||||
- `companion-panels.md` — контент расширенной подсказки и контекстного меню
|
|
||||||
- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок)
|
|
||||||
- `charts.md` — диаграммы и планировщик
|
|
||||||
- `report-form.md` — свойства формы отчёта
|
|
||||||
- `type-system-advanced.md` — наборы и составные типы
|
|
||||||
- `table-advanced.md` — расширенные свойства таблиц
|
|
||||||
- `layout-advanced.md` — тонкая компоновка и геометрия
|
|
||||||
|
|
||||||
## Автогенерация
|
## Автогенерация
|
||||||
|
|
||||||
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
|
||||||
|
- **Обработчики событий**: `"on": ["OnChange"]` → `ОрганизацияПриИзменении`
|
||||||
- **Namespace**: все 17 namespace-деклараций
|
- **Namespace**: все 17 namespace-деклараций
|
||||||
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
|
||||||
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
# Оформление
|
|
||||||
|
|
||||||
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
|
|
||||||
|
|
||||||
## Оформление элемента (цвета / шрифты / граница)
|
|
||||||
|
|
||||||
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
|
|
||||||
|
|
||||||
| Ключ | Что задаёт |
|
|
||||||
|------|-----------|
|
|
||||||
| `textColor` | Цвет текста |
|
|
||||||
| `backColor` | Цвет фона |
|
|
||||||
| `borderColor` | Цвет рамки |
|
|
||||||
| `font` | Шрифт |
|
|
||||||
| `border` | Граница |
|
|
||||||
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup` — `titleTextColor`/`titleFont` заголовка страницы/попапа |
|
|
||||||
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
|
|
||||||
|
|
||||||
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
|
|
||||||
|
|
||||||
### Цвет
|
|
||||||
|
|
||||||
Строка в одной из форм:
|
|
||||||
|
|
||||||
| Форма | Значение |
|
|
||||||
|-------|----------|
|
|
||||||
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
|
|
||||||
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
|
|
||||||
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
|
|
||||||
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
|
|
||||||
|
|
||||||
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
|
|
||||||
|
|
||||||
### Шрифт (`font` / `titleFont` / `footerFont`)
|
|
||||||
|
|
||||||
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
|
|
||||||
- Объект — задаются только нужные атрибуты:
|
|
||||||
|
|
||||||
| Ключ | Назначение |
|
|
||||||
|------|-----------|
|
|
||||||
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
|
|
||||||
| `faceName` | Имя гарнитуры (для собственного шрифта) |
|
|
||||||
| `height` | Размер |
|
|
||||||
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
|
|
||||||
| `scale` | Масштаб, % |
|
|
||||||
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "label": "Внимание!", "textColor": "web:FireBrick",
|
|
||||||
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Граница (`border`)
|
|
||||||
|
|
||||||
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
|
|
||||||
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
|
|
||||||
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
|
|
||||||
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Условное оформление формы (`conditionalAppearance`)
|
|
||||||
|
|
||||||
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"conditionalAppearance": [
|
|
||||||
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
|
|
||||||
"appearance": { "ЦветФона": "style:FormBackColor" },
|
|
||||||
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `selection` | array | Имена форматируемых полей формы |
|
|
||||||
| `filter` | array | Условие применения (грамматика — ниже) |
|
|
||||||
| `appearance` | object | Словарь «параметр платформы: значение» |
|
|
||||||
| `presentation` | string / object | Подпись правила в списке настроек |
|
|
||||||
| `use` | bool | `false` — правило отключено |
|
|
||||||
| `viewMode` | string | Режим отображения настройки |
|
|
||||||
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
|
|
||||||
|
|
||||||
### filter
|
|
||||||
|
|
||||||
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"filter": [
|
|
||||||
"Статус = 3",
|
|
||||||
{ "field": "Сумма", "op": ">=", "value": 1000 },
|
|
||||||
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
|
||||||
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
|
|
||||||
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
|
|
||||||
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
|
||||||
|
|
||||||
### appearance
|
|
||||||
|
|
||||||
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
|
|
||||||
|
|
||||||
Значения:
|
|
||||||
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
|
|
||||||
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
|
|
||||||
- голая строка → нелокализованный литерал (`""` → пустое значение);
|
|
||||||
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
|
|
||||||
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"conditionalAppearance": [
|
|
||||||
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
|
|
||||||
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
|
|
||||||
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
|
|
||||||
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# Диаграммы, диаграмма Ганта, планировщик
|
|
||||||
|
|
||||||
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
|
|
||||||
|
|
||||||
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
|
|
||||||
2. **Элемент** формы, привязанный к реквизиту через `path`.
|
|
||||||
|
|
||||||
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
|
|
||||||
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
|
|
||||||
```
|
|
||||||
|
|
||||||
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
|
|
||||||
|
|
||||||
## Типы реквизита и элемента
|
|
||||||
|
|
||||||
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|
|
||||||
|---------|-----------|---------------|---------------------------------|
|
|
||||||
| Диаграмма | `chart` | `d5p1:Chart` | — |
|
|
||||||
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
|
|
||||||
| Планировщик | `planner` | `pl:Planner` | — |
|
|
||||||
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
|
|
||||||
| Период | `periodField` | `v8:StandardPeriod` | — |
|
|
||||||
| Дендрограмма | `dendrogram` | — | — |
|
|
||||||
|
|
||||||
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
|
|
||||||
|
|
||||||
### Элемент диаграммы Ганта (`ganttTable`)
|
|
||||||
|
|
||||||
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "ganttChart": "Ганта", "path": "Ганта",
|
|
||||||
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Design-time конфиг диаграммы (`chart`)
|
|
||||||
|
|
||||||
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
|
|
||||||
|
|
||||||
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
|
|
||||||
"chartType": "Line",
|
|
||||||
"isSeriesDesign": true, "realSeriesCount": "2",
|
|
||||||
"realSeriesData": [
|
|
||||||
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
|
|
||||||
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
|
|
||||||
"isIndicator": false, "colorPriority": false }
|
|
||||||
],
|
|
||||||
"isShowTitle": true, "title": "Продажи",
|
|
||||||
"isShowLegend": true, "legendPlacement": "Bottom",
|
|
||||||
"paletteKind": "Auto"
|
|
||||||
} }
|
|
||||||
```
|
|
||||||
|
|
||||||
Смысловое ядро для правки:
|
|
||||||
|
|
||||||
| Ключ | Назначение |
|
|
||||||
|------|------------|
|
|
||||||
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
|
|
||||||
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
|
|
||||||
| `isShowTitle` + `title` | Показ и текст заголовка |
|
|
||||||
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
|
|
||||||
| `paletteKind` | Палитра (`Auto` / …) |
|
|
||||||
| `bkgColor` / `labelsColor` / … | Базовые цвета |
|
|
||||||
|
|
||||||
Формы значений внутри `chart`:
|
|
||||||
|
|
||||||
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
|
|
||||||
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
|
|
||||||
- **`border`** — `{ width, style }`.
|
|
||||||
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
|
|
||||||
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
|
|
||||||
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
|
|
||||||
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
|
|
||||||
|
|
||||||
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
|
|
||||||
|
|
||||||
### Диаграмма Ганта (`d5p1:GanttChart`)
|
|
||||||
|
|
||||||
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
|
|
||||||
|
|
||||||
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
|
|
||||||
|
|
||||||
## Design-time конфиг планировщика (`planner`)
|
|
||||||
|
|
||||||
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
|
|
||||||
"items": [
|
|
||||||
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
|
|
||||||
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
|
|
||||||
],
|
|
||||||
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
|
|
||||||
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
|
|
||||||
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
|
|
||||||
} }
|
|
||||||
```
|
|
||||||
|
|
||||||
Минимум — один `item`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ `planner` | Тип | Назначение |
|
|
||||||
|----------------|-----|------------|
|
|
||||||
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
|
|
||||||
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
|
|
||||||
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
|
|
||||||
| `timeScale` | object | Шкала времени (см. ниже) |
|
|
||||||
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
|
|
||||||
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
|
|
||||||
| `border` | border | Рамка `{ width, style }` |
|
|
||||||
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
|
|
||||||
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
|
|
||||||
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
|
|
||||||
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
|
|
||||||
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
|
|
||||||
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
|
|
||||||
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
|
|
||||||
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
|
|
||||||
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
|
|
||||||
|
|
||||||
Шкала времени (`timeScale`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
"timeScale": {
|
|
||||||
"placement": "Left",
|
|
||||||
"levels": [ { "measure": "Hour", "interval": 1 } ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
|
|
||||||
|
|
||||||
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
|
|
||||||
|
|
||||||
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Параметры выбора и связь по типу
|
|
||||||
|
|
||||||
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Контрагент", "path": "Объект.Контрагент",
|
|
||||||
"choiceParameters": [
|
|
||||||
{ "name": "Отбор.Активный", "value": true },
|
|
||||||
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
|
|
||||||
],
|
|
||||||
"choiceParameterLinks": [
|
|
||||||
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
|
|
||||||
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
|
|
||||||
],
|
|
||||||
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Параметры выбора (`choiceParameters`)
|
|
||||||
|
|
||||||
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
|
|
||||||
|
|
||||||
- `name` — имя параметра (`"Отбор.Активный"`).
|
|
||||||
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
|
|
||||||
|
|
||||||
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"choiceParameters": [
|
|
||||||
"Отбор.Активный=true",
|
|
||||||
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Связи параметров выбора (`choiceParameterLinks`)
|
|
||||||
|
|
||||||
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
|
|
||||||
|
|
||||||
- `name` — имя параметра выбора.
|
|
||||||
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
|
|
||||||
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Договор", "path": "Объект.Договор",
|
|
||||||
"choiceParameterLinks": [
|
|
||||||
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Связь по типу (`typeLink`)
|
|
||||||
|
|
||||||
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
|
|
||||||
|
|
||||||
- `dataPath` — путь к полю, задающему тип.
|
|
||||||
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
|
|
||||||
|
|
||||||
```json
|
|
||||||
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
|
|
||||||
```
|
|
||||||
|
|
||||||
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"typeLink": "Объект.ВидСубконто"
|
|
||||||
"typeLink": "Объект.ВидСубконто#1"
|
|
||||||
```
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Командный интерфейс формы
|
|
||||||
|
|
||||||
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
|
|
||||||
|
|
||||||
- `commandBar` — командная панель формы;
|
|
||||||
- `navigationPanel` — панель навигации.
|
|
||||||
|
|
||||||
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"commandInterface": {
|
|
||||||
"commandBar": [
|
|
||||||
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
|
|
||||||
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
|
|
||||||
"CommonCommand.История"
|
|
||||||
],
|
|
||||||
"navigationPanel": {
|
|
||||||
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
|
|
||||||
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Элемент-команда
|
|
||||||
|
|
||||||
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
|
|
||||||
|
|
||||||
```json
|
|
||||||
"CommonCommand.История"
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
|
|
||||||
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
|
|
||||||
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
|
|
||||||
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
|
|
||||||
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
|
|
||||||
| `index` | int | Порядок команды внутри группы |
|
|
||||||
| `attribute` | string | Путь реквизита для элемента панели навигации |
|
|
||||||
|
|
||||||
## Две формы записи панели
|
|
||||||
|
|
||||||
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
|
|
||||||
|
|
||||||
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"commandBar": [
|
|
||||||
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
|
|
||||||
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"navigationPanel": {
|
|
||||||
"important": [ "CommonCommand.СвязанныеДокументы" ],
|
|
||||||
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
|
|
||||||
"seeAlso": [ "CommonCommand.Заметки" ]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи-группы дерева зависят от панели:
|
|
||||||
|
|
||||||
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
|
|
||||||
- `commandBar`: `important`, `createBasedOn`;
|
|
||||||
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
|
|
||||||
|
|
||||||
## Скрыть видимую команду
|
|
||||||
|
|
||||||
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"commandBar": [
|
|
||||||
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Показать команду только некоторым ролям:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "command": "Form.Command.Печать", "defaultVisible": false,
|
|
||||||
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
|
|
||||||
```
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# Companion-панели и расширенная подсказка элемента
|
|
||||||
|
|
||||||
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{ "table": "Список", "path": "Список",
|
|
||||||
"commandBar": { "children": [ … ] },
|
|
||||||
"contextMenu": { "children": [ … ] },
|
|
||||||
"extendedTooltip": "Двойной клик открывает карточку" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Расширенная подсказка (`extendedTooltip`)
|
|
||||||
|
|
||||||
Подсказка-надпись рядом с элементом. Две формы записи.
|
|
||||||
|
|
||||||
**Текст-форма** — просто текст подсказки:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"extendedTooltip": "Укажите ИНН контрагента"
|
|
||||||
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
|
|
||||||
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
|
|
||||||
```
|
|
||||||
|
|
||||||
- строка — ru-текст;
|
|
||||||
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
|
|
||||||
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
|
|
||||||
|
|
||||||
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"extendedTooltip": {
|
|
||||||
"text": "Перейти к инструкции",
|
|
||||||
"hyperlink": true,
|
|
||||||
"textColor": "web:Blue",
|
|
||||||
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи own-content объекта (все необязательны):
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
|
|
||||||
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
|
|
||||||
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
|
|
||||||
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
|
|
||||||
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
|
|
||||||
| `width` / `height` | number | Размеры |
|
|
||||||
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
|
|
||||||
| `titleHeight` | number | Высота заголовка |
|
|
||||||
| `horizontalStretch` | bool | Горизонтальное растяжение |
|
|
||||||
| `verticalAlign` | string | Вертикальное выравнивание |
|
|
||||||
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
|
|
||||||
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
|
|
||||||
|
|
||||||
## Командная панель (`commandBar`)
|
|
||||||
|
|
||||||
Собственная командная панель элемента (обычно таблицы или группы).
|
|
||||||
|
|
||||||
**Значение** — массив или объект:
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
|
|
||||||
|
|
||||||
"commandBar": {
|
|
||||||
"autofill": false,
|
|
||||||
"horizontalAlign": "Right",
|
|
||||||
"children": [
|
|
||||||
{ "button": "Создать", "command": "СоздатьЭлемент" },
|
|
||||||
{ "buttonGroup": "Печать", "children": [ … ] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
|
|
||||||
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
|
|
||||||
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
|
|
||||||
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
|
|
||||||
|
|
||||||
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
|
|
||||||
|
|
||||||
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
|
|
||||||
|
|
||||||
## Контекстное меню (`contextMenu`)
|
|
||||||
|
|
||||||
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
|
|
||||||
|
|
||||||
"contextMenu": {
|
|
||||||
"autofill": false,
|
|
||||||
"children": [
|
|
||||||
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `children` | array | Пункты меню — обычная грамматика кнопок |
|
|
||||||
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
|
|
||||||
|
|
||||||
## Пример: таблица со своим меню и инфо-баннером
|
|
||||||
|
|
||||||
```jsonc
|
|
||||||
{ "table": "Заказы", "path": "Объект.Заказы",
|
|
||||||
"extendedTooltip": {
|
|
||||||
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
|
|
||||||
"formatted": true
|
|
||||||
},
|
|
||||||
"commandBar": {
|
|
||||||
"autofill": false,
|
|
||||||
"horizontalAlign": "Right",
|
|
||||||
"children": [
|
|
||||||
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
|
|
||||||
{ "button": "Удалить", "command": "УдалитьЗаказ" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"contextMenu": {
|
|
||||||
"children": [
|
|
||||||
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
|
|
||||||
{ "buttonGroup": "Экспорт", "children": [
|
|
||||||
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
|
|
||||||
]
|
|
||||||
} }
|
|
||||||
```
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
# Динамический список
|
|
||||||
|
|
||||||
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Список", "type": "DynamicList", "main": true,
|
|
||||||
"settings": { "mainTable": "Catalog.Контрагенты" } }
|
|
||||||
```
|
|
||||||
|
|
||||||
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
|
|
||||||
|
|
||||||
## Источник данных
|
|
||||||
|
|
||||||
Два взаимоисключающих режима:
|
|
||||||
|
|
||||||
**Таблично-ориентированный** — основная таблица метаданных:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settings": { "mainTable": "Catalog.Контрагенты" }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Запросный** — произвольный запрос:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settings": {
|
|
||||||
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
|
|
||||||
"mainTable": "Document.Заказ"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
|
|
||||||
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
|
|
||||||
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
|
|
||||||
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
|
|
||||||
|
|
||||||
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
|
|
||||||
|
|
||||||
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
|
|
||||||
|
|
||||||
## Параметры запроса (`parameters`)
|
|
||||||
|
|
||||||
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"settings": {
|
|
||||||
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
|
|
||||||
"parameters": [
|
|
||||||
"Артикул",
|
|
||||||
"Маска: string = %",
|
|
||||||
{ "name": "ВидЦен", "valueListAllowed": true },
|
|
||||||
{ "name": "Период", "type": "dateTime" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
|
|
||||||
|
|
||||||
## Значения параметров в настройках (`dataParameters`)
|
|
||||||
|
|
||||||
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"dataParameters": [ "Организация = _", "ВидЦен" ]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поля набора (`fields`)
|
|
||||||
|
|
||||||
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"fields": [
|
|
||||||
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
|
|
||||||
{ "field": "Остаток", "valueType": "number(15,2)" }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
|
|
||||||
|
|
||||||
## Вычисляемые поля (`calculatedFields`)
|
|
||||||
|
|
||||||
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"calculatedFields": [
|
|
||||||
"Метка = Code + \" \" + Description",
|
|
||||||
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Объектная форма — для `presentationExpression` / `orderExpression`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
|
|
||||||
"valueType": "string(10)", "presentationExpression": "Code" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Отбор (`filter`)
|
|
||||||
|
|
||||||
Shorthand `"Поле оператор значение @флаги"` или объект:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"filter": [
|
|
||||||
"Организация = _ @off @user",
|
|
||||||
"Сумма > 1000",
|
|
||||||
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
|
|
||||||
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
|
|
||||||
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
|
|
||||||
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
|
|
||||||
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
|
|
||||||
|
|
||||||
## Сортировка (`order`)
|
|
||||||
|
|
||||||
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"order": [ "Дата desc", "Наименование", "Auto" ]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Группировка строк (`grouping`)
|
|
||||||
|
|
||||||
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"grouping": "Контрагент > Договор"
|
|
||||||
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
|
|
||||||
```
|
|
||||||
|
|
||||||
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
|
|
||||||
|
|
||||||
## Условное оформление (`conditionalAppearance`)
|
|
||||||
|
|
||||||
```json
|
|
||||||
"conditionalAppearance": [
|
|
||||||
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# Продвинутая раскладка
|
|
||||||
|
|
||||||
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
|
|
||||||
|
|
||||||
## Выравнивание внутри родителя
|
|
||||||
|
|
||||||
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
|
|
||||||
|
|
||||||
| Ключ | Значения | Что выравнивает |
|
|
||||||
|------|----------|-----------------|
|
|
||||||
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
|
|
||||||
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
|
|
||||||
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
|
|
||||||
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
|
|
||||||
|
|
||||||
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "button": "ОК", "groupHorizontalAlign": "Right" }
|
|
||||||
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
|
|
||||||
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ограничение максимального размера
|
|
||||||
|
|
||||||
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `maxWidth` | число | Жёсткий максимум ширины элемента |
|
|
||||||
| `maxHeight` | число | Жёсткий максимум высоты элемента |
|
|
||||||
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
|
|
||||||
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
|
|
||||||
|
|
||||||
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
|
|
||||||
"horizontalStretch": true, "autoMaxWidth": false }
|
|
||||||
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поведение при вводе и активации
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
|
|
||||||
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
|
|
||||||
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
|
|
||||||
```
|
|
||||||
|
|
||||||
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
|
|
||||||
|
|
||||||
## Перетаскивание
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
|
|
||||||
|
|
||||||
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
|
|
||||||
|
|
||||||
## Закрепление колонки в таблице (`fixingInTable`)
|
|
||||||
|
|
||||||
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
|
|
||||||
|
|
||||||
| Значения |
|
|
||||||
|----------|
|
|
||||||
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
|
||||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
|
||||||
{ "input": "Количество", "path": "Объект.Товары.Количество" },
|
|
||||||
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
|
|
||||||
|
|
||||||
## Ячейки колонок: шапка и подвал
|
|
||||||
|
|
||||||
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
|
|
||||||
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
|
|
||||||
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
|
|
||||||
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
|
|
||||||
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
|
|
||||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
|
|
||||||
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
|
|
||||||
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Адаптивная важность (`displayImportance`)
|
|
||||||
|
|
||||||
| Значения |
|
|
||||||
|----------|
|
|
||||||
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
|
|
||||||
|
|
||||||
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
|
|
||||||
```
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# Форма отчёта
|
|
||||||
|
|
||||||
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
|
|
||||||
|
|
||||||
```json
|
|
||||||
"properties": {
|
|
||||||
"reportFormType": "Main",
|
|
||||||
"reportResult": "РезультатОтчета",
|
|
||||||
"detailsData": "ДанныеРасшифровки"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
|
|
||||||
|
|
||||||
## Тип формы отчёта (`reportFormType`)
|
|
||||||
|
|
||||||
Роль формы в составе отчёта:
|
|
||||||
|
|
||||||
| Значение | Назначение |
|
|
||||||
|----------|-----------|
|
|
||||||
| `Main` | Основная форма отчёта (результат + настройки) |
|
|
||||||
| `Settings` | Форма настроек |
|
|
||||||
| `Variant` | Форма варианта |
|
|
||||||
|
|
||||||
```json
|
|
||||||
"reportFormType": "Main"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Привязка к компоновке
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
|
|
||||||
| `detailsData` | string | Имя реквизита данных расшифровки |
|
|
||||||
| `variantAppearance` | string | Имя реквизита оформления варианта |
|
|
||||||
|
|
||||||
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
|
|
||||||
|
|
||||||
## Группа пользовательских настроек (`customSettingsFolder`)
|
|
||||||
|
|
||||||
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
|
|
||||||
|
|
||||||
```json
|
|
||||||
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Прочие свойства компоновки
|
|
||||||
|
|
||||||
Редкие, задавайте только при явной необходимости:
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
|
|
||||||
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
|
|
||||||
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
|
|
||||||
|
|
||||||
## Реалистичный пример
|
|
||||||
|
|
||||||
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"reportFormType": "Main",
|
|
||||||
"reportResult": "РезультатОтчета",
|
|
||||||
"detailsData": "ДанныеРасшифровки",
|
|
||||||
"customSettingsFolder": "ГруппаПользовательскихНастроек"
|
|
||||||
},
|
|
||||||
"attributes": [
|
|
||||||
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
|
|
||||||
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
|
|
||||||
],
|
|
||||||
"elements": [
|
|
||||||
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
|
|
||||||
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
|
|
||||||
"titleLocation": "none" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Доступ по ролям
|
|
||||||
|
|
||||||
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
|
|
||||||
|
|
||||||
| Ключ | Владелец | Смысл |
|
|
||||||
|------|----------|-------|
|
|
||||||
| `userVisible` | элемент формы | пользовательская видимость элемента |
|
|
||||||
| `view` | реквизит формы | право просмотра |
|
|
||||||
| `edit` | реквизит формы | право редактирования |
|
|
||||||
| `use` | команда формы | доступность команды |
|
|
||||||
|
|
||||||
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
|
|
||||||
|
|
||||||
## Значение
|
|
||||||
|
|
||||||
Две формы (одинаковы для всех четырёх ключей):
|
|
||||||
|
|
||||||
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Поле", "userVisible": false }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Реквизит",
|
|
||||||
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
|
|
||||||
|
|
||||||
## Имя роли
|
|
||||||
|
|
||||||
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
|
|
||||||
|
|
||||||
## Примеры
|
|
||||||
|
|
||||||
Элемент скрыт у всех пользователей:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "input": "Комментарий", "userVisible": false }
|
|
||||||
```
|
|
||||||
|
|
||||||
Реквизит не виден никому и редактируется только одной ролью:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "СуммаБонуса",
|
|
||||||
"view": false,
|
|
||||||
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Поле доступно для просмотра всем, но редактируемо только администратору:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Статус",
|
|
||||||
"view": true,
|
|
||||||
"edit": { "common": false, "roles": { "Администратор": true } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Команда недоступна по умолчанию, разрешена только бухгалтеру:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "ПровестиЗакрытие",
|
|
||||||
"use": { "common": false, "roles": { "Бухгалтер": true } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Обратный случай — доступно всем, кроме одной роли:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "РедактироватьЦену",
|
|
||||||
"edit": { "common": true, "roles": { "Кладовщик": false } } }
|
|
||||||
```
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Спец-поля «документ/датчик»
|
|
||||||
|
|
||||||
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
|
|
||||||
|
|
||||||
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
|
|
||||||
|
|
||||||
| Ключ типа | Тип реквизита |
|
|
||||||
|-----------|---------------|
|
|
||||||
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
|
|
||||||
| `html` | `string` |
|
|
||||||
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
|
|
||||||
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
|
|
||||||
| `progressBar` | число |
|
|
||||||
| `trackBar` | число |
|
|
||||||
|
|
||||||
## spreadsheet — поле табличного документа
|
|
||||||
|
|
||||||
Просмотр/редактирование табличного документа (отчёт, печатная форма).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
|
|
||||||
"titleLocation": "none", "readOnly": true,
|
|
||||||
"output": "Disable", "protection": true }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
|
||||||
| `protection` | bool | Защита от изменений |
|
|
||||||
| `edit` | bool | Разрешить редактирование |
|
|
||||||
| `showGrid` | bool | Показывать сетку |
|
|
||||||
| `showHeaders` | bool | Показывать заголовки строк/колонок |
|
|
||||||
| `showGroups` | bool | Показывать группировки |
|
|
||||||
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
|
|
||||||
| `showCellNames` | bool | Показывать имена ячеек |
|
|
||||||
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
|
|
||||||
| `viewScalingMode` | string | Режим масштабирования просмотра |
|
|
||||||
| `selectionShowMode` | string | Режим отображения выделения |
|
|
||||||
| `pointerType` | string | Тип указателя |
|
|
||||||
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
|
|
||||||
|
|
||||||
## html — поле HTML-документа
|
|
||||||
|
|
||||||
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
|
|
||||||
"output": "Enable", "warningOnEditRepresentation": false }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `output` | string | Использование вывода: `Enable` / `Disable` |
|
|
||||||
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
|
|
||||||
|
|
||||||
## textDoc — поле текстового документа
|
|
||||||
|
|
||||||
Просмотр/редактирование текстового документа.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
|
||||||
|
|
||||||
## formattedDoc — поле форматированного документа
|
|
||||||
|
|
||||||
Просмотр/редактирование форматированного документа.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
|
|
||||||
|
|
||||||
## progressBar — поле индикатора
|
|
||||||
|
|
||||||
Индикатор прогресса. Реквизит — числовой.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "progressBar": "Прогресс", "path": "Прогресс",
|
|
||||||
"minValue": 0, "maxValue": 100, "showPercent": true }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
|
||||||
| `showPercent` | bool | Показывать проценты |
|
|
||||||
|
|
||||||
## trackBar — поле ползунка
|
|
||||||
|
|
||||||
Регулятор-ползунок. Реквизит — числовой.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "trackBar": "Масштаб", "path": "Масштаб",
|
|
||||||
"minValue": 20, "maxValue": 400, "markingStep": 20 }
|
|
||||||
```
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
|
|
||||||
| `step` | число | Шаг изменения |
|
|
||||||
| `largeStep` | число | Крупный шаг |
|
|
||||||
| `markingStep` | число | Шаг разметки |
|
|
||||||
| `markingAppearance` | string | Оформление разметки |
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# Таблица — продвинутые возможности
|
|
||||||
|
|
||||||
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
|
|
||||||
|
|
||||||
## Представление (`representation`)
|
|
||||||
|
|
||||||
Как таблица рисует строки:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список", "representation": "Tree" }
|
|
||||||
```
|
|
||||||
|
|
||||||
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
|
|
||||||
|
|
||||||
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
|
|
||||||
|
|
||||||
## Выделение и текущая строка
|
|
||||||
|
|
||||||
| Ключ | Значения | Назначение |
|
|
||||||
|------|----------|-----------|
|
|
||||||
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
|
|
||||||
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
|
|
||||||
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поиск при вводе (`searchOnInput`)
|
|
||||||
|
|
||||||
Поведение встроенного поиска при наборе текста в таблице:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
|
|
||||||
```
|
|
||||||
|
|
||||||
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
|
|
||||||
|
|
||||||
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
|
|
||||||
|
|
||||||
## Прочие свойства таблицы
|
|
||||||
|
|
||||||
| Ключ | Тип | Назначение |
|
|
||||||
|------|-----|-----------|
|
|
||||||
| `useAlternationRowColor` | bool | Чередование цвета строк |
|
|
||||||
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
|
|
||||||
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
|
|
||||||
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
|
|
||||||
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
|
|
||||||
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
|
|
||||||
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
|
|
||||||
|
|
||||||
## Фиксация колонки (`fixingInTable`)
|
|
||||||
|
|
||||||
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Товары", "path": "Объект.Товары", "columns": [
|
|
||||||
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
|
|
||||||
{ "input": "Количество", "path": "Объект.Товары.Количество" }
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
`Left` / `Right` / `None`.
|
|
||||||
|
|
||||||
## Исключённые команды (`excludedCommands`)
|
|
||||||
|
|
||||||
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Товары", "path": "Объект.Товары",
|
|
||||||
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
|
|
||||||
```
|
|
||||||
|
|
||||||
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
|
|
||||||
|
|
||||||
## Дополнения командной панели (`additions`)
|
|
||||||
|
|
||||||
Дополнения — это «представления» встроенного поиска таблицы:
|
|
||||||
|
|
||||||
- `searchString` — отображение строки поиска,
|
|
||||||
- `viewStatus` — состояние просмотра,
|
|
||||||
- `searchControl` — управление поиском.
|
|
||||||
|
|
||||||
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
|
|
||||||
|
|
||||||
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список",
|
|
||||||
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список", "commandBar": [
|
|
||||||
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
|
|
||||||
]}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
|
|
||||||
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
|
|
||||||
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
|
|
||||||
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
|
|
||||||
|
|
||||||
## Таблица динамического списка
|
|
||||||
|
|
||||||
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
|
|
||||||
|
|
||||||
| Ключ | Тип | Умолчание | Назначение |
|
|
||||||
|------|-----|-----------|-----------|
|
|
||||||
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
|
|
||||||
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
|
|
||||||
| `autoRefresh` | bool | `false` | Автообновление списка |
|
|
||||||
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
|
|
||||||
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
|
|
||||||
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
|
|
||||||
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
|
|
||||||
| `showRoot` | bool | `true` | Показывать корень |
|
|
||||||
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
|
|
||||||
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
|
|
||||||
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "table": "Список", "path": "Список",
|
|
||||||
"representation": "Tree",
|
|
||||||
"rowPictureDataPath": "Список.DefaultPicture",
|
|
||||||
"choiceFoldersAndItems": "FoldersAndItems",
|
|
||||||
"allowRootChoice": true,
|
|
||||||
"updateOnDataChange": "DontUpdate" }
|
|
||||||
```
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Продвинутые конструкции типов
|
|
||||||
|
|
||||||
Примитивы (`string(n)`, `number(p,s)`, `boolean`, `date`/`dateTime`, …) и одиночные ссылки (`CatalogRef.Контрагенты`, `DocumentRef.Заказ`, `EnumRef.X`, …) описаны в основной инструкции. Здесь — типы, которые нельзя выразить одним именем: составные типы, наборы типов и платформенные наборы ссылок.
|
|
||||||
|
|
||||||
Любая из этих конструкций пишется в поле `type` реквизита, реквизита-параметра или поля.
|
|
||||||
|
|
||||||
## Составные типы
|
|
||||||
|
|
||||||
Несколько типов на одном реквизите — части перечисляются через разделитель `" | "` (можно `+`). Реквизит сможет принимать значение любого из перечисленных типов:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Плательщик",
|
|
||||||
"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Смешивать можно типы из разных категорий — ссылки, примитивы, наборы типов:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Источник",
|
|
||||||
"type": "CatalogRef.Контрагенты | DocumentRef.Заказ | string(150)" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждая часть — самостоятельный токен из этого файла или из основной инструкции. Порядок частей произвольный.
|
|
||||||
|
|
||||||
## Наборы типов (TypeSet)
|
|
||||||
|
|
||||||
«Набор типов» подставляется вместо конкретного типа — это один токен, а не перечисление. Применимо и в составном типе как одна из частей.
|
|
||||||
|
|
||||||
| Токен `type` | Смысл |
|
|
||||||
|------|-------|
|
|
||||||
| `"DefinedType.ИмяТипа"` | определяемый тип конфигурации |
|
|
||||||
| `"Characteristic.ИмяПлана"` | тип значения характеристики (по плану видов характеристик) |
|
|
||||||
| `"AnyRef"` | любая ссылка |
|
|
||||||
| `"AnyIBRef"` | любая ссылка информационной базы |
|
|
||||||
|
|
||||||
Определяемый тип — реквизит принимает то, что задано в определяемом типе конфигурации (например `DefinedType.ДенежнаяСумма`):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Сумма", "type": "DefinedType.ДенежнаяСумма" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Характеристика — тип значения берётся из плана видов характеристик:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Значение", "type": "Characteristic.ДополнительныеРеквизиты" }
|
|
||||||
```
|
|
||||||
|
|
||||||
## Платформенные наборы ссылок
|
|
||||||
|
|
||||||
«Голый» ссылочный токен **без `.Имя`** означает «любая ссылка этой категории объектов»:
|
|
||||||
|
|
||||||
| Токен `type` | Смысл |
|
|
||||||
|------|-------|
|
|
||||||
| `"CatalogRef"` | любая ссылка справочника |
|
|
||||||
| `"DocumentRef"` | любая ссылка документа |
|
|
||||||
| `"EnumRef"` | любая ссылка перечисления |
|
|
||||||
| `"ExchangePlanRef"` | любая ссылка плана обмена |
|
|
||||||
| `"TaskRef"` | любая ссылка задачи |
|
|
||||||
| `"BusinessProcessRef"` | любая ссылка бизнес-процесса |
|
|
||||||
| `"ChartOfCharacteristicTypesRef"` | любая ссылка плана видов характеристик |
|
|
||||||
| `"ChartOfAccountsRef"` | любая ссылка плана счетов |
|
|
||||||
| `"ChartOfCalculationTypesRef"` | любая ссылка плана видов расчёта |
|
|
||||||
|
|
||||||
Различие с одиночной ссылкой — только в наличии `.Имя`:
|
|
||||||
|
|
||||||
- `"CatalogRef.Валюты"` — конкретный справочник «Валюты»;
|
|
||||||
- `"CatalogRef"` — любой справочник.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "ЛюбойСправочник", "type": "CatalogRef" }
|
|
||||||
```
|
|
||||||
|
|
||||||
Эти наборы тоже комбинируются в составном типе:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{ "name": "Объект", "type": "CatalogRef | DocumentRef" }
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
name: form-decompile
|
|
||||||
description: Декомпиляция управляемой формы 1С (Form.xml) в JSON-черновик в формате form-compile. Используй для scaffold новой формы по образцу или структурного рефакторинга. Не для точечных правок
|
|
||||||
argument-hint: <FormPath> [-OutputPath <out.json>]
|
|
||||||
disable-model-invocation: true
|
|
||||||
allowed-tools:
|
|
||||||
- Bash
|
|
||||||
- Read
|
|
||||||
- Write
|
|
||||||
- Glob
|
|
||||||
---
|
|
||||||
|
|
||||||
# /form-decompile — JSON-черновик из Form.xml управляемой формы
|
|
||||||
|
|
||||||
Читает Form.xml и эмитит компактный JSON в формате `form-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
|
|
||||||
|
|
||||||
## Когда использовать
|
|
||||||
|
|
||||||
- **Scaffold новой формы по образцу** — взять существующую форму, получить JSON, поправить и скомпилировать в новую.
|
|
||||||
- **Структурный рефакторинг** — перебрать дерево элементов, реквизиты, команды.
|
|
||||||
|
|
||||||
## Когда **не** использовать
|
|
||||||
|
|
||||||
- **Точечные правки готовой формы** (добавить элемент, реквизит, команду) → `/form-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает форму целиком, может терять непокрытые конструкции и даёт большой diff. `/form-edit` правит адресно.
|
|
||||||
|
|
||||||
## Параметры
|
|
||||||
|
|
||||||
| Параметр | Описание |
|
|
||||||
|----------|----------|
|
|
||||||
| `FormPath` | Путь к Form.xml (обязательный) |
|
|
||||||
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
python "${CLAUDE_SKILL_DIR}/scripts/form-decompile.py" -FormPath "<Form.xml>" -OutputPath "<out.json>"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что получаешь
|
|
||||||
|
|
||||||
JSON-черновик в формате `/form-compile` — **не полное обратимое представление**: раундтрип `xml → json → xml` не гарантируется, часть конструкций DSL не покрывает и **теряет молча**.
|
|
||||||
|
|
||||||
Критичные конструкции (`ConditionalAppearance` со scope, design-time диаграммы/планировщики на реквизите, неизвестный тип элемента, не-Form root) → скрипт падает с ненулевым кодом и сообщением в stderr; для правок такой формы — `/form-edit`.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. `/form-decompile <Form.xml> -OutputPath draft.json` — получить черновик.
|
|
||||||
2. Поправить JSON под задачу.
|
|
||||||
3. `/form-compile -JsonPath draft.json -OutputPath new/Form.xml` — собрать обратно.
|
|
||||||
4. `/form-validate` + `/form-info` — проверить результат.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user