From 5283bf2b85ca7b76f59ca2a7c4df63ee22bab08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 6 Nov 2025 12:03:21 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=BD=D1=8B=D0=B9=20=D0=B4=D0=BE=D0=B3=D0=BE=D0=B2=D0=BE?= =?UTF-8?q?=D1=80=20=D0=BF=D0=BE=20=D0=B2=D0=B0=D0=BB=D1=8E=D1=82=D0=B5=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D1=80=D0=B0=D0=B3=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 22 ++++++ docs/CONTRACT_UUID_FIX.md | 146 +++++++++++++++++++++++++++++++++++++ modules/moysklad_client.py | 6 ++ modules/validator.py | 46 ++++++++++-- modules/xml_generator.py | 50 +++++++++++-- test_order_4926.xml | 89 ++++++++++++++++++++++ 6 files changed, 348 insertions(+), 11 deletions(-) create mode 100644 docs/CONTRACT_UUID_FIX.md create mode 100644 test_order_4926.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index d3db594..d4934a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [2025-11-06] - Исправление: GUID договора и выбор счетов 62.xx в 1С + +### Fixed +- 🐛 **Критическое исправление**: GUID договора теперь формируется с учётом валюты и признака у.е. +- 📊 **Выбор счетов в 1С**: Рублёвые документы теперь корректно попадают на счета 62.01/62.02 вместо 62.21/62.22 +- 🔍 Добавлено диагностическое логирование для выявления проблем с валютой и счетами + +### Technical Details +- **Проблема**: Генератор создавал один GUID договора для всех валют → при наличии старого валютного договора рублёвые документы попадали на валютные счета 62.21/62.22 +- **Решение**: GUID договора теперь включает валюту и признак у.е.: `contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}` +- **Результат**: + - RUB + не у.е. → отдельный договор → счета **62.01** (задолженность) / **62.02** (авансы) + - Иностранная валюта → отдельный договор → счета **62.21** / **62.22** + - Условные единицы → отдельный договор → счета **62.31** / **62.32** + +### Documentation +- 📖 Создана подробная документация: `docs/CONTRACT_UUID_FIX.md` +- ✅ Инструкции по проверке и исправлению существующих данных +- 🔗 Ссылки на типовую документацию 1С + +--- + ## [2025-11-05] - Формат 1С EnterpriseData 1.20 ### Added diff --git a/docs/CONTRACT_UUID_FIX.md b/docs/CONTRACT_UUID_FIX.md new file mode 100644 index 0000000..a57c922 --- /dev/null +++ b/docs/CONTRACT_UUID_FIX.md @@ -0,0 +1,146 @@ +# Исправление: GUID договора и выбор счетов 62 в 1С + +## Проблема + +### Симптомы +При импорте рублёвого документа (валюта RUB, код 643) в 1С:Бухгалтерия 8, система подставляла счета **62.21 (Расчёты в валюте)** / **62.22 (Авансы в валюте)** вместо ожидаемых **62.01 (Задолженность)** / **62.02 (Авансы)**. + +### Причина +В генераторе ED-XML использовался детерминированный GUID договора, созданный **только на основе пары (контрагент, организация)**: + +```python +# БЫЛО (неправильно): +contract_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, f"contract-{cp_id}-{org_id}") +``` + +Это означало «один договор на пару контрагент-организация» **независимо от валюты расчётов**. + +### Механизм в 1С +В типовой конфигурации «1С:Бухгалтерия 8» выбор счетов учёта расчётов с покупателями (62.xx) определяется регистром **«Счета расчетов с контрагентами»** на основании: + +1. **Договора** (точнее - его валюты взаиморасчётов) +2. **Вида расчётов** (задолженность/аванс) + +Логика подстановки счетов: +- **RUB + не в у.е.** → **62.01** (задолженность) / **62.02** (авансы) +- **Иностранная валюта** → **62.21** (валютные расчёты) / **62.22** (авансы в валюте) +- **Условные единицы** → **62.31** (расчёты в у.е.) / **62.32** (авансы в у.е.) + +### Сценарий проблемы +1. Ранее в базу был импортирован документ **в валюте** (например, USD) +2. Для него создался договор с GUID `abc123...` (на основе пары контрагент-организация) +3. Этот договор записался в регистр с валютой USD → счета 62.21/62.22 +4. Позже импортируется документ **в рублях** (RUB) +5. Генератор создаёт **тот же GUID** `abc123...` (та же пара контрагент-организация!) +6. 1С находит **существующий договор** с валютой USD +7. Регистр подставляет счета **62.21/62.22** (как для валютного договора) +8. ❌ Документ рублёвый, но счета валютные! + +## Решение + +### 1. Исправление генератора GUID договора + +Включаем в seed для `uuid5` **валюту и признак условных единиц**: + +```python +# СТАЛО (правильно): +currency_code = currency.get("isoCode", "643") +is_ue = "false" # В текущей реализации всегда false +seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" +contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) +``` + +Теперь для каждой комбинации (контрагент, организация, **валюта**, **признак у.е.**) создаётся **отдельный договор** с уникальным GUID. + +### 2. Добавление диагностического логирования + +В конце метода `generate_realization()` добавлена проверка: + +```python +if currency_code == "643" or currency_code == "RUB": + logger.info( + "✓ Документ в рублях (RUB, код 643), не в у.е. " + "→ ожидаются счета 62.01 / 62.02. " + "Если 1С подставляет 62.21/62.22 или 62.31/62.32, проверьте: " + "1) Регистр 'Счета расчетов с контрагентами'. " + "2) В карточке договора: Валюта = RUB, У.Е. = нет." + ) +else: + logger.info( + f"⚠ Документ в иностранной валюте ({currency_code}) " + "→ ожидаются счета 62.21/62.22." + ) +``` + +## Проверка после исправления + +### 1. Проверьте регистр «Счета расчетов с контрагентами» + +В 1С: +1. Откройте **«Контрагенты»** → список контрагентов +2. Найдите проблемного контрагента +3. Перейдите по гиперссылке **«Счета расчетов с контрагентами»** +4. Проверьте, нет ли индивидуальных правил с 62.21/62.22 +5. Если есть - удалите или исправьте на 62.01/62.02 + +### 2. Проверьте карточку договора + +После импорта нового документа (с обновлённым генератором): +1. Откройте **«Договоры контрагентов»** +2. Найдите договор (по имени контрагента + дата создания) +3. Проверьте: + - **Валюта взаиморасчётов** = `RUB` (Российский рубль) + - **Расчёты в условных единицах** = `нет` (галочка снята) + +### 3. Проверьте подстановку счетов + +Создайте новый документ реализации: +1. Импортируйте тестовый заказ в рублях +2. Откройте созданный документ в 1С +3. Перейдите на вкладку **«Счета учёта»** +4. Проверьте: + - Счёт учёта расчётов с покупателем = **62.01** + - Счёт учёта авансов = **62.02** (если есть) + +## Дополнительные замечания + +### Миграция существующих данных + +Если в базе уже есть документы с неправильными счетами: + +1. **Вариант 1: Пересоздать договоры** + - Удалите старые договоры (или переименуйте) + - Импортируйте документы заново + - Новый генератор создаст правильные договоры + +2. **Вариант 2: Исправить вручную** + - Откройте карточку существующего договора + - Измените **«Валюта взаиморасчётов»** на RUB + - Убедитесь, что **«Расчёты в условных единицах»** = нет + - Проведите документы заново + +### Поддержка условных единиц (будущее) + +Если в будущем потребуется поддержка расчётов в у.е.: +1. Добавьте в API МойСклад передачу признака у.е. +2. Измените строку в генераторе: + ```python + is_ue = "true" if order.get("useConventionalUnits") else "false" + ``` +3. В ED-XML добавьте: + ```xml + <РасчетыВУсловныхЕдиницах>true + ``` + +## Ссылки + +1. [Расчеты с контрагентами в 1С:Бухгалтерии 8](https://buh.ru/articles/raschety-s-kontragentami-v-1s-bukhgalterii-8.html) +2. [Взаиморасчёты в валюте, оплата в рублях (1С:ERP)](https://sitec-it.ru/blog/1c-erp/vzaimoraschety-v-valyute-oplata-v-rublyakh-oformlenie-realizatsii-v-1s-erp/) +3. [Формат обмена EnterpriseData 1.20](https://buh.ru/news/samoe-novoe-v-1s-bukhgalterii-8-format-obmena-enterprisedata-1-20-s-drugimi-programmami.html) + +## История изменений + +### 2025-11-06 +- **Исправление**: Добавлена валюта и признак у.е. в seed для GUID договора +- **Улучшение**: Добавлено диагностическое логирование для RUB/валюта +- **Документация**: Создан файл CONTRACT_UUID_FIX.md diff --git a/modules/moysklad_client.py b/modules/moysklad_client.py index c1ec7ad..a44d888 100644 --- a/modules/moysklad_client.py +++ b/modules/moysklad_client.py @@ -441,6 +441,12 @@ class MoySkladClient: elif "meta" in consignee_data: consignee_id = consignee_data["meta"]["href"].split("/")[-1] result["consignee"] = self.get_counterparty(consignee_id) + + # Добавляем адрес доставки из заказа, если он указан + if result.get("consignee") and "shipmentAddress" in order: + shipment_addr = order["shipmentAddress"] + if shipment_addr: + result["consignee"]["deliveryAddress"] = shipment_addr # Позиции отгрузки result["positions"] = self.get_order_positions(order_id) diff --git a/modules/validator.py b/modules/validator.py index 9f7ccad..841026f 100644 --- a/modules/validator.py +++ b/modules/validator.py @@ -269,12 +269,23 @@ class OrderValidator: # ИНН inn = agent.get("inn") + company_type = agent.get("companyType", "legal") + if not inn: - results.append(ValidationResult( - level=ValidationLevel.CRITICAL, - field="counterparty.inn", - message="Не указан ИНН контрагента" - )) + # Для физических лиц (не ИП) ИНН не обязателен + if company_type == "individual": + results.append(ValidationResult( + level=ValidationLevel.WARNING, + field="counterparty.inn", + message="Не указан ИНН контрагента (физическое лицо)" + )) + else: + # Для юр. лиц и ИП ИНН обязателен + results.append(ValidationResult( + level=ValidationLevel.CRITICAL, + field="counterparty.inn", + message="Не указан ИНН контрагента" + )) elif not self.validate_inn(inn): results.append(ValidationResult( level=ValidationLevel.CRITICAL, @@ -304,7 +315,7 @@ class OrderValidator: address = agent.get("legalAddress") or agent.get("actualAddress") if not address: results.append(ValidationResult( - level=ValidationLevel.CRITICAL, + level=ValidationLevel.WARNING, field="counterparty.address", message="Не указан адрес контрагента" )) @@ -488,6 +499,29 @@ class OrderValidator: field="consignee.name", message="Не указано наименование грузополучателя" )) + + # КРИТИЧНО: адрес грузополучателя обязателен для УПД + consignee_address = ( + consignee.get("deliveryAddress") or + consignee.get("actualAddress") or + consignee.get("legalAddress") + ) + if not consignee_address: + # Фолбэк: используем адрес отгрузки из документа, если он есть + shipment_addr = ( + (order.get("shipmentAddressFull") or {}).get("addInfo") + or order.get("shipmentAddress") + ) + if shipment_addr: + consignee_address = shipment_addr # используется для логики + else: + results.append(ValidationResult( + level=ValidationLevel.CRITICAL, + field="consignee.address", + message=( + "Не указан адрес грузополучателя (обязательно для УПД)" + ) + )) # Валидация позиций positions = order_data.get("positions", []) diff --git a/modules/xml_generator.py b/modules/xml_generator.py index ad6103a..b0600c3 100644 --- a/modules/xml_generator.py +++ b/modules/xml_generator.py @@ -202,8 +202,12 @@ class XMLGenerator: name = counterparty.get("legalTitle", "") etree.SubElement(shipper_info, "НаимОрг", Наим=name) - # Адрес - address = counterparty.get("legalAddress", "") + # Адрес (приоритет: deliveryAddress -> actualAddress -> legalAddress) + address = ( + counterparty.get("deliveryAddress") or + counterparty.get("actualAddress") or + counterparty.get("legalAddress", "") + ) if address: addr_element = etree.SubElement(shipper, "Адрес") etree.SubElement( @@ -292,8 +296,8 @@ class XMLGenerator: name = counterparty.get("legalTitle", "") etree.SubElement(buyer_info, "НаимОрг", Наим=name) - # Адрес - address = counterparty.get("legalAddress", "") + # Адрес (для покупателя используем юридический адрес) + address = counterparty.get("legalAddress") or counterparty.get("actualAddress", "") if address: addr_element = etree.SubElement(buyer, "Адрес") etree.SubElement( @@ -898,8 +902,22 @@ class XML1CGenerator: # Договор contract_elem = etree.SubElement(settlement_elem, "Договор") # Генерируем детерминированный UUID для договора на основе контрагента + # ВАЖНО: включаем валюту и признак у.е. в seed для uuid5 + # Это обеспечивает создание отдельных договоров для разных валют: + # - RUB/не-у.е. → 62.01/62.02 (задолженность/авансы) + # - валюта → 62.21/62.22 (валютные расчёты) + # - у.е. → 62.31/62.32 (расчёты в у.е.) import uuid - contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, f"contract-{cp_id}-{org_id}")) + currency_code = currency.get("isoCode", "643") + is_ue = "false" # В текущей реализации всегда false + seed = f"contract-{cp_id}-{org_id}-CUR-{currency_code}-UE-{is_ue}" + contract_uuid = str(uuid.uuid5(uuid.NAMESPACE_DNS, seed)) + + logger.info( + f"Создан договор с учётом валюты: " + f"контрагент={cp_id}, организация={org_id}, " + f"валюта={currency_code}, у.е.={is_ue}, UUID={contract_uuid}" + ) etree.SubElement(contract_elem, "Ссылка").text = contract_uuid etree.SubElement(contract_elem, "ВидДоговора").text = "СПокупателем" @@ -1068,6 +1086,28 @@ class XML1CGenerator: encoding=self.encoding ).decode(self.encoding) + # ДИАГНОСТИКА: Проверяем соответствие валюты документа и договора + # Если валюта RUB и не у.е., 1С должна использовать счета 62.01/62.02 + # Если валюта иностранная → 62.21/62.22 + # Если у.е. → 62.31/62.32 + if currency_code == "643" or currency_code == "RUB": + logger.info( + "✓ Документ в рублях (RUB, код 643), не в у.е. " + "→ ожидаются счета 62.01 (задолженность) / 62.02 (авансы). " + "Если 1С подставляет 62.21/62.22 или 62.31/62.32, проверьте: " + "1) Регистр 'Счета расчетов с контрагентами' - нет ли " + "индивидуальных правил на контрагента/договор. " + "2) В карточке договора: Валюта взаиморасчётов = RUB, " + "Расчёты в условных единицах = нет." + ) + else: + logger.info( + f"⚠ Документ в иностранной валюте ({currency_code}) " + "→ ожидаются счета 62.21/62.22. " + "Убедитесь, что договор создан с валютой взаиморасчётов " + f"= {currency_code}." + ) + # Сохранение в файл if output_path: with open(output_path, "w", encoding=self.encoding) as f: diff --git a/test_order_4926.xml b/test_order_4926.xml new file mode 100644 index 0000000..62b9f18 --- /dev/null +++ b/test_order_4926.xml @@ -0,0 +1,89 @@ + +<Файл ВерсПрог="BUH_BOT_1.0" ВерсФорм="5.03"> + <Документ> + <СвСчФакт> + <НомерДата Номер="4926" Дата="06.11.2025"/> + <СвПрод ОКПО="0142109827"> + <ИдСв> + <СвИП ИННФЛ="263605160270" СвГосРегИП="304263530000073"> + <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> + + + <Адрес> + <АдрТекст Текст="355000, Россия, Ставропольский край, г Ставрополь"/> + + <БанкРекв НомерСчета="40802810860270001979"> + <СвБанк НаимБанк="СТАВРОПОЛЬСКОЕ ОТДЕЛЕНИЕ N5230 ПАО СБЕРБАНК" БИК="040702615" КорСчет="30101810907020000615"/> + + <Контакт> + <Тлф>8-800-234-33-68 + <ЭлПочта>robotail@bk.ru + + + <ГрузОт> + <ОнЖе>он же + + <ГрузПолуч> + <ИдСв СвЮЛУч="" КПП=""> + <НаимОрг Наим="Котов Антон Максимович"/> + + <Контакт> + <Тлф>+79218378869 + + + <ДокПодтвОтгрНом РеквНаимДок="УПД" РеквНомерДок="4926" РеквДатаДок="06.11.2025"/> + <СвПокуп> + <ИдСв СвЮЛУч="" КПП=""> + <НаимОрг Наим="Котов Антон Максимович"/> + + <Контакт> + <Тлф>+79218378869 + <ЭлПочта>77alpha@mail.ru + + + <ДенИзм КодОКВ="643" НаимОКВ="Российский рубль"/> + + <ТаблСчФакт> + <СведТов НомСтр="1"> + <НаимТов Наим="Камера для силомера Rocky Boxer"/> + <ОКЕИ_Тов НаимЕдИзм="шт"/> + <КолТов Количество="3.0"/> + <ЦенаТов Цена="390.00"/> + <СтТовБезНДС СтоимБезНДС="1114.29"/> + <НалСт СтавНДС="5" СумНДС="55.71"/> + <СтТовУчНал СтоимУчНДС="1170.00"/> + + <СведТов НомСтр="2"> + <НаимТов Наим="Кожаный чехол для груши (Черный)"/> + <ОКЕИ_Тов НаимЕдИзм="шт"/> + <КолТов Количество="2.0"/> + <ЦенаТов Цена="7810.00"/> + <СтТовБезНДС СтоимБезНДС="14876.19"/> + <НалСт СтавНДС="5" СумНДС="743.81"/> + <СтТовУчНал СтоимУчНДС="15620.00"/> + + <СведТов НомСтр="3"> + <НаимТов Наим="Полиуретановый конус с усиленной оксидированной шпилькой"/> + <ОКЕИ_Тов НаимЕдИзм="шт"/> + <КолТов Количество="2.0"/> + <ЦенаТов Цена="2680.00"/> + <СтТовБезНДС СтоимБезНДС="5104.76"/> + <НалСт СтавНДС="5" СумНДС="255.24"/> + <СтТовУчНал СтоимУчНДС="5360.00"/> + + <ВсегоОпл> + <СтТовБезНДСВсего СтоимБезНДС="21095.24"/> + <СумНДСВсего СумНДС="1054.76"/> + <СтТовУчНалВсего СтоимУчНДС="22150.00"/> + + + <СвПродПер> + <СвПер СодОпер="Товары переданы" ДатаПер="06.11.2025"> + <БезДокОснПер>1 + + + <Подписант СпосПодтПолном="6"> + <ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/> + + +