Особенности разделения объектной модели документа и базы данных в 1С 7.7. Забавный глюк




Принцип обмена данными из 1С с сайтом (на MySQL) и выдачи (публикации) этих данных по запросу.
PHP-Скрипт автоматической загрузки данных из файла данных в формате CSV в базу данных сайта работающего на WordPress.

В продолжение моей темы: 1С:Альфа-Авто Автосалон Автосервис: обмен с сайтом.
С помощью данного скрипта можно загружать в автоматическом режиме, по расписанию, данные сервисных книжек (ремонтов авто) из 1С:Альфа-Авто Автосалон Автосервис.
Также можно загружать данные в ручном режиме: для этого делается скрытая страница, где размещается специальная кнопка.
Комментарии размещенные внутри скрипта разъяснят логику и порядок действия.
Комментарии с "/////    echo" использовались для отладки.
Дополнительно создана таблица для журналирования результатов загрузки данных.
Скрипт включает в себя защиту от SQL инъекций (думаю безопасность соблюдена в полной мере).
В кратце:
1. Пишется скрипт, который запускает этот.
2. Создается регламентное задание в WordPress, по которому запускается скрипт из п.1. 
3. Этот скрипт осуществляет проверку на существование файла обмена в папке.
4. Если данные не новые, загрузка не производится.
5. Если данные новые, очищается таблица сервисных книжек.
6. Загружаются новые данные.

Собственно сам скрипт:

<?php // Полная загрузка сервисных книжек, создан 2026-01-05 12:44:55

global $wpdb2;
global $failure;
global $file_hist;

/////  echo '<H2><b>Старт загрузки</b></H2><br>';

$failure=FALSE;
//подключаемся к базе
$wpdb2 = include_once 'connection.php'; ; // подключаемся к MySQL
// если не удалось подключиться, и нужно оборвать PHP с сообщением об этой ошибке
if (!empty($wpdb2->error))
{
/////   echo '<H2><b>Ошибка подключения к БД, завершение.</b></H2><br>';
$failure=TRUE;
wp_die( $wpdb2->error );
}

$m_size_file=0;
$m_mtime_file=0;
$m_comment='';
/////проверка существования файлов выгрузки из 1С
////файл выгрузки сервисных книжек
$file_hist = ABSPATH.'/_1c_alfa_exchange/AA_hist.csv';
if (!file_exists($file_hist))
{
/////   echo '<H2><b>Файл обмена с сервисными книжками не существует.</b></H2><br>';
$m_comment='Файл обмена с сервисными книжками не существует';
$failure=TRUE;
}

/////инициируем таблицу лога
/////если не существует файла то возврат и ничего не делаем
if ($failure){
///включает защиту от SQL инъекций и данные можно передавать как есть, например: $_GET['foo']
/////   echo '<H2><b>Попытка вставить запись в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>$m_comment));
wp_die();
/////    echo '<H2><b>Возврат в начало.</b></H2><br>';
return $failure;
}
/////проверка лога загрузки, что бы не загружать тоже самое
$masiv_data_file=stat($file_hist);   ////передаем в массив свойство файла
$m_size_file=$masiv_data_file[7];    ////получаем размер файла
$m_mtime_file=$masiv_data_file[9];   ////получаем дату модификации файла
////создаем запрос на получение последней удачной загрузки
////выбираем по штампу времени создания (редактирования) файла загрузки AA_hist.csv, $m_mtime_file

/////   echo '<H2><b>Размер файла: '.$m_size_file.'</b></H2><br>';
/////   echo '<H2><b>Штамп времени файла: '.$m_mtime_file.'</b></H2><br>';
/////   echo '<H2><b>Формирование запроса на выборку из лога</b></H2><br>';
////препарируем запрос
$text_zaprosa=$wpdb2->prepare("SELECT * FROM `vin_logs` WHERE `last_mtime_upload` = %s", $m_mtime_file);
$results=$wpdb2->get_results($text_zaprosa);

