Автоматическое резервирование в MS Dynamics 365 for Finance and operations
Резервирование может происходить не только при резервировании вручную с формы складских проводок, но и при смене статуса расхода складской проводки на другие кроме физически зарезервировано (StatusIssue::ReservPhysical) и в момент комплектации (StatusIssue:: OnOrder и StatusIssue:: Picked) или при автоматическом резервировании. В данной статье будет рассмотрено автоматическое резервирование в MS Dynamics 365 в момент создания строки заказа.
Основные классы, содержащие алгоритмы резервирования и определяющие поведение объектов данных, представлены тремя семействами. Класс, который непосредственно предназначен для резервирования номенклатур и смены статуса складской проводки на физ. зарезервировано — InventUpd_Reservation наследник класса InventUpdate. Для получения параметров и проведения проверок используются классы семейства InventMovement и InventType.
Contents
Автоматическое резервирование при вставке записи.
Один из моментов резервирования — это вставка строки заказа (например, SalesLine, PurchLine, InventJournalTrans, ProdBOM, SalesQuotationLine). В методе Insert или Update экземпляр класса InventUpd_Estimated запускает процесс резервирования (метод updateReservation).
public void updateReservation() { InventUpd_Reservation reservation; InventQty reservQty; PdsCWInventQty cwReservQty = 0; if (movement.canBeReserved() && movement.mustBeAutoReserved() ) { if (! movement.updateReqExplodeReservation()) { if (movement_Orig) { if (allowReserveReversed && (movement_Orig.transDate() != movement.transDate() || preEstimated * estimated < 0)) { reservation = InventUpd_Reservation::newParameters( movement, inventDimCriteria, inventDimParm, 0, -movement.transIdSum().reserved(), true, false, -movement.transIdSum().pdsCWReserved()); reservation.updateNow(); } } … }
Метод canBeReserved класса InventMovement запускает метод семейства классов InventType.canInventBeReserved(). Изначально возвращаемое значение True, но в некоторых наследниках, например, SalesLineType_Journal или PurchLineType_Journal метод перекрыт значением False. Каждый наследник InventMovement в методе new перекрывает создание экземпляра класса InventType. Например, InventMov_Sales
protected void new( SalesLine _salesOrderLine, InventType _inventType = SalesLineType::construct(_salesOrderLine)) { salesOrderLine = _salesOrderLine; super(_salesOrderLine,_inventType); }
Метод mustBeAutoReserved изначально возвращает false, и перекрыт на многих наследниках. Метод на InventMov_Sales
public boolean mustBeAutoReserved() { if (!this.inventTable().inventItemType().canBeAutoReserved()) { return false; } this.initForAutoReservation(); return salesOrderLine.Reservation == ItemReservation::Automatic || salesOrderLine.Reservation == ItemReservation::Explosion; }
Метод inventItemType.canBeAutoReserved возвращает true. У класса inventItemType в Dynamics 365 только два наследника для типов «Номенклатура» и «Услуга», наследник для типа «Услуга» возвращает false. Поле salesLine.Reservation наследуется из шапки заказа, в шапке задается вручную, нужно ли автоматически резервировать строки.
Метод updateReqExplodeReservation возвращает false если не перекрыт в наследниках. Для строк продаж в методе проверятся значение поля «Контроль даты поставки» или тип автоматического резервирования.
public boolean updateReqExplodeReservation() { if (salesOrderLine.DeliveryDateControlType == SalesDeliveryDateControlType::CTP) { // an explosion is executed later and will do reservation according to setting return true; } else { if (salesOrderLine.Reservation == ItemReservation::Explosion) { // run explosion now and do reservation ReqCalcExplodeSales::newMovement(this, true).runOperation(); return true; } } return super(); }
В методе InventUpdate.setParmAllowDefault задается параметр allowReserveReversed = movement.inventModelGroup().ReserveReversed (метод возвращает InventTable::find(this.itemId()).modelGroup(), для продаж не перекрыт), и movement_Orig = movement.newMovement_Orig(), в этом случае для таблицы буфером является запись до изменений (orig).
FEFO и FIFO при резервировании
«Управление датой по принципу FEFO» (InventModelGroup.PdsCheck) и «FIFO с контролем по дате» (ReserveByDate). От этих флагов зависит какой запрос будет выполняться при поиске доступных для резервирования запасов в методе InventUpd_Reservation.updateReserveMore
pdsCheck = InventTable::find(movement.itemId()).pdsCheck(); … if (inventDimParm.ItemIdFlag && pdsCheck && movement.itemId() && InventDimParm::activeDimFlag(movement.inventDimGroupSetup()).InventBatchIdFlag && !inventDimParmFixed.InventBatchIdFlag) { querySum = InventSum::pds_FEFOQuery( movement.itemId(), inventDimCriteria, inventDimParm, InventDimParm::activeDimFlag(movement.inventDimGroupSetup())); mustIncludeDeltaQueryAggregated = mustIncludeInventSumDelta ? true : false; mustIncludeInventSumDelta = false; // Only consider records with quantity values. SysQuery::findOrCreateRange(querySum.dataSourceTable(tableNum(InventSum)), fieldNum(InventSum, ClosedQty)).value(queryValue(NoYes::No)); } else { querySum = InventSum::newQueryReservation(movement.itemId(),inventDimCriteria,inventDimParm,InventDimParm::activeDimFlag(movement.inventDimGroupSetup()), movement.pdsCWItem()); }
Из условия видно, что, если у номенклатуры активна партия и «Управление датой по принципу FEFO» (InventModelGroup.PdsCheck) включено, то в запрос к InventSum добавляется партия метод InventSum::pds_FEFOQuery. Запрос имеет следующий вид
SELECT * FROM InventBatch(DS_INVENTBATCH) JOIN * FROM InventDim(InventDim_1) GROUP BY InventBatch.ExpDate ASC --(или InventBatch.ExpDate ASC в зависимости от значения InventModelGroup. PdsPickCriteria) ON InventBatch.inventBatchId = InventDim.inventBatchId JOIN SUM(PostedQty), SUM(PostedValue), SUM(PhysicalValue), SUM(Deducted), SUM(Registered), SUM(Received), SUM(Picked), SUM(ReservPhysical), SUM(ReservOrdered), SUM(OnOrder), SUM(Ordered), SUM(Arrived), SUM(QuotationReceipt), SUM(QuotationIssue), SUM(PhysicalInvent), SUM(PostedValueSecCur_RU), SUM(PhysicalValueSecCur_RU), SUM(AvailPhysical), SUM(AvailOrdered) FROM InventSum(InventSum_1) GROUP BY InventSum.ItemId ASC ON InventDim.inventDimId = InventSum.InventDimId AND InventBatch.itemId = InventSum.ItemId AND ((Closed = 0)) AND ((ItemId = ''))
Иначе запрос состоит только из InventSum и InventDim метод InventSum::newQueryReservation, и учет даты идет либо только по FIFO либо по полю InventSum.LastUpdDateExpected ожидаемая дата последней проводки (InventModelGroup.ReserveReversed назад от даты отгрузки), что схоже с LIFO, но дата в обратном порядке учитывается только если разрешено резервировать в заказанных. Метод InventUpd_Reservation.updateReserveMore.
this.parmAllowReserveByDate(inventModelGroup.ReserveByDate); (allowReserveByDate) … this.parmAllowReserveReversed(inventModelGroup.ReserveReversed); … switch ((loopReserveType!=0) ? 2 : 1) //проверка сначала в физически доступных { case #AvailPhysical : querySum.dataSourceTable(tableNum(InventSum)).addRange(fieldNum(InventSum,AvailPhysical)).value('!0'); if (mustIncludeInventSumDelta) { queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addRange(fieldNum(InventSumDeltaAggrSumView,AvailPhysical)).value('!0'); } if (allowReserveByDate) { querySum.dataSourceTable(tableNum(InventSum)).addSortField(fieldNum(InventSum,LastUpdDatePhysical)); if (mustIncludeInventSumDelta) { queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addSortField(fieldNum(InventSumDeltaAggrSumView,LastUpdDatePhysical)); } } break; case #AvailOrdered : // потом в заказанных qrAvailOrdered = querySum.dataSourceTable(tableNum(InventSum)).addRange(fieldNum(InventSum,AvailOrdered)); qrAvailOrdered.value(strFmt('((%1 != 0.0) || (%2 != 0.0))', fieldStr(InventSum, AvailOrdered), fieldStr(InventSum, OnOrder))); // We check OnOrder to handle negative physical scenarios with no existing on-hand qrAvailOrdered.status(RangeStatus::Locked); if (mustIncludeInventSumDelta) { qrAvailOrdered = queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addRange(fieldNum(InventSumDeltaAggrSumView,AvailOrdered)); qrAvailOrdered.value(strFmt('((%1 != 0.0) || (%2 != 0.0))', fieldStr(InventSumDeltaAggrSumView, AvailOrdered), fieldStr(InventSumDeltaAggrSumView, OnOrder))); // We check OnOrder to handle negative physical scenarios with no existing on-hand qrAvailOrdered.status(RangeStatus::Locked); } if (allowReserveByDate) { if (loopReserveType == 1 && allowReserveReversed) { querySum.dataSourceTable(tableNum(InventSum)).addSortField(fieldNum(InventSum,LastUpdDateExpected),SortOrder::Descending); querySum.dataSourceTable(tableNum(InventSum)).addRange(fieldNum(InventSum,LastUpdDateExpected)).value('..'+queryValue(movement.transDate())); if (mustIncludeInventSumDelta) { queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addSortField(fieldNum(InventSumDeltaAggrSumView,LastUpdDateExpected),SortOrder::Descending); queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addRange(fieldNum(InventSumDeltaAggrSumView,LastUpdDateExpected)).value('..'+queryValue(movement.transDate())); } } else { querySum.dataSourceTable(tableNum(InventSum)).addSortField(fieldNum(InventSum,LastUpdDateExpected),SortOrder::Ascending); if (mustIncludeInventSumDelta) { queryDelta.dataSourceTable(tableNum(InventSumDeltaAggrSumView)).addSortField(fieldNum(InventSumDeltaAggrSumView,LastUpdDateExpected),SortOrder::Ascending); } } } break; }
Проверка перед резервированием с помощью формы «Авторезервирование».
Форма «Авторезервирование» вызывается в validateWrite DataSource формы методом InventMovement::setAutoReserving (например, на форме SalesTable в методе SalesLine.validateWrite метод SalesTableForm.salesLine_validateWrite запускает проверку). Форма «Авторезервирование» предназначена для предупреждения о том, что все обновленное количество не может быть зарезервировано полностью, для наглядности выводится доступное количество, и предлагает следующие действия с вновь введенным количеством в заказе: изменить, отменить обновление или оставить без изменений. Если обновление количества не было отменено, то метод InventMovement::setAutoReserving возвращает True и в методе insert/update таблицы происходит резервирование на доступное количество и создается новая складская проводка со статусом «Заказано» для оставшегося количества. Ниже приведен пример кода, как можно запустить все проверки, используемые методом InventMovement::setAutoReserving, перед резервированием без запуска формы «Авторезервирование»
InventMovement movement; InventMovement movement_Orig; str inventDimCriteriaPrefix; boolean canReserved, ret; InventDim inventDimCriteria; InventDimParm ; int infoCount = infologLine(), infoDocCount, infoErrorsCount; str docInfo = strFmt("%1, %2", _docNum, _lineNum); movement = InventMovement::construct(_buffer); if (!movement.canBeReserved()) return true; if (! movement.inventTable().checkValid(movement.itemId())) { if (_showError) error("@SYS18447"); return false; } if (!movement.inventTable().inventItemType().mustInventBeControlled()) return true; movement_Orig = InventMovement::construct(_buffer.orig()); docInfo += ' ' + strFmt("@SYS24300", movement.itemId()); info(docInfo); inventDimCriteriaPrefix = movement.inventdim().preFix(); if (inventDimCriteriaPrefix) info(inventDimCriteriaPrefix); infoDocCount = infologLine(); inventDimCriteria = movement.inventdim(); inventDimParm = movement.initDimParmAutoReserv(inventDimCriteria); // Transfers need to enforce warehouse for the reservation even when they may have not // have it set as physical inventory. Otherwise circular reservations may occur. if ( inventDimCriteria.InventLocationId != '' && movement.isTransfer()) { inventDimParm.InventLocationIdFlag = NoYes::Yes; } inventDimParm.setAllProductDimensions(); // Product dimensions must be specified on movement and should be enforced - even when blank. if (movement.pdsSameLotReservation()) { if (InventSameBatchReserveCheck::newFromDim(movement, inventDimCriteria, inventDimParm, true).run() == PdsSameLotError::None) canReserved = true; } else if (InventMovement::canAutoReserveQuantity(_buffer, true)) canReserved = true; if (canReserved && movement.transIdSum().onOrder()) { InventUpd_Reservation::newMovement(movement, movement.transIdSum().onOrder(), true, false, true , movement.transIdSum().pdsCWOnOrder()).updateNow(); } infoErrorsCount = infologLine(); if (infoErrorsCount == infoDocCount) { infolog.clear(infoCount); if (canReserved) ret = true; else ret = false; } else ret = false; if (!_showError) infolog.clear(infoCount); return ret;
От знака переданного количества в метод InventUpd_Reservation::newMovement зависит будет зарезервировано (отрицательное количество) или наоборот снят резерв (положительное). Если будет передано количество большее чем физически доступно, то резервирование не произойдет. Для проверки доступного количества можно использовать метод InventOnHandQty. availPhysical
InventDim inventDimLoc; InventDimParm inventDimParm; InventMovement inventMovement; ; inventMovement = InventMovement::construct(_inventJournalTrans); inventDimLoc = InventDim::find(_inventJournalTrans.InventDimId); inventDimParm.initPrimaryDimReservation(inventMovement.inventDimGroupSetup(), inventDimLoc); return InventOnHandQty::newItemDim( _inventJournalTrans.ItemId, inventDimLoc, inventDimParm).availPhysical();
Comments
So empty here ... leave a comment!