diff --git a/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/IReceiptSchedulePA.java b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/IReceiptSchedulePA.java new file mode 100644 index 00000000000..2f276909b85 --- /dev/null +++ b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/IReceiptSchedulePA.java @@ -0,0 +1,49 @@ +/* + * #%L + * de.metas.swat.base + * %% + * Copyright (C) 2020 metas GmbH + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package de.metas.inoutcandidate.api; + +import de.metas.inoutcandidate.model.I_M_ReceiptSchedule; +import de.metas.order.OrderId; +import de.metas.process.PInstanceId; +import de.metas.util.ISingletonService; +import lombok.NonNull; +import org.adempiere.ad.dao.IQueryBuilder; +import org.adempiere.ad.dao.IQueryFilter; + +import java.util.Properties; + +/** + * Implementers give database access to {@link I_M_ReceiptSchedule} instances (DAO). + * + * @author dp + * + */ + +public interface IReceiptSchedulePA extends ISingletonService +{ + IQueryBuilder createQueryForShipmentScheduleSelection(Properties ctx, IQueryFilter userSelectionFilter); + + boolean existsExportedReceiptScheduleForOrder(@NonNull final OrderId orderId); + + void updateExportStatus(final String exportStatus, final PInstanceId pinstanceId); +} diff --git a/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/impl/ReceiptSchedulePA.java b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/impl/ReceiptSchedulePA.java new file mode 100644 index 00000000000..ae11ed7312e --- /dev/null +++ b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/api/impl/ReceiptSchedulePA.java @@ -0,0 +1,189 @@ +/* + * #%L + * de.metas.swat.base + * %% + * Copyright (C) 2020 metas GmbH + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package de.metas.inoutcandidate.api.impl; + +import com.google.common.collect.ImmutableSet; +import de.metas.cache.CacheMgt; +import de.metas.cache.model.CacheInvalidateMultiRequest; +import de.metas.inoutcandidate.ReceiptScheduleId; +import de.metas.inoutcandidate.api.IReceiptSchedulePA; +import de.metas.inoutcandidate.exportaudit.APIExportStatus; +import de.metas.inoutcandidate.model.I_M_ReceiptSchedule; +import de.metas.order.OrderId; +import de.metas.process.PInstanceId; +import de.metas.util.Services; +import lombok.NonNull; +import org.adempiere.ad.dao.IQueryBL; +import org.adempiere.ad.dao.IQueryBuilder; +import org.adempiere.ad.dao.IQueryFilter; +import org.adempiere.ad.dao.impl.ModelColumnNameValue; +import org.adempiere.ad.trx.api.ITrx; + +import java.util.Properties; + +public class ReceiptSchedulePA implements IReceiptSchedulePA +{ + private final IQueryBL queryBL = Services.get(IQueryBL.class); + + /** When mass cache invalidation, above this threshold we will invalidate ALL shipment schedule records instead of particular IDS */ + private static final int CACHE_INVALIDATE_ALL_THRESHOLD = 200; + + @Override + public IQueryBuilder createQueryForShipmentScheduleSelection(final Properties ctx, final IQueryFilter userSelectionFilter) + { + final IQueryBuilder queryBuilder = queryBL + .createQueryBuilder(I_M_ReceiptSchedule.class, ctx, ITrx.TRXNAME_None) + .filter(userSelectionFilter) + .addEqualsFilter(I_M_ReceiptSchedule.COLUMNNAME_Processed, false) + .addOnlyActiveRecordsFilter() + .addOnlyContextClient(); + + return queryBuilder; + } + + @Override + public boolean existsExportedReceiptScheduleForOrder(final @NonNull OrderId orderId) + { + return queryBL + .createQueryBuilder(I_M_ReceiptSchedule.class) + .addOnlyActiveRecordsFilter() + .addEqualsFilter(I_M_ReceiptSchedule.COLUMNNAME_C_Order_ID, orderId) + .addEqualsFilter(I_M_ReceiptSchedule.COLUMNNAME_Processed, false) + .addInArrayFilter(I_M_ReceiptSchedule.COLUMNNAME_ExportStatus, APIExportStatus.EXPORTED_STATES) + .create() + .anyMatch(); + } + + @Override + public void updateExportStatus(final String exportStatus, final PInstanceId pinstanceId) + { + updateColumnForSelection( + I_M_ReceiptSchedule.COLUMNNAME_ExportStatus, + exportStatus, + false /* updateOnlyIfNull */, + pinstanceId, + false /* invalidate */ + ); + } + + /** + * Mass-update a given shipment schedule column. + * + * If there were any changes and the invalidate parameter is on true, those shipment schedules will be invalidated. + * + * @param inoutCandidateColumnName {@link I_M_ShipmentSchedule}'s column to update + * @param value value to set (you can also use {@link ModelColumnNameValue}) + * @param updateOnlyIfNull if true then it will update only if column value is null (not set) + * @param selectionId ShipmentSchedule selection (AD_PInstance_ID) + * @param trxName + */ + private final void updateColumnForSelection( + final String inoutCandidateColumnName, + final ValueType value, + final boolean updateOnlyIfNull, + final PInstanceId selectionId, + final boolean invalidate) + { + // + // Create the selection which we will need to update + final IQueryBuilder selectionQueryBuilder = queryBL + .createQueryBuilder(I_M_ReceiptSchedule.class) + .setOnlySelection(selectionId) + .addEqualsFilter(I_M_ReceiptSchedule.COLUMNNAME_Processed, false) // do not touch the processed shipment schedules + ; + + if (updateOnlyIfNull) + { + selectionQueryBuilder.addEqualsFilter(inoutCandidateColumnName, null); + } + final PInstanceId selectionToUpdateId = selectionQueryBuilder.create().createSelection(); + if (selectionToUpdateId == null) + { + // nothing to update + return; + } + + // + // Update our new selection + final int countUpdated = queryBL.createQueryBuilder(I_M_ReceiptSchedule.class) + .setOnlySelection(selectionToUpdateId) + .create() + .updateDirectly() + .addSetColumnValue(inoutCandidateColumnName, value) + .execute(); + + // + // Cache invalidate + // We have to do this even if invalidate=false + cacheInvalidateBySelectionId(selectionToUpdateId, countUpdated); + } + + private void cacheInvalidateBySelectionId( + @NonNull final PInstanceId selectionId, + final long estimatedSize) + { + final CacheInvalidateMultiRequest request; + if (estimatedSize < 0) + { + // unknown estimated size + request = CacheInvalidateMultiRequest.allRecordsForTable(I_M_ReceiptSchedule.Table_Name); + } + else if (estimatedSize == 0) + { + // no records + // unknown estimated size + request = null; + } + else if (estimatedSize <= CACHE_INVALIDATE_ALL_THRESHOLD) + { + // relatively small amount of records + // => fetch and reset individually + final ImmutableSet shipmentScheduleIds = queryBL.createQueryBuilder(I_M_ReceiptSchedule.class) + .setOnlySelection(selectionId) + .create() + .listIds(ReceiptScheduleId::ofRepoId); + if (!shipmentScheduleIds.isEmpty()) + { + request = CacheInvalidateMultiRequest.rootRecords(I_M_ReceiptSchedule.Table_Name, shipmentScheduleIds); + } + else + { + // no records found => do nothing + request = null; + } + } + else + { + // large amount of records + // => instead of fetching all IDs better invalidate the whole table + request = CacheInvalidateMultiRequest.allRecordsForTable(I_M_ShipmentSchedule.Table_Name); + } + + // + // Perform the actual cache invalidation + if (request != null) + { + CacheMgt.get().resetLocalNowAndBroadcastOnTrxCommit(ITrx.TRXNAME_ThreadInherited, request); + } + } +} diff --git a/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/exportaudit/APIExportStatus.java b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/exportaudit/APIExportStatus.java index 46733201539..92cb7d0d37c 100644 --- a/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/exportaudit/APIExportStatus.java +++ b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/exportaudit/APIExportStatus.java @@ -23,6 +23,7 @@ package de.metas.inoutcandidate.exportaudit; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import de.metas.inoutcandidate.model.X_M_ShipmentSchedule; import de.metas.order.InvoiceRule; @@ -44,6 +45,8 @@ public enum APIExportStatus implements ReferenceListAwareEnum ExportedAndForwarded(X_M_ShipmentSchedule.EXPORTSTATUS_EXPORTED_AND_FORWARDED), ExportedAndError(X_M_ShipmentSchedule.EXPORTSTATUS_EXPORTED_FORWARD_ERROR); + public static final ImmutableSet EXPORTED_STATES = ImmutableSet.of(Exported, ExportedAndError, ExportedAndForwarded); + @Getter private final String code; diff --git a/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/process/M_ReceiptSchedule_ChangeExportStatus.java b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/process/M_ReceiptSchedule_ChangeExportStatus.java new file mode 100644 index 00000000000..5ef9a4c9aef --- /dev/null +++ b/backend/de.metas.swat/de.metas.swat.base/src/main/java/de/metas/inoutcandidate/process/M_ReceiptSchedule_ChangeExportStatus.java @@ -0,0 +1,91 @@ +/* + * #%L + * de.metas.swat.base + * %% + * Copyright (C) 2020 metas GmbH + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 2 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program. If not, see + * . + * #L% + */ + +package de.metas.inoutcandidate.process; + +import de.metas.i18n.AdMessageKey; +import de.metas.inoutcandidate.api.IReceiptSchedulePA; +import de.metas.inoutcandidate.model.I_M_ReceiptSchedule; +import de.metas.process.IProcessPrecondition; +import de.metas.process.IProcessPreconditionsContext; +import de.metas.process.JavaProcess; +import de.metas.process.PInstanceId; +import de.metas.process.Param; +import de.metas.process.ProcessPreconditionsResolution; +import de.metas.util.Services; +import lombok.NonNull; +import org.adempiere.ad.dao.IQueryBuilder; +import org.adempiere.ad.dao.IQueryFilter; +import org.adempiere.exceptions.AdempiereException; + +public class M_ReceiptSchedule_ChangeExportStatus extends JavaProcess implements IProcessPrecondition +{ + private final IReceiptSchedulePA receiptSchedulePA = Services.get(IReceiptSchedulePA.class); + + private static final AdMessageKey MSG_NO_UNPROCESSED_LINES = AdMessageKey.of("receiptschedule.noUnprocessedLines"); + + private static final String PARAM_ExportStatus = "ExportStatus"; + @Param(parameterName = PARAM_ExportStatus, mandatory = true) + private String exportStatus; + + @Override + public ProcessPreconditionsResolution checkPreconditionsApplicable(@NonNull final IProcessPreconditionsContext context) + { + if (context.isNoSelection()) + { + return ProcessPreconditionsResolution.rejectBecauseNoSelection(); + } + + return ProcessPreconditionsResolution.accept(); + } + + @Override + protected void prepare() + { + final IQueryFilter userSelectionFilter = getProcessInfo().getQueryFilterOrElseFalse(); + final IQueryBuilder queryBuilderForShipmentSchedulesSelection = receiptSchedulePA.createQueryForShipmentScheduleSelection(getCtx(), userSelectionFilter); + + // Create selection and return how many items were added + final int selectionCount = queryBuilderForShipmentSchedulesSelection + .create() + .createSelection(getPinstanceId()); + + if (selectionCount <= 0) + { + throw new AdempiereException(MSG_NO_UNPROCESSED_LINES) + .markAsUserValidationError(); + } + + } + + @Override + protected String doIt() throws Exception + { + final PInstanceId pinstanceId = getPinstanceId(); + + // update delivery date + // no invalidation + receiptSchedulePA.updateExportStatus(exportStatus, pinstanceId); + + return MSG_OK; + } +}