682 lines
29 KiB
C#
682 lines
29 KiB
C#
using Sarand.BusinessMgr.Net.Standard.Base.App;
|
|
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
|
|
using Sarand.BusinessMgr.Net.Standard.Base.Core;
|
|
using Sarand.Net.Standard.Base.App;
|
|
using Sarand.Net.Standard.Base.Bus;
|
|
using Sarand.Net.Standard.Base.Core;
|
|
using Sarand.Net.Standard.Base.Dbas;
|
|
using Sarand.Net.Standard.Base.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
namespace Sarand.BusinessMgr.Net.Standard.Base.Mgr
|
|
{
|
|
//=======================================================================
|
|
// Stock manager - Allocations.
|
|
//=======================================================================
|
|
partial class SbmStockMgr
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// Definitions.
|
|
|
|
// Stock movement data.
|
|
// Used to plan and store movements to represent allocation.
|
|
private class StockMovementData
|
|
{
|
|
public StockMovementData()
|
|
{
|
|
}
|
|
public StockMovementData(DateTime movementDate, decimal quantity, bool actioned, SysId locationIdFrom, SysId locationIdTo)
|
|
{
|
|
this.MovementDate = movementDate;
|
|
this.Quantity = quantity;
|
|
this.Actioned = actioned;
|
|
this.LocationIdFrom = locationIdFrom;
|
|
this.LocationIdTo = locationIdTo;
|
|
}
|
|
// Properties.
|
|
public DateTime MovementDate { get; set; }
|
|
public decimal Quantity { get; set; }
|
|
public bool Actioned { get; set; }
|
|
public SysId LocationIdFrom { get; set; }
|
|
public SysId LocationIdTo { get; set; }
|
|
public String Notes { get; set; }
|
|
// Utilities.
|
|
public static decimal GetQuantityTotal(IEnumerable<StockMovementData> values)
|
|
{
|
|
return Math.Max(0.0m, values.Sum(item => item.Quantity));
|
|
}
|
|
};
|
|
|
|
//-------------------------------------------------------------------
|
|
// Allocations.
|
|
//-------------------------------------------------------------------
|
|
public bool AllowStockAllocations(SbmTransaction transaction)
|
|
{
|
|
// Transaction must exist:
|
|
if (transaction == null || !SysId.IsValid(transaction.Id)) return false;
|
|
// Commodity Stock must be allowed:
|
|
if (StockControlConfig.AllowStockControl()) return false;
|
|
if (AllowConfigOption(Sbm.ConfigOption.Commodities)) return false;
|
|
// Transaction must be a sales or purchase order:
|
|
if (transaction.TransactionType != Sbm.TransactionType.SalesOrder && transaction.TransactionType != Sbm.TransactionType.PurchaseOrder) return false;
|
|
// Transaction's allocations must not be managed by accounts system.
|
|
if (transaction.TransactionType != Sbm.TransactionType.SalesOrder && AccountsDfn.IsAllocationManagedByAccounts(Sbm.TransactionType.SalesOrder)) return false;
|
|
if (transaction.TransactionType != Sbm.TransactionType.PurchaseOrder && AccountsDfn.IsAllocationManagedByAccounts(Sbm.TransactionType.PurchaseOrder)) return false;
|
|
return true;
|
|
}
|
|
|
|
public bool AutoAllocateTransaction(SbmTransaction transaction)
|
|
{
|
|
if (!AllowStockAllocations(transaction)) return false;
|
|
if (transaction.GetStockAllocationStatus() != Sbm.ItemStockAllocationStatus.None) return false; // Already allocated.
|
|
bool bAutoAllocateTrans = false;
|
|
SaStatus eStatusTrans = transaction.Status;
|
|
SaStatus eStatusAutoAllocate = StockControlConfig.GetAutomaticStockAllocationStatus(transaction.TransactionType);
|
|
if (!bAutoAllocateTrans)
|
|
bAutoAllocateTrans = (eStatusAutoAllocate != SaStatus.Unknown && eStatusTrans == eStatusAutoAllocate);
|
|
if (!bAutoAllocateTrans)
|
|
bAutoAllocateTrans = (eStatusAutoAllocate == SaStatus.Preparation && (eStatusTrans == SaStatus.InProgress || eStatusTrans == SaStatus.Completed));
|
|
if (!bAutoAllocateTrans)
|
|
bAutoAllocateTrans = (eStatusAutoAllocate == SaStatus.InProgress && eStatusTrans == SaStatus.Completed);
|
|
return bAutoAllocateTrans;
|
|
}
|
|
|
|
public bool IsStockControlAllowed(SbmTransaction transaction, SbmRequirement requirement)
|
|
{
|
|
// Stock allocations allowed, commodity is stock type.
|
|
if (!AllowStockAllocations(transaction)) return false;
|
|
if (requirement == null || !SysId.IsValid(requirement.CommodityId)) return false;
|
|
// Determine whether stock should be managed for this item:
|
|
Sbm.CommodityType commodityType = CommodityMgr.GetTypeForCommodityId(requirement.CommodityId);
|
|
if (!IsStockManagedFor(commodityType)) return false;
|
|
return true;
|
|
}
|
|
|
|
public bool IsStockControlComplete(SbmTransaction transaction, SbmRequirement requirement)
|
|
{
|
|
// Either item completed.
|
|
if (transaction != null && transaction.Status == SaStatus.Completed) return true;
|
|
if (requirement != null && requirement.Status == SaStatus.Completed) return true;
|
|
return false;
|
|
}
|
|
|
|
public bool IsStockControlCancelled(SbmTransaction transaction, SbmRequirement requirement)
|
|
{
|
|
// Either item cancelled.
|
|
if (transaction == null || transaction.Status == SaStatus.Cancelled) return true;
|
|
if (requirement == null || requirement.Status == SaStatus.Cancelled) return true;
|
|
if (!SysId.IsValid(requirement.CommodityId)) return true; // Any previous allocation will be cancelled.
|
|
return false;
|
|
}
|
|
|
|
public bool AllocateStockByProductionQuantity(SbmTransaction transaction)
|
|
{
|
|
// For sales orders if stock is allocated and production quantity used.
|
|
if (!AllowStockAllocations(transaction)) return false;
|
|
if (!transaction.IsSalesType()) return false;
|
|
if (!StockControlConfig.AllocateByProductionQuantity()) return false;
|
|
return true;
|
|
}
|
|
|
|
public bool ActionStockByDispatchedQuantity(SbmTransaction transaction)
|
|
{
|
|
// For sales orders all completed dispatched items are actioned movements.
|
|
if (!AllowStockAllocations(transaction)) return false;
|
|
if (!transaction.IsSalesType()) return false;
|
|
if (!StockControlConfig.ActionByDispatchedQuantity()) return false;
|
|
return true;
|
|
}
|
|
|
|
public decimal GetAllocationForSourceItem(SbmItemId itemId)
|
|
{
|
|
ClearErrorState();
|
|
decimal allocatedQuantity = 0.0m;
|
|
SbmMovement.Params prmMovement = new SbmMovement.Params();
|
|
prmMovement.ItemTypeSource = (Sbm.ItemType)itemId.Type;
|
|
prmMovement.ItemIdsSource.Add(itemId.Id);
|
|
foreach (SbmMovement movement in GetMovementList(prmMovement))
|
|
{
|
|
if (movement.Status == SaStatus.Awaiting || movement.Status == SaStatus.Actioned)
|
|
allocatedQuantity += movement.Quantity;
|
|
}
|
|
return allocatedQuantity;
|
|
}
|
|
|
|
//===========================================================================
|
|
// T r a n s a c t i o n A l l o c a t i o n
|
|
//===========================================================================
|
|
public bool AllocateTransaction(SbmTransaction transaction, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (transaction == null || !SysId.IsValid(transaction.Id))
|
|
return SetErrorState("Existing transaction must be supplied");
|
|
#if DEBUG
|
|
SaLogger.Debug($"{SysUtil.Fn} ### Allocating transaction ref {transaction.Ref} [Id={SysId.ToLong(transaction.Id)}]");
|
|
#endif
|
|
List<SbmRequirement> requirements = TransactionMgr.GetRequirementsForTransaction(transaction.Id);
|
|
RefreshAllocations(transaction, requirements, true, dbUpdateInfo);
|
|
return OK;
|
|
}
|
|
|
|
public bool UnAllocateTransaction(SbmTransaction transaction, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (transaction == null || !SysId.IsValid(transaction.Id))
|
|
return SetErrorState("Existing transaction must be supplied");
|
|
#if DEBUG
|
|
SaLogger.Debug($"{SysUtil.Fn} ### Unallocating transaction ref {transaction.Ref} [Id={SysId.ToLong(transaction.Id)}]");
|
|
#endif
|
|
// Clear transaction movements:
|
|
ClearMovementsForSourceItem(Sbm.ItemType.Transaction, transaction.Id);
|
|
// Clear requirement movements:
|
|
List<SbmRequirement> requirements = TransactionMgr.GetRequirementsForTransaction(transaction.Id);
|
|
foreach (SbmRequirement requirement in requirements)
|
|
{
|
|
ClearMovementsForSourceItem(Sbm.ItemType.Requirement, requirement.Id);
|
|
requirement.ClearAllocationStatus();
|
|
}
|
|
TransactionMgr.UpdateRequirements(transaction, requirements, dbUpdateInfo);
|
|
// Update the transaction:
|
|
transaction.ClearAllocationStatus();
|
|
TransactionMgr.UpdateTransaction(transaction, dbUpdateInfo);
|
|
return OK;
|
|
}
|
|
|
|
//===========================================================================
|
|
// A l l o c a t e S t o c k
|
|
//===========================================================================
|
|
public bool AllocateStock(SbmTransaction transaction, SbmRequirement requirement, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (transaction == null || !SysId.IsValid(transaction.Id) || requirement == null || !SysId.IsValid(requirement.Id)) return SetErrorState("Transaction and requirement must be supplied");
|
|
|
|
//-----------------------------------------------------------------------
|
|
if (AllowStockAllocations(transaction))
|
|
{
|
|
// This transaction can have stock allocated.
|
|
// Transactions may need to be auto-allocated under some circumstances:
|
|
if (AutoAllocateTransaction(transaction))
|
|
{
|
|
AllocateTransaction(transaction, dbUpdateInfo);
|
|
}
|
|
// Allocate stock against item:
|
|
if (transaction.GetStockAllocationStatus() == Sbm.ItemStockAllocationStatus.Full)
|
|
requirement.FullAllocation = true;
|
|
if (RefreshAllocation(transaction, requirement, false, dbUpdateInfo))
|
|
{
|
|
#if DEBUG
|
|
SaLogger.Debug($"{SysUtil.Fn} ### Allocating stock for '{requirement.ProductCode}' [Id={SysId.ToLong(requirement.Id)}] on transaction ref {transaction.Ref} [Id={SysId.ToLong(transaction.Id)}]");
|
|
#endif
|
|
// Shouldn't need to update transaction as its allocation status should not have changed.
|
|
}
|
|
}
|
|
//-----------------------------------------------------------------------
|
|
else
|
|
{
|
|
// Stock is not allocated directly against this item.
|
|
// Check if allocation of associated transactions is affected.
|
|
}
|
|
//-----------------------------------------------------------------------
|
|
|
|
return OK;
|
|
}
|
|
|
|
//===========================================================================
|
|
// R e f r e s h A l l o c a t i o n s
|
|
//===========================================================================
|
|
public bool RefreshAllocations(SbmTransaction transaction, List<SbmRequirement> requirements, bool fullAllocation, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (!AllowStockAllocations(transaction)) return true;
|
|
|
|
bool autoAllocateTrans = (fullAllocation || AutoAllocateTransaction(transaction));
|
|
bool transFullyAllocated = transaction.FullAllocation || requirements.IsFullAllocation();
|
|
bool transPartiallyAllocated = transaction.PartialAllocation || requirements.IsPartialAllocation();
|
|
if (!autoAllocateTrans && !transFullyAllocated && !transPartiallyAllocated) return true;
|
|
|
|
// Initialise location information:
|
|
if (!StockDatabaseIsValid()) return true;
|
|
if (!SysId.IsValid(LocationIdDefaultSales)) SetErrorState("Default sales location not specified");
|
|
|
|
// Identify the stock location for this transaction.
|
|
bool bTransIntoStock = transaction.IsPurchaseType();
|
|
HashSet<SysId> commodityIdsToCalculate = new HashSet<SysId>();
|
|
SysId locationIdStockTransaction = transaction.LocationIdStock;
|
|
if (!SysId.IsValid(locationIdStockTransaction)) locationIdStockTransaction = transaction.IsSalesType() ? LocationIdDefaultSales : LocationIdDefaultPurchase;
|
|
|
|
if (!HasErrorState())
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// Verify Stock Movements that are directly associated with the transaction:
|
|
SbmMovement.Params prmMovement = new SbmMovement.Params();
|
|
prmMovement.ItemType = Sbm.ItemType.Commodity;
|
|
prmMovement.ItemTypeSource = Sbm.ItemType.Transaction;
|
|
prmMovement.ItemIdsSource.Add(transaction.Id);
|
|
foreach (SbmMovement movement in GetMovementList(prmMovement))
|
|
{
|
|
// Set the location specified by the transaction:
|
|
if (bTransIntoStock)
|
|
{
|
|
if (SysId.IsValid(movement.LocationIdTo) && !movement.LocationIdTo.Equals(locationIdStockTransaction))
|
|
movement.LocationIdTo = locationIdStockTransaction;
|
|
}
|
|
else
|
|
{
|
|
if (SysId.IsValid(movement.LocationIdFrom) && movement.LocationIdFrom.Equals(locationIdStockTransaction))
|
|
movement.LocationIdFrom = locationIdStockTransaction;
|
|
}
|
|
// Set the status:
|
|
if (transaction.TransactionStatusCat == SbmTransaction.StatusCat.Open)
|
|
movement.Status = SaStatus.Awaiting;
|
|
else if (transaction.Status == SaStatus.Completed)
|
|
movement.Status = SaStatus.Actioned;
|
|
else if (transaction.Status == SaStatus.Cancelled)
|
|
movement.Status = SaStatus.Cancelled;
|
|
if (movement.Changed)
|
|
{
|
|
if (UpdateMovement(movement, dbUpdateInfo))
|
|
commodityIdsToCalculate.Add(movement.ItemId);
|
|
else SetErrorState($"Failed to update stock movement - {ErrorStateMessage}");
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Verify stock movements associated with each requirement:
|
|
if (requirements == null)
|
|
requirements = TransactionMgr.GetRequirementsForTransaction(transaction.Id);
|
|
foreach (SbmRequirement requirement in requirements)
|
|
{
|
|
RefreshAllocation(transaction, requirement, fullAllocation, dbUpdateInfo);
|
|
if (HasErrorState()) break;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Update transaction allocation status:
|
|
if (!HasErrorState())
|
|
{
|
|
transaction.ClearAllocationStatus();
|
|
if (requirements.IsFullAllocation()) transaction.FullAllocation = true;
|
|
else if (requirements.IsPartialAllocation()) transaction.PartialAllocation = true;
|
|
TransactionMgr.UpdateTransaction(transaction, dbUpdateInfo);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Recalculate stock levels if required:
|
|
if (!commodityIdsToCalculate.IsEmpty())
|
|
{
|
|
CalculateStockLevels(commodityIdsToCalculate, dbUpdateInfo);
|
|
}
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
//===========================================================================
|
|
// R e f r e s h A l l o c a t i o n
|
|
//===========================================================================
|
|
public bool RefreshAllocation(SbmTransaction transaction, SbmRequirement requirement, bool fullAllocation, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (!AllowStockAllocations(transaction)) return true;
|
|
if (requirement == null || !SysId.IsValid(requirement.Id)) return SetErrorState("Invalid requirement specified.");
|
|
|
|
// Requirement allocation status:
|
|
// Check whether stock allocation is automatic:
|
|
bool autoAllocateTrans = (fullAllocation || AutoAllocateTransaction(transaction));
|
|
bool reqntFullyAllocated = (requirement.FullAllocation || autoAllocateTrans);
|
|
|
|
// Identify default stock location for the transaction:
|
|
SysId locationIdStockTransaction = transaction.LocationIdStock;
|
|
if (!SysId.IsValid(locationIdStockTransaction)) locationIdStockTransaction = transaction.IsSalesType() ? LocationIdDefaultSales : LocationIdDefaultPurchase;
|
|
|
|
// Identify the stock location for this requirement:
|
|
SysId locationIdStockRequirement = requirement.LocationIdStock;
|
|
if (!SysId.IsValid(locationIdStockRequirement)) locationIdStockRequirement = locationIdStockTransaction;
|
|
|
|
// Calculate required movements:
|
|
List<StockMovementData> lMovement = GetRequiredMovements(transaction, requirement, locationIdStockRequirement, reqntFullyAllocated);
|
|
|
|
// Store the movements:
|
|
if (StoreAllocation(transaction, requirement, lMovement, dbUpdateInfo))
|
|
{
|
|
// Derive requirement's allocation status:
|
|
if (reqntFullyAllocated)
|
|
requirement.FullAllocation = true;
|
|
else
|
|
requirement.PartialAllocation = true;
|
|
// NB 'StoreAllocation' recalculates commodity stock if necessary - doesn't need to be done here.
|
|
}
|
|
|
|
// Update the requirement if necessary (eg. allocation status changed):
|
|
if (requirement.Changed)
|
|
{
|
|
TransactionMgr.UpdateRequirement(requirement, dbUpdateInfo);
|
|
}
|
|
|
|
return OK;
|
|
}
|
|
|
|
//===========================================================================
|
|
// R e q u i r e d M o v e m e n t s
|
|
//===========================================================================
|
|
// Stock Control.
|
|
// 1. Awaiting movement quantity:
|
|
// - None if order completed.
|
|
// - If 'Allocation by production qty (Sales Orders only):
|
|
// - Production quantity into stock.
|
|
// NB Actioned on last dispatch date if qty actioned by dispatch.
|
|
// - Otherwise:
|
|
// - Sales transaction:
|
|
// - Order quantity out of stock.
|
|
// - Purchase transaction:
|
|
// - Order quantity into stock.
|
|
// 2. Actioned movement quantity
|
|
// - If 'Actioned by dispatched quantity':
|
|
// - Actioned movements for each completed dispatched quantity (Sales Orders only) by dispatch completion date.
|
|
// - Otherwise, if order completed:
|
|
// - Actioned movement for order quantoty by order completion date.
|
|
// - In either case, if order completed and 'Allocation by production qty:
|
|
// - Additional actioned movement for production quantity into stock
|
|
//---------------------------------------------------------------------------
|
|
private List<StockMovementData> GetRequiredMovements(SbmTransaction transaction, SbmRequirement requirement, SysId locationId, bool fullyAllocate)
|
|
{
|
|
List<StockMovementData> movements = new List<StockMovementData>();
|
|
if (transaction == null || requirement == null) return movements;
|
|
if (!IsStockControlAllowed(transaction, requirement) || IsStockControlCancelled(transaction, requirement)) return movements;
|
|
|
|
// Stock location.
|
|
SysId locationIdStock = locationId;
|
|
if (!SysId.IsValid(locationIdStock))
|
|
locationIdStock = (transaction.IsPurchaseType() ? LocationIdDefaultPurchase : LocationIdDefaultSales);
|
|
if (!SysId.IsValid(locationIdStock)) return movements;
|
|
|
|
// Stock Control configuration:
|
|
bool movementIntoStock = transaction.IsPurchaseType();
|
|
bool stockControlComplete = IsStockControlComplete(transaction, requirement);
|
|
bool allocateByProductionQty = AllocateStockByProductionQuantity(transaction);
|
|
bool actionByDispatchedQuantity = ActionStockByDispatchedQuantity(transaction);
|
|
|
|
// Production quantity.
|
|
decimal quantityProduction = requirement.QuantityProd;
|
|
|
|
// Action movements for completed dispatched items:
|
|
decimal quantityActionedDispatchedTotal = 0.0m;
|
|
DateTime finalDispatchDate = DateTime.MinValue;
|
|
if (actionByDispatchedQuantity)
|
|
{
|
|
// Stock is 'actioned' by quantities on associated completed sales dispatch items.
|
|
SbmRequirement.Params prmRequirement = new SbmRequirement.Params();
|
|
prmRequirement.TransactionType = Sbm.TransactionType.SalesDispatch;
|
|
prmRequirement.RequirementIdsSource.Add(requirement.Id);
|
|
prmRequirement.TransactionStatusCat = SbmTransaction.StatusCat.Completed;
|
|
prmRequirement.StatusCat = SbmRequirement.StatusCat.NotCancelled;
|
|
// Define columns to retrieve:
|
|
ColumnDfn colTransRef = TransactionMgr.GetRequirementColumnDfn(SbmRequirement.Column.Trans_Ref);
|
|
ColumnDfn colTransClosed = TransactionMgr.GetRequirementColumnDfn(SbmRequirement.Column.Trans_Closed);
|
|
ColumnDfn colReqntQty = TransactionMgr.GetRequirementColumnDfn(SbmRequirement.Column.Quantity);
|
|
List<ColumnDfn> queryColumns = new List<ColumnDfn>() { colTransRef, colTransClosed, colReqntQty };
|
|
// Sort columns.
|
|
prmRequirement.SortColumns.Add(colTransClosed);
|
|
// Query dispatch items.
|
|
using (DbResultSet resultSet = TransactionMgr.ExecuteRequirementQuery(queryColumns, prmRequirement))
|
|
{
|
|
while (resultSet != null && resultSet.HasRows && resultSet.NextRecord())
|
|
{
|
|
NamedValue.Map values = resultSet.GetRecordValues();
|
|
// Create actioned movement for this completed dispatched quantity.
|
|
StockMovementData stockMovementData = new StockMovementData()
|
|
{
|
|
MovementDate = values.GetDate(colTransClosed.DataName),
|
|
Quantity = values.GetDecimal(colReqntQty.DataName),
|
|
LocationIdFrom = locationIdStock,
|
|
Actioned = true,
|
|
Notes = "Dispatch ref. " + values.GetString(colTransRef.DataName)
|
|
};
|
|
movements.Add(stockMovementData);
|
|
// Update totals.
|
|
quantityActionedDispatchedTotal += stockMovementData.Quantity;
|
|
if (!DateUtil.IsValid(finalDispatchDate) || finalDispatchDate < stockMovementData.MovementDate) finalDispatchDate = stockMovementData.MovementDate;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Derive stock allocation date from transaction and requirement.
|
|
// If the stock is actioned by dispatched qty and at least one completed
|
|
// dispatch was found, use the latest dispatch date.
|
|
DateTime stockAllocationDate = transaction.GetStockAllocationDate(requirement);
|
|
if (stockControlComplete && DateUtil.IsValid(finalDispatchDate))
|
|
stockAllocationDate = finalDispatchDate;
|
|
|
|
// Additional movements.
|
|
if (!allocateByProductionQty)
|
|
{
|
|
// Movement for order quantity (reduced by actioned dispatched quantity).
|
|
decimal fQtyMovement = 0.0m;
|
|
if (fullyAllocate)
|
|
fQtyMovement = requirement.Quantity;
|
|
else
|
|
// Requirement is not fully allocated - allocated quantity remains as it is already.
|
|
fQtyMovement = GetAllocationForSourceItem(requirement.GetItemId());
|
|
fQtyMovement -= quantityActionedDispatchedTotal;
|
|
if (fQtyMovement > 0.01m)
|
|
{
|
|
StockMovementData stockMovementData = new StockMovementData()
|
|
{
|
|
MovementDate = stockAllocationDate,
|
|
Quantity = fQtyMovement,
|
|
Actioned = stockControlComplete,
|
|
Notes = "Order ref. " + transaction.Ref
|
|
};
|
|
if (movementIntoStock)
|
|
stockMovementData.LocationIdTo = locationIdStock;
|
|
else stockMovementData.LocationIdFrom = locationIdStock;
|
|
movements.Add(stockMovementData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Movement for remaining production quantity.
|
|
if (quantityProduction > 0.01m)
|
|
{
|
|
StockMovementData stockMovementData = new StockMovementData()
|
|
{
|
|
MovementDate = stockAllocationDate,
|
|
Quantity = quantityProduction,
|
|
LocationIdTo = locationIdStock,
|
|
Actioned = stockControlComplete,
|
|
Notes = "Production qty"
|
|
};
|
|
movements.Add(stockMovementData);
|
|
}
|
|
}
|
|
return movements;
|
|
}
|
|
|
|
//===========================================================================
|
|
// S t o r e A l l o c a t i o n
|
|
//===========================================================================
|
|
// Public version.
|
|
// Single stock location specified and rules determine whether movement is into or out of stock.
|
|
// Only used by Sage Import?
|
|
private bool StoreAllocation(SbmTransaction transaction, SbmRequirement requirement, decimal quantityAwaiting, decimal quantityActioned, SysId locationIdStockPreferred, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (transaction == null || requirement == null) return SetErrorState("Item not specified for stock allocation.");
|
|
// Delivered quantity cannot be negative:
|
|
if (quantityActioned < 0.0m)
|
|
{
|
|
quantityActioned = 0.0m;
|
|
#if DEBUG
|
|
SaLogger.Debug($"{SysUtil.Fn} ### Negative delivered quantity specified.");
|
|
#endif
|
|
}
|
|
|
|
// Stock location.
|
|
SysId locationIdStockFrom = new SysId();
|
|
SysId locationIdStockTo = new SysId();
|
|
PopulateDefaultLocations();
|
|
SysId locationIdStock = locationIdStockPreferred;
|
|
if (!SysId.IsValid(locationIdStock))
|
|
locationIdStock = transaction.IsPurchaseType() ? LocationIdDefaultPurchase : LocationIdDefaultSales;
|
|
if (SysId.IsValid(locationIdStock))
|
|
{
|
|
bool bIntoStock = transaction.IsPurchaseType();
|
|
if (quantityAwaiting < 0)
|
|
{
|
|
bIntoStock = !bIntoStock;
|
|
quantityAwaiting *= -1;
|
|
}
|
|
if (bIntoStock)
|
|
locationIdStockTo = locationIdStock;
|
|
else locationIdStockFrom = locationIdStock;
|
|
}
|
|
else return SetErrorState("Invalid stock location specified.");
|
|
|
|
// Calculate the required movements:
|
|
List<StockMovementData> movements = new List<StockMovementData>();
|
|
if (quantityActioned > 0.01m)
|
|
{
|
|
// Actioned quantity movement:
|
|
StockMovementData stockMovementData = new StockMovementData()
|
|
{
|
|
MovementDate = transaction.GetStockAllocationDate(requirement),
|
|
Quantity = quantityActioned,
|
|
LocationIdFrom = locationIdStockFrom,
|
|
LocationIdTo = locationIdStockTo,
|
|
Actioned = true,
|
|
Notes = ""
|
|
};
|
|
movements.Add(stockMovementData);
|
|
}
|
|
if (quantityAwaiting > 0.01m)
|
|
{
|
|
// Awaiting quantity movement:
|
|
StockMovementData stockMovementData = new StockMovementData()
|
|
{
|
|
MovementDate = transaction.GetStockAllocationDate(requirement),
|
|
Quantity = quantityAwaiting,
|
|
LocationIdFrom = locationIdStockFrom,
|
|
LocationIdTo = locationIdStockTo,
|
|
Actioned = false,
|
|
Notes = ""
|
|
};
|
|
movements.Add(stockMovementData);
|
|
}
|
|
return StoreAllocation(transaction, requirement, movements, dbUpdateInfo);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// Internal version.
|
|
// To or From stock location is specified as direction of movement has been pre-determined.
|
|
private bool StoreAllocation(SbmTransaction transaction, SbmRequirement requirement, List<StockMovementData> requiredMovements, DbUpdateInfo dbUpdateInfo)
|
|
{
|
|
ClearErrorState();
|
|
if (transaction == null || requirement == null) return SetErrorState("Item not specified for stock allocation.");
|
|
if (!AllowStockAllocations(transaction)) return true;
|
|
|
|
// Commodity list - those that need recalculation once the movements have been updated:
|
|
List<SysId> commodityIds = new List<SysId>();
|
|
commodityIds.AddId(requirement.CommodityId);
|
|
bool movementsUpdated = false;
|
|
|
|
// Movement type:
|
|
Sbm.MovementType movementTypeAllocation = transaction.IsPurchaseType() ? Sbm.MovementType.PurchaseAllocation : Sbm.MovementType.SalesAllocation;
|
|
|
|
if (!AllocateStockByProductionQuantity(transaction) && !ActionStockByDispatchedQuantity(transaction) && requiredMovements.Count > 1)
|
|
{
|
|
#if DEBUG
|
|
SaLogger.Debug($"{SysUtil.Fn} ### More than 1 required movement specified for 'Standard' stock control.");
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------
|
|
// Retrieve stock movements for this requirement:
|
|
List<SbmMovement> movements = new List<SbmMovement>();
|
|
SbmMovement.Params prmMovement = new SbmMovement.Params();
|
|
prmMovement.ItemType = Sbm.ItemType.Commodity;
|
|
prmMovement.ItemTypeSource = Sbm.ItemType.Requirement;
|
|
prmMovement.ItemIdsSource.Add(requirement.Id);
|
|
foreach (SbmMovement stockMovement in GetMovementList(prmMovement))
|
|
{
|
|
if (SysId.IsValid(requirement.CommodityId) && stockMovement.ItemType == Sbm.ItemType.Commodity && stockMovement.ItemId == requirement.CommodityId)
|
|
{
|
|
// Movement is for correct commodity:
|
|
movements.Add(stockMovement);
|
|
}
|
|
else
|
|
{
|
|
if (stockMovement.ItemType == Sbm.ItemType.Commodity && !commodityIds.Contains(stockMovement.ItemId))
|
|
commodityIds.Add(stockMovement.ItemId);
|
|
// Movement no longer required:
|
|
DeleteMovement(stockMovement, dbUpdateInfo);
|
|
}
|
|
}
|
|
movements.Sort(SbmMovementList.SortMode.MovementSortAscend);
|
|
|
|
//---------------------------------------------------------------
|
|
// Store the required movements:
|
|
foreach (StockMovementData movementRequired in requiredMovements)
|
|
{
|
|
// Reuse existing movement if available.
|
|
SbmMovement movementStock = null;
|
|
if (movements.Any())
|
|
{
|
|
movementStock = movements.ElementAt(0);
|
|
movements.RemoveAt(0);
|
|
}
|
|
// Create new movement if none available:
|
|
if (movementStock == null)
|
|
movementStock = CreateMovement(Sbm.ItemType.Commodity, requirement.CommodityId, movementTypeAllocation);
|
|
if (movementStock != null)
|
|
{
|
|
movementStock.Movement = movementRequired.MovementDate;
|
|
movementStock.Quantity = movementRequired.Quantity;
|
|
movementStock.LocationIdFrom = movementRequired.LocationIdFrom;
|
|
movementStock.LocationIdTo = movementRequired.LocationIdTo;
|
|
movementStock.ItemTypeSource = Sbm.ItemType.Requirement;
|
|
movementStock.ItemIdSource = requirement.Id;
|
|
if (!StringUtil.IsBlank(movementRequired.Notes))
|
|
movementStock.Notes = movementRequired.Notes;
|
|
movementStock.Status = movementRequired.Actioned ? SaStatus.Actioned : SaStatus.Awaiting;
|
|
if (SysId.IsValid(movementStock.Id))
|
|
{
|
|
// Update existing movement:
|
|
if (movementStock.Changed)
|
|
{
|
|
if (UpdateMovement(movementStock, dbUpdateInfo))
|
|
movementsUpdated = true;
|
|
else SetErrorState("Failed to update movement - " + ErrorStateMessage);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (InsertMovement(movementStock, dbUpdateInfo))
|
|
movementsUpdated = true;
|
|
else SetErrorState("Failed to add movement - " + ErrorStateMessage);
|
|
}
|
|
}
|
|
else SetErrorState("Failed to create movement - " + ErrorStateMessage);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Remove any remaining movements:
|
|
foreach (SbmMovement movement in movements)
|
|
{
|
|
DeleteMovement(movement, dbUpdateInfo);
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
// Calculate the current stock take for this commodity (and any others touched above):
|
|
if (!HasErrorState() && movementsUpdated)
|
|
{
|
|
CalculateStockLevels(commodityIds, dbUpdateInfo);
|
|
}
|
|
return OK;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|