Автоматическое резервирование в MS Dynamics 365 for Finance and operations

Резервирование может происходить не только при резервировании вручную с формы складских проводок, но и при смене статуса расхода складской проводки на другие кроме физически зарезервировано (StatusIssue::ReservPhysical) и в момент комплектации (StatusIssue:: OnOrder и StatusIssue:: Picked) или при автоматическом резервировании. В данной статье будет рассмотрено автоматическое резервирование в MS Dynamics 365 в момент создания строки заказа.
Основные классы, содержащие алгоритмы резервирования и определяющие поведение объектов данных, представлены тремя семействами. Класс, который непосредственно предназначен для резервирования номенклатур и смены статуса складской проводки на физ. зарезервировано — InventUpd_Reservation наследник класса InventUpdate. Для получения параметров и проведения проверок используются классы семейства InventMovement и InventType.

Автоматическое резервирование при вставке записи.

Один из моментов резервирования — это вставка строки заказа (например, 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!

Добавить комментарий

Sidebar