if ($results)
{   foreach ( $results as $r)
{
////если штамп времени и размер файла совпадают, возврат
if (($r->last_mtime_upload==$m_mtime_file) && ($r->last_size_upload==$m_size_file))
{////echo '<H2><b>Возврат в начало, т.к. найдена запись в логе.</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>$m_mtime_file,'last_size_upload'=>$m_size_file,'comment'=>'Загрузка отменена, новых данных нет, т.к. найдена запись в логе.'));
wp_die();
return $failure;
}
}
}
////если данные новые, пишем в лог запись о начале загрузки
/////echo '<H2><b>Попытка вставить запись о начале загрузки в лог таблицу</b></H2><br>';
$insert_fail_zapros=$wpdb2->insert('vin_logs', array('time_stamp'=>time(),'last_mtime_upload'=>0, 'last_size_upload'=>$m_size_file, 'comment'=>'Начало загрузки'));

////очищаем таблицу
$clear_tbl_zap=$wpdb2->prepare("TRUNCATE TABLE %s", 'vin_history');
$clear_tbl_zap_repl=str_replace("'","`",$clear_tbl_zap);
$results=$wpdb2->query($clear_tbl_zap_repl);
/////   echo '<H2><b>Очистка таблицы сервисных книжек</b></H2><br>';
if (empty($results))
{
/////   echo '<H2><b>Ошибка очистки таблицы книжек, завершение.</b></H2><br>';
//// если очистка не удалась, возврат
$failure=TRUE;
wp_die();
return $failure;
}

////загружаем данные
$table='vin_history';         // Имя таблицы для импорта
//$file_hist Имя CSV файла, откуда берется информация     // (путь от корня web-сервера)
$delim=';';          // Разделитель полей в CSV файле
$enclosed='"';      // Кавычки для содержимого полей
$escaped='\

