Пример создания нового складского документа
Предисловие
Я однажды уже писал об архитектуре модуля логистики в DAX. Думаю что рассказ был бы не полным, если бы я не привел конкретный пример того, как можно дополнять логистический модуль новыми типами складских документов. Пример, который я буду рассматривать в этой статье, сделан по мотивам доработки, реализованной для конкретного проекта. Эта доработка успешно эксплуатируется почти два года и я уверен в принципиальной работоспособности предложенного подхода. Тем не менее, хочу предупредить что:
- Я разрабатывал этот пример с ноля и особо глубоко не тестировал. В нем могут быть достаточно глупые ошибки
- Исходный вариант был реализован на версии 3.0, при переходе на 4ку я мог чего-то упустить
- Не следует считать мой стиль программирования образцовым. Метками я не пользуюсь и best practice не всегда соблюдаю.
- Я не уверен в том, что предложенная модификация хорошо укладывается в идеологию MRP. С другой стороны - для торговых организаций такая доработка может быть очень полезна.
Короче говоря - прощу относится к прилагающемуся примеру не как к готовому модулю, а как к действующему макету.
Бизнес-проблема
Внедрение в крупной торговой организации. За любой ходовой товар идет непрерывная борьба между сейлами. Поскольку неходовой товар стараются не закупать, сейлы борются вообще почти за любой товар. Использование стандартной функциональности DAX по работе с резервами не устраивает пользователей. Регулярно возникает необходимость перенести резерв с одного заказа на другой (более срочный). Если просто снять резерв с одного заказа и потом поставить резерв на другой заказ, есть большие шансы что в процессе переноса, снятый резерв будет поставлен на свой заказ каким-то другим сейлом.
Кроме того, выяснилось что в условиях конкуренции за товар, сейлы приноровились создавать фиктивные заказы. По таким заказам никогда не происходит реальная отгрузка. Единственная цель существования подобных заказов - это использовать их для хранения заначки из резервов. Таким образом, реально резерв в системе бывает двух типов: нормальный резерв, привязанный к обычному заказу и глобальный резерв сейла, который привязан к некому фиктивному заказу (хотя по смыслу, скорее должен быть привязан к сейлу, а не заказу). Руководство коммерческих департаментов в принципе не возражает против существования подобных заначек продавцов, но хотело бы как-то цивилизовать работу с ними. По крайней мере необходимо разделять в системе информацию об обычных резервах и подобных стратегических резервах продавца.
Общий дизайн
Предлагается реализовать новую сущность - клипборд резервов. В системе может существовать любое количество именованных клипбордов резервов. Доступ к клипборду настраивается с точностью до пользователей или групп пользователей. Во всех складских документах, кроме обычной функции резервирования становятся доступными два специальных пункта меню “Резервировать из клипборда”,”Снять резерв в клипборд”. Перевод резерва с обычного складского документа в клипборд или наоборот, происходит как одна целостная операция в одной транзакции.
Для того чтобы отделить нормальные резервы, от резервов лежащих в клипборде необходимо в форме “Запасы в наличии” и других аналогичных формах, добавить после колонки “Физически зарезервировано” колонку “В том числе - клипборды”.
Путеводитель по классам логистики
В первом приближении - понятно как с точки зрения системы должен выглядеть клипборд резервов. Это должно быть некое подобие заказа, c шапкой и строками. Шапка должна соответствовать самому клипборду, а строки - строкам лежащего в клипборде товара. Исходя из этой идеи, можно конечно было бы попытать реализовать клипборд резервов, совершив жестокое и противоестественное насилие над обычными заказами, но это не наш метод.
Предлагается внести в систему логистики Dynamics AX совершенно новый, отдельный тип складского документа - строка клипборда резервов.
Напомню о подходе, который используется в DAX для организации связи исходных складских документов (таких как строки заказов, закупок, складских журналов, клипборда резервов), с общими таблицами логистического модуля (то есть - складскими проводками, остатками и тп). Созданием и обновлением складских проводок занимаются наследники класса InventUpdate. Каждой операции над складскими проводками (соответственно - и над складскими документами) соответствует один из классов наследников:
| Класс | Действие |
| inventUpd_estimated | Создание строки складского документа. Генерация (или обновление) складской проводки в статусе “Заказано” или “В заказе” |
| InventUpd_reservation | Резервирование. Резервирование и снятие резервов |
| InventUpd_registration | Регистрация |
| InventUpd_Picked | Комплектация |
| InventUpd_Physical | Физические складские движения (скажем - по отборочной накладной) |
| InventUpd_Financial | Финансовые складские движения. (Скажем - по накладной или по обычному складскому журналу) |
| inventUpd_Arrived | Специфический тип складских движений по журналу прибытия из WMS |
| inventUpd_deleteMovement | Удаляет складские проводки при уменьшении количества в исходном документе или вообще удаления такового |
| InventUpd_ChangeDimension | Культурно меняет складскую аналитику в складских проводках, проверяя при этом наличие резервов, изменяя складскую аналитику во встречной проводке по переносу и тп |
| InventUpd_ChildReference | Связывает дочерние и родительские складские проводки (например - строки проводки по строкам и по шапке производственного журнала спецификаций) |
При этом, каждый из этих классов НИЧЕГО не знает о специфике, связанной с конкретным исходным складским документом. Когда этим классам требуется получить какую-то информацию из исходного складского документа (например - код номенклатуры или складской аналитики) или выполнить какое-то действие, специфичное для данного складского документа (например - создать проводку в ГК по финансовым складским движениям), они обращаются к классам семейства InventMov*, которые и предоставляют подобный сервис. В каждом из наследников класса InventMovement есть внутренняя переменная, в которой хранится экземпляр записи складского документа, на основании которой и создается складская проводка. При создании класса inventMovement, система использует constructor controlled inheritance. Строка складского документа передается в метод InventMovement::construct, который на основании информации из tableId этой строки, создает тот или иной наследник класса InventMovement. Классы inventMovement - это не просто дополнение к классам inventUpdate; На самом деле - это просто набор неких сервисов, связанных со складскими документами, которые вызываются не только из классов inventUpdate, но и из других мест системы.
Поскольку ниже я буду описывать ключевые методы класса inventMovement, надо сразу же ввести некоторую условную классификацию методов:
- Методы чтения. Так я буду называть методы, которые возвращают информацию из строки складского документа. Они конечно могут ее трансформировать или как-то подменять, но глубокой переработки возвращаемой информации эти методы не предполагают
- Методы параметризации. Эти методы также возвращают некий параметр, от которого зависит способ работы вызывающего класса. В отличие от методов чтения, эти методы либо возвращают константу, определяемую спецификой самого класса (например - поддерживает ли этот класс отслеживание недоотгруженного/недопоставленного остатка), либо значение некой настроечной таблицы, достаточно косвенно зависящее от складского документа.
- Методы обновления. Эти методы обновляют данные в исходном документе.
- Методы действия. Методы выполняют какие-то обновления данных, не связанные с обновлением складского документа. Например - создание проводок ГК, создание карантинного журнала и тп.
- Методы проверки. Вызываются системой перед выполнением каких-то действий. Должны не только вернуть true/false, но и выдать какое-то сообщение об ошибке, если таковая случилась.
- Сервисные методы. Фактически - все прочие методы.
Всю эту классификацию методов, я ввел только для того чтобы как-то описать принципы переопределения методов в классах, наследованных от inventMovement. Почти всегда приходится переопределять в своем наследнике inventMovement методы чтения. Методы параметризации переопределять обычно тоже приходится, но далеко не все (поскольку в базовом классе inventMovement методы параметризации обычно возвращают какие-то разумные значения). Методы записи тоже приходится переопределять. Методы действия, проверки и сервисные методы переопределяются не часто.
По моему правильный подход к переопределению методов в своем наследнике inventMovement такой: Сначала сделать маленький прототип, в котором переопределены только минимальный набор методов чтения и записи. В ходе тестирования мы неоднократно наткнемся на ошибку, связанную с отсутствием переопределения метода. После того как наша доработка перестала вылетать с выдачей сообщения об отсутствии определения метода, мы уже можем посмотреть на смысловую часть работы нашей модификации. Если что-то работает не так как нам хотелось бы, есть большие шансы что ситуацию можно будет исправить, переопределив в нашем классе некоторые из методов. Кроме того - некоторые методы вызываются параметрически. То есть - если у нас метод mustBeRemainControlled возвращает true, то нам придется реализовывать методы addRemainXXX, потому что первый параметрический метод, собственно и включил их вызов.
Ниже я привожу список основных методов класса inventMovement:
| Метод | Тип | Описание |
| accountBalanceAutoLossProfit | Параметризация | Счет ГК, с которого списываются материалы при включенном автоматическом списании. Используется в закупках при включенной галке “Отходы в заказе”. В общем случае - совпадает со складским счетом accountBalanceSheet() |
| accountBalanceSheet | Параметризация | Основной складской (inventory) счет ГК, на котором хранится номенклатура |
| accountOperations | Параметризация | Коррсчет складской операции. Коррсчет на который расходуется/с которого приходуется номенклатура по данной складской операции |
| accountOperationsAutoLossProfit | Параметризация | Счет ГК, на который списываются материалы при включенном автоматическом списании. Используется в закупках при включенной галке “Отходы в заказе”. По умолчанию - счет прибылей и убытков |
| accountPhysical | Параметризация | Складской счет, на котором хранится номенклатура оприходованная физически (если включена разноска физических складских движений) |
| AccountStdProfit | Параметризация | Счет для разноски прибыльных отклонений по номенклатуре со стандартной себестоимостью |
| AccountStdLoss | Параметризация | Счет для разноски убыточных отклонений по номенклатуре со стандартной себестоимостью |
| AccountStdOffset | Параметризация | Коррсчет списания отклонения по номенклатуре со стандартной себестоимостью. В общем случае - совпадает со складским счетом accountBalanceSheet() |
| addRemainFinancialUnit, addRemain Physical, addRemainPhysicalUnit |
Обновление | Методы вызываются ядром логистики для того чтобы наследники класса InventMovement могли обновить в складских документах недопоставленные/недополученные остатки (по физическим или финансовым операциям; в единицах складского учета или в единицах складского документа) |
| addTransQty, addTransQtyUnit |
Обновление | Методы вызываются ядром логистики для того чтобы наследники класса InventMovement могли обновить в складских документах количество (в единицах складского учета или в единицах складского документа). |
| autoReserveQty | Сервис | Возвращает количество для автоматического резервирования. |
| batchExpDate, batchProdDate |
Сервис | Дата устаревания и дата создания партии. Используется для заполнения полей в таблице партий при автоматическом создании таковых. |
| buffer | Сервис | Возвращает текущую копию записи складского документа (со всеми внесенными методами обновления изменениями) |
| bufferDB | Сервис | Возвращает ИСХОДНУЮ (заново прочитанную из БД) копию записи складского документа |
| canBeReserved, canBeUpdatedEstimated, canBeUpdatedFinancial, canbeUpdatedPhysical, canBeUpdatedPicked, canBeUpdatedRegistered |
Параметризация | Разрешают/запрещают выполнение складских обновлений соответствующего типа |
| checkUpdateArrived, checkUpdateEstimated, checkUpdateFinancial, checkUpdatePhysical, checkUpdateRegistered, checkUpdateStandard |
Проверка | Методы вызываются перед выполнением складских обновлений соответствующего типа. checkUpdateStandard вызывается перед выполнением любых обновлений |
| createInventTransPosting | Параметризация | Разрешает/запрещает создание записей в Разноске Складских Операций (inventTransPosting). |
| checkAllowManualMarking | Параметризация | Разрешает/запрещает ручное маркирование созданных по данному документу складских проводок (в форме маркирования) |
| createQuarantineOrder | Действие | Создает карантинный заказ |
| custVendAC | Чтение | Возвращает код клиента/поставщика из исходного складского документа. Этот код пишется ТОЛЬКО в складские проводки и ни на что больше не влияет. Можно как некую дополнительную аналитику в журналах переноса, например, добавить код клиента, для которого товар переносится со склада на склад. Может оказаться, что это поле проще вытащить в отчет из inventTrans, чем из исходного документа |
| Dimension | Чтение | Как правило не нужно переопределять. Стандартный метод ищет в строке складского документа поле Dimension и возвращает его значение |
| estimatedFinancialValue | Сервис | Возвращает ожидаемую себестоимость по данной проводке ДО проведения финансового обновления. Для приходных проводок - берет цену из таблицы номенклатуры (Складскую цену); для расходных - считает предполагаемую мгновенную себестоимость списания. При расчете себестоимости списания, принимаются во внимание маркировки проводок и указанные номера возвращаемого лота. |
| financialIssueCostValye | Сервис | Возвращает мгновенную себестоимость списания. Система использует именно этот метод, чтобы посчитать значение поля costAmount в проводках списания. Если этот метод подменить - можно списывать товар по какой-то произвольной себестоимости. Однако, я бы такой подход не рекомендовал. |
| initInventTransFinancial | Чтение | Заполняет в inventTrans те поля, которые должны быть туда скопированы из исходного документа в момент финансового обновления складской проводки |
| initInventTransPhysical | Чтение | Заполняет в inventTrans те поля, которые должны быть туда скопированы из исходного документа в момент физического обновления складской проводки |
| initInventTransFromBuffer | Чтение | Заполняет в inventTrans те поля, которые должны быть туда скопированы из исходного документа в момент создания складской проводки, или ее обновления, порожденного обновлением складского документа. Метод работает только для проводок в статусе “Заказано/В заказе” и “Зарезервировано физически/Зарезервировано в заказанных” |
| InventDim | Сервис | Возвращает складскую аналитику, связанную с складским документом |
| InventDimid | Чтение | Возвращает код складской аналитики из складского документа |
| InventDimItemDimensions | Сервис | Возвращает складскую аналитику, связанную с складским документом. При этом вся не номенклатурная аналитика - принудительно очищается. |
| inventDimMerged | Сервис | Строит складскую аналитику приходной проводки по складской аналитике расходной проводки (для транспортировок) |
| InventLocationId | Сервис | Возвращает склад из складской аналитики складского документа |
| inventModelGroup | Сервис | Возвращает группу складских моделей для номенклатуры по данному складскому документу |
| inventRefTransId | Чтение | Возвращает номер лота, к которому нужно автоматически примаркировать данную складскую проводку. |
| InventTable | Сервис | Возвращает InventTable для номенклатуры из складского документа |
| InventTableInvent, InventTableModule |
Сервис | Возвращает InventTableModule для номенклатуры из складского документа |
| isReturned | Сервис | Данный складской документ является возвратом |
| isTransfer | Параметризация | Данный складской документ является переносом. (то есть - строкой журнала переноса, карантинного заказа, транспортировки и тп) |
| itemId | Чтение | Код номенклатуры |
| modelGroupId | Сервис | Код складской модели, к которой относится номенклатура |
| moduleType | Параметризация | Складской модуль (закупки, продажи,хранение, производство) к которому относится данный складской документ. В данный момент используется только для функции inventTableModule |
| mustBeBookedBalanceSheet | Параметризация | Включает разноску финансовых операций по документу на инвентарные счета |
| mustBeBookedOperations | Параметризация | Включает разноску финансовых операций по документу на затратные счета. |
| mustBeBookedOnHand | Параметризация | Включает разноску финансовых операций по документу.Два предыдущих метода не взаимоисключающие, они просто влияют на создание двух половинок проводки (постингов) |
| mustBeBookedPhysically | Параметризация | Включает разноску физических операций по документу. |
| mustBeAutoReserved | Параметризация | Включает режим автоматического резервирования |
| mustBeExpectedAgain | параметризация | Включает режим, при котором после полной отгрузки/получения, система возвращает значение недопоставленного/недополученного остатка в исходное количество. (Как заказах типа “Подписка”) |
| mustBePicked | Параметризация | Включает обязательную комплектацию |
| mustBeRegistered | Параметризация | Включает обязательную регистрацию |
| mustBeRemainControlled | Параметризация | Включает обновление недопоставленных/недополученных количеств в документе. Если этот метод возвращает true, необходимо определять методы addRemainXXX(), setRemainXXX(), remainXXX() |
| mustBeSameTransSign | Параметризация | Включает проверку соответствия знака количества в складской проводки и знака количества в складском документе. Фактически - этот режим предполагает невозможность возвратов по документу. |
| mustBeUnitControled | Параметризация | Включает обновление недопоставленных/недополученных количеств в единицах измерения документа. Если этот метод возвращает true, необходимо определять методы addRemainXXXUnit(), setRemainXXXUnit(), remainXXXUnit(),transUnitId |
| mustBeUpdateExpected | Параметризация | Включает создание по складскому документу складских проводок в статусе “В заказе”/”Заказано” |
| mustCheckLocationBlocking | Параметризация | Отключает проверку разноски на заблокированный склад. Позволяет разносить журнал инвентаризации |
| mustCreateQuarantineOrder | Параметризация | Включает создание журнала карантинного заказа |
| mustUpdateInventTableCostPrice | параметризация | Включает обновление последней закупочной цены в таблице inventTableModule |
| mustUpdateInventTransFields | Сервис | Управляет обновлением полей в складских проводках. Если данная функция вернула true, система заново вызывает метод initFromBuffer и перезаполняет поля складских проводок из складского документа. Метод должен возвращать true, если в складском документе произошли серьезные изменения и необходимо скопировать эти изменения в складскую проводку. |
| newMovement_Orig | Сервис | Создает объект inventMovement, на основании неизмененной версии складского документа. Короче говоря - inventMovement::constuct(table.orig()) |
| offsetAccountPhysical | Параметризация | Коррсчет для проводок по физическим движениям |
| overDeliveryPct | Параметризация | Возвращает процент допустимой перепоставки |
| postingXXX | Параметризация | Множество методов, возвращающих значение перечислимого типа ledgerPosting. Это тип используется при разноске соответствующей операции в ГК |
| remainPhysical, remainPhysicalUnit, remainFinancial remainFinancialUnit |
Чтение | Возвращает недопоставленное/недополученное количество в единицах измерения складского учета или единицах измерения заказа |
| serialProdDate | Чтение | Возвращает дату создания партии (для записи в таблицу партий) |
| setInventDimId | Запись | Обновляет код складской аналитики в складском документе. Реально этот метод переопределяется нечасто, поскольку обычно складская аналитика в исходном складском документе не меняется вслед за изменением складской аналитики в складских проводках. |
| setRemainPhysical, setRemainPhysicalUnit |
Запись | Обновляет недопоставленное/недополученное количество в складском документе |
| setTransQty, setTransQtyUnit |
Запись | Изменяет количество в складском документе |
| subDeliveryPct | Параметризация | Возвращает процент допустимой недопоставки |
| transDate | Чтение | Дата складского документа |
| transId | Чтение | Номер лота |
| transIdReturn | Чтение | Номер возвращаемого лота |
| transIdSum | Сервис | Возвращает объект типа inventTransIdSum, позволяющий быстро посчитать количества по складским проводкам в разных статусах обновления по данному лоту |
| transItemBOMid, transItemRouteId |
Чтение | Возвращают код спецификации и маршрута по данному складскому документу |
| transQty, transQtyUnit |
Чтение | Общее количество по складскому документу в единицах складского хранения и единицах складского документа |
| transRefId | Чтение | Возвращает идентификатор первичного документа (типа номера заказа, закупки, журнала и тп) |
| transSign | Параметризация | Возвращает знак складской проводки. По умолчанию -1 (Отрицательный знак - списание) |
| TransType | Чтение | Тип складского документа (и складской проводки - соответственно) |
| TransUnitId | Чтение | Код единицы измерения из складского документа |
| updateBuffer | Действие | Записывает на диск запись складского документа. |
| updateDoBuffer() | действие | Записывает на диск запись складского документа вызовом doUpdate() |
| updateLedgerFinancial | Действие | Создает проводку ГК по финансовым складским приходам |
| updateLedgerAdjust | Действие | Создает проводку ГК по финансовым складским списаниям. Кроме того - создает проводку корректировки между разницей мгновенной и закупочной стоимостей по возврату закупок |
| updateLedgerPhysical | Действие | Создает проводку ГК по физическим складским движениям |
Если просмотреть иерархию классов-наследников InventMovement, можно обнаружить там классы, которые не создаются в inventMovement::cnstructNoThrow(). Это классы inventMov_Vir_Transfer, inventMov_Vir_Counting, inventMov_Vir_QuarantineLoss и их суперкласс inventMov_Virtuel, которые используются для создания складских движений полностью программным путем - без создания соответствующего складского документа. Ниже я привожу табличку с описанием того, зачем эти классы используются в системе.
| Класс | Применение |
| inventMov_Virtuel | 1. Автоматическое списание финансово оприходованного товара. Происходит при указании галочки “Отходы” в закупках. 2. Списания потерянного товара по транспортировке 3. Списания потерь в производственных заказах |
| inventMov_Vir_Transfer | Программный перенос номенклатуры с палетты на палетту в модуле расширенного склада. Вообще - может переносить номенклатуру с одной складской аналитики на другую |
| inventMov_Vir_QuarantineLoss | Списание некондиционного товара по карантинному заказу. |
| InventMov_Vir_Counting | Используется для списания/оприходования товара при оперативной инвентаризации товара из WMS-модуля (короче говоря - инвентаризации не через журнал инвентаризации) |
В заключение, надо сказать о том как обычно строится взаимодействие классов inventMovement и inventUpdate. В любом классе inventUpd имеется стандартный метод-конструктор с именем типа newParameters. Кроме того, для всех (или почти всех) складских документов, которые могут порождать подобный вид складских операций, заводится специальный метод newXXX (например - newPurchInvoice,newSalesInvoice,newInventJournal и тп). После того как объект inventUpdate создан, само действие выполняется вызовом inventUpdate.updateNow().
Создание нового типа складского документа
После того как теоретическая часть закончена, можно приступить к практическим занятиям
Код примера приложен к статье. Он был реализован на DAX 4.0SP2
Для начала создаем собственно таблицы, в которых у нас будут хранится строки клипборда резервов - reserveClipBoardLine. Строка эта, само собой, должна содержать код номенклатуры, код складской аналитики,количество и номер лота. Кроме того, в нашем случае она будет содержать код клипборда резервов. Номер лота будем генерировать и присваивать в методах update и insert. Обратите внимание на шаблонный кусок кода, вызывающий inventUpd_estimated.updateNow(), чтобы создавать складские проводки в статусе Заказанно.
Сам список клипбордов резервов лежит в таблице reserveClipboard.
Предполагается, что пользователи не будут часто работать с таблицей строк клипборда резервов напрямую (то есть - не через функции “Резервировать из клипборда”/”Снять резерв из клипборда”). Тем не менее, из соображений хорошего тона нам придется реализовать форму редактирования строк клипборда резервов. Сама по себе форма особого интереса не представляет. Любопытным, однако, будет посмотреть на доработки, которые понадобилось сделать для того чтобы заинтегрировать ее с системой управления доступностью/недоступностью складской аналитики для редактирования.
Для начала стоит посмотреть на то, как эта форма работает с классом inventDimCtrl_Frm_mov. Этот класс является наследником класса inventDimCtrl_Frm, который и отвечает за работу с отображением складской аналитики на форме. Я не буду разбирать работу этих классов в целом. Ниже описаны доработки, которые пришлось сделать для того чтобы поддержать интеграцию новой формы со стандартным механизмом работы со складской аналитикой.
| Изменено | Подробности |
| таблица inventDimSetup | Добавлено новое поле для запрещения/разрешения редактирования данной складской аналитики в форме клипборда резервов. Метод transType2FieldId изменен таким образом, чтобы поддержать связь между новым типом складской проводки и новым полем. |
| Таблица inventDimSetupGrid | Добавлено новое поле для запрещения/разрешения отображения складской аналитики в строках грида формы клипборда резервов. Метод movement2FieldId изменен таким образом, чтобы поддержать связь между новым типом складской проводки и новым полем. Метод InitAllYes изменен таким образом, чтобы поддержать инициализацию нового поля. |
Теперь можно приступить к собственно реализации класса, реализующего новый тип складских перемещений. Для начала - нам надо добавить новое значение в перечислимый тип InventTransType, являющийся, фактически, списком возможных складских документов, на основании которых генерируются складские проводки. После этого можно приступать к собственно разработке нашего нового наследника класса inventMovement.
| Метод | Пояснение |
| addTransQty | Добавляет количество к количеству в строке клипборда резервов. Обычно этот метод не используется и не переопределяется. Я его использую для того, чтобы можно было бы увеличивать количество в строке клипборда резервов перед резервированием. Перед резервированием товара на строку резервов, надо сначала увеличить количество в строке клипборда, чтобы потом было чего резервировать. |
| canBeUpdatedPhysical | Запрещаем физические операции |
| canBeUpdatedFinancial | Запрещаем финансовые операции |
| canBeUpdatedPicked | Запрещаем комплектацию |
| canBeUpdatedRegistered | Запрещаем регистрацию |
| inventDimId | Возвращает код складской аналитики |
| itemId | Возвращает код номенклатуры |
| remainPhysical | Возвращает недопоставленный остаток. Хотя контроль остатка не требуется и метод mustBeRemainControlled не переопределен, этот метод все равно вызывается при создании проводок в статусе Заказано. |
| transDate | Нужно для заполнения ожидаемой даты в проводках со статусом Заказано. Возвращает текущую дату. |
| transQty | Возвращает количество из исходного документа. Количество возвращается отрицательное, поскольку это списание. |
| transRefId | Возвращает имя клипборда. Пусть оно пишется в inventTrans.transRefId - для удобство сбора разнообразных отчетов. |
| transType | Возвращает тип складской проводки transType::reserveClipboard |
Для того чтобы завершить интеграцию вновь созданного класса в систему логистики, нам осталось доработать метод InventMovement::constructNoThrow() таким образом чтобы он создавал объект типа inventMov_reserveClipboardLine. Кроме того, нам надо аналогичным образом доработать метод inventTrans.inventMovement().
Теперь можно приступить к собственно разработке классов, которые будут перекидывать резерв из обычного складского документа в клипборд и наоборот.
Эти функции выполняются классами reserveFromClipboard и unreserveFromClipboard. Эти классы делают достаточно трививальную вещь - открывают транзакцию, снимают резерв в одном месте и ставят резерв в другом месте. Я думаю - несложно будет разобраться по исходному коду примера. Хочу обратить внимание на использование методов addTrans() и updateBuffer() для того чтобы увеличить количество в строке клипборда резервов перед резервированием в клипборде или уменьшения количества - после снятия резерва из клипборда. Также надо обратить внимание на использование метода transIdSum() для подсчета количества в разных статусах в складских проводках по данному лоту. Даже после обновления статусов проводок в методе inventUpdate.writeInventTrans(), этот метод возвращает объект inventTransIdSum(), возвращающий правильные количества. Пункты меню, связанные с новыми классами, добавлены только в форму заказов. В реальных условиях они, само собой, были добавлены во все складские документы, порождающие списания.
Завершая рассказ о классах, работающих с клипбордом резервов, хочу отметить что в этом демонстрационном примере я не стал реализовывать проверку прав доступа к клипборду резервов. Кроме того - я добавил новые пункты меню резервирования только в форму заказов. В реальной задаче - этих ограничений не было.
Добавление полей в таблицу “Запасы в наличии”
В первом приближении - добавление новых количественных полей в таблицу inventSum (Запасы в наличии) - это достаточно простой процесс. Обновление этой таблицы производится вызовами ее методов addInventTransOnSum и subInventTransOnSum, которые вызываются в момент обновления записей в таблице inventTrans. В этих методах имеется оператор switch, который, в зависимости от статусов прихода и расхода, добавляет (или вычитает) количество в складской проводке к количеству в одном из количественных полей inventSum. Так что в первом приближении, все что нам надо сделать - это добавить в эти методы маленькую веточку в оператор case, которая в зависимости от типа проводки добавляла бы количество из складской проводки не только в обычное поле, но и в наше новое поле “физически зарезервировано в клипборде”.
Остальные доработки связаны с появившимся в версии DAX 4.0 механизмом отложенного обновления остатков. В связи с этим, нам приходится добавлять поля и обновлять методы таблицы inventSumDelta. Особенно важно, не забыть включить новое поле в группы полей deltaFields и deltaFieldsQty в таблицах inventSum и inventSumDelta. Как я уже писал в одной из своих предыдущих статей, система для быстрого обновления таблицы InventSum использует native SQL, который перегоняет накопленные обновления из таблицы inventSumDelta в таблицу inventSum. Именно для генерации корректного списка обновляемых полей и необходимы эти группы полей. Более того, поскольку при использовании Oracle, это обновление делается с помощью хранимой процедуры, нам после установки этой доработки необходимо будет выполнить метод SQLOracleInitStoredProcs::initOracleSPIMTS(), чтобы пересоздать эту хранимую процедуру на сервере.
Необходимо также, добавить новое поле в макрос inventSumFields, перекомпилировав после этого все классы и таблицы. Кроме того - я добавил новые методы в класса inventTransidSum и inventOnHand. Я этими новыми методами не пользуюсь, но из соображений хорошего тона в программировании эти методы добавить надо.
Наконец, надо изменить класс InventSumRecalcItem. Этот класс используется для пересчета таблицы InventSum. Поскольку он, перед вызовом методом inventSum.addInventTransOnSum и inventSum.subInventTransOnSum() группирует строки таблицы inventTrans по статусу, разнообразным датам, складской аналитике и номенклатуре, нам для правильной работы доработки придется добавить туда еще и тип проводки.
Хотя в данной доработке я создаю новые поля в inventSum на основании стандартных полей inventTrans, но в целом этот механизм достаточно удобно применять и для всяких дополнительных статусных полей. Например, на одном из проектов, мы добавили в шапку закупки новое перечислимое поле “Статус доставки”, в котором указывалась стадия доставки товара от производственных площадок производителя (зарубежного кстати) до локальных складов организации, в которой шло внедрение. Далее, мы растиражировали это поле в складские проводки, а затем добавили в таблицу запасов в наличии новые поля, в которых было указано количество в каждой из стадий доставки. После этого пользователям в форме “В наличии” стала доступна информация не только о том сколько товара уже заказано, но и о том где этот заказанный товар находится.
Вместо завершения
В приложенном проекте имеется группа объектов inventTransCreateDemo и вложенный в нее класс InvVirDemo, которые не имеют никакого отношения к рассматриваемому примеру с клипбордами резервов. Просто я решил, что раз уж я рассказал в этой статье о классах виртуальных складских движений (inventMov_virtual и наследники), то имеет смысл привести пример их использования. Класс invVirDemo запрашивает у пользователя номенклатуру, количество, себестоимость, два склада и два коррсчета. После этого система создает приход номенклатуры на один из складов с первого коррсчета, делает перенос номенклатуры на второй склад и списывает со второго склада на второй коррсчет. Я думаю в классе достаточно подробные комментарии чтобы разобраться с ним самостоятельно.
Также хочу заметить, что на мой взгляд, не стоит слишком увлекаться использованием класса inventMov_Virtuel для создания складских движений. На мой взгляд, этот класс придуман для тех случаев, когда какой-то кусок функциональности ИНОГДА должен создавать складские проводки. Если вы пишете какое-то полноценное расширение логистики, то правильнее создавать новый тип складского движения и нового наследника класса inventMovement.





ноября 3, 2007 at 1:06 дп
Судя по названию метода
SQLOracleInitStoredProcs::initOracleSPIMTS(),
Таблица
inventSumDelta
используется только при включенной IMTS ?
ноября 6, 2007 at 4:08 пп
Да - действительно. Только нужно помнить, что в 4ой версии IMTS выключить невозможно. Она там всегда включенная. Так что таблица используется всегда.
мая 20, 2008 at 8:50 дп
Отличная статья! но архив битый..
мая 26, 2008 at 2:11 пп
Исправлено