В этой статье мы подробно разберем, как решить типичные проблемы при работе со справочниками и реализовать сложный учет по методу FIFO (First-In, First-Out) для ценных бумаг в 1С v7.7. Мы рассмотрим не только исправление ошибок в коде, но и фундаментальные архитектурные решения, которые позволят создать надежную и прозрачную систему учета.
Наш читатель столкнулся с проблемой: элемент справочника не перезаписывался, хотя, по отладчику, код выполнялся. Причина крылась в некорректном использовании методов для поиска и выбора элементов.
Изначальный код выглядел так:
TransactionReference = CreateObject("Reference.Transaction");
...
Buys.Rest = Buys.Rest - Sales.Rest;
TransactionReference.UseOwner(Sales.Ticker);
TransactionReference.SelectItems();
TransactionReference.FindByCode(Buys.TransactionID.Код);
If TransactionReference.Selected() = 1 Then
TransactionReference.Rest = Buys.Rest;
TransactionReference.Write();
EndIf;
SelectItems(): Этот метод предназначен для начала циклической выборки элементов справочника. При его использовании без последующего цикла по элементам (например, Пока TransactionReference.GetItem() = 1 Цикл ... КонецЦикла) он бесполезен, а в данном контексте даже может сбивать с толку. Нам нужно не выбирать элементы для перебора, а найти конкретный по коду.FindByCode(): Сам по себе метод правильный, но его использование может быть неточным, если не указать контекст поиска. В случае подчиненных справочников или справочников с неуникальными кодами, необходимо уточнять, где именно искать.TransactionReference.UseOwner(Sales.Ticker), чтобы ограничить поиск элементами, подчиненными конкретному владельцу. Однако, комбинация SelectItems() и FindByCode() может некорректно работать.Для корректного поиска и перезаписи элемента справочника следует:
TransactionReference.SelectItems(), так как оно лишнее.FindByCode(). Метод FindByCode() может принимать второй необязательный параметр, указывающий режим поиска: 0 — во всем справочнике, 1 — среди элементов текущего владельца, 2 — среди элементов текущего владельца и в его иерархии. Если ваш справочник подчинен, и вы хотите искать только среди элементов конкретного владельца, используйте 1.Исправленный код будет выглядеть так:
TransactionReference = CreateObject("Reference.Transaction");
...
Buys.Rest = Buys.Rest - Sales.Rest;
TransactionReference.UseOwner(Sales.Ticker);
// TransactionReference.SelectItems(); // Эту строку удаляем
TransactionReference.FindByCode(Buys.TransactionID.Код, 1); // Добавляем параметр 1
If TransactionReference.Selected() = 1 Then
TransactionReference.Rest = Buys.Rest;
TransactionReference.Write();
EndIf;
После внесения этих изменений, элемент справочника должен корректно находиться и перезаписываться. Наш читатель подтвердил, что данное изменение помогло решить исходную проблему.
Изначальная проблема с перезаписью остатков в справочнике — это лишь вершина айсберга. Более глубокая проблема заключалась в самой архитектуре учета. Хранение остатков (таких как Buys.Rest) напрямую в элементах справочника является некорректным подходом для систем 1С. Такой метод не обеспечивает:
Для учета остатков и оборотов в 1С v7.7 существуют два основных механизма:
Этот механизм основан на привычном для бухгалтеров Плане счетов и концепции двойной записи (дебет/кредит). Если вы знакомы с бухгалтерским учетом и вам важна полноценная двойная запись для контроля активов и пассивов, этот вариант будет наиболее понятным и надежным.
SHARES. Также потребуются счета для денежных средств (BANK, BROKER), комиссий (FEES), налогов (TAXES) и финансовых результатов (PL).SHARES нам понадобятся следующие субконто:
Брокер (тип "Справочник.Брокеры" или "Перечисление.Брокеры"). Позволит группировать акции по брокерским счетам.Акция (тип "Справочник.Акции"). Позволит учитывать каждую ценную бумагу отдельно.ПартияЗакупа (тип "Документ.ПокупкаЦБ" или "Справочник.ТранзакцииПокупок"). Это критически важное субконто для FIFO. Каждая уникальная покупка (или часть покупки, если она имеет уникальную дату/время/цену) должна быть идентифицирована этим субконто. В качестве значения субконто удобнее всего использовать ссылку на сам документ покупки ценных бумаг.Регистры — это более гибкий механизм, предназначенный для хранения и обработки больших объемов данных без привязки к двойной записи. Регистры можно представить как "многомерные кубы" данных, которые позволяют получать информацию в различных разрезах. Если двойная запись не является критичной (например, для чисто управленческого учета без строгой привязки к балансу), регистры могут быть более производительными и гибкими.
Брокер, Акция, ПартияЗакупа (ссылка на документ покупки или уникальный идентификатор лота).Количество (акций) и Сумма (стоимость покупки). Важно: не храним `ЕдиничнаяЦена` как ресурс, поскольку она не суммируется. Единичная цена всегда вычисляется как Сумма / Количество.ВидОперации (BUY/SALE) для регистра остатков излишен, так как тип движения (+/-) уже указывает на это.Для нашей задачи, учитывая, что читатель использует полноценную двойную запись, бухгалтерский учет на Плане счетов является предпочтительным решением.
Мы рассмотрим, как реализовать метод FIFO, используя рекомендованную структуру бухгалтерского учета с тремя субконто на счете SHARES: Брокер, Акция, ПартияЗакупа.
При проведении документа покупки формируется проводка, увеличивающая остаток акций на счете SHARES:
// В модуле проведения документа "ПокупкаЦБ"
// Создаем объект проводки
Проводка = CreateObject("Проводка");
Проводка.New();
Проводка.Дебет = Счет.Shares; // Счет учета акций
Проводка.Субконто(1, Док.Брокер); // Субконто 1: Брокер из шапки документа
Проводка.Субконто(2, Док.Акция); // Субконто 2: Акция из табличной части
Проводка.Субконто(3, Док); // Субконто 3: Ссылка на текущий документ покупки (ПартияЗакупа)
Проводка.Кредит = Счет.Broker; // Счет брокера, откуда ушли деньги
Проводка.Сумма = Док.СуммаСделки; // Сумма покупки
Проводка.Количество = Док.Количество; // Количество акций
Проводка.Валюта = Док.ВалютаСделки; // Валюта, если счет валютный
Проводка.Записать();
Важный момент: В качестве третьего субконто (ПартияЗакупа) мы используем ссылку на сам документ Док. Это позволяет однозначно идентифицировать каждую партию акций по уникальному документу покупки.
Этот документ будет сложнее, так как необходимо реализовать логику FIFO. При проведении документа продажи:
SHARES на момент проведения документа, детализируя их до третьего субконто (ПартияЗакупа) и сортируя по дате/времени документа покупки (для FIFO).
// В модуле проведения документа "ПродажаЦБ"
// Параметры для получения остатков
ДатаПроведения = Док.Дата;
Брокер = Док.Брокер;
Акция = Док.Акция;
КоличествоПродано = Док.Количество; // Общее количество акций, которое хотим продать
СуммаПродажи = Док.СуммаСделки;
// Получаем остатки по партиям закупа
ТЗОстаткиПартий = CreateObject("ТаблицаЗначений");
// Создаем запрос к БухИтогам, который вернет остатки по счету SHARES
// в разрезе Брокера, Акции и ПартииЗакупа (ДокументаПокупки)
// Отсортировать нужно по дате документа ПартииЗакупа (Субконто3)
// Примерный запрос (реализация через запрос к БухИтогам более сложна,
// здесь приводим концепцию)
//
// Для упрощения примера, представим, что мы получаем ТЗ с колонками:
// "ДокументЗакупа", "ОстатокКоличество", "ОстатокСумма"
// Это концептуальный блок, который на реальном 1С v7.7 будет реализован
// через объект "БухгалтерскиеИтоги" или запрос к ним.
// Пример получения остатков:
// БухИт = CreateObject("БухгалтерскиеИтоги");
// БухИт.ИспользоватьСубконто(ВидСубконто.Брокер, Брокер);
// БухИт.ИспользоватьСубконто(ВидСубконто.Акция, Акция);
// БухИт.ИспользоватьСубконто(ВидСубконто.ПартияЗакупа); // Без указания значения, чтобы получить по всем партиям
// БухИт.ВыбратьСубконто(ВидСубконто.ПартияЗакупа);
// БухИт.НаДату(ДатаПроведения); // На дату документа продажи
// ... далее цикл по полученным партиям для сортировки и списания
// Для примера создадим фиктивную ТЗ
ТЗОстаткиПартий.НоваяКолонка("ДокументЗакупа", "Документ");
ТЗОстаткиПартий.НоваяКолонка("ОстатокКоличество", "Число");
ТЗОстаткиПартий.НоваяКолонка("ОстатокСумма", "Число");
// Заполняем ТЗ данными (на практике это будут реальные остатки по БухИтогам)
// Представим, что у нас есть 2 партии закупа:
// ДокументПокупки1 (дата 01.01.21), 10 шт, 1000 руб.
// ДокументПокупки2 (дата 05.01.21), 15 шт, 1650 руб.
Строка = ТЗОстаткиПартий.НоваяСтрока();
Строка.ДокументЗакупа = ПолучитьДокумент(ПолучитьДату(2021,1,1), "ПокупкаЦБ", "000000001"); // Пример ссылки
Строка.ОстатокКоличество = 10;
Строка.ОстатокСумма = 1000;
Строка = ТЗОстаткиПартий.НоваяСтрока();
Строка.ДокументЗакупа = ПолучитьДокумент(ПолучитьДату(2021,1,5), "ПокупкаЦБ", "000000002"); // Пример ссылки
Строка.ОстатокКоличество = 15;
Строка.ОстатокСумма = 1650;
// Сортировка (если БухИтоги не вернули отсортированными)
ТЗОстаткиПартий.Сортировать("ДокументЗакупа.ДатаДок"); // Сортируем по дате документа закупа
ОсталосьПродать = КоличествоПродано;
ОбщаяСебестоимостьПродажи = 0;
ТЗОстаткиПартий.ВыбратьСтроки();
Пока ТЗОстаткиПартий.ПолучитьСтроку() = 1 И ОсталосьПродать > 0 Цикл
ДокументЗакупа = ТЗОстаткиПартий.ДокументЗакупа;
КоличествоПартии = ТЗОстаткиПартий.ОстатокКоличество;
СуммаПартии = ТЗОстаткиПартий.ОстатокСумма;
// Определяем, сколько акций из текущей партии списываем
СписываемоеКоличество = Мин(ОсталосьПродать, КоличествоПартии);
// Рассчитываем себестоимость списываемой части
// ЕдиницаЦенаПартии = СуммаПартии / КоличествоПартии;
СебестоимостьСписания = (СуммаПартии / КоличествоПартии) * СписываемоеКоличество;
ОбщаяСебестоимостьПродажи = ОбщаяСебестоимостьПродажи + СебестоимостьСписания;
// Формируем проводку списания с SHARES
Проводка = CreateObject("Проводка");
Проводка.New();
Проводка.Дебет = Счет.Broker; // Счет брокера, куда приходят деньги (или SHARES_Продажи)
Проводка.Кредит = Счет.Shares; // Счет учета акций
Проводка.Субконто(1, Брокер);
Проводка.Субконто(2, Акция);
Проводка.Субконто(3, ДокументЗакупа); // Указываем конкретную партию закупа
Проводка.Сумма = СебестоимостьСписания;
Проводка.Количество = СписываемоеКоличество;
Проводка.Записать();
// Рассчитываем финансовый результат по списанной части
ЦенаПродажиЕд = СуммаПродажи / КоличествоПродано; // Усредненная цена продажи за единицу
ПрибыльУбытокПоЧасти = (ЦенаПродажиЕд * СписываемоеКоличество) - СебестоимостьСписания;
Если ПрибыльУбытокПоЧасти > 0 Тогда
// Прибыль
Проводка = CreateObject("Проводка");
Проводка.New();
Проводка.Дебет = Счет.Shares; // Или счет Брокера, в зависимости от методологии
Проводка.Кредит = Счет.PL; // Счет прибыли/убытка
Проводка.Субконто(1, Брокер);
Проводка.Субконто(2, Акция);
Проводка.Субконто(3, ДокументЗакупа); // Для детализации П/У по партиям
Проводка.Сумма = ПрибыльУбытокПоЧасти;
Проводка.Записать();
ИначеЕсли ПрибыльУбытокПоЧасти < 0 Тогда
// Убыток
Проводка = CreateObject("Проводка");
Проводка.New();
Проводка.Дебет = Счет.PL; // Счет прибыли/убытка
Проводка.Субконто(1, Брокер);
Проводка.Субконто(2, Акция);
Проводка.Субконто(3, ДокументЗакупа); // Для детализации П/У по партиям
Проводка.Кредит = Счет.Shares; // Или счет Брокера
Проводка.Сумма = Абс(ПрибыльУбытокПоЧасти);
Проводка.Записать();
КонецЕсли;
ОсталосьПродать = ОсталосьПродать - СписываемоеКоличество;
КонецЦикла;
// Проводка на общую сумму поступления денег от продажи (для баланса)
// Она может быть одной на весь документ продажи
Проводка = CreateObject("Проводка");
Проводка.New();
Проводка.Дебет = Счет.Broker; // Счет брокера, куда приходят деньги
Проводка.Кредит = Счет.Shares; // Или счет для учета доходов от продажи акций (90.1)
Проводка.Субконто(1, Брокер);
Проводка.Сумма = СуммаПродажи;
Проводка.Записать();
BROKER (или временного счета продаж) и кредиту счета SHARES, указывая ту конкретную ПартиюЗакупа, из которой происходит списание. Сумма списания рассчитывается исходя из себестоимости этой партии.PL с детализацией до ПартииЗакупа. Это позволяет видеть прибыль/убыток по каждой конкретной партии акций.Таким образом, мы полностью переходим от хранения остатков в справочниках к использованию мощного механизма бухгалтерского учета, который автоматически поддерживает актуальные остатки и обороты, а также позволяет получать финансовые результаты с необходимой детализацией.
Для обеспечения максимальной точности FIFO, особенно для внутридневных операций, критически важно, чтобы каждая торговая операция (покупка или продажа) была отражена в системе с точной датой и временем. Если брокерский отчет содержит несколько операций за один день, и их последовательность имеет значение, то обрабатывать его как единый документ с одной табличной частью будет сложно и рискованно.
Мы рекомендуем следующий подход:
Документ.ПокупкаЦБ или Документ.ПродажаЦБ). Эти документы будут иметь точную дату и время операции, указанные в брокерском отчете.Документ.ОперацияЦБ) с точной датой и временем из отчета.Преимущества такого подхода:
Вы можете посмотреть, как реализован подобный механизм агрегации документов в типовой конфигурации 1С v7.7 "Торговля и Склад", в обработке "Банковская выписка". Она загружает множество банковских операций и создает для каждой отдельные документы, а сама выступает в роли удобного интерфейса для работы с ними.
Еще одна распространенная проблема при создании документов "на лету" — это некорректная установка даты и времени. Наш читатель столкнулся с тем, что документ всегда создавался с временем 12:00:00, несмотря на использование УстановитьВремя().
Метод УстановитьВремя() в 1С v7.7 предназначен для установки только временной части уже существующей даты документа. Когда вы создаете новый документ с помощью BuyDoc.Новый(), его дата по умолчанию устанавливается на текущую системную дату (обычно с нулевым временем, т.е. 00:00:00). Если вы затем используете УстановитьВремя(), вы устанавливаете время на 12:00:00 (или другое значение по умолчанию), но дата документа остается текущей системной датой. Если вы хотите установить дату и время, отличающиеся от текущей системной даты, этого метода недостаточно.
Для корректной установки даты и времени нового документа необходимо сначала сформировать полную дату со временем (объект Дата) из ваших строковых данных и затем присвоить ее свойству Дата документа. Это обеспечит, что документ будет записан с точной датой и временем, что критически важно для хронологического учета FIFO.
Рассмотрим пример кода:
// Предположим, у нас есть строковые переменные
// DocDateStr (например, "02.02.21") и DocTimeStr (например, "21:59:06")
// 1. Распознаем составляющие даты:
День = Число(Сред(DocDateStr, 1, 2));
Месяц = Число(Сред(DocDateStr, 4, 2));
Год = Число(Сред(DocDateStr, 7, 2));
// Внимательно с форматом года: если "21", то это 2021.
// Если в вашем отчете год указан полностью "2021", то берем 4 символа.
// Если "21" и нужно трактовать как 2021: Год = Число(Сред(DocDateStr, 7, 2)); If Год < 50 Then Год = Год + 2000 Else Год = Год + 1900;
// 2. Распознаем составляющие времени:
Час = Число(Сред(DocTimeStr, 1, 2));
Минута = Число(Сред(DocTimeStr, 4, 2));
Секунда = Число(Сред(DocTimeStr, 7, 2));
// 3. Создаем объект Дата со всеми компонентами:
ПолнаяДатаВремя = Дата(Год, Месяц, День, Час, Минута, Секунда);
// 4. Создаем новый документ:
BuyDoc = CreateObject("Document.ПокупкаЦБ"); // Или любой другой тип документа
BuyDoc.Новый();
// 5. Присваиваем полную дату и время документу:
BuyDoc.Дата = ПолнаяДатаВремя;
// ... Здесь заполняем остальные реквизиты документа ...
// 6. Записываем документ:
BuyDoc.Записать();
Этот подход гарантирует, что каждый документ-транзакция будет иметь точно заданную дату и время, что является фундаментом для корректного расчета FIFO и построения точных отчетов, особенно когда операции происходят в течение одного дня.
Мы рассмотрели ключевые аспекты реализации учета ценных бумаг по методу FIFO в 1С v7.7. Наши основные рекомендации:
SHARES с тремя субконто: Брокер, Акция, ПартияЗакупа (ссылка на документ покупки).Дата документа полноценный объект Дата, содержащий все компоненты (год, месяц, день, час, минута, секунда).Следуя этим рекомендациям, вы сможете построить надежную, прозрачную и эффективную систему учета ценных бумаг в 1С v7.7, способную корректно рассчитывать финансовые результаты по методу FIFO.