26 Comments

  1. vcv

    Вполне логично. После НайтиДокумент вы имеете в памяти закешированные данные объекта и работаете с ними. И тут бабах, меняете состояние объекта в базе. Разом получаете конфликт — в базе одна информация, а в памяти другая. По хорошему в подобных случаях языку бы выдавать ошибку блокировки объекта…

    Reply ↓
  2. CheBurator

    Статья изобилует ошибками.

    на типовой ТиС

    1.

    Оператор добавил в проведенную расходную накладную товар, которого нет на остатке и попытался перепровести её. Документ сначала записался, но обработка проведения завершилась с ошибкой: недостаточно товара на складе и движения документа не изменились. Таким образом в документе мы видим большее количество товара, сумма документа увеличилась, мы даже можем напечатать его в таком состоянии.

    — все неверно.

    Речь в данном примере идет ОБ ИНТЕРАКТИВНОМ записи и проведении документа (работает оператор). Здесь:

    Документ записался. Но не провелся. Транзакция откатилась. ДОКУМЕНТ В БАЗУ НЕ ЗАПИСАН. ИЗМЕНЕН, НО НЕ ЗАПИСАН — об этом свидетельствует заначок незаписанной модификации {*} в заголовке формы документа. таким образом мы в документе видим бОльшее количество товара, но оно в базу не записано. Мы можем напечатать что угодно — но это не значит что напечатанное соответствует реальности. При печати из МОДИФИЦИРОВАННОЙ ФОРмы (если мне не изменяет память) через ВПФ или встроенную печформу — надо анализировать признак модифицированности формы (при передаче в процедуру печати группового контекста формы) или печатать АКТУАЛЬНЫЙ ДОКУМЕНТ по передаваемой ссылке на документ. и цифры в печформе будут соответсоввать реальности. или тому как вы хотите.

    таким образом — данный абзац автора демонстрирует всего лишь а) возможно, невнимательность) и б) незнание особенности программной реализации кода конфигурации (полностью типовой ТиС у меня под рукой нет, но насколько мне помнится там при попытке печати модифицированного но не записанного документа выдавалось предупреждение; наставиать не буду, факт лишь то — что при незаписанных данных обеспечить печать ПРАВИЛЬНЫХ данных — это всего лишь надо знать и уметь использовать предоставляемые платформой возможности).

    Reply ↓
  3. CheBurator
    Лечится такая ситуация очень просто. После шага 3 надо было Записать() документ,

    — более того, исходный код обработки НЕВЕРЕН. так как не обеспечивает логическую непротиворечивость данных. От реквизита Документ_1 зависит проведение документа. Вы меняете этот реквизит У ПРОВЕДЕННОГО ДОКУМЕНТА, но при этом мало того что не записываете — ДАЖЕ ЕСЛИ И ЗАПИСАЛИ — его надо еще дополнительно ОБЯЗАТЕЛЬНО ПРОВЕСТИ.

    то есть в вашем случае код обработки должен выглядеть так:

    Процедура Выполнить()

    док2 = СоздатьОбъект(«Документ.Документ_2»);

    док2.НайтиДокумент(ВыбДокумент_2);

    док2.Документ_1 = ВыбДокумент_1;

    док2.Записать(); //д.б.обязательно в соответстии с логикой использования документа_2

    док2.Провести(); //д.б.обязательно в соответстии с логикой использования документа_2

    //А ЗДЕСЬ ДАЛЬШЕ ДЕЛАЙТЕ ЧТО ХОТИТЕ С ДОКУМЕНТОМ_2

    Сообщить(«1. Перед распроведением Документ 1: «+док2.Документ_1);

    док2.СделатьНепроведенным();

    Сообщить(«2. После распроведения Документ 1: «+док2.Документ_1);

    док2.Записать();

    Сообщить(«3. После записи Документ 1: «+док2.Документ_1);

    док2.Провести();

    Сообщить(«4. После проведения Документ 1: «+док2.Документ_1);

    КонецПроцедуры

    Reply ↓
  4. CheBurator

    с такой поправкой — все отрабатывает как надо — в базе появляется документ2 в котором в шапке заполнен документ1

    Reply ↓
  5. CheBurator

    (1)

    После НайтиДокумент вы имеете в памяти закешированные данные объекта и работаете с ними. И тут бабах, меняете состояние объекта в базе. Разом получаете конфликт — в базе одна информация, а в памяти другая. По хорошему в подобных случаях языку бы выдавать ошибку блокировки объекта…

    — как ты себе это представляешь?

    прочитал я объект — меняю его реквизиты

    в это время в другой сессии ктото прочитал этот обьект, поменял его напрочь и успел записать. а я еще все меняю и потом записываю — и все изменения второй сесии — похерятся нафиг. Это штатное поведение клюшек. Чтение — всегда грязное.

    а в 8-ке — да, при попытке записи измененного объекта — выдается ошибка.

    Reply ↓
  6. CheBurator

    Глючок, конечно, занятный

    Суть его в том, что по факту записи документа в базу после распроведения не происходит — почему так ХЗ, надо поинтереосваться у знающих людей.

    а так — взять на заметку что после для избежания такого выверта надо либо как отметил выше автор(с моим уточнением) перед распроведением записать+провести документ (то есть программировать как надо по логике — а не тяп-ляп на скорую руку), либо актуализировать документ

    Сообщить(«1. Перед распроведением Документ 1: «+док2.Документ_1);

    док2.СделатьНепроведенным();

    док2.НайтиДокумент(ВыбДокумент_2);

    Сообщить(«2. После распроведения Документ 1: «+док2.Документ_1);

    Reply ↓
  7. Vortigaunt

    Я не говорил, что речь идет о типовой ТиС. Но как ни странно именно в ТиС я такую ситуацию и наблюдал. ТиС тоже есть разных версий и ее зачастую дописывают. А есть еще ТиС для Украины с которой я в основном и работаю, и она сильно отличается от российской. Я утверждаю лишь то, что если в коде модуля формы не значится ПриЗаписиПерепроводить(1), то есть возможность записать проведенный документ без его перепроведения. Какие это вызывает проблемы — указано в статье.

    Reply ↓
  8. Vortigaunt

    (7)

    (то есть программировать как надо по логике — а не тяп-ляп на скорую руку)

    В статье приведен код, очищенный и подготовленный, так сказать. Ясное дело, что по нему сразу видно ошибку. Реальный же код, который выдал эту ошибку был более громоздкий и запутанный. Как минимум распроведение документа скрывалось в отдельной процедуре. Процедура эта была добавлена в процессе доработки: нужно было менять время существующих документов.

    И если эту особенность не знать или не думать в эту сторону, то с отладчиком можно рехнуться))

    Reply ↓
  9. CheBurator

    (9) насчет рехнутьяс — это точно.

    а то что распроведение было «спрятано» — ну так это и есть — смастерили костыль на скорую руку, без понимания логики ранее написанного запутанного и громоздкого кода.. 😉

    Reply ↓
  10. CheBurator

    Пообщался со спецами.

    Условно (связано с программисткими виртуальными функциями и пр.шнягой):

    Модифицировать реквизиты нужно ПОСЛЕ ПОСЛЕДНЕГО ЧТЕНИЯ объекта (или записывать модифицированное перед чтением).

    если ты смодифицировал реквизиты, а потом явно или неявно перечитал в память объект — то в зависимости от кучи условий (так как разные функции чтения читают разный набор полей) может быть либо ожидаемый, либо неожидаемый результат.

    Причем в зависимости от методов чтения объекта — могут получаться разные «версии» полей одного и того же объекта.

    И типа в этом случае глюка:

    НайтиДокумент дал объект с набором полей «версия1»

    СделатьНеПоведенным для этого же объекта породил набор полей «версия2».

    причем в общем случае перечень полей (а это СВОЙСТВА объекта) в разных версиях может быть разным.

    и записать() использует вторую версию полей (с незаполненной шапкой Документ_1)

    а провести() использует первую версию полей с заполненным шапкой Документ1

    То есть вот так очень условно и очень мутно — ибо программеры человеческим языком для простых девЕлоперов пояснить не могут. 😉

    Reply ↓
  11. CheBurator

    СделатьНеПоведенным — вызывает чтение объекта из базы… и порождает вторую «версию» полей объекта (набор полей не обяхательно совпадает с набором полей из первой версии). и одновременно у объекта есть поле «Документ1» которое заполнено и второе, которое назаполнено.

    И как себя теперь поведет система при дальнейшем использовании объекта — мы уже ХЗ.

    поэтому программить надо аккуратно. Именно то о чем я говорил выше. Расставлляя костыли — смотреть чтобы костыль был поставлен в нужное место.

    а в исходном варианте — как написал сам автор — костылей было столько, что еще один костыль среди этого леса поставили не там где надо…

    Reply ↓
  12. CheBurator

    В итоге все что нужно (как написал выше) — модифицировать ПОСЛЕ псоледнего чтения, т.е. после распроведения.

    такой код — ведет себя ожидаемо и предсказуемо

    //*******************************************

    Процедура Выполнить()

    док2 = СоздатьОбъект(«Документ.Документ_2»);

    док2.НайтиДокумент(ВыбДокумент_2);

    Сообщить(«1. Перед распроведением Документ 1: «+док2.Документ_1);

    док2.СделатьНепроведенным();

    Сообщить(«2. После распроведения Документ 1: «+док2.Документ_1);

    док2.Документ_1 = ВыбДокумент_1;

    Сообщить(«3. Перед записи Документ 1: «+док2.Документ_1);

    док2.Записать();

    Сообщить(«3. После записи Документ 1: «+док2.Документ_1);

    док2.Провести();

    Сообщить(«4. После проведения Документ 1: «+док2.Документ_1);

    КонецПроцедуры

    Reply ↓
  13. Vortigaunt

    (11) Спасибо за проявленный интерес и предоставленный отзыв специалистов по внутреннему миру платформы.

    Я правильно понял, что метод СделатьНеПроведенным() создает новый экземпляр объекта куда перечитывает из базы поля документа, которые мы в свою очередь еще не обновили? Только выходит, что ссылка на этот экземпляр не возвращается в нашу переменную и поэтому Метод Записать() не обновляет данные в базе. Ведь мы вызываем метод Записать() из первого объекта, а он уже «не связан» с данными?

    Просто я себе никак не могу представить одновременное существование разных состояний одного экземпляра объекта.

    Reply ↓
  14. CheBurator

    (14) не, немножко не так (надо учитывать что мой пересказ — это некая мутная сказка про то что мне пытались рассказать умные люди). как-то так:

    объект (1Сный док2) остается тот же самый

    но

    НайтиДокумент — реализуется некоей функцией чтения, которая читает из базы в поля объекта соответствующие поля базы

    СделатьНепроведенным — реализуется ДРУГОЙ функцией чтения, которая тоже читает в поля объекта из базы, но не обязательно тот же самый набор полей, могут читаться ограниченный набор полей — и под этот набор создается новая область в памяти (какие-то виртуальные программистские функции я хз что это за хрень)

    условно:

    в памяти имеем после этих двух методов

    Объект (он один)

    |___>(набор полей#1) — использовался при проведении

    |___>(набор полей#2) — использовался при записи

    Мое объяснение — это как пересказ тупым то что он услышал от умных, то есть полный бред… то есть весьма условное.

    Излагать связно, коротко и понятно — должен спец. но они страдают невозможностью доступно изложить неспециалисту узкоспециализированные вещи — поэтому весьма условные и мутные аналогии

    Итог прост

    1. сначала прочитай все что нужно — потом модифицируй и записывай

    2. смодифицировал но не записал и перечитываешь тот же самый объект — а) смысл этого непонятен б) могут быть глюки если перечитка внутри 1С реализуется (в зависимости от разных услвоий) другим вариантом функции чтения..

    бред короче несу

    Reply ↓
  15. CheBurator

    (14)

    Просто я себе никак не могу представить одновременное существование разных состояний одного экземпляра объекта.

    я тоже, если рассматривать обьект как простую линейную структуру. Но там все сложнее — объект — это куча связанных структур/списков… или например как реквизиты объекта — это ведь даже в СП они называются СВОЙСТВА и может быть одновременно один и тот же объект полученный разными МЕТОДАМИ (НайтиПоРеквизиту,ПолучитьЭлемент() итд. а разные МЕТОДЫ имеют свой набор СВОЙСТВ (прочитанных полей-реквизитов)

    короче не надо меня слушать.. бредятина…

    спецы ща ржут в голос наблюдая как один слепой пытается объяснить другому слепому что такое «слон»

    Reply ↓
  16. hogik

    (14)

    Просто я себе никак не могу представить одновременное существование разных состояний одного экземпляра объекта.

    Дмитрий.

    Ваше утверждение

    При этом все отметки о модифицированности документа снимаются

    можно принять как объяснение происходящих процессов в движке 1С-а. Только снимается не «отметки … документа», а «отметки» об модификации «свойств» (полей документа) в оперативной памяти. И не заморачиваться — каким образом реализованы в движке эти самые «отметки».

    Reply ↓
  17. vcv

    (6)

    как ты себе это представляешь?

    Чисто теоретически, в 1С8 сделано правильно. По крайней мере при интерактивной работе. Чтение грязное. Но первая же попытка внести изменения в объект приводит к его блокировке. А штатное поведение клюшек работает на уровне прошлого века. Никакого защищенного/безопасного программирования и прочих серебрянных пуль 21 века 🙂

    Reply ↓
  18. vcv

    (4) Еще попытки-исключение не хватает и транзакции. Вдруг документ запишется, а при проведении возникнет ошибка.

    Reply ↓
  19. vcv

    (14) Скорее всего СделатьНеПроведённым() никакого нового экземпляра объекта не создаёт. Лезет в базу данных, помечает документ как непроведённый, удаляет его движения, пересчитывает итоги. А объект в памяти остаётся как есть. Со всеми изменениями. Больше хитростей в методе Провести. Он работает с объектом в памяти. Но с инициализацией объекта там есть конкретные косяки.

    Reply ↓
  20. vcv

    (14)

    Просто я себе никак не могу представить одновременное существование разных состояний одного экземпляра объекта.

    Ты не поверишь 🙂

    Док1 = СоздатьОбъект(«Документ»);

    Док2 = СоздатьОбъект(«Документ»);

    Док1.НайтиДокумент(Док);

    Док2.НайтиДокумент(Док);

    Это, конечно, не «один экземпляр объекта», но кто его знает, как всё устроено внутри. При вызове СделатьНепроведенным явно не нужна вся информация о документе, поэтому незачем тащить документ в память. И незачем пользоваться объектом в памяти, потому что непроведенным делается документ в базе, а не в памяти.

    А то, что 1С разрешает такой конфликт заполучить на ровном месте, это уже другой вопрос.

    Reply ↓
  21. Vortigaunt

    (21) Вы привели пример создания 2-х экземпляров объекта. Мне кажется если у одного из них попытаться либо Записать(), либо Провести() либо СделатьНеПроведенным() сработает исключение «Объект заблокирован». Но не уверен точно. Надо проверить.

    Reply ↓
  22. Vortigaunt

    (20) Такое объяснение не дает ответ на вопрос, почему метод Записать() не возвращает наши измененные значения реквизитов из объекта в памяти в запись в базе данных.

    Reply ↓
  23. vcv

    (23)

    Такое объяснение не дает ответ на вопрос…

    Боюсь, что ответ на вопрос, получить невозможно в принципе. Ответить могут только разработчики 1С 7.7, которые уже давно забыли что это такое. Мы можем только гадать. Предположите, что 1С пытается оптимизировать обращение с базой данных и не писать те реквизиты документа, которые не изменялись.

    Проверить это предположение вы легко можете сами, если есть под рукой sql-база. Ловите профайлером запрос на изменение данных и смотрите, все ли реквизиты в него попали. Возьмётесь?

    P.S.

    Ну и зачем вам этот ответ? Вы подметили тонкое место, на котором могут огрести проблем изготовители бардачного кода. Отлично. Но при правильном кодировании проблемы возникнуть не должно

    НачатьТранзакцию

    Попытка

    СделатьНепроведенным

    <изменения в документе>

    Записать

    Провести

    ЗафиксироватьТранзакцию

    Исключение

    ОтменитьТранзакцию

    КонецПопытки

    Reply ↓
  24. hogik

    (20)

    (23)

    Дмитрий, Владислав.

    Я посмотрел трассировку действий движка 1С-а при выполнении СделатьНеПроведённым() с помощью https://infostart.ru/public/15211/

    Да. Чтения самого документа там нету. Есть обращения (чтение/запись/удаление) к журналу документов, ссылкам документов, регистрам. Но, для осознания происходящего в тесте из данной публикации надо иметь в виду, что для работы с полями одного и того же документа используется несколько «массивов» (списков) в оперативной памяти 1С-а. И значения полей «перемещаются» и предоставляются разным алгоритмам из разных «массивов». Например (очень примерно!!!), присвоение «док2.Документ_1 = ВыбДокумент_1» помещает значение в «массив» переменных текущей функции. Алгоритм присвоения значения переменной «понимает», что речь идёт о поле документа. И помещает это же значений ещё и в другой «массив» для последующего выполнения Записать() или Провести(). И уже этот «массив» использует движок (что я уже и вижу в трассировке) для выдачи команд СУБД по переносу значений полей в буфер ввода/вывода СУБД. Этот второй «массив» очищается, меняет состав строк, пересоздаётся и т.д. в зависимости от используемых типов/видов/методов объекта 1С-а по работе с базой данных. Логично предположить, что метод СделатьНеПроведённым() очищает этот второй «массив».

    Reply ↓
  25. Vortigaunt

    (13) Кстати да. Я провел небольшое тестирование. При любом присваивании в реквизиты документа после метода СделатьНепроведенным() (причем неважно в какой реквизит) объект опять оживает, и метод Записать() записывает данные в базу.

    Reply ↓
  26. hogik

    (26)

    Дмитрий.

    Я сделал сравнение двух трассировок для:

    1)

    Док.Поле1=»11111″;
    Док.СделатьНепроведенным();
    Док.Записать();
    Док.Провести();

    2)

    Док.Поле1=»11111″;
    Док.СделатьНепроведенным();
    Док.Поле3=»33333″;
    Док.Записать();
    Док.Провести();

    В первом случае в таблицы БД документа ничего не пишется. Обновляется только журнал документов. Во втором случае производится обновление только таблица шапки документа (Поле1 и Поле3 — реквизиты шапки). Аналогичные действия для табличной части документа вызывают обновление только табличной части. Т.е. в кишках 1С-а существует два «флага» (признака) модифицированности для шапки и для табличной части.

    Т.е. надо всегда делать как написано в (24) сообщении.

    Только поменять порядок операторов:

    НачатьТранзакцию
    Попытка 
    Reply ↓

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *