diff --git a/server/AyaNova/biz/WorkOrderBiz.cs b/server/AyaNova/biz/WorkOrderBiz.cs index dae995b3..340b21af 100644 --- a/server/AyaNova/biz/WorkOrderBiz.cs +++ b/server/AyaNova/biz/WorkOrderBiz.cs @@ -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 diff --git a/server/AyaNova/models/WorkOrderItemLabor.cs b/server/AyaNova/models/WorkOrderItemLabor.cs index c5288c3c..51cafdcc 100644 --- a/server/AyaNova/models/WorkOrderItemLabor.cs +++ b/server/AyaNova/models/WorkOrderItemLabor.cs @@ -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 diff --git a/server/AyaNova/util/AySchema.cs b/server/AyaNova/util/AySchema.cs index 7bf07c64..e91bdf85 100644 --- a/server/AyaNova/util/AySchema.cs +++ b/server/AyaNova/util/AySchema.cs @@ -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