DisclaimerВ качестве некоторой преамбулы: В этой статье я время от времени буду рассказывать о некоторых чисто финансовых концепциях. Тем не менее, хочу заранее уточнить что я никогда не получал формального образования в области финансов, а самостоятельно учился финансам по переводным книжкам из ООНовсковской серии по финансовому учету и рассказам своих коллег-консультантов с финансовым бэкграундом. Поэтому, вполне вероятно, что часть используемой мною терминологии будет не привычна для людей с классическим российским финансово/бухгалтерским образованием. Тем не менее, я надеюсь , что по сути я не написал ничего принципиально неправильного. Cтатья рассчитана на архитекторов решения, которым важно понимать, как в Dynamics AX сделана работа с ГК, а не вообще как устроен российский и западный учет в абстрактной бухгалтерии.
Зачем вообще нужна разноска в ГК?Сразу хочу сказать, что разноска в ГК с точки зрения DAX, является некоторым финальным результатом проведения операции. Данные из ГК почти нигде (за исключением собственно финансовой отчетности типа баланса /"отчета о прибылях и убытках" или распределения затрат) не используются системой как некоторые входные данные, на основании которых выполняются дальнейшие операции. Пишу об этом потому, что я регулярно сталкивался с ситуацией, когда не очень опытные консультанты (особенно – пришедшие с 1c 7ой версии) писали в ТЗ что-то типа "Для расчета суммы проводимой системой со счета НДС входящего на счет НДС оплаченного, надо сосчитать обороты по счету 19.xx по документу ГК, связанному с данной накладной и умножить его на процент сопоставления". Так вот, идеология Dynamics AX предполагает, что все суммы, необходимые для операций системы, должны храниться не в таблице проводок в ГК, а в каких-то других таблицах - таблице регистров (типа проводок по поставщику, клиенту, налоговых или складских проводок), или в таблицах результирующих документов (накладных, отборочных накладных, письмах-напоминаниях и т.п.). В нашем примере – данные можно взять из таблицы налоговых проводок. Если вы пользуетесь стандартной функциональностью, то с большой вероятностью, любую сумму нужную вам для дальнейших операций с документом вы сможете найти среди полей тех таблиц, о которых я написал чуть выше. Даже если вы каким-то образом дорабатываете функциональность, то в ситуации, при которой вам не хватило данных для дальнейшего расчета в этих таблицах, следует попытаться дополнить эти таблицы новыми полями и написать алгоритм их заполнения, а не пытаться собрать данные по ГК. Допустим, мы реализовали особо хитрый расчет комиссионных продавца при оформлении накладной по заказу, который при разноске накладной проводит сумму комиссионных как сторно какого-то рабочего счета (76.xx) на реализацию (90.1). Теперь нам нужно написать процедуру начисления квартальной премии. Эта процедура должна пробежаться по всем продажам за период и создать проводку задолженности перед персоналом (emplTrans_ru) в корреспонденции с этим рабочим счетом. В подобной ситуации не следует пытаться посчитать сумму оборотов счета 76.xx по каждой накладной. Правильнее добавить в шапку накладной сумму комиссионного вознаграждения (да и ответственного продавца заодно), и просто заполнять ее при разноске исходной накладной. Тогда для организации процедуры начисления квартальной премии достаточно будет просто пробежаться по списку накладных. Единственное исключение из этого подхода, которое я вижу – это учет затрат и доходов. Я в свое время долго думал, не следовало ли бы разработчикам прикладного ядра Dynamics AX разработать какую-то специальный регистр затрат (например, expenseTrans), чтобы все проводки по счетам прибылей и убытков (возможно и по рабочим затратным счетам – типа 25,26,44) сопровождались бы проводками в эту таблицу. Но после долгих обсуждений с коллегами и приятелями, мы пришли к выводу, что при более или менее адекватной организации учета затрат в учетной политике, для получения всей информации по структуре затрат и доходов достаточно счета ГК и финансовой аналитики – то есть как раз той информации, которая и так есть в ГК. Соответственно – какой-то специальный регистр учета затрат не нужен, поскольку его функции выполняет сама главная книга. Кстати – помнится, в старых версиях Dynamics AX вообще можно было выключить разноску в ГК конфигурационным ключом. (Я правда не пробовал). При этом система полностью сохраняла работоспособность, за исключением все тех же финансовых отчетов и распределения затрат. Начиная, кажется, с версии 3.0, возможность отключить разноску в ГК исчезла, поэтому прописывать в системе какой-то план счетов все-таки приходится всегда. Возникает вопрос – если разноска в ГК нужна только для формирования стандартных финансовых отчетов (баланс, отчет о прибылях и убытках, отчет о движении денежных средств), то может и не стоит особо париться с настройкой какого-то осмысленного плана счетов на тех проектах, на которых полноценный финансовый учет не внедряется? К моему глубокому сожалению, в нашей стране произошла некоторая утрата бухгалтерской традиции. Ведь бухгалтерский учет был придуман не для того чтобы порождать отчеты для налоговой, а для того чтобы позволить владельцу или управляющему бизнеса получать информацию о том, что собственно в этом бизнесе творится. А у нас, к сожалению, во многих компаниях преобладает ощущение, что целью работы бухгалтера является подготовка налоговой отчетности. Ну а поскольку налоговая отчетность сдается раз в месяц (или реже), то бухгалтера проводят операции с большим опозданием, поскольку им проще отложить проведение операции, чем потом разбираться с ошибками. Кроме того – от бухгалтеров часто приходиться слышать что-то типа "А почему я должна вносить в аналитику проводки статью бюджета ? Ведь для налоговой это не нужно…". В тоже время финансовый отдел – ведет свой собственный учет, по какой-то собственной технологии, под лозунгом – "У нас учет управленческий, нам в налоговую отчетность не сдавать, так что нам на нормы учета наплевать – как захотим учитывать – так и будем". При этом этот, так называемый, "управленческий учет" зачастую просто не использует принцип двойной записи. Каждый регистр ведется в отдельной табличке Excel, план счетов не используется. И хотя, в принципе, финансист понимает из институтского курса, что регистр кредиторки в общем-то должен соответствовать регистру поступления на склад, но никакой выверки финансист не делает, поскольку при таком способе учета это слишком затратно. Если в такой ситуации начать внедрение без проработанной учетной политики, то есть большие шансы, что после того как внедрение логистики на игрушечном плане счетов будет завершено, вы получите от заказчика огромный набор excel-евских таблиц с присказкой "а вот теперь, на втором этапе проекта, все наши управленческие отчеты надо сделать в DAX, на основании тех управленческих данных, которые в ней есть". И вот на этой стадии приходиться либо программировать огромное количество отчетов вручную, либо строить сложнейшие OLAP-кубы, с которыми пользователю, будет слишком тяжело работать. При этом если бы изначально был продуман нормальный план счетов и аналитика, существенная часть отчетов можно было бы реализовать через генератор финансовой отчетности. Как показывает практика, если в компании имеется даже очень сложный план счетов и аналитика, все-таки можно разработать удобный для пользователя механизм заполнения аналитик, счетов ГК и профилей разноски, а затем настроить отчеты, собираемые по ГК. А вот если разумного плана счетов (пусть даже слишком сложного и чересчур детализованного) нету – то приходится разрабатывать огромное количество отчетов, собирающих данные из всех мыслимых и немыслимых таблиц системы. По хорошему - возникновение такой ситуации говорит о том, что на самом деле, компании перед внедрением системы очень стоило бы нанять какую-то фирму, специализирующуюся на управленческом консалтинге, чтобы та работала план счетов и учетную политику, адекватную структуре финансовой отчетности. Проблема в том, что во многих фирмах просто нет финансистов, которые когда-то сталкивались напрактике с вменяемой учетной политикой. Поэтому понимание того, что проблемы сбора финансовых отчетов в первую очередь решаются с помощью адекватных учетных процедур и только во вторую – с помощью ИСУП/ERP – отсутствует. Как результат - зачастую с помощью внедрения ERP-системы фирма пытается бороться с проблемами, большая часть которых порождена отсутствием структурированного финансового учета, а не собственно отсутствием автоматизации. Что делать в такой ситуации – не знаю. Можно попытаться все-таки, собственными силами и на уровне собственного разумения разработать какой-то, похожий на правду, план счетов, чтобы уже не нем можно было показать финансистам, что оказывается многие нужные им для оперативного учета цифры можно получать не только из Excel, но и по оборотам или сальдо ГК. Правда – обычно на разработку даже такого, не совсем настоящего, плана счетов у внедренцев не хватает времени… В общем – не понимание заказчиком, а зачастую и внедренцем смысла учета по главной книге является одним из основных источников проблем на проектах. Что такое номер ваучера?Дальше стоит поговорить о номере документа ГК (он же ваучер, он же "Операция" – в 4ой версии). Мне рассказывали следующую "городскую легенду" о происхождении этого понятия. Вроде бы, в докомпьютерные времена в западных бухгалтериях каждый год заказывали пачку специально напечатанных документов строгой отчетности, чуть ли не с водяными знаками на них. Именно эти документы и назывались ваучерами. При поступлении некого исходного документа в бухгалтерию для дальнейшей обработки, специально обученный человек отрывал от пачки очередной ваучер с очередным номером и регистрировал в журнале информацию о поступившем документе и том номере ваучера, который на него пришелся. Дальше ваучер подклеивался к исходному документу и после проведения такового по регистрам, оба они отправлялись в архив. При этом, во всех бухгалтерских регистрах и проводках указывалось не долгое и нудное описание первичного документа, а тот номер ваучера, который был к нему прикреплен. Ну и поскольку ваучера были документами строгой отчетности, то в их нумерации не должно было быть пропусков, чтобы не получилось, что какой-то хозяйственный документ поступил в бухгалтерию, но потом был почему-то уничтожен. (И именно этому мы, якобы, обязаны появлением понятия непрерывных номерных серий!). Я лично, не верю в правдивость этой городской легенды. Мои попытки найти в интернете какие-то упоминания об использовании термина 'ваучер' по отношению к этим гипотетическим бумажкам-липучкам ничего не дали; Да и сами бумажки-липучки вроде бы были изобретены фирмой 3M уже после массовой компьютеризации учета. Тем не менее – я привожу здесь эту историю, поскольку она достаточно точно отражает то, для чего номер ваучера используется в Dynamics AX. Попросту говоря – этой некий идентификатор хозоперации, который позволяет связать проведенные документы с записями в регистрах и в главной книге. Строго говоря – термин "документ ГК", используемый в старом переводе Dynamics AX был тоже не совсем корректным. Дело в том, что номер ваучера использовался, например, для связи журнала переноса или карантинного заказа с складскими проводками. В то же время, никаких проводок в ГК в стандартной версии Dynamics AX по этим документам не происходит. Поэтому, принятый в 4ой версии термин "Операция", пожалуй чуть более корректен (хотя и слишком широк). В данном документе я буду использовать для этого номера термин "номер ваучера", как наименее неоднозначный. Немного о самой таблице 'проводок'Я сознательно заключил слово проводки, в заголовке раздела, в кавычки. Дело в том, что в DAX, строго говоря, нет таблицы, соответствующей понятию проводки в российском понимании – то есть чего-то такого с счетом дебета, счетом кредита, суммой и аналитиками. Каждая запись в таблице LedgerTrans – это информация об одной половинке проводки в российском понимании – то есть – либо дебетовой либо кредитовой части проводки. В 3ей версии эта таблица называлась в российской локализации "Бухгалтерские проводки", что с точки зрения российского бухгалтера не очень корректно. В 4ой версии, ее переименовали в "Операции ГК", что в целом гораздо корректней, но чересчур громоздко. Я буду эту таблицу и вообще эти половинки проводок называть "постингами". Этот термин, конечно плохо звучит, но он не нагружен в российской финансовой терминологии какими-то дополнительными смыслами. В дальнейшем изложении мы отвлечемся на какое-то время от проблемы корреспонденции счетов и будет считать, что у нас все проводки сложные и состоят из некоторого количества постингов. В определенный момент, мы вернемся к вопросу корреспонденции счетов и поговорим как система превращает сложные проводки в простые. Так вот – рассмотрим основные поля этой самой таблицы постингов: | Поле | Описание | | AccountNum | Счет ГК | | TransDate | Дата проводки | | Voucher | Номер ваучера. | | Txt | Текст проводки | | AmountMST | Сумма в валюте учета | | AmountCur | Сумма в валюте операции | | CurrencyCode | Код валюты | | DocumentDate | Дата исходного документа | | DocumentNum | Номер исходного документа | | TransType | Тип транзакции. В первом приближении – модуль – источник проводки. Информационное поле | | Qty | Количество | | JournalNum | Номер журнала ГК. Для проводок из прочих источников не заполняется | | Posting | Тип разноски. В большинстве случаев – чисто информационный. Однако – время от времени используется в механизме корреспонденции счетов. | | Correct | Признак сторнирования. | | Crediting | Признак кредитовой проводки | | AmountMSTSecond | Сумма во вторичной валюте учета | | EuroTriangulation | Признак триангуляции | | PeriodCode | Позволяет различить открывающие/закрывающие и нормальные проводки | | OperationsTax | Зона учета | | taxRefId | Поле используется для связи с taxTrans. Генерируется системой автоматически. | | BondBatch_ru и BondBatchTrans_ru | Ссылка на вторую половинку проводки – постинг на корсчет. Подробности о механизме корреспонденции счетов – в следующих разделах |
Теперь немного комментариев: - В принципе, начиная с 3ей версии системные механизмы поддерживают создание постингов по одному ваучеру с разными датами. В реальности – этот механизм не используется. Но в принципе подобного эффекта, вероятно, можно добиться если включить в журнале ГК использование ручной номерной серии для создания номеров ваучера, а затем ручками создать несколько строк с одинаковым номером ваучера, но разной датой…
- Также стоит обратить внимание на то, что в таблице постингов отсутствует информация о том, по какому курсу был выполнен пересчет из валюты операции в валюту учета. Само ядро механизма разноски в ГК ничего не знает о курсах валют; формально – сумма в валюте операции и сумма в валюте учета могут указываться независимо. Тем не менее, использование штатных API вызовов для создания проводок предполагает, что все-таки при создании проводок будет указана сумма в валюте операции и курс, а сумму в валютах учета система рассчитает сама.
- В том случае, если разноска ваучера происходит с использованием уровня детализации "Подробности", текст разных проводок по одному ваучеру может не совпадать.
- Дата и номер исходного документа нужны, скорее, для удобства бухгалтера, чем для работы самой системы. При этом дата документа может не совпадать с датой разноски.
- Поле количество заполняется достаточно редко. Насколько я понимаю, это некий рудимент идеи, в соответствии с которой те организации, которые не купили модуль логистики, все-таки могут вести количественный учет с помощью КОЛИЧЕСТВЕННЫХ оборотов по главной книге. Реально идея не прижилась. Насколько я помню, это поле заполняется только для постингов по модулю проектов. В проводках из логистического модуля оно, как ни странно – пустое (но можно легко доделать классы логистики так чтобы это поле заполнялось в соответствии с количеством в складской проводке). На моей практике, это поле пригодилось для распределения затрат пропорционально натуральному показателю. Изначально был написан модуль распределения затрат и закрытия счетов, который распределял затраты, накопленные на одном из счетов, пропорционально оборотам или сальдо по ГК другого счета. Потом возникла необходимость доработать этот модуль таким образом, чтобы он распределял затраты пропорционально отработанным часам или списанным количествам. В этой ситуации оказалось что проще всего доработать модуль распределения затрат, так чтобы он использовал не сумму, а количество из постинга и доработать саму процедуру разноски постингов базы распределения так, чтобы эта процедура подставляла в постинг количество. К слову сказать, если механизм корреспонденции счетов отключен, то через общий журнал можно проводить проводки с нулевой суммой и ненулевым количеством. К сожалению, с российским механизмом корреспонденции счетов этот механизм не совместим.
- Флажок correct реверсирует логику проставления признака проставления crediting. Для обычных постингов этот флажок ставиться для при отрицательной сумме; Для сторнирующих постингов – при положительной.
- Механизм триангуляции был придуман в то время когда Европа только начинала переходить на евро. При этом отчетность велась в национальной валюте (немецких марках например), но все курсы приходилось пересчитывать по отношению к евро, который тогда был виртуальной валютой, не имеющей печатных дензнаков. Так вот в те времена, в системе в качестве валюты триангуляции указывалась ЕВРО,а в постинги пришлось записывать признак использования триангуляции, чтобы после замены способа пересчета между валютами,можно было сторнировать операции по историческим настройкам пересчета, а не по текущим.
- Существует заблуждение, состоящее в том, что поле "Вторичный валютный курс" в общем журнале используется для указания курса для пересчета во вторичную валюту. Говорится что-то типа "У нас для бухгалтерии нужна первичная валюты учета – рубль, а для коммерсантов – вторичная – доллар. Поэтому при проведении операции в фунтах мы будем в валютном курсе указывать курс к рублю, а во вторичном валютном курсе – к доллару". На самом деле – вторичный валютный курс относится к той же ситуации с триангуляцией. Если пересчет из валюты операции (фунта скажем) в валюту учета (марку) у нас идет через евро (валюту триангуляции), то хорошо бы уметь в ручную указывать курс и фунта к евро и марки к евро. Так вот в такой ситуации основной курс – это курс валюты операции к валюте триангуляции, в вторичный курс – это курс валюты учета к валюте триангуляции.
- Что такое зона учета ? Существует возможность разделения постингов в главной книге на разные зоны учета. То есть – грубо говоря можно в рамках одной компании делать постинги на одни и те же счета в зоне налогового учета, текущего (обычного ) учета и оперативного учета. Что такое оперативный учет – не знаю, вероятно какой-то механизм, который позволяет делать совсем посторонние постинги для каких-то внутренних совсем уж не связанных с налоговой или бухгалтерской отчетностью целей. По большому счету – механизм этот не очень прижился. Во первых – при разноске большей части документов, система ВСЕГДА подставляет режим ТЕКУЩЕГО отчета. Во вторых – во всех регистрах (проводках по поставщику, клиенту, складу и тп) поле для отделения зоны учета отсутствует. Поэтому, хотя в настройках журнала ГК и можно указать зону учета, отличную от текущего, но реально это будет работать только для тех строк журнала, в которых и по дебету и по кредиту стоят счета ГК. То есть – можно конечно и поставщика в строку журнала поставить будет, только вот в таблице проводок по поставщику, получившаяся проводка будет неотличима от проводки по текущему учету. Ну а обычные российские бухгалтера в подобной ситуации предпочитают вести учет на забалансовых счетах в текущем учете, а не в какой-то отдельной зоне учета про которую в российских ПБУ ничего не пишут. Кроме того – при разработке российского модуля двухвалютного склада, была сделана еще одна зона учета, в которую попадают проводки во вторичной валюте из этого модуля. Но я вообще редко видел чтобы этим модулем пользовались, а там где его все-таки использовали – разноску в ГК отключали.
В связи с таблицей постингов надо упомянуть две другие важные таблицы: LedgerBalancesTrans и ledgerBalancesDimTrans. Данные таблицы используются для быстрого подсчета оборотов по счету за период и обновляются при добавлении данных в ledgerTrans. Не буду подробно рассматривать все поля этих таблиц. Попросту говоря, в них лежит номер счета (или номер счета и аналитика), дата и сумма различных оборотов по дебету и кредиту данного счета. Более интересным является использование поля LedgerBalances.Variant. До версии 3.0 все данные об оборотах по данному счету (или данному счету ианалитике) за данный день хранились в одной записи этих таблиц. Это часто приводило к ситуации блокировки работы со счетом на длительное время. Представим себе, что у нас в начале разноски журнала в 2000 строк, обновились данные об обороте за сегодняшнее число по счету 60.01. В полном соответствии с идеологией транзакционной модели работы с данными, запись с LedgerBalancesTrans с этими оборотами блокировалась до конца транзакции. Поэтому другие пользователи, которые пытались разнести на этот счет какую-то свою операцию, ставились в очередь за ресурс и ждали окончания разноски этого журнала из 2000 строк. Ну и поскольку такой журнал разносится достаточно долго, все это вызывало кучу взаимных блокировок и недовольства пользователей. В версии 3.0, данные по оборотам разнесены на 20 строчек данных таблиц. При этом при записи в таблицу постингов, система случайным образом выбирает одну из этих строчек и обновляет ее. В итоге – вероятность взаимоблокировок упала в 20 раз. В то же время - при подсчете данных об оборотах 20 кратное увеличение не очень большой таблицы не приводит к существенному снижению быстродействия. Немного о программном API создания постинговВсе API для создания постингов в ledgerTrans базируется на трех основных классах – LedgerVoucher, ledgerVoucherObject и ledgerVoucherTransObject. Коротко говоря, LedgerVoucherTransObject – это класс, который инкапсулирует один постинг; LedgerVoucherObject – это класс инкапсулирующий ваучер, а ledgerVoucher – это несколько загадочный класс, который инкапсулирует несколько ваучеров. Зачем нужен последний класс – я не совсем понял; В версии 2.1 и 2.5 его аналогов не было. Возможно – разработчики решили сделать его как некую как-бы транзакционную обертку, которая позволяет создавать несколько взаимосвязанных ваучеров одной операцией – так чтобы они либо все попали в таблицу LedgerTrans, либо все не попали. Часть методов я не буду описывать; Они мне за все время ни разу не понадобились, да и я думаю, что с ними не трудно разобраться самостоятельно. Рассмотрим эти классы поподробнее: LedgerVoucherЭтот класс является наиболее общим и объемлющим классом системы создания постингов. Он самым первым создается и вызовом его же метода заканчивается разноска в ГК. В методах parm* этого класса можно задать наиболее глобальные умолчания для создания дальнейших постингов в ГК. Обычно в этом классе используется всего три метода: static LedgerVoucher newLedgerPost(DetailSummary _detailSummary, SysModule _sysModule, NumberSequenceCode _voucherSeriesCode, TransactionLogType _transactionLogType = TransactionLogType::Unspecified, TransTxt _transactionLogTxt = '', boolean _approveJournal = false, boolean _posting = true) Метод-конструктор: _detailSummary – уровень детализации (подробности ниже) _sysModule – модуль породивший проводку. Реально – позволяет системе проверить права пользователя на разноску в данном модуле и данном периоде. _voucherSeriesCode – код номерной серии, используемой для генерации номеров ваучера по данному объекту. Как ни странно – данный номер серии не используется системой для АВТОМАТИЧЕСКОЙ генерации номеров ваучера. Реально - он нужен только для некоторых проверок номера ваучера, которые для российских условий не актуальны. _transactionLogType, _TransactionLogTxt – тип и текст операции, которые попадут в аудиторский след (TransactionLog), при разноске. _approveJournal – включается для журналов с режимом утверждения. Я на практике не использовал _posting – отключает собственно финальную запись в данных в таблицу постингов (ledgerTrans). То есть – все проверки проходят, но никаких следов операций не остается. На практике – не использовал. boolean end() - этот метод завершает работу с классом и собственно и приводит к записи (или не записи -если проверки не прошли :-) ) данных в таблицу постингов. void addVoucher(LedgerVoucherObject _ledgerVoucherObject) - добавляет к классу объект типа LedgerVoucherObject, который собственно и реализует львиную долю логики работы с ГК. LedgerVoucherObjectЭтот класс инкапсулирует в себе один ваучер. По большому счету – большая часть public методов данного класса позволяет указывать (или получать) умолчания для объектов ledgerVoucherTransObject, добавляемых к данному ваучеру. Есть один полезный метод, связанный с доступом к системе корреспондирования счетов (о которой мы поговорим попозже) и еще несколько внутренних методов, которые делают много интересных вещей, но вызываются эти методы из других классов работы с ГК, а не из прикладного кода. public NoYes parmCorrection(NoYes _correction = correction) – признак сторнирования public container parmDocument( DocumentDate _documentDate = documentDate, DocumentNum _documentNum = documentNum) – дата и номер документа public LedgerTransType parmLedgerTransType(LedgerTransType _ledgerTransType = ledgerTransType) – тип транзакции public OperationsTax parmOperationsTax(OperationsTax _operationsTax = operationsTax) – зона учета public PeriodCode parmPeriodCode(PeriodCode _periodCode = periodCode) – вид периода (открывающий/закрывающий/обычный) public TransDate parmTransDate(TransDate _transDate = transDate) – дата постинга public Voucher parmVoucher(Voucher _voucher = voucher) – номер ваучера public TransTxt lastTransTxt(TransTxt _transTxt = lastTransTxt) – текст проводки public boolean parmVoucherCheck(boolean _voucherCheck = voucherCheck) - режим проверки дублирования номера ваучера. Позволяет отключить для данного ваучера проверку дублирования номера (даже если в параметрах ГК она включена). Режим очень полезен, когда вам надо несколько связанных по смыслу операций делать в ответ на разные действия пользователя, но при этом также хочется, чтобы эти разные операции шли с одним номером ваучера. В этом случае – вы просто при создании постингов по второй, третей и тп операциям в цепочке, подставляете старый номер ваучера и отключаете режим проверки. public AmountCur lastAmountCur() – возвращает сумму в валюте операции из последнего добавленного к ваучеру постинга public AmountCur lastAmountMST() – возвращает сумму в первичной валюте учета из последнего добавленного к ваучеру постинга public AmountMSTSecondary lastAmountMSTSecondary_RU() – возвращает сумму во вторичной валюте учета из последнего добавленного к ваучеру постинга public CurrencyCode lastCurrency() – возвращает код валюты операции из последнего добавленного к ваучеру постинга. public void addTrans( LedgerVoucherTransObject _ledgerTransObject, boolean _allocate = true) важнейший метод. Добавляет к данному ваучеру объект типа ledgerVoucherTransObject (попросту говоря – posting). Второй параметр, в принципе, позволяет отключить операцию распределения, даже если таковое настроено в плане счетов для данного счета. public static LedgerVoucherObject newVoucher( Voucher _voucher, TransDate _transDate = systemdateget(), SysModule _sysModule = SysModule::None, LedgerTransType _ledgerTransType = LedgerTransType::None, NoYes _correction = NoYes::No, OperationsTax _operationsTax = OperationsTax::Current, DocumentNum _documentNum = "", DocumentDate _documentDate = dateNull(), Map _tmpVoucherMap = null, AcknowledgementDate _acknowledgementDate = dateNull(), boolean _checkVoucher = true) Конструктор класса _voucher – номер ваучера ( надо генерировать самостоятельно из номерной серии) _transDate – дата операции _SysModule – модуль ledgerTransType – тип транзакции _correction – признак сторнирования _operationsTax – зона учета _documentNum – номер исходного документа _documentDate – дата исходного документа _tmpVoucherMap – используется при разноске журналов ГК. В обычной практике – не используется _acknowledgmentDate – в российских реалиях не используется. Похоже что в некоторых западных системах учета требуется при разноске операций по счетам расчетов, указывать не только дату учета документа в нашей учетной системе, но и дату получения документа контрагентом _checkVoucher – позволяет отключить проверку дублирования номера ваучера. LedgerVoucherTransObjectКласс инкапсулирует один постинг. В общем-то, в животике этого класса содержится приватная переменная типа ledgerTrans; Соответственно – большая часть методов просто изменяет или возвращает поля этой таблицы. AmountCur parmAmountCur(AmountCur _amountCur = ledgerTrans.AmountCur) – сумма постинга в валюте операции AmountMST parmAmountMST(AmountMST _amountMST = ledgerTrans.AmountMST) – сумма постинга в первичной валюте учета. AmountMSTSecondary parmAmountMSTSecondary(AmountMSTSecondary _amountMSTSecondary = ledgerTrans.AmountMSTSecond) – сумма постинга во вторичной валюте учета CurrencyCode parmCurrencyCode(CurrencyCode _currencyCode = ledgerTrans.CurrencyCode) – код валюты операции EUROTriangulation parmEUROTriangulation(EUROTriangulation _EUROTriangulation = ledgerTrans.EUROTriangulation) – признак использования триангуляции. Обратим внимание на тот факт, что в общем случае – можно задавать суммы в валютах учета и валюте операции совершенно без относительного какого-то курса (все равно – системного или введенного в ручную для данного ваучера). Однако, с точки зрения соответствия учета принципу сравнимости, прямые манипуляции с суммами в проводке (да и к слову сказать - использование курса валюты, отличающихся от системного), является дурным тоном. NoYes parmCorrect(NoYes _parmCorrect = ledgerTrans.Correct) – признак сторнирования в постинге Dimension parmDimension(Dimension _dimension = ledgerTrans.Dimension) – аналитика постинга DocumentDate parmDocumentDate(DocumentDate _documentDate = ledgerTrans.DocumentDate) – дата первичного документа DocumentNum parmDocumentNum(DocumentNum _documentNum = ledgerTrans.DocumentNum) – номер первичного документа LedgerAccount parmLedgerAccount(LedgerAccount _ledgerAccount = ledgerTrans.AccountNum) – номер счета ГК LedgerDetailLevel parmLedgerDetailLevel(LedgerDetailLevel _ledgerDetailLevel = ledgerDetailLevel) – уровень детализации LedgerJournalId parmLedgerJournalId(LedgerJournalId _ledgerJournalId = ledgerTrans.JournalNum) – номер журнала LedgerPostingType parmLedgerPostingType(LedgerPostingType _ledgerPostingType = ledgerTrans.Posting) – тип разноски LedgerTransType parmLedgerTransType(LedgerTransType _ledgerTransType = ledgerTrans.TransType) – тип транзации OperationsTax parmOperationsTax(OperationsTax _operationsTax = ledgerTrans.OperationsTax) – зона учета PeriodCode parmPeriodCode(PeriodCode _periodCode = ledgerTrans.PeriodCode) – тип периода учета (открывающий/закрывающий/обычный) Qty parmQty(Qty _qty = ledgerTrans.Qty) – количество recId parmSourceRecId(recId _recId = sourceRecId)public tableId parmSourceTableId(tableId _sourceTableId = sourceTableId) – RecId и tableid той записи в таблице исходных документов, на основании которой был сгенерирован постинг. Используется не часто (по моему – только при разноске журналов ГК). Судя по всему, критично для генерации значения taxRefId, связывающего постинги и налоговые проводки в taxTrans. Кроме того, в режиме детализации "Сводка", система не схлопывает постинги с разными recId исходного документа. TransDate parmTransDate(TransDate _transDate = ledgerTrans.TransDate) – дата операции TransTxt parmTransTxt(TransTxt _transTxt = ledgerTrans.Txt) – текст постинга. Если выбран режим детализации "Детально", то у каждого постинга может быть свой собственный текст в поле Txt. Voucher parmVoucher(Voucher _voucher = ledgerTrans.Voucher) – номер ваучера Конструкторы: static LedgerVoucherTransObject newCreateTrans( LedgerVoucherObject _ledgerVoucherObject, LedgerPostingType _ledgerPostingType, LedgerAccount _ledgerAccount, Dimension _dimension, CurrencyCode _currencyCode, AmountCur _amountCur, tableId _sourceTableId, recId _sourceRecId, LedgerQty _qty = 0, ExchRate _exchRate = 0, ExchRate _exchRateSecond = 0, ExchRatesTriangulation _exchRatesTriangulation = UnknownNoYes::Unknown, boolean _markBridging = false, ProjLedger _projLedger = null, AmountMST _amountMST = 0) Конструктор для обычных постингов. Сумма в валютах учета рассчитывается на основании указаных курсов или по системным курсам (если вместо курсов передали ноли). static LedgerVoucherTransObject newTransExchAdj(LedgerPostingType_posting, Voucher _voucher, TransDate _transDate, LedgerAccount _ledgerAccount, Dimension _dimension, CurrencyCode _currencyCode, AmountMST _amountMST, AmountMSTSecondary _amountMSTSecondary = 0, boolean _correct = NoYes::No, EUROTriangulation _EUROTriangulation = NoYes::No) Конструктор для постингов по курсовой разнице. Странненький – включает режим сторнирования только если указан флажок сторнирования и сумма курсовой меньше ноля. public static LedgerVoucherTransObject newTransExchAdjMST_RU(LedgerVoucherObject_ledgerVoucherObject, LedgerPostingType _posting, LedgerAccount _ledgerAccount, Dimension _dimension, CurrencyCode _currencyCode, AmountMST _amountMST, boolean _correct = _ledgerVoucherObject.parmCorrection()) Еще один конструктор для постингов по курсовой разнице. Более правильный – лишен побочных эффектов на тему флажка сторнирования. public static LedgerVoucherTransObject newTransExchAdjSecondary_RU(LedgerVoucherObject_ledgerVoucherObject, LedgerPostingType _posting, LedgerAccount _ledgerAccount, Dimension _dimension, CurrencyCode _currencyCode, AmountMSTSecondary _amountMSTSecondary = 0.0, boolean _correct = NoYes::No, OperationsTax _operationsTax = OperationsTax::Current) Конструктор для постингов по курсовой разнице во вторичной валюте static LedgerVoucherTransObject newTransRoundOff(LedgerPostingType_posting, Voucher _voucher, TransDate _transDate, LedgerAccount _ledgerAccount, Dimension _dimension, CurrencyCode _currencyCode, AmountMST _amountMST, AmountMSTSecondary _amountMSTSecondary = 0) Задумывался как конструктор для постингов по округлениям (о них – ниже). Зачастую используется как конструктор постингов по курсовой разнице. Теперь немного о том как всем этим пользоваться: - Создаем объект типа LedgerVoucher.
- Создаем объект типа ledgerVoucherObject и добавляем его к объекту ledgerVoucher методом addVoucher()
- Создаем нужное количество классов ledgerVoucherTransObject, и добавляем каждый из них к объекту ledgerVoucherObject методом addTrans().
- Вызываем метод end() класса ledgerVoucher. При этом проводки сталкиваются из структур в памяти в обычную таблицу постингов.
Некоторое примечание. В принципе – можно добавлять объекты ledgerVoucherTransObject напрямую к объекту ledgerVoucher, с помощью метода addTrans. В этом случае, система сама найдет (или создаст) объект ledgerVoucherObject внутри данного ledgerVoucher и затем добавит к нему объект ledgerVoucherTransObject. Мне, правда, вариант с ручным созданием ledgerVoucherObject нравится больше, поскольку этот объект нам все равно понадобится для работы с корреспонденцией. Несколько общих замечаний о механизме разноски в ГКНесколько выше, я упоминал по поводу уровня детализации при разноске в ГК. Существует два уровня детализации "Детально" и "Сводка". Если для ваучера включен режим детализации "Детально", то запись данных в таблицу ledgerTrans происходит прямолинейно: Сколько раз добавили к ваучеру объект LedgerVoucherTransObject – столько записей в ledgerTrans и создалось. Уровень детализации "сводка" перед записью в таблицу ledgerTrans группирует и суммирует постинги по следующим параметрам: - Номер ваучера
- Дата операции
- Номер счета
- Тип периода учета
- Зона учета
- Аналитика
- Код валюты
- Тип транзакции
- Уровень вложенности распределения (для проводок автоматического распределения)
- Признак сторнирования
- Номер журнала
- Тип разноски по ГК
- Дата исходного документа
- Номер исходного документа
- Ссылка на платежную проводку (paymReference)
- Ссылка на налоговую проводку (taxRefId)
- Дата подтверждения (AcknowledgementDate)
- RecId таблицы с исходным документом.
Хочу заметить, что при группировке, текст постинга (ledgerTrans.txt) не используется. Поэтому при разноске в режиме сводки, невозможно указать разные текст для разных постингов. Режим сводки нужен в первую очередь для того, чтобы уменьшить число избыточных и одинаковых в экономическом смысле постингов в таблице LedgerTrans. Кроме того – говоря о режиме "сводка" надо отметить, что если обороты по счету в пределах одного ваучера оказались нулевыми, то в таблицу постингов ничего не пишется. То есть – если у нас были постинги, грубо говоря: | Д 20 10000 руб | | | Д 90 80руб | | | | К 20 10000руб | | | К 41 80руб |
То в итоге в главную книгу попадет два постинга: Замечу, что через интерфейс можно настраивать режим детализации только для журналов (Главной книги, складских, производственных и т.д.). Для большей части стандартных разносок, режим "Сводка" прошит в исходном коде. В некоторых случаях мне удавалось успешно менять этот режим на "Детально", но 100% уверенности, что это всегда работает у меня нет. Мне кажется, что если абсолютно необходимо добиться отсутствия "схлопывания" постингов по разным строкам документа в один, правильнее будет указывать при создании постингов tableId и recId исходных строк документа, а не баловаться с изменением уровня детализации. Теперь нам надо поговорить об округлениях. Во первых – для того чтобы разноска работала правильно, при создании объекта ledgerVoucherTransObject необходимо передавать туда сумму в валюте операции, округленную до значения точности округления в справочнике валют для данной валюты. То есть – если попытаться провести операцию на 10 рублей и 89.2 копейки – система сама ничего не округлит и выдаст странное сообщение об ошибке – что-то типа "Единица 10.892 10.89 к разноске на счет 60.01 слишком мала." Во вторых – поскольку система сама пересчитывает из валюты операции в валюту учета, вполне возможна ситуация при которой сумма в одной из валют учета по ваучеру не балансирует по дебету и кредиту. Например: - валюта учета – доллар
- Проводим закупку из 200 строк, в каждой из которых приходуется сумма порядка 10 или 11 евроцентов. Каждая строка за счет округления, порождает постинг с суммой 13 центов.
- По кредиту проходит сумма порядка 20 евро, которые пересчитываются в сумму 27 Долларов.
- В итоге – у нас по дебету проходит 26 долларов, а по кредиту – 27.
Для разрешения такой ситуации в параметрах модуля ГК есть настройка границы округления в первичной и вторичной валюте. Если сумма разницы между оборотами дебетами и кредита в валютах учета меньше этой границы, то система делает по данному ваучера дополнительную проводку, у которой валюте операции стоит ноль, а в одной из валют учета – сумма разницы. При этом корсчет этой проводки берется из настроек системных счетов ГК. Ну и наконец надо упомянуть о том, как система проверяет ошибки при разноске. В версии 2.5 и 2.1 контроль соответствия аналитик, тип разноски в ГК параметрам заданным в настройках плана счетов ограничениям производился непосредственно в момент создания объекта- постинга (аналога объекта ledgerVoucherTransObject версии 3.0 и выше). В версии 3.0 и выше, такая проверка делается по умолчанию при завершении разноски ваучера, когда уже не возможно посмотреть в отладчике – какой кусок кода породил такую странную проводку. В такой ситуации можно посоветовать на некоторое время вставить в метод ledgerVoucherObject.addTrans() вызов ledgerVoucherTransObject.checkData(), чтобы поймать ошибку. О соответствии постингов в ГК и записей в регистрахЕсли хорошенько порыться в исходных текстах DAX, обнаружиться, что в системе довольно мало мест, в которых создание постинга в ГК не сопровождалось бы записью в какую-то из регистровых таблиц (custTrans,vendTrans,taxTrans,inventTrans,markupTrans и тп). При этом, создание постинга и создание записи в таблицы регистра делается одним и тем же классом, поэтому, в общем случае, не может возникать ситуация при которой у нас, например, проводки по поставщику и постинг на 60ый счет пошли с разными суммами или разными аналитиками. При этом для себя, я разделяю такие регистры на первичные – которые создаются при разноске исходного документа и вторичные – которые создаются при каких-то действиях над уже созданными первичными регистрами. Примерами первичных регистров являются таблицы CustTrans,VendTrans,TaxTrans, примерами вторичных регистров – custSettlement, VendSettlement,inventSettlement и тп Ниже приведен краткий перечень классов, отвечающих за создание записей в основных первичных регистрах: | Регистр | Класс | | CustTrans | CustVoucher и наследники | | VendTrans | VendVoucher и наследники | | inventTrans | InventUpdate и наследники, inventMovement и наследники | | TaxTrans | Tax и наследники | | MarkupTrans | Markup и наследники | | EmplTrans_ru | EmplVoucher_ru | | BankTrans | BankVoucher | | ProjEmplTrans,ProjCostTrans,ProjItemTrans etc | ProjPost и наследники (кстати – архитектура модуля проектов довольно сильно отличается от всего остального). Существует гипотеза о том что он был написан партнером и включен в базовую поставку только в версии 2.1 |
На мой взгляд – единственным существенным исключением из подхода, при котором и постинги и записи в регистре создает один и тот же механизм является метод разноски операций по западным или российским модулям основных средств. В них разноска в регистровую таблицу идет в классах AssetPost и RAsssetPost соответственно, а разноска в ГК – в классах LedgerJournalTransUpdateAsset и ledgerJournalTransUpdateAsset_ru. Это не очень красиво с точки зрения концептуальной целостности подхода, но поскольку проводки по ОС создаются почти исключительно из классов разноски по журналу ГК (те самые классы ledgerJournalTransUpdateAsset* ) - это приемлемо. Прежде чем говорить о проблеме корреспонденции счетов, которую я так старательно избегал в предыдущем изложении, давайте посмотрим на то как постинги в ГК и записи в регистры создаются, например при разноске накладной по заказу, которая выполняется в классе salesFormletter_Invoice: - Создается объект ledgerVoucher
- Для каждой строки накладной вызывается метод updateInventory, который с помощью классов inventMovement и inventUpd_financial создает проводки списания в таблице inventTrans и постинги списания по ГК Для каждой строки накладной со скидкой создается два постинга на сумму скидки
- Для каждой строки накладной создается запись в таблице строк накладных (custInvoiceTrans)
- Для каждой строки накладной создается (ну или не создается :-) ) запись и разноска по комиссии продавца. (Comission.run)
- Для каждой строки накладной разносится в ГК сумма выручки.
- Для каждой строки накладной разносятся в ГК накладные расходы по данной строке
- После того, как обработка всех построчных операций завершена, система рассчитывает общую сумму накладной
- Создается шапка накладной (custInvoiceJour)
- Разносятся в ГК накладные расходы, не привязанные к строке накладной (markup.post)
- Разносятся в ГК и в таблицу проводок по налогам (taxTrans) налоги по заказу. (В классе taxSalesInvoice)
- Разносится в ГК и в таблицу проводок по клиенту (custTrans) информация о клиентской задолженности.
- Разносится в ГК общая скидка по накладной.
- Объект ledgerVoucher завершается вызовом метода end().
А теперь вспомним о проблеме корреспонденции счетов. В российской учетной традиции, не принято делать сложные проводки, состоящие из нескольких постингов. У каждой проводки должен быть свой персональный счет дебета и счет кредита – и точка. Попробуем представить, что мы занимаемся локализацией DAX и у нас стоит задача как-то добиться того, чтобы все проводки в системе были простыми. Первая приходящая в голову мысль – сделать какую-то надстройку над классами LedgerVoucher*, которая принимала бы данные о простой проводке (то есть – сумма, счет дебета, счет кредита и тп), а затем создавала бы два постинга, попутно добавляя в записи в ledgerTrans ссылки на соседнюю проводку. Однако – если хорошенько подумать, становится очевидно, что это ни коем образом не выход. Если посмотреть на логику работы системы, то становится понятно, что каждый из классов разноски в регистр не знает и не должен знать ничего о том с какого корсчета проводится та операция, за которую он отвечает. Например: класс vendVoucher просто разносит задолженность поставщику в vendTrans и ГК и ему совершенно все равно - куда эта задолженность затем попадет – в модуль логистики (и складские счета), в модуль проектов (и счета НЗП), куда-то на общехозяйственные расходы или прямиком на прибыли и убытки. Класс Tax занимается разноской налогов и ему все равно с какого счета они начисляются. Ну и так далее. Соответственно, нет никаких шансов переделать систему таким образом чтобы все проводки изначально создавались как простые – с дебетом и кредитом одновременно. Можно было бы конечно попытаться переделать систему таким образом, чтобы в классах создания регистровых записей не делалось разноски в ГК, а вместо этого добавить разноску в ГК куда-то в конец кода исполняющего разноску каждой операции, но это потребовало бы непомерной переделки всего приложения, после которой ничего общего между локальной версией и международным функционалом не осталось бы. Кроме того, ситуация при которой разноской в ГК и в регистры делается принципиально разными кусками кода чревата сложными ошибками с расползанием данных в регистрах и в главной книге. В нескольких последующих разделах мы рассмотрим как эта проблема решена в Dynamics AX. Но прежде чем мы приступим к описанию способа решения проблемы, давайте попробуем понять -как ведут учет в учетных системах без корреспонденции счетов. Как можно жить без корреспонденции счетов?Хочу опять сделать маленький дисклеймер. Тот подход, который я тут описываю я подчерпнул из разговоров с умными людьми :-) Хотя часть информации нашла свое подтверждение в книжках из ООНовской серии по международному учету ("Серия ООН по международному учету и аудиту (пер. с англ.)"), но книжки эти, по большей части, описывают подходы к оценке активов и финансовых результатов, а не сами учетные процедуры. Кроме того - он достаточно хорошо объясняет некоторые особенности Dynamics AX. Итак – представляем себе следующую ситуацию. Первая стадия перегонки нефти. В ректификационную колону подается нефть. На выходе ежедневно получаем ароматику, бензин, керосин, дизельное топливо, топочный мазут и битумы. При этом с мазутом мы имеем достаточно интересную ситуацию. Он не только производится в процессе перегонки, но попутно также и потребляется для нагрева колонны. В российском учете мы можем отделить в рамках одной хозоперации списание и получение мазута за счет механизма корреспонденции счетов. Если у нас в сутки перегоняется мазута на 1 миллион и тратится его на 20000, то у нас при разноске суточного акта получаться, помимо всего прочего еще и такие проводки | Д 20. Ректификация | К 10. Мазут | 20000 (потребление) | | Д 10. Мазут | К 20. Ректификация | 1000000 (выработка) |
В западном учете корреспонденция счетов отсутствует, обороты принципиально не важны, соответственно – эти две проводки выродяться до одной: | Д10. Мазут 980000 | | | | К20. Ректификация 980000 |
Возникает вопроc: если нам понадобится узнать – сколько мазута было потрачено на нагрев колоны - как мы сможем получить эту информацию ? Так вот – вроде бы в западном учете в таких случаях заводят два счета: один – "Потребление мазута", второй – "Выработка мазута". Тогда – наша проводка будет выглядеть так: | Д Выработка мазута 1000000 | | | | К Ректификация – выработка 100000 | | Д Ректификация- нагрев 20000 | | | | К Потребление мазута 20000 |
Таким образом, те данные, которые в российском учете получаются из данных об обороте, в западном учете живут на отдельных рабочих счетах. Чтобы посчитать потребление или выработку за период– приходится считать разницу сальдо по счету на начало и на конец периода. Чтобы посчитать текущий остаток - приходится к складскому счету (с мазутом на начало периода) добавлять сальдо счета выработки мазута и вычитать сальдо счета потребления мазута. У этой учетной схемы есть и другая интересная особенность. Очевидно, что для того чтобы в конце периода можно было построить нормальный баланс, мы должны закрыть сальдо рабочих счетов потребления и выработки на складской балансовый счет запасов мазута. Однако, если мы просто так закроем эти счета 31ым декабря, то мы уже не сможем посчитать потребление мазута за декабрь по формуле, указанной парой параграфов выше, поскольку сальдо на 31 декабря по этим счетам будет нулевым. Для того чтобы преодолеть подобную проблему, закрытие счетов в западном учете идет в отдельной заключительной ведомости, операции в которой принципиально отделены от обычных операций, связанных с хозяйственной деятельностью предприятия. В DAX для этих целей в таблице постингов было введено разделение на типы периодов. Обычные операции живут в обычном периоде; Закрывающие операции – в закрывающем периоде, а открывающие – в открывающем. Операция закрытия периода должна делаться через функциональность Заключительной ведомости в периодических операциях модуля ГК. При этом в закрывающем периоде делаются эти самые заключительные проводки и в открывающем периоде АВТОМАТИЧЕСКИ делаются открывающие проводки, создающие в открывающем периоде сальдо на счетах. Хотя в российском учете всю эту функциональность использовать обычно не приходится, но помнить о ее наличии следует. Именно из за подобного подхода, функция расчета курсовой разницы по главной книге, подсчитывает сальдо суммируя проводки С НАЧАЛА ОТКРЫВАЮЩЕГО ПЕРИОДА. Делается это потому, что в западной учетной процедуре в открывающем периоде содержаться обороты, равные сальдо нормальных операций за все предыдущие периоды. Соответственно – чтобы посчитать текущее сальдо – не надо суммировать все постинги с начала времен; Достаточно просуммировать их с начала открывающего периода. При этом на российских внедрениях открывающие и закрывающие периоды все равно создаются – при создании финансового года, а закрывающих и открывающих проводок в них обычно нету, поскольку заключительная ведомость редко используется. Соответственно – приходится модифицировать процедуру расчета курсовой разницы по ГК таким образом, чтобы она учитывала проводки с начала времен, а не с начала открывающего периода. Корреспонденция счетов – немного историиЧтобы понять логику работы системы корреспонденции счетов в DAX, полезно слегка углубится в историю вопроса. Как известно, предшественником системы Axapta являлась система Concorde. (Я в статье обычно политкорректно употребляю термин Dynamics AX, но здесь у нас пойдет речь о настолько давних временах, в которых наша система называлась Аксаптой и выпускалась фирмой Damgaard Data). Для дальнейшего изложения договоримся, что процедурой корреспондирования счетов называется процедура, которая:1. Расщепляет постинги, которым соответствует более одного корреспондирующего постинга2. Связывает друг с другом (например – заполняя какое-то ключевое поле) корреспондирующие друг с другом постинги по дебету и кредиту. По отзывам очевидцев, корреспонденция счетов в Конкорде была реализована очень просто. Время от времени вручную запускалась процедура, которая в пакетном режиме корреспондировала, по эвристическому принципу, накопившиеся некорреспондирующие проводки. Эвристика отталкивалась, в первую очередь, от конкордовского аналога поля Posting. Например – если у нас есть постинг типа равным "Накладная по закупке", то этот постинг надо закорреспондировать со всеми остальными постингами по данному ваучеру; Если у нас есть постинг типа "Задолженость по клиентской накладной", то его надо корреспондировать со всеми постингами типа "Реализация"; ну и т.д. Как я понимаю, для сложных случаев, когда эвристика не отрабатывала, был режим ручного корреспондирования, при котором можно было залезть в неоткорреспондированый ваучер и ручками указать – какие постинги должны корреспондировать друг с другом. При проектировании системы корреспондирования для версии Axapta 2.1 (и эта система дожила до версии 2.5Sp1), проектировщики в общем-то приняли правильное (но половинчатое :-) ) решение: Поскольку в момент записи информации о постингах на диск имеется больше контекстной информации, чем в момент периодической процедуры корреспондирования, то и эвристики будут работать лучше. Соответственно – в самом конце класса LedgerVoucher старой Аксапты, уже после записи постингов на диск, запускался класс корреспондирования. Он делал две вещи: - Записывал во временную таблицу корреспондирования информацию о том какой счет с каким корреспондирует. (Там кроме счетов хранились еще аналитики, суммы в первичной и вторичной валютах и кажется что-то еще)
- На основании этой временной таблицы, разбивал и корреспондировал друг с другом постинги.
В зависимости от того, какой модуль указывался при создании объекта LedgerVoucher, система подставляла разные классы корреспондирования, реализующие разные эвристики. Кроме того – в некоторых особо сложных местах кода, в которых случались очень уж оригинальные схемы корреспонденции , при создании класса LedgerVoucher туда передавалась ссылка на специализированный класс корреспонденции, который реализовывал именно эту оригинальную схему корреспонденции. Поскольку время от времени контекстной информации не хватало (например – при разноске журналов ГК, в режиме "Сводка") имелся механизм логинга, при котором система складывала во временную таблицу информацию о всех постингах, еще до их группировки. Затем эвристический класс корреспондирования эту информацию использовал. Наконец – для самого примитивного варианта корреспондирования, при котором обе половинки проводки создавались в одном куске кода, был режим, который позволял добавить к временной таблице корреспондирования грубо говоря ссылки на два только что созданных постинга. Те внедренцы, которые успели поработать с версиями 2.1-2.5 Sp1 до сих пор с содроганием вспоминают ошибки и странности тогдашней системы корреспонденции. Из наиболее мне запомнившихся: - Эвристика отрабатывала не всегда. В тоже время – режима ручного корреспондирования не было. И если в момент завершения операции с ГК обнаруживались неоткорреспондированные постинги – система выдавала ошибку (причем оператором print) и не давала завершить разноску документа. В результате – некоторые "несчастливые" исходные документы вообще нельзя было разнести.
- Если операции шли только во вторичной валюте – механизм корреспонденции отрабатывал далеко не всегда, поскольку во временную таблицу корреспондирования, помнится, заносились только суммы в первичной валюте.
- Постинги с округлениями вообще не попадали в механизм корреспонденции. Соответственно – при просмотре проводок по ваучеру, бухгалтер мог увидеть проводку из одной половинки.Программировать механизм корреспонденции для своих собственных модулей было вообще очень сложно, поскольку какого-то целостного подхода не существовало.
- Наиболее мне запомнился класс корреспондирования складских журналов. Время выполнения операции корреспондирования было пропорционально 4ой
(ЧЕТВЕРТОЙ) степени от числа строк в журнале. Поэтому журнал из 500 строк разносился, скажем, за 1 час, а журнал из 1500 строк разносился трое с лишним суток.
Новый механизм корреспонденции счетовВ версии 2.5 Sp1HF1 (Первый HotFix к первому сервис-паку) был реализован принципиально новый механизм корреспондирования. Хотя с тех пор он несколько изменился, но подход остался прежним: Используются скобки корреспондирования и лог корреспондирования. Что такое скобки корреспондирования? Представим себе, что у нас есть отдельный кусок кода, который создает некий целостностный с экономической точки зрения набор постингов. Например – класс salesFormLetter_invoice создает внутри себя постинги на счета реализации и счет задолженности. Внутри этого класса происходит обращение (в конечном итоге) к методу inventUpdate.updateLedgerAdjustment , который создает постинги списания товара со складского счета на счет затрат периода. Также внутри класса salesFormLetter_invoice вызывается класс taxSalesInvoice, в котором создаются постинги создания задолженности по налогам и постинги расходов на налоги. Если в старой версии, система в самом конце обработки пыталась откорреспондировать ВСЕ созданные постинги, то в новой версии – каждый из этих отдельных классов обладает способностью самостоятельно указывать режим корреспондирования созданных им постингов. При этом, класс разноски налогов не имеет никакого представления о том, чего происходит со списанием со склада или реализацией и задолженностью. Класс разноски списания со склада не имеет ни малейшего представления о том, чего именно случится с товаром после списания, будут ли с этой операции начислены какие-то налоги и тп. Фактически, в начале каждого из таких кусков кода происходит вызов некого метода- открывающей скобки корреспондирования(подробности позже), который создает внутри объекта ledgerVoucher протокол со списком созданных постингов.Все созданные после этого постинги попадают в данный протокол. В конце такого куска кода, система пробегается по созданному логу, и указывает какие из созданных постингов корреспондируют друг с другом. В конце куска кода производится вызов метода-закрывающей скобки корреспонденции, при котором происходит удаление лога. В случае использования вложенных скобок корреспонденции, постинги протоколируются как во внутреннем (вложенном) логе, так и во всех внешних (облегающих) логах корреспонденции. Но поскольку при обработке облегающего лога корреспонденции, система по умолчанию игнорирует уже откорреспондированные постинги, то внешний обработчик корреспонденции не натыкается на постинги созданные и обработанные во вложенном куске кода. Ну то есть – если попробовать записать в математическом виде последовательность вызовов из примера разноски накладной по заказу, то получится примерно следующее: - Создается объект ledgerVoucher (
- (Для каждой строки накладной вызывается метод updateInventory, который с помощью классов inventMovement и inventUpd_financial создает проводки списания в таблице inventTrans и постинги списания по ГК) //корреспондируются складской счет и счет себестоимости продаж
- (Для каждой строки накладной со скидкой создается два постинга на сумму скидки)//корреспондируются счета скидки
- Для каждой строки накладной создается запись в таблице строк накладных (custInvoiceTrans)
- (Для каждой строки накладной создается (ну или не создается ) запись и разноска по комиссии продавца.) //корреспондируются счета комиссии
- Для каждой строки накладной разносится в ГК сумма выручки.
- (Для каждой строки накладной разносятся в ГК накладные расходы по данной строке) //корреспондируются счета накладных расходов
- После того, как обработка всех построчных операций завершена, система рассчитывает общую сумму накладной
- Создается шапка накладной (custInvoiceJour)
- (Разносятся в ГК накладные расходы, не привязанные к строке накладной) //корреспондируются счета накладных расходов
- (Разносятся в ГК и в таблицу проводок по налогам (taxTrans) налоги по заказу. )//корреспондируется счет налогов и счет затрат на налоги.
- Разносится в ГК и в таблицу проводок по клиенту (custTrans) информация о клиентской задолженности.
- (Разносится в ГК общая скидка по накладной.)//корреспондируются счета общей скидки
- ) //корреспондируется счет клиентской задолжености и счет выручки
- Объект ledgerVoucher завершается вызовом метода end().
Видно, что к моменту закрытия завершающей скобки корреспонденции в 14 пункте, у нас остались не откорреспондироваными только постинги реализации и постинги клиентской задолженности. Все остальные постинги уже были откорресподированы в соответствующих подсистемах. Хочу отметить, что разработчики расставили скобки корреспонденции и вызовы методов корреспондирования только для тех сочетаний постингов, которые имеют смысл с точки зрения российского учета. В принципе, если включить некоторые настройки, ориентированные на западный финансовый функционал, можно заставить систему генерировать такие постинги, для которых корреспондирование не отработает. В этом случае, в периодических операциях модуля ГК можно запросить список неоткорреспондированых постингов и откорреспондировать их в ручную. Немного об API корреспондированияПри разработке новой версии модуля корреспондирования, разработчики решили постараться по минимуму модифицировать стандартные международные классы ledgerVoucher, ledgerVocuherObject и ledgerVoucherTransObject. Большая часть самого кода корреспондирования находится в классе LedgerBondServer_ru, методы которого активно вызываются внутренними методами классов разноски в ГК. Тем не менее – методы этого класса не предназначены для вызова прикладным кодом. Прикладной API представляется методами класса LedgerBondClient_ru, который инкапуслирует обращения к внутренней логике системы корреспондирования. Для получения экземпляра класса LedgerBondCLient_ru предназначен метод ledgerBondClient_ru класса ledgerVoucherObject. Далее описаны основные методы класса ledgerBondCLient Скобки корреспондированияДля создания открывающей скобки корреспондирования используется вызов addNewLogObject(). Для закрытия скобки корреспонденции используется вызов removeCurrentLogObject() Работа с логом корреспондированияСсылкой на единичный постинг в логе корреспондирования является объект типа LedgerBondVrefId_RU. В принципе – это просто целое число, которое ссылается на порядковый номер постинга в логе корреспондирования. Для получения эой ссылки используются следующие методы: public LedgerBondVrefId_RU lastVrefId(int _offset = 0) – возвращает ссылку на последний созданный постинг. Если _offset==-1 – на предпоследний и так далее. Метод используется достаточно часто. Для того чтобы закорреспондировать две последние созданные проводки используется устойчивый паттерн: ledgerBondClient.bondVRef2VRef(ledgerBondClient.lastVrefId(), ledgerBondClient.lastVrefId(-1)); public LedgerBondVrefId_RU findVRefByPostingType(LedgerPostingType _postingType,TransDate _transDate = ledgerVoucherObject.parmTransDate(), boolean _completelyBonded = false) Метод используется для получения ссылки на последний созданный постинг с posting==_postingType. _TransDate – позволяет искать только среди постингов на некоторую дату, _completlyBonded – разрешает искать среди уже полностью откорреспондированых постингов. Метод используется, например, в классе salesFormLetter_invoice.postJournal(), чтобы добится того чтобы все созданные постинги реализации корреспондировали с постингом задолжености (который как раз ищется по LedgerPostingType::CustBalance) public LedgerBondLog_RU getCreditLog(TransDate _transDate = ledgerVoucherObject.parmTransDate(), LedgerBondLog_RU _log = this.currentLog(_transDate));public LedgerBondLog_RU getDebitLog(TransDate _transDate = ledgerVoucherObject.parmTransDate(), LedgerBondLog_RU _log = this.currentLog(_transDate)); Методы возвращают кредитовый и дебитовый логи постингов. Пример использования – метод LedgerJournalCheckPost.bondJournalVoucher(). В этом методе дебетовый и кредитовый логи сортируются по recId исходной записи и корреспондируются друг с другом. Вообще в российском учете этот метод редко бывает полезным. public TmpLedgerBondLogTable_RU log2Table(LedgerBondLog_RU _log) Метод позволяет превратить лог корреспонденции во временную таблицу типа tmpLedgerBondLogTable_ru. В некоторых, особо тяжелых случаях, эта таблица используется для того чтобы можно было побродить по логу корреспонденции. Пример использования – метод markupAdjustment.doBondProportional(), который корреспондирует все счета кредита по накладным расходам со всеми счетами дебета по складским проводкам, при доначислении накладных расходов по закупочной накладной. public LedgerBondLog_RU currentLog(TransDate _transDate = ledgerVoucherObject.parmTransDate(), boolean _excludeCompletelyBonded = true). Возвращает текущий лог корреспондирования без разделения на дебетовую и кредитовую части. public void flushCurrentLog() – очищает текущий лог корреспондирования Методы корреспондированияНиже описаны собственно методы, указывающие какие два постинга должны быть закорреспондированы. public void bondVRef2VRef(LedgerBondVrefId_RU _vRefId1, LedgerBondVrefId_RU _vRefId2, Amount _amount = 0.0, LedgerBondAmountType_RU _amountType = LedgerBondAmountType_RU::Currency, LedgerBondOrder_RU _bondOrder = LedgerBondOrder_RU::Auto, boolean _roundToCredit = false) Главный и фундаментальный метод указания корреспондирования. Все остальные являются надстройками на bondVRef2VRef. Параметры: _vRefId1,_vRefId2 – ссылки на постинги, которые должны быть закорреспондированы. _amount – сумма корреспондирования. В какой валюте эта сумма – указывается следующим параметром _AmountType – указывает – в чем задана сумма корреспондирования предыдущего параметра. Варианты – в валюте операции, в первичной валюте учета, во вторичной валюте учета. По умолчанию – в валюте операции. _bondOrder – задает – кто из _vRefId1 и _vRefId2 относится к кредитовому постингу, а кто – к дебитовому. В случае -_bondOrder== LedgerBondOrder_RU::CreditToDebit – первый VrefId – кредитовый, второй – дебетовый. В случае _bondOrder== LedgerBondOrder_RU::DebitToCredit – первый VRefId – дебетовый; второй – кредитовый. По умолчанию - _bondOrder==LedgerBondOrder_RU::Auto. В этом варианте – система заглядывает в данные о первом из постингов и исходя из того – кредитовый он или дебетовый – сама решает где у нас дебет, а где кредит… _roundToCredit – при расчете корреспондируемых суммы – округлять результат по кредитовому постингу. Теперь немножко о том, зачем появилась необходимость в параметрах _AmountType и _roundToCredit. Дело в том, что в принципе – если забыть про возможность корреспонденции счетов и российский учет, в DAX вполне можно создать по одному документу 5 разносок В РАЗНЫХ валютах. Главное, чтобы обороты в первичной и вторичной валютах по данному документу ГК равнялись нолю (точнее – были бы меньше границы округления в национальной и вторичной валюте). Так вот – в принципе, механизм корреспонденции в DAX позволяет откорреспондировать два постинга в РАЗНЫХ валютах. Хотя с точки зрения российского учета, проводка у которой по дебету и кредиту стоят разные валюты операций является изрядным абсурдом, но тем не менее, с использованием механизма корреспонденции подобную проводку можно создать. Помнится, в ранних версиях механизма авансовых отчетов, система как раз порождала по закрытию проводки с разными валютами по дебету и кредиту. Для того чтобы корректно отслеживать сумму корреспондирования во всех трех валютах (валюте операции, первичной валюте учета и вторичной валюте учета), система рассчитывает сумму корреспондирования в двух оставшихся валютах (то есть – не в той которая была передана в bondVRef2VRef) через пропорцию. Так вот параметр roundToCredit и позволяет регулировать – суммы из какого из постингов будут использоваться для расчета этой пропорции – из дебетового или кредитового. public void bondVRef2Log(LedgerBondVrefId_RU _vRefId, container _log = this.currentLog()) Корреспондирует указанный Vref со всеми остальными проводками в неком логе корреспонденции (по умолчанию – в текущем). Подразумевается что постинг определяемый _VrefId и все остальные постинги в логе находятся по разным сторонам проводок. public void bondLastVRef2CurrentLog(TransDate _transDate = ledgerVoucherObject.parmTransDate()) Корреспондирует последний постинг в текущем логе корреспонденции со всеми остальными. По большому счету – это частный случай предыдущей функции. Передаваемый параметр позволяет ограничить корреспондирование постингами на некую дату. Не припоминаю чтобы данный параметр где-то использовался. Существует еще два метода корреспондирования bondLog2Log и bondLogBySourceRecId. Они предназначены для корреспондирования множества кредитовых и дебетовых проводок по одному ваучеру и как правило применяются для проводок порожденных по многострочным документам (типа журналов). Поскольку штука это достаточно экзотическая, я не буду про них здесь писать. Заканчивая разговор о методах корреспондирования, хочу еще раз подчеркнуть, что они лишь создают в памяти некую структуру, описывающую правила корреспондирования. Реальное расщепление постингов происходит где-то в недрах кода, вызываемого из класса ledgerVoucher.end(). Вместо примера и заключенияВ последнем разделе этой статьи я хотел привести полный пример создания проводок и установления корреспонденции между ними. Но потом я вспомнил, что в DAX 3.0 имеется целых два джобика с подобными примерами – tutorial_bondTestJob_RU и tutorialBondDateTestJob_ru. Думаю – правильнее изучать API по первоисточнику :-) Ну и наконец – хочу еще раз подчеркнуть , что хотя существенную часть статьи составляет описание программного API для работы с ГК, все таки она была написана как некое описание концепции работы с ГК и корреспонденцией счетов в Dynamics AX, а не как краткое пособие для программиста по созданию проводок. |