fix: корректный договор по валюте и адреса контрагентов

This commit is contained in:
admin i 2025-11-06 12:03:21 +03:00
parent a16ffca725
commit 5283bf2b85
6 changed files with 348 additions and 11 deletions

View File

@ -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

146
docs/CONTRACT_UUID_FIX.md Normal file
View File

@ -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

View File

@ -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)

View File

@ -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", [])

View File

@ -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:

89
test_order_4926.xml Normal file
View File

@ -0,0 +1,89 @@
<?xml version='1.0' encoding='UTF-8'?>
<Файл ВерсПрог="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">
<ФИО Фамилия="Беляев" Имя="Андрей" Отчество="Юрьевич"/>
</Подписант>
</Документ>
</Файл>