This commit is contained in:
2021-05-21 00:01:57 +00:00
parent 96b78856da
commit 0e224d44d3
3 changed files with 247 additions and 191 deletions

View File

@@ -585,16 +585,16 @@ namespace AyaNova.Biz
}
//update pricing
foreach (WorkOrderItem wi in wo.Items)
{
foreach (WorkOrderItemLabor o in wi.Labors)
await LaborSetPrice(o, mContractInEffect);
foreach (WorkOrderItemTravel o in wi.Travels)
TravelSetListPrice(o, mContractInEffect);
foreach (WorkOrderItemPart o in wi.Parts)
PartSetListPrice(o, mContractInEffect);
}
// //update pricing
// foreach (WorkOrderItem wi in wo.Items)
// {
// // foreach (WorkOrderItemLabor o in wi.Labors)
// // await LaborSetPrice(o, mContractInEffect);
// foreach (WorkOrderItemTravel o in wi.Travels)
// TravelSetListPrice(o, mContractInEffect);
// foreach (WorkOrderItemPart o in wi.Parts)
// PartSetListPrice(o, mContractInEffect);
// }
await ct.SaveChangesAsync();
return wo;
@@ -1716,12 +1716,12 @@ namespace AyaNova.Biz
//by default apply all automatic actions with further restrictions possible below
bool ApplyTax = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If taxes haven't change then no need to update taxes
if (newObj.ChargeTaxCodeId == oldObj.ChargeTaxCodeId)
ApplyTax = false;
@@ -1747,7 +1747,7 @@ namespace AyaNova.Biz
newObj.TaxName = t.Name;
}
}
}
}
}
@@ -1879,7 +1879,7 @@ namespace AyaNova.Biz
return null;
else
{
await LaborBizActionsAsync(AyaEvent.Created, newObject, null, null);
// await LaborBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemLabor.AddAsync(newObject);
@@ -1927,7 +1927,7 @@ namespace AyaNova.Biz
await LaborValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await LaborBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
// await LaborBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
@@ -2014,162 +2014,233 @@ namespace AyaNova.Biz
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
ServiceRate Rate = null;
if (o.ServiceRateId != null)
o.ServiceRateViz = await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync();
Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId);
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
if (Tax != null)
o.TaxCodeSaleViz = Tax.Name;
o.PriceViz = 0;
if (Rate != null)
{
o.CostViz = Rate.Cost;
o.ListPriceViz = Rate.Charge;
o.ChargeUnitViz = Rate.Unit;
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.ManualPrice != null)
o.PriceViz = (decimal)o.ManualPrice;
else
{
//not manual so could potentially have a contract adjustment
var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged service rate contract discount, that takes precedence
if (c.ContractServiceRateOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count))
if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
{
if (csr.OverridePct != 0)
{
pct = csr.OverridePct / 100;
cot = csr.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0)
{
pct = c.ServiceRatesOverridePct / 100;
cot = c.ServiceRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = o.CostViz + (o.CostViz * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.ServiceRateQuantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
var netPrice = (o.Price * o.ServiceRateQuantity);
if (o.TaxAPct != 0)
if (Tax != null)
{
o.TaxAViz = netPrice * (o.TaxAPct / 100);
}
if (o.TaxBPct != 0)
{
if (o.TaxOnTax)
if (Tax.TaxAPct != 0)
{
o.TaxBViz = (netPrice + o.TaxAViz) * (o.TaxBPct / 100);
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
else
if (Tax.TaxBPct != 0)
{
o.TaxBViz = netPrice * (o.TaxBPct / 100);
}
}
o.LineTotalViz = netPrice + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task LaborBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLabor newObj, WorkOrderItemLabor oldObj, IDbContextTransaction transaction)
{
//automatic actions on record change, called AFTER validation
//currently no processing required except for created or modified at this time
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//SET TAXES AND PRICING
//by default apply all automatic actions with further restrictions possible below
bool ApplyTax = true;
bool SetPrice = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If it wasn't a service rate or quantity change there is no need to set pricing
if (newObj.ServiceRateId == oldObj.ServiceRateId && newObj.ServiceRateQuantity == oldObj.ServiceRateQuantity)
{
SetPrice = false;
}
//If taxes haven't change then no need to update taxes
if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
ApplyTax = false;
}
//Tax code
if (ApplyTax)
{
//Default in case nothing to apply
newObj.TaxAPct = 0;
newObj.TaxBPct = 0;
newObj.TaxOnTax = false;
newObj.TaxName = "";
if (newObj.TaxCodeSaleId != null)
{
var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
if (t != null)
if (Tax.TaxOnTax)
{
newObj.TaxAPct = t.TaxAPct;
newObj.TaxBPct = t.TaxBPct;
newObj.TaxOnTax = t.TaxOnTax;
newObj.TaxName = t.Name;
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
//Pricing
if (SetPrice)
{
var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
await LaborSetPrice(newObj, Contract);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// SET PER UNIT LIST PRICE
//
//(called by woitemlabor save and also by header save on change of contract)
private async Task LaborSetPrice(WorkOrderItemLabor o, Contract c)
{
//default in case nothing to apply
o.Cost = 0;
o.ListPrice = 0;
o.Price = 0;
//in v7 it was ok to have no service rate selected
//not sure why but carried forward to v8 so..
if (o.ServiceRateId == null)
return;
var Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ServiceRateId);
if (Rate == null)
{
AddError(ApiErrorCode.NOT_FOUND, "generalerror", "Service rate not found");//this should never happen, no point in localizing
return;
}
o.Cost = Rate.Cost;
o.ListPrice = Rate.Charge;
o.Price = o.ListPrice;//default is list price unless a contract overrides it
if (c == null)
return;//No contract so bail out now, it's done
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged service rate contract discount, that takes precedence
if (c.ContractServiceRateOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count))
if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
{
if (csr.OverridePct != 0)
{
var pct = csr.OverridePct / 100;
//found a match, apply the discount and return
if (csr.OverrideType == ContractOverrideType.CostMarkup)
o.Price = o.Cost + (o.Cost * pct);
else if (csr.OverrideType == ContractOverrideType.PriceDiscount)
o.Price = o.ListPrice - (o.ListPrice * pct);
return;
}
}
}
//No tag discounts, so check for a generic one
if (c.ServiceRatesOverridePct == 0)
return;// no generic discount for all items so bail now
{
var pct = c.ServiceRatesOverridePct / 100;
//Contract has a generic override so apply it
if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
o.Price = o.Cost + (o.Cost * pct);
else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
o.Price = o.ListPrice - (o.ListPrice * pct);
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //BIZ ACTIONS
// //
// //
// private async Task LaborBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLabor newObj, WorkOrderItemLabor oldObj, IDbContextTransaction transaction)
// {
// //automatic actions on record change, called AFTER validation
// //currently no processing required except for created or modified at this time
// if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
// return;
// //SET TAXES AND PRICING
// //by default apply all automatic actions with further restrictions possible below
// bool ApplyTax = true;
// bool SetPrice = true;
// //if modifed, see what has changed and should be re-applied
// if (ayaEvent == AyaEvent.Modified)
// {
// //If it wasn't a service rate or quantity change there is no need to set pricing
// if (newObj.ServiceRateId == oldObj.ServiceRateId && newObj.ServiceRateQuantity == oldObj.ServiceRateQuantity)
// {
// SetPrice = false;
// }
// //If taxes haven't change then no need to update taxes
// if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
// ApplyTax = false;
// }
// //Tax code
// if (ApplyTax)
// {
// //Default in case nothing to apply
// newObj.TaxAPct = 0;
// newObj.TaxBPct = 0;
// newObj.TaxOnTax = false;
// newObj.TaxName = "";
// if (newObj.TaxCodeSaleId != null)
// {
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
// if (t != null)
// {
// newObj.TaxAPct = t.TaxAPct;
// newObj.TaxBPct = t.TaxBPct;
// newObj.TaxOnTax = t.TaxOnTax;
// newObj.TaxName = t.Name;
// }
// }
// }
// //Pricing
// if (SetPrice)
// {
// var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
// await LaborSetPrice(newObj, Contract);
// }
// }
// ////////////////////////////////////////////////////////////////////////////////////////////////
// // SET PER UNIT LIST PRICE
// //
// //(called by woitemlabor save and also by header save on change of contract)
// private async Task LaborSetPrice(WorkOrderItemLabor o, Contract c)
// {
// //default in case nothing to apply
// o.Cost = 0;
// o.ListPrice = 0;
// o.Price = 0;
// //in v7 it was ok to have no service rate selected
// //not sure why but carried forward to v8 so..
// if (o.ServiceRateId == null)
// return;
// var Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ServiceRateId);
// if (Rate == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "generalerror", "Service rate not found");//this should never happen, no point in localizing
// return;
// }
// o.Cost = Rate.Cost;
// o.ListPrice = Rate.Charge;
// o.Price = o.ListPrice;//default is list price unless a contract overrides it
// if (c == null)
// return;//No contract so bail out now, it's done
// //POTENTIAL CONTRACT ADJUSTMENTS
// //First check if there is a matching tagged service rate contract discount, that takes precedence
// if (c.ContractServiceRateOverrideItems.Count > 0)
// {
// //Iterate all contract tagged items in order of ones with the most tags first
// foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count))
// if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
// {
// if (csr.OverridePct != 0)
// {
// var pct = csr.OverridePct / 100;
// //found a match, apply the discount and return
// if (csr.OverrideType == ContractOverrideType.CostMarkup)
// o.Price = o.Cost + (o.Cost * pct);
// else if (csr.OverrideType == ContractOverrideType.PriceDiscount)
// o.Price = o.ListPrice - (o.ListPrice * pct);
// return;
// }
// }
// }
// //No tag discounts, so check for a generic one
// if (c.ServiceRatesOverridePct == 0)
// return;// no generic discount for all items so bail now
// {
// var pct = c.ServiceRatesOverridePct / 100;
// //Contract has a generic override so apply it
// if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
// o.Price = o.Cost + (o.Cost * pct);
// else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
// o.Price = o.ListPrice - (o.ListPrice * pct);
// }
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION

View File

@@ -25,43 +25,29 @@ namespace AyaNova.Models
public decimal NoChargeQuantity { get; set; }
public long? ServiceBankId { get; set; }
public long? TaxCodeSaleId { get; set; }
[NotMapped]
public string TaxCodeSaleViz { get; set; }
/*
//from case 3748
- Cost, this is the entered Cost for the item at time of selection
- ListPrice, [REQUIRED] this is the un-discounted per unit price of the item be it labor, parts, loaner, travel etc. What is normally charged at that moment in time for this item if it wasn't discounted.
- Price, [NULLABLE] this is the EFFECTIVE PER UNIT PRICE either contract determined price calculated from ListPrice, otherwise it's a copy of the list price. User with sufficient rights can can override it to enter a different amount which effectively supports manual discount feature in v7 which was by %. This is the *actual* price used in calculations.
- TaxName copied from selected tax code, not directly editable
- TaxAPct copied from selected tax code, not directly editable
- TaxBPct copied from selected tax code, not directly editable
- TaxOnTax copied from selected tax code not directly editable
- TaxAViz, [calculated NOT persisted] (Price*Quantity)+TaxAPct
- TaxBViz, [calculated NOT persisted] (Price*Quantity)+TaxBPct OR if TaxOnTax = TaxAViz+TaxBPct
- LineTotalViz, [calculated NOT persisted] this is effectively Price * quantity + Taxes, handy for reports and viewing not editable as it's not stored
*/
//PRICE FIELDS
[Required]
public decimal Cost { get; set; }
[Required]
public decimal ListPrice { get; set; }
[Required]
public decimal Price { get; set; }
public string TaxName { get; set; }
[Required]
public decimal TaxAPct { get; set; }
[Required]
public decimal TaxBPct { get; set; }
[Required]
public bool TaxOnTax { get; set; }
//Standard pricing fields (mostly to support printed reports though some show in UI)
//some not to be sent with record depending on role (i.e. cost and charge in some cases)
public decimal? ManualPrice { get; set; }//user entered manually overridden price, if null then ignored in calcs otherwise this *is* the price even if zero
[NotMapped]
public decimal TaxAViz { get; set; }
public decimal CostViz { get; set; }//cost from source record (e.g. serviceRate) or zero if no cost entered
[NotMapped]
public decimal TaxBViz { get; set; }
public decimal ListPriceViz { get; set; }//List price from source record (e.g. serviceRate) or zero if no cost entered
[NotMapped]
public decimal LineTotalViz { get; set; }
public string ChargeUnitViz { get; set; }//"each", "hour" etc
[NotMapped]
public decimal PriceViz { get; set; }//per unit price used in calcs after discounts or manual price if non-null or just ListPrice if no discount or manual override
[NotMapped]
public decimal NetViz { get; set; }//quantity * price (before taxes line total essentially)
[NotMapped]
public decimal TaxAViz { get; set; }//total amount of taxA
[NotMapped]
public decimal TaxBViz { get; set; }//total amount of taxB
[NotMapped]
public decimal LineTotalViz { get; set; }//line total netViz + taxes

View File

@@ -778,16 +778,15 @@ $BODY$ LANGUAGE PLPGSQL STABLE");
//WORKORDERITEM EXPENSE
await ExecQueryAsync("CREATE TABLE aworkorderitemexpense (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, workorderitemid BIGINT NOT NULL REFERENCES aworkorderitem (id), "
+ "description TEXT, name TEXT, totalcost DECIMAL(38,18) NOT NULL default 0, chargeamount DECIMAL(38,18) NOT NULL default 0, taxpaid DECIMAL(38,18) NOT NULL default 0, "
+ "chargetaxcodeid BIGINT REFERENCES ataxcode ON DELETE SET NULL, reimburseuser BOOL NOT NULL, userid BIGINT REFERENCES auser, chargetocustomer BOOL NOT NULL, "
+ "chargetaxcodeid BIGINT REFERENCES ataxcode, reimburseuser BOOL NOT NULL, userid BIGINT REFERENCES auser, chargetocustomer BOOL NOT NULL, "
+ "taxapct DECIMAL(8,5) NOT NULL default 0, taxbpct DECIMAL(8,5) NOT NULL default 0, taxontax BOOL NOT NULL default false, taxname TEXT "
+ ")");
//WORKORDERITEM LABOR
await ExecQueryAsync("CREATE TABLE aworkorderitemlabor (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, workorderitemid BIGINT NOT NULL REFERENCES aworkorderitem (id), "
+ "userid BIGINT REFERENCES auser, servicestartdate TIMESTAMP, servicestopdate TIMESTAMP, servicerateid BIGINT REFERENCES aservicerate, servicedetails text, "
+ "serviceratequantity DECIMAL(19,5) NOT NULL default 0, nochargequantity DECIMAL(19,5) NOT NULL default 0, servicebankid BIGINT REFERENCES aservicebank, taxcodesaleid BIGINT REFERENCES ataxcode ON DELETE SET NULL, "
+ "cost DECIMAL(38,18) NOT NULL default 0, listprice DECIMAL(38,18) NOT NULL default 0, price DECIMAL(38,18) NOT NULL default 0, taxapct DECIMAL(8,5) NOT NULL default 0, "
+ "taxbpct DECIMAL(8,5) NOT NULL default 0, taxontax BOOL NOT NULL default false, taxname TEXT "
+ "serviceratequantity DECIMAL(19,5) NOT NULL default 0, nochargequantity DECIMAL(19,5) NOT NULL default 0, servicebankid BIGINT REFERENCES aservicebank, "
+ "taxcodesaleid BIGINT REFERENCES ataxcode, manualprice DECIMAL(38,18) "
+ ")");
//WORKORDERITEM LOAN