public async Task <OperationResponse> IsValidForSubmittion(Guid costId) { var cost = await _efContext.Cost .Include(c => c.Parent) .ThenInclude(p => p.Agency) .Include(c => c.LatestCostStageRevision) .ThenInclude(r => r.ProductDetails) .FirstOrDefaultAsync(c => c.Id == costId); if (cost == null) { throw new EntityNotFoundException <Cost>(costId); } var productionDetails = _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(cost.LatestCostStageRevision); var isDpv = productionDetails?.DirectPaymentVendor != null; var sapCode = isDpv ? productionDetails.DirectPaymentVendor.SapVendorCode : cost.Parent.Agency.Labels.GetSapVendorCode(); var isValid = !string.IsNullOrWhiteSpace(sapCode); var errorMessage = isValid ? string.Empty : MissingVendorErrorMessage; return(new OperationResponse(isValid, errorMessage)); }
public async Task <PaymentAmountResult> CalculatePaymentAmount(Guid costStageRevisionId, bool persist = true) { var revision = await _efContext.CostStageRevision .Include(csr => csr.CostFormDetails) .Include(r => r.CostStage).ThenInclude(cs => cs.Cost) .Include(r => r.CostStage).ThenInclude(cs => cs.CostStageRevisions) .Include(r => r.StageDetails) .Include(r => r.ProductDetails) .Include(r => r.CostStageRevisionPaymentTotals) //.AsNoTracking() .FirstOrDefaultAsync(csr => csr.Id == costStageRevisionId); var costStage = revision.CostStage; var cost = costStage.Cost; var stageDetailsForm = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(revision); var productionDetailsForm = _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(revision); //ADC-2690 revises paymentCostTotal calculation for re-opened Final Actual stage var previousPaymentCalculations = new List <CostStageRevisionPaymentTotal>(); //get the latest costtotal from last approved final actual var previousPaymentCostTotal = new CostStageRevisionPaymentTotal(); //flag to determine if this calculation is for the first FA or subsequent FAs bool isFirstFA = true; //check if the current cost has any Final Actual stage approved var approvedFinalActualStage = cost.CostStages.Find(x => x.Key == CostStages.FinalActual.ToString())?.CostStageRevisions.Find(a => a.Status == CostStageRevisionStatus.Approved); //if there is no final actual stage approve, then keep the current calculation as is, which is working correctly. if (approvedFinalActualStage == null) { previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotals(cost.Id, revision.CostStage.Id); } else { //here is the area we do the calculation for re-opened FAs //Get All Cost Payment Totals for the current Final Actual stage previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotalsFinalActual(cost.Id, revision.CostStage.Id); //extract values of CostTotal rowns of approved FA and order by calculated datetime var previousPaymentCostTotals = previousPaymentCalculations.Where(x => x.LineItemTotalType == Constants.CostSection.CostTotal && x.CostStageRevision.Status == CostStageRevisionStatus.Approved) .OrderBy(x => x.CalculatedAt).ToList(); //check if there is at least 1 approved FA if (previousPaymentCalculations.Any() && previousPaymentCostTotals.Any()) { //if there is an approved FA, it means there is an inprogress FA, and we shall need to get the last FA for subtraction later: Grand total at Final actual -II minus Grand total in Final actual -I previousPaymentCostTotal = previousPaymentCostTotals[previousPaymentCostTotals.Count() - 1]; //flag up this is not the first FA isFirstFA = false; } else { //otherwise, keep the calculation as is previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotals(cost.Id, revision.CostStage.Id); } } var costLineItems = await _costStageRevisionService.GetCostLineItems(costStageRevisionId); var totals = _pgTotalsBuilder.Build(stageDetailsForm, costLineItems, costStage.Key); var previousPaymentTotals = _pgTotalPaymentsBuilder.Build(previousPaymentCalculations); // these are totals of remaining balance //changed for ADC-2690 var totalRemainingPayment = new PgPaymentRule() { StageTotals = totals, BudgetRegion = stageDetailsForm.BudgetRegion?.Key, ContentType = stageDetailsForm.ContentType?.Key, CostType = cost.CostType.ToString(), CostStages = costStage.Key, ProductionType = Constants.ProductionType.ProductionTypeList.FirstOrDefault(a => a == stageDetailsForm.ProductionType?.Key), DirectPaymentVendorId = productionDetailsForm.DirectPaymentVendor?.Id, IsAIPE = stageDetailsForm.IsAIPE, // we need this to match with the rules' TotalCostAmount field TotalCostAmount = totals.TotalCostAmountTotal, // this is for detailed split InsuranceCost = totals.InsuranceCostTotal - previousPaymentTotals.InsuranceCostPayments, TechnicalFeeCost = totals.TechnicalFeeCostTotal - previousPaymentTotals.TechnicalFeeCostPayments, TalentFeeCost = totals.TalentFeeCostTotal - previousPaymentTotals.TalentFeeCostPayments, PostProductionCost = totals.PostProductionCostTotal - previousPaymentTotals.PostProductionCostPayments, ProductionCost = totals.ProductionCostTotal - previousPaymentTotals.ProductionCostPayments, OtherCost = totals.OtherCostTotal - previousPaymentTotals.OtherCostPayments, TargetBudgetTotalCost = totals.TargetBudgetTotal - previousPaymentTotals.TargetBudgetTotalCostPayments, CostCarryOverAmount = previousPaymentTotals.CarryOverAmount }; //check if this is not the calculation for the first FA then do the subtraction: Grand total at Final actual -II minus Grand total in Final actual -I //if not keep as is if (!isFirstFA) { //if this is not the first FA, it means we would have to calculated TotalCost AKA CostTotal equal Grand total at Final actual -II minus Grand total in Final actual -I totalRemainingPayment.TotalCost = totals.TotalCostAmountTotal - previousPaymentCostTotal.LineItemFullCost; } else { // we use this to calculate the outstanding balance where there is no detailed split totalRemainingPayment.TotalCost = totals.TotalCostAmountTotal - previousPaymentTotals.TotalCostPayments; } _logger.Information($"Calculating payment amount for cost: {cost.CostNumber} at stage: {costStage.Key} revision: {revision.Id}"); // these are actual payment splits (percentages of totals) var paymentAmount = await GetPaymentAmount(totalRemainingPayment, previousPaymentTotals, totals, productionDetailsForm.DirectPaymentVendor?.ProductionCategory); if (paymentAmount != null) { if (!persist) { return(paymentAmount); } var totalRemainingAmount = (PgPaymentRule)paymentAmount.TotalRemainingPayment; var nextStages = await _stageService.GetAllUpcomingStages(costStage.Key, BuType.Pg, cost.Id); if (nextStages != null) { paymentAmount.ProjectedPayments = GetNextStagesPaymentAmounts(paymentAmount, totalRemainingPayment, nextStages); } var alreadySaved = await _costStageRevisionService.GetCostStageRevisionPaymentTotals(revision); if (alreadySaved == null || !alreadySaved.Any()) { await SaveTotals(costStageRevisionId, paymentAmount, totalRemainingAmount, isFirstFA); foreach (var projectedPayment in paymentAmount.ProjectedPayments) { await SaveTotals(costStageRevisionId, projectedPayment, totalRemainingAmount, isFirstFA); } } // UserIdentity in the parameters of below method is used only to log an activity when IO number gets changed. // Therefore we can pass any not null object here await _customObjectDataService.Save(costStageRevisionId, CustomObjectDataKeys.PgPaymentRuleInput, totalRemainingPayment, new UserIdentity()); await _customObjectDataService.Save(costStageRevisionId, CustomObjectDataKeys.PgMatchedPaymentRule, paymentAmount.MatchedPaymentRule, new UserIdentity()); return(paymentAmount); } _logger.Error($"Payment amount NOT calculated for cost: {cost.CostNumber} at stage: {costStage.Key} revision: {revision.Id} using rule: {totalRemainingPayment}!"); return(null); }
public async Task <ServiceResult <Dictionary <string, Guid> > > Update(ExcelCellValueLookup entries, Guid userId, Guid costId, Guid costStageRevisionId) { if (entries == null) { Logger.Warning("Param entries is null reference. This means the uploaded budget form was not read correctly."); return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage)); } if (userId == Guid.Empty) { Logger.Warning("Param userId is Guid.Empty."); return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage)); } if (costId == Guid.Empty) { Logger.Warning("Param costId is Guid.Empty."); return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage)); } if (costStageRevisionId == Guid.Empty) { Logger.Warning("Param costStageRevisionId is Guid.Empty."); return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage)); } if (entries.Count == 0) { Logger.Warning("Param entries is empty collection. This means the uploaded budget form was not read correctly."); return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage)); } var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId); var productionForm = await _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(costStageRevisionId); var costStageRevision = await _efContext.CostStageRevision .Include(csr => csr.CostStage) .ThenInclude(csr => csr.CostStageRevisions) .FirstOrDefaultAsync(c => c.Id == costStageRevisionId); //Update the Agency Currency, if different var agencyCurrencyCode = GetAgencyCurrencyCode(entries); // return a dictionary of the currencies that have been modified i.e. agency, dpv, section. // Key is agency, dpv or section name and value is the currencyId. The FE has all the Ids and can update itself. var currencies = new Dictionary <string, Guid>(); var serviceResult = new ServiceResult <Dictionary <string, Guid> >(currencies); if (stageForm.AgencyCurrency != agencyCurrencyCode) { if (CanUpdateAgencyCurrency(costStageRevision)) { var stageDetails = await _efContext.CustomFormData.FirstOrDefaultAsync(c => c.Id == costStageRevision.StageDetailsId); UpdateAgencyCurrency(stageDetails, agencyCurrencyCode); var agencyCurrencyId = await GetCurrencyId(agencyCurrencyCode); currencies[AgencyCurrencyKey] = agencyCurrencyId; } else { var error = new FeedbackMessage( $"The agency currency you have chosen in the budget form, {agencyCurrencyCode} does not match your datasheet of {stageForm.AgencyCurrency}. There are two options to progress this:"); error.AddSuggestion("You may change the currency of your budget form to match the datasheet currency and re-upload the Budget form."); error.AddSuggestion( "You may cancel this datasheet, create a new cost and select the required agency currency. After cancelling your cost please contact P&G. They will raise and issue credit for any monies you have received against the original PO."); serviceResult.AddError(error); return(serviceResult); } } // The Cost Section Currencies done by the ICostLineItemUpdater because each CostLineItem has a currency. // On the UI, the Cost Section currency is set by rolling up from the first CostLineItem. // Here we extrapolate the currencies for the DPV and the cost line item sections in order to simplify the // front end code. At this present time, the FE calculates the CLI Section currency based on the first item it finds. if (productionForm.DirectPaymentVendor != null) { var dpvCurrencyCode = GetDpvCurrencyCode(entries, stageForm); var dpvCurrencyId = await GetCurrencyId(dpvCurrencyCode); var productionDetails = await _efContext.CustomFormData.FirstOrDefaultAsync(c => c.Id == costStageRevision.ProductDetailsId); UpdateDpvCurrency(productionDetails, dpvCurrencyId); currencies[DirectPaymentVendorCurrencyKey] = dpvCurrencyId; } return(serviceResult); }