This commit is contained in:
@@ -1,18 +1,56 @@
|
||||
# SVC-CONTRACTS Placeholder
|
||||
# CONTRACTS
|
||||
|
||||
This is a placeholder page for sections that are not written yet
|
||||
## Overview
|
||||
|
||||
NOTES FOR DOC
|
||||
A contract is used to automatically enforce special pricing rules and response time commitments on work orders.
|
||||
|
||||
- Contract pricing is normally set upon the *creation* of an object that might have it's price affected by a work order (e.g. a Labor item or part added to a workorder). This is done once at the time of creation of that record and done for all items on the workorder if the Contract is changed manually on the work order after it was created. This means if a workorder that has already been saved has it's selected Contract modified to change the discount (for example) then existing items on that workorder will not automatically be changed to the new discount but items added after the Contract is modified will take the new discount. If you want to re-set all the prices on the work order to take into account the new Contract terms then de-select the Contract on the workorder, save it, then re-select it again to trigger a re-set of all prices.
|
||||
## Contract form fields and options
|
||||
|
||||
- Contract change on workorder will trigger an update of all Labor, Travel and Part item's Cost, ListPrice and Price on that workorder based on their current cost and list price settings as well as any Contract adjustments if applicable. This also means any manually adjusted prices on the workorder will be overwritten upon save if the Contract has changed.
|
||||
todo: this section
|
||||
|
||||
- TAGS: tagged override items are matched to objects when the object has **ALL** the tags specified in the Contract override. If a Contract has a Labor price override when tagged with "red, green" then the work order item Labor record *must* have both the "red" and "green" tags amongst it's tags to match. E.G. a Contract part override for Travel rates tagged with "zone-5, van" would match with a Travel rate tagged with "zone-5,zone-6,zone-7,van" (both tags present) but would not match with a Travel rate tagged with "zone-5, car" (only one tag present).
|
||||
|
||||
- PRECEDENCE: Tagged Contract price overrides take precedence over general Contract price overrides. For example, if a Contract has a general discount for Labor rates and also has a tag override for a Labor record and the tags match then the tag override is the one used and the general override is ignored. If no tags matched then the general price override would be applied.
|
||||
|
||||
Conflict - multiple potential matches:
|
||||
|
||||
## Automatic contract selection rules
|
||||
|
||||
### Customer or Head office Contract
|
||||
|
||||
When a work order is saved for the first time or subsequently saved with a change of Customer, AyaNova will attempt to select the most applicable contract for that work order based on the pre-selected Customer or that Customer's Head office by checking those object's for a Contract that is not expired and applying it to the Work order.
|
||||
|
||||
Note that a change of Customer **and** a manual change of Contract on the same save is ambiguous and AyaNova will still automatically set the Contract based on the Customer or Head office if applicable and effectively ignore the manual setting. However if neither the Customer or the Head office has a Contract then the manual setting will still be preserved in this case.
|
||||
|
||||
|
||||
### Unit contract
|
||||
|
||||
When a Unit is selected on a work order and that Unit has a Contract, AyaNova will use that Contract for the entire work order automatically when that work order is first saved with the new Unit selection.
|
||||
|
||||
Multiple Units with Contracts on the same work order are not permitted. AyaNova would not be able to determine which Contract should take effect when there are multiple Unit's with Contracts on the same work order so for this reason it will not allow a Unit to be added with a Contract if there is already a Unit with a Contract in effect on the Work order. To avoid this scenario only select one Contract holding Unit per work order, use a Customer or HeadOffice Contract instead of a Unit Contract or manually select a Contract instead of using automatic Contract selection if you want full control over what Contract is in effect in order to apply to multiple Units on the same work order.
|
||||
|
||||
### Contract expiry
|
||||
|
||||
Contract expiry dates are checked at the time a Contract is automatically selected for a work order *only*. If a work order is in progress and has items added after it's Contract has expired the Contract will still apply to that work order and any new items added to that work order.
|
||||
|
||||
## How a Contract effects pricing
|
||||
|
||||
Contract pricing is set automatically in two ways:
|
||||
|
||||
- On the save of an object that might have it's price affected by a work order (e.g. a Labor item or Part added to a workorder) the price is calculated when the object is first added and saved or changed to a new item for example changing the service rate or part to a different selection.
|
||||
|
||||
- On selection of a different Contract for the entire work order all Contract price affected items on the work order will be re-visited and have their prices updated if applicable.
|
||||
|
||||
Prices are *not* automatically updated on a work order for existing items when the Contract itself has it's discounts modified. When new items are added to the work order then yes the new contract pricing will take effect for those items only but not the prior entered items. If you want a work order to have all it's prices updated to reflect the Contract change then you will need to de-select the Contract, save the work order then re-select and save again in order to force a re-calculation of all the existing items on the work order.
|
||||
|
||||
### What about manually entered prices?
|
||||
|
||||
Any manually adjusted prices on the workorder will be overwritten upon save if the Contract has changed.
|
||||
|
||||
## How Contract tagged override items are applied
|
||||
|
||||
Tagged override items are matched to objects when the object has **ALL** the tags specified in the Contract override. If a Contract has a Labor price override when tagged with "red, green" then the work order item Labor record *must* have both the "red" and "green" tags amongst it's tags to match. E.G. a Contract part override for Travel rates tagged with "zone-5, van" would match with a Travel rate tagged with "zone-5,zone-6,zone-7,van" (both tags present) but would not match with a Travel rate tagged with "zone-5, car" (only one tag present).
|
||||
|
||||
Tagged Contract price overrides take precedence over general Contract price overrides. For example, if a Contract has a general discount for Labor rates and also has a tag specific override for a Labor rate and the tags match between the Work order item labor record and the Contract tagged labor price override then the tag override is the one used and the general override is ignored. If no tags matched then the general price override would be applied.
|
||||
|
||||
### Conflict - multiple potential matches with tagged Contract override items
|
||||
|
||||
Contract tagged items are checked against the object in order of the Contract tagged override with the most tags to the Contract override with the least tags. This is done to try to make the most specific possible tag match. For example, a Contract with 3 tagged labor price overrides and their tags selected are: "green", "red, green, blue", "orange, green". AyaNova will attempt to match by starting with the Contract labor override with the most tags: "red,green,blue" then "orange,green" and finally "green". If a record is saved tagged with "orange,red,yellow,green,black,blue" it will be matched to the first item tagged "red,green,blue" as it has the most terms that match and it's override terms will be used and the other two will not be used.
|
||||
|
||||
- Expiration: Contract expiry dates are checked at the time of work order **creation**. If a work order is in progress and has items added after it's Contract has expired the Contract will still apply to that work order.
|
||||
@@ -12,14 +12,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace AyaNova.Biz
|
||||
{
|
||||
/*
|
||||
###############
|
||||
|
||||
|
||||
todo: remember, some users should not even have data sent from the server / scrubbed and not affect updating.
|
||||
for example a user may not be able to see part costs so that should not even be sent over the wire
|
||||
workorder will have to handle that as necessary and expect sometimes data is not forthcoming
|
||||
*/
|
||||
|
||||
|
||||
internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject
|
||||
{
|
||||
@@ -86,7 +79,7 @@ namespace AyaNova.Biz
|
||||
//(seeder or api user, not something AyaNova front end would do)
|
||||
if (newObject.Items.Count > 0)//our front end will post the header alone on new so this indicates a fully populated wo was saved
|
||||
{
|
||||
await GetCurrentContractFromContractIdAsync(newObject.ContractId);
|
||||
await GetCurrentContractFromContractIdAsync(newObject.ContractId);
|
||||
await ProcessChangeOfContractAsync(newObject.Id);
|
||||
}
|
||||
|
||||
@@ -224,7 +217,7 @@ namespace AyaNova.Biz
|
||||
}
|
||||
bool contractChanged = false;
|
||||
long? newContractId = null;
|
||||
if (putObject.ContractId != dbObject.ContractId)
|
||||
if (putObject.ContractId != dbObject.ContractId)//manual change of contract
|
||||
{
|
||||
contractChanged = true;
|
||||
newContractId = putObject.ContractId;
|
||||
@@ -328,38 +321,55 @@ namespace AyaNova.Biz
|
||||
|
||||
//CREATION ACTIONS
|
||||
if (ayaEvent == AyaEvent.Created)
|
||||
{ //applied at time of creation only
|
||||
//CONTRACT AUTO SET
|
||||
if (newObj.ContractId == null && newObj.CustomerId != 0)
|
||||
{
|
||||
//customer->headoffice
|
||||
var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).Select(z => new { headofficeId = z.HeadOfficeId, contractId = z.ContractId }).FirstOrDefaultAsync();
|
||||
if (cust.contractId == null && cust.headofficeId != null)
|
||||
{
|
||||
var hoContractId = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.headofficeId).Select(z => z.ContractId).FirstOrDefaultAsync();
|
||||
if (hoContractId != null)
|
||||
newObj.ContractId = hoContractId;
|
||||
}
|
||||
else
|
||||
newObj.ContractId = cust.contractId;
|
||||
}
|
||||
{
|
||||
await AutoSetContractAsync(newObj);
|
||||
return;
|
||||
}
|
||||
|
||||
//RESPONSE TIME / COMPLETE BY AUTO SET
|
||||
//precedence: manually pre-set -> contract -> global biz
|
||||
if (newObj.CompleteByDate != null)
|
||||
//MODIFIED ACTIONS
|
||||
if (ayaEvent == AyaEvent.Modified)
|
||||
{
|
||||
//if customer changed then contractId must be re-checked
|
||||
if (newObj.CustomerId != oldObj.CustomerId)
|
||||
{
|
||||
if (newObj.ContractId != null)
|
||||
{
|
||||
await GetCurrentContractFromContractIdAsync(newObj.ContractId);
|
||||
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
||||
newObj.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge!=TimeSpan.Zero)
|
||||
await AutoSetContractAsync(newObj);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task AutoSetContractAsync(WorkOrder newObj)
|
||||
{
|
||||
//CONTRACT AUTO SET
|
||||
if (newObj.ContractId == null && newObj.CustomerId != 0)
|
||||
{
|
||||
//unit->customer->headoffice
|
||||
var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).Select(z => new { headofficeId = z.HeadOfficeId, contractId = z.ContractId }).FirstOrDefaultAsync();
|
||||
if (cust.contractId == null && cust.headofficeId != null)
|
||||
{
|
||||
var hoContractId = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.headofficeId).Select(z => z.ContractId).FirstOrDefaultAsync();
|
||||
if (hoContractId != null)
|
||||
newObj.ContractId = hoContractId;
|
||||
}
|
||||
else
|
||||
newObj.ContractId = cust.contractId;
|
||||
}
|
||||
|
||||
//RESPONSE TIME / COMPLETE BY AUTO SET
|
||||
//precedence: manually pre-set -> contract -> global biz
|
||||
if (newObj.CompleteByDate != null)
|
||||
{
|
||||
if (newObj.ContractId != null)
|
||||
{
|
||||
await GetCurrentContractFromContractIdAsync(newObj.ContractId);
|
||||
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
||||
newObj.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge != TimeSpan.Zero)
|
||||
newObj.CompleteByDate = DateTime.UtcNow.Add(AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,7 +519,7 @@ namespace AyaNova.Biz
|
||||
|
||||
|
||||
//If Contract has response time then set CompleteByDate
|
||||
if (mContractInEffect!=null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
||||
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
|
||||
{
|
||||
wo.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
|
||||
}
|
||||
@@ -4164,6 +4174,7 @@ namespace AyaNova.Biz
|
||||
return null;
|
||||
else
|
||||
{
|
||||
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
|
||||
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
|
||||
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
|
||||
await ct.WorkOrderItemUnit.AddAsync(newObject);
|
||||
@@ -4208,6 +4219,9 @@ namespace AyaNova.Biz
|
||||
dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
|
||||
await UnitValidateAsync(putObject, dbObject);
|
||||
if (HasErrors) return null;
|
||||
|
||||
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
|
||||
|
||||
ct.Replace(dbObject, putObject);
|
||||
try
|
||||
{
|
||||
@@ -4305,6 +4319,10 @@ namespace AyaNova.Biz
|
||||
//skip validation if seeding
|
||||
// if (ServerBootConfig.SEEDING) return;
|
||||
|
||||
//TODO: ADD VALIDATIONS:
|
||||
// - A work order *MUST* have only one Unit with a Contract, if there is already a unit with a contract on this workorder then a new one cannot be added and it will reject with a validation error
|
||||
|
||||
|
||||
//run validation and biz rules
|
||||
bool isNew = currentObj == null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user