node-autotask 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MCP_TOOL_CATALOG.json +4 -3
- package/MCP_TOOL_MANIFEST.md +5 -5
- package/N8N_MCP_MANIFEST.json +2 -2
- package/N8N_MCP_MANIFEST.md +3 -3
- package/capabilities.json +1238 -1237
- package/dist/capabilities.cjs +2 -2
- package/dist/capabilities.js +1 -1
- package/dist/{chunk-SRJI5XUZ.cjs → chunk-CJA6L6NR.cjs} +135 -135
- package/dist/{chunk-JKZVCFRY.cjs → chunk-CXXNONRP.cjs} +1237 -1236
- package/dist/{chunk-3WIDDEHP.js → chunk-DDZXN6LN.js} +2 -2
- package/dist/{chunk-GD4JRPYF.js → chunk-IKJLIDK3.js} +1 -1
- package/dist/{chunk-5N6DYKXO.cjs → chunk-KB2ANVVR.cjs} +2 -2
- package/dist/{chunk-TELBX6HK.cjs → chunk-MTDCLVQQ.cjs} +797 -603
- package/dist/{chunk-4A6XLQHE.js → chunk-OZD5FQM4.js} +1237 -1236
- package/dist/{chunk-MY7ZSXY3.js → chunk-Y6O5VVQ4.js} +387 -193
- package/dist/{index-D8I6taEo.d.cts → index-DbMKWnEX.d.cts} +96 -35
- package/dist/{index-uAxAcRol.d.ts → index-TuqNq3Ro.d.ts} +96 -35
- package/dist/index.cjs +19 -19
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -4
- package/dist/operations/index.cjs +103 -103
- package/dist/operations/index.d.cts +2 -2
- package/dist/operations/index.d.ts +2 -2
- package/dist/operations/index.js +2 -2
- package/dist/pool.cjs +8 -8
- package/dist/pool.d.cts +1 -1
- package/dist/pool.d.ts +1 -1
- package/dist/pool.js +4 -4
- package/dist/resources/index.cjs +4 -4
- package/dist/resources/index.d.cts +1 -1
- package/dist/resources/index.d.ts +1 -1
- package/dist/resources/index.js +3 -3
- package/docs/API.md +190 -51
- package/package.json +1 -1
|
@@ -33,10 +33,10 @@ import {
|
|
|
33
33
|
unwrapItem,
|
|
34
34
|
unwrapItems,
|
|
35
35
|
unwrapOperation
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-IKJLIDK3.js";
|
|
37
37
|
import {
|
|
38
38
|
CAPABILITIES
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-OZD5FQM4.js";
|
|
40
40
|
|
|
41
41
|
// src/resources/appointments.ts
|
|
42
42
|
var AppointmentsResource = class extends BaseResource {
|
|
@@ -4549,7 +4549,8 @@ var PROFITABILITY_OPT_KEYS = [
|
|
|
4549
4549
|
"groupByActivity",
|
|
4550
4550
|
"staleAfterDays",
|
|
4551
4551
|
"maxTicketsForJoin",
|
|
4552
|
-
"limit"
|
|
4552
|
+
"limit",
|
|
4553
|
+
"includeRows"
|
|
4553
4554
|
];
|
|
4554
4555
|
var LABOUR_METHOD_SET = [
|
|
4555
4556
|
"billing_item_our_cost",
|
|
@@ -4561,7 +4562,15 @@ var PROFITABILITY_MAX_WINDOW_MONTHS = 36;
|
|
|
4561
4562
|
var RECURRING_DRIFT_EPSILON = 5e-3;
|
|
4562
4563
|
var ESTIMATE_DEVIATION_THRESHOLD_PCT = 10;
|
|
4563
4564
|
var CLOSED_STATUS_LABEL_RE = /resolved|completed|cancelled|canceled|closed|deleted/i;
|
|
4564
|
-
var PROFITABILITY_WIRE_BUDGET =
|
|
4565
|
+
var PROFITABILITY_WIRE_BUDGET = 21;
|
|
4566
|
+
var BLOCK_HOURS_LABEL_RE = /block\s*hours?/i;
|
|
4567
|
+
var TIME_AND_MATERIALS_LABEL_RE = /time.*materials?/i;
|
|
4568
|
+
var NOT_ACTIVE_LABEL_RE = /cancel|inactive/i;
|
|
4569
|
+
var SUBSCRIPTION_NOT_ACTIVE_STATUS_IDS = /* @__PURE__ */ new Set([2, 3]);
|
|
4570
|
+
var PERIOD_TYPE_MONTHLY_FACTOR = { 2: 1, 3: 1 / 3, 4: 1 / 6, 5: 1 / 12 };
|
|
4571
|
+
var PERIOD_TYPE_ONE_TIME = 1;
|
|
4572
|
+
var SERVICE_LINE_FKS = ["contractServiceID", "contractServiceBundleID", "contractServicePeriodID", "contractServiceBundlePeriodID", "contractServiceAdjustmentID", "contractServiceBundleAdjustmentID"];
|
|
4573
|
+
var CHARGE_LINE_FKS = ["contractChargeID", "ticketChargeID", "projectChargeID"];
|
|
4565
4574
|
var DATE_RE2 = /^\d{4}-\d{2}-\d{2}$/;
|
|
4566
4575
|
function num2(v) {
|
|
4567
4576
|
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
@@ -4597,17 +4606,81 @@ function monthsBetween(dateFrom, dateTo) {
|
|
|
4597
4606
|
}
|
|
4598
4607
|
return out;
|
|
4599
4608
|
}
|
|
4609
|
+
function dayAfter(date) {
|
|
4610
|
+
return new Date(Date.parse(date) + 864e5).toISOString().slice(0, 10);
|
|
4611
|
+
}
|
|
4600
4612
|
function monthBounds(month, dateFrom, dateTo) {
|
|
4601
4613
|
const y = Number(month.slice(0, 4));
|
|
4602
4614
|
const m = Number(month.slice(5, 7));
|
|
4603
4615
|
const first = `${month}-01`;
|
|
4604
|
-
const
|
|
4605
|
-
const
|
|
4616
|
+
const nextFirst = new Date(Date.UTC(y, m, 1)).toISOString().slice(0, 10);
|
|
4617
|
+
const toExcl = dayAfter(dateTo);
|
|
4606
4618
|
return {
|
|
4607
4619
|
start: first > dateFrom ? first : dateFrom,
|
|
4608
|
-
|
|
4620
|
+
endExclusive: nextFirst < toExcl ? nextFirst : toExcl
|
|
4609
4621
|
};
|
|
4610
4622
|
}
|
|
4623
|
+
function chargeCost(charge) {
|
|
4624
|
+
const ext = charge["extendedCost"];
|
|
4625
|
+
const fromUnits = num2(charge["unitCost"]) * num2(charge["unitQuantity"]);
|
|
4626
|
+
if (typeof ext === "number" && Number.isFinite(ext) && !(ext === 0 && fromUnits !== 0)) return ext;
|
|
4627
|
+
return fromUnits;
|
|
4628
|
+
}
|
|
4629
|
+
function isOneTimePeriod(periodType, label) {
|
|
4630
|
+
return label !== void 0 ? /one[\s-]*time/i.test(label) : periodType === PERIOD_TYPE_ONE_TIME;
|
|
4631
|
+
}
|
|
4632
|
+
function periodMonthlyFactor(periodType, label) {
|
|
4633
|
+
if (label !== void 0) {
|
|
4634
|
+
if (/semi/i.test(label)) return 1 / 6;
|
|
4635
|
+
if (/quarter/i.test(label)) return 1 / 3;
|
|
4636
|
+
if (/annual|year/i.test(label)) return 1 / 12;
|
|
4637
|
+
if (/month/i.test(label)) return 1;
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
return typeof periodType === "number" ? PERIOD_TYPE_MONTHLY_FACTOR[periodType] ?? null : null;
|
|
4641
|
+
}
|
|
4642
|
+
var BILLING_ITEM_TYPE_KIND = {
|
|
4643
|
+
1: "labour",
|
|
4644
|
+
2: "labour",
|
|
4645
|
+
3: "charges",
|
|
4646
|
+
4: "expenses",
|
|
4647
|
+
5: "other",
|
|
4648
|
+
6: "recurring",
|
|
4649
|
+
7: "charges",
|
|
4650
|
+
8: "other"
|
|
4651
|
+
};
|
|
4652
|
+
function billingLineType(item) {
|
|
4653
|
+
const byType = BILLING_ITEM_TYPE_KIND[num2(item["billingItemType"])];
|
|
4654
|
+
if (byType !== void 0) return byType;
|
|
4655
|
+
if (num2(item["timeEntryID"]) > 0) return "labour";
|
|
4656
|
+
if (num2(item["expenseItemID"]) > 0) return "expenses";
|
|
4657
|
+
if (CHARGE_LINE_FKS.some((k) => num2(item[k]) > 0)) return "charges";
|
|
4658
|
+
if (SERVICE_LINE_FKS.some((k) => num2(item[k]) > 0)) return "recurring";
|
|
4659
|
+
return "other";
|
|
4660
|
+
}
|
|
4661
|
+
function internalAmount(row, internalKey, localKey) {
|
|
4662
|
+
const v = row[internalKey];
|
|
4663
|
+
return typeof v === "number" && Number.isFinite(v) ? v : num2(row[localKey]);
|
|
4664
|
+
}
|
|
4665
|
+
function cents(v) {
|
|
4666
|
+
return v === null ? null : Math.round(v * 100) / 100;
|
|
4667
|
+
}
|
|
4668
|
+
async function readPicklists(http, entityPath, operation) {
|
|
4669
|
+
const res = await http.request({ method: "GET", path: `${entityPath}/entityInformation/fields`, operation });
|
|
4670
|
+
const body = typeof res === "object" && res !== null ? res : {};
|
|
4671
|
+
const fields = Array.isArray(body["fields"]) ? body["fields"] : [];
|
|
4672
|
+
const out = /* @__PURE__ */ new Map();
|
|
4673
|
+
for (const f of fields) {
|
|
4674
|
+
if (f["isPickList"] !== true || typeof f["name"] !== "string" || !Array.isArray(f["picklistValues"])) continue;
|
|
4675
|
+
const values = /* @__PURE__ */ new Map();
|
|
4676
|
+
for (const p of f["picklistValues"]) {
|
|
4677
|
+
const id = typeof p["value"] === "string" ? Number.parseInt(p["value"], 10) : Number.NaN;
|
|
4678
|
+
if (Number.isFinite(id)) values.set(id, String(p["label"] ?? ""));
|
|
4679
|
+
}
|
|
4680
|
+
out.set(f["name"], values);
|
|
4681
|
+
}
|
|
4682
|
+
return out;
|
|
4683
|
+
}
|
|
4611
4684
|
function dateToDay(v) {
|
|
4612
4685
|
if (typeof v !== "string" || v.length < 10) return null;
|
|
4613
4686
|
const d = Date.parse(v.slice(0, 10));
|
|
@@ -4616,7 +4689,7 @@ function dateToDay(v) {
|
|
|
4616
4689
|
function validateProfitabilityOptions(contractId, opts) {
|
|
4617
4690
|
if (opts === void 0 || (typeof opts !== "object" || opts === null || Array.isArray(opts))) {
|
|
4618
4691
|
throw new AutotaskConfigError(
|
|
4619
|
-
`profitability: opts
|
|
4692
|
+
`profitability: opts is required \u2014 the options object with dateFrom and dateTo (YYYY-MM-DD) (got ${opts === void 0 ? "undefined" : Array.isArray(opts) ? "an array" : typeof opts})`,
|
|
4620
4693
|
{ operation: "contracts.profitability" }
|
|
4621
4694
|
);
|
|
4622
4695
|
}
|
|
@@ -4656,7 +4729,7 @@ function validateProfitabilityOptions(contractId, opts) {
|
|
|
4656
4729
|
if (method === "provided" && rate === null) {
|
|
4657
4730
|
problems.push(`labourRatePerHour is required (finite number >= 0) when labourCostMethod is "provided" (got ${String(opts.labourRatePerHour)})`);
|
|
4658
4731
|
}
|
|
4659
|
-
for (const key of ["includeRecurringCosts", "includeUnbilledRevenue", "groupByActivity"]) {
|
|
4732
|
+
for (const key of ["includeRecurringCosts", "includeUnbilledRevenue", "groupByActivity", "includeRows"]) {
|
|
4660
4733
|
const v = opts[key];
|
|
4661
4734
|
if (v !== void 0 && typeof v !== "boolean") problems.push(`${key} must be a boolean (got ${String(v)})`);
|
|
4662
4735
|
}
|
|
@@ -4714,7 +4787,8 @@ function validateProfitabilityOptions(contractId, opts) {
|
|
|
4714
4787
|
groupByActivity: opts.groupByActivity ?? true,
|
|
4715
4788
|
staleAfterDays: stale,
|
|
4716
4789
|
maxTicketsForJoin: mtj,
|
|
4717
|
-
limit
|
|
4790
|
+
limit,
|
|
4791
|
+
includeRows: opts.includeRows ?? false
|
|
4718
4792
|
};
|
|
4719
4793
|
}
|
|
4720
4794
|
async function boundedPart2(http, path, conditions, limit, operation, includeFields) {
|
|
@@ -4873,7 +4947,7 @@ async function expandByMonth(http, path, fullWindowCond, limit, operation, field
|
|
|
4873
4947
|
const cond = [
|
|
4874
4948
|
...fullWindowCond.filter((c) => c.field !== dateField),
|
|
4875
4949
|
{ field: dateField, op: "gte", value: b.start },
|
|
4876
|
-
{ field: dateField, op: "
|
|
4950
|
+
{ field: dateField, op: "lt", value: b.endExclusive }
|
|
4877
4951
|
];
|
|
4878
4952
|
const page = await boundedPart2(http, path, cond, limit, operation, fields);
|
|
4879
4953
|
wires.issued += 1;
|
|
@@ -4888,7 +4962,15 @@ async function expandByMonth(http, path, fullWindowCond, limit, operation, field
|
|
|
4888
4962
|
}
|
|
4889
4963
|
const partNote = `per-month windowing fired: ${months.length} month read(s) added after the full-window page carried a live cursor (no cursor walk \u2014 the single-page house rule)` + (fullMonths.length > 0 ? ` | month ${fullMonths.join(", ")} hit the limit of ${limit} \u2014 figures cover the returned rows only; widen with a narrower window or higher limit` : "");
|
|
4890
4964
|
wires.note = (wires.note ? wires.note + " | " : "") + partNote;
|
|
4891
|
-
return { items: all, capped: cappedMonths.length > 0, hasMore: fullMonths.length > 0, note: partNote, status: "ok" };
|
|
4965
|
+
return { items: all, capped: cappedMonths.length > 0, hasMore: fullMonths.length > 0, note: partNote, status: "ok", truncatedMonths: fullMonths };
|
|
4966
|
+
}
|
|
4967
|
+
async function degradable(read) {
|
|
4968
|
+
try {
|
|
4969
|
+
return await read();
|
|
4970
|
+
} catch (err) {
|
|
4971
|
+
if (!(err instanceof AutotaskError)) throw err;
|
|
4972
|
+
return { items: [], capped: false, hasMore: false, absent: err.code, absentMessage: err.message };
|
|
4973
|
+
}
|
|
4892
4974
|
}
|
|
4893
4975
|
function fmtHours(h) {
|
|
4894
4976
|
const r = Math.round(h * 100) / 100;
|
|
@@ -4904,16 +4986,29 @@ function pctOrNa(r) {
|
|
|
4904
4986
|
function dedupeSortCap(ids, limit) {
|
|
4905
4987
|
return [...new Set(ids)].sort((a, b) => Number(a) - Number(b)).slice(0, limit);
|
|
4906
4988
|
}
|
|
4907
|
-
async function profitability(resource, contractId, opts,
|
|
4908
|
-
const v = validateProfitabilityOptions(contractId, opts
|
|
4989
|
+
async function profitability(resource, contractId, opts, rawHttp) {
|
|
4990
|
+
const v = validateProfitabilityOptions(contractId, opts);
|
|
4909
4991
|
const OP2 = "contracts.profitability";
|
|
4910
4992
|
const id = parseIdentifier(contractId, "contractId");
|
|
4911
4993
|
const idNum = typeof id === "number" ? id : Number(id);
|
|
4912
|
-
const wires = { issued: 0, budget: PROFITABILITY_WIRE_BUDGET };
|
|
4994
|
+
const wires = { issued: 0, requests: 0, budget: PROFITABILITY_WIRE_BUDGET };
|
|
4913
4995
|
const asOf = (/* @__PURE__ */ new Date()).toISOString();
|
|
4914
4996
|
const asOfMs = Date.parse(asOf);
|
|
4997
|
+
const toExcl = dayAfter(v.dateTo);
|
|
4998
|
+
const windowOn = (field) => [{ field, op: "gte", value: v.dateFrom }, { field, op: "lt", value: toExcl }];
|
|
4999
|
+
const http = { request: (o) => {
|
|
5000
|
+
wires.requests += 1;
|
|
5001
|
+
return rawHttp.request(o);
|
|
5002
|
+
} };
|
|
5003
|
+
const readPart = (path, conditions, fields) => {
|
|
5004
|
+
wires.issued += 1;
|
|
5005
|
+
return degradable(() => boundedPart2(http, path, conditions, v.limit, OP2, fields));
|
|
5006
|
+
};
|
|
4915
5007
|
const skipPart = (note) => ({ items: [], capped: false, hasMore: false, status: "skipped", note });
|
|
4916
5008
|
const stampPart = (key, raw, conditions, extraNotes) => {
|
|
5009
|
+
if (raw.absent !== void 0) {
|
|
5010
|
+
return { items: [], capped: false, hasMore: false, status: "absent", absent: raw.absent, note: `absent: the read failed (${raw.absent}: ${raw.absentMessage ?? ""}) \u2014 the monthly figures it feeds carry it in coverage_gaps` };
|
|
5011
|
+
}
|
|
4917
5012
|
const st = unbilledWorkPartStatus(key, conditions, raw.items);
|
|
4918
5013
|
return {
|
|
4919
5014
|
items: raw.items,
|
|
@@ -4925,12 +5020,26 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
4925
5020
|
emptyReason: raw.items.length === 0 && st.status === "ok" ? EMPTY_REASON_STAMPS[key]?.query : void 0
|
|
4926
5021
|
};
|
|
4927
5022
|
};
|
|
5023
|
+
const truncatedMonths = {};
|
|
5024
|
+
const windowedPart = async (name, key, path, cond, fields, dateField) => {
|
|
5025
|
+
const first = await readPart(path, cond, fields);
|
|
5026
|
+
if (first.absent !== void 0 || !first.hasMore) return { part: stampPart(key, first, cond), rows: first.items };
|
|
5027
|
+
const wp = await degradable(() => expandByMonth(http, path, cond, v.limit, OP2, fields, dateField, v.dateFrom, v.dateTo, wires));
|
|
5028
|
+
if (!("truncatedMonths" in wp)) return { part: stampPart(key, wp, cond), rows: [] };
|
|
5029
|
+
const { truncatedMonths: tm, ...part } = wp;
|
|
5030
|
+
truncatedMonths[name] = tm;
|
|
5031
|
+
return { part, rows: wp.items };
|
|
5032
|
+
};
|
|
5033
|
+
const contractCached = rawHttp.readCache?.get(`/V1.0/Contracts/${id}`) !== void 0;
|
|
4928
5034
|
const contract = await resource.get(id);
|
|
4929
5035
|
wires.issued += 1;
|
|
5036
|
+
if (!contractCached) wires.requests += 1;
|
|
4930
5037
|
const contractRow = contract;
|
|
4931
5038
|
const companyID = num2(contractRow["companyID"]);
|
|
5039
|
+
const contractType = num2(contractRow["contractType"]);
|
|
4932
5040
|
let customer = null;
|
|
4933
5041
|
let customerStatus = "skipped";
|
|
5042
|
+
let customerAbsent;
|
|
4934
5043
|
if (companyID > 0) {
|
|
4935
5044
|
try {
|
|
4936
5045
|
const res = await http.request({
|
|
@@ -4942,49 +5051,61 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
4942
5051
|
customer = item ?? null;
|
|
4943
5052
|
customerStatus = "ok";
|
|
4944
5053
|
} catch (err) {
|
|
4945
|
-
if (!(err instanceof
|
|
5054
|
+
if (!(err instanceof AutotaskError)) throw err;
|
|
5055
|
+
if (!(err instanceof NotFoundError)) {
|
|
5056
|
+
customerStatus = "absent";
|
|
5057
|
+
customerAbsent = err.code;
|
|
5058
|
+
}
|
|
4946
5059
|
}
|
|
4947
5060
|
wires.issued += 1;
|
|
4948
5061
|
}
|
|
5062
|
+
let contractTypeLabel = null;
|
|
5063
|
+
if (contractType > 0) {
|
|
5064
|
+
wires.issued += 1;
|
|
5065
|
+
try {
|
|
5066
|
+
contractTypeLabel = (await readPicklists(http, "/V1.0/Contracts", OP2)).get("contractType")?.get(contractType) ?? null;
|
|
5067
|
+
} catch (err) {
|
|
5068
|
+
if (!(err instanceof AutotaskError)) throw err;
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
const contractTypeKnown = contractTypeLabel !== null;
|
|
5072
|
+
const isBlockHours = contractTypeKnown && BLOCK_HOURS_LABEL_RE.test(contractTypeLabel);
|
|
5073
|
+
const isTimeAndMaterials = contractTypeKnown && TIME_AND_MATERIALS_LABEL_RE.test(contractTypeLabel);
|
|
4949
5074
|
const servicesCond = [{ field: "contractID", op: "eq", value: id }];
|
|
4950
|
-
const servicesRaw = await
|
|
4951
|
-
|
|
4952
|
-
const
|
|
4953
|
-
const
|
|
4954
|
-
wires.issued += 1;
|
|
5075
|
+
const servicesRaw = await readPart("/V1.0/ContractServices/query", servicesCond, ["id", "serviceID", "unitPrice", "unitCost", "internalCurrencyUnitPrice", "internalDescription"]);
|
|
5076
|
+
const unitsCond = [{ field: "contractID", op: "eq", value: id }, ...windowOn("startDate")];
|
|
5077
|
+
const units = await windowedPart("contractServiceUnits", "contract_service_units", "/V1.0/ContractServiceUnits/query", unitsCond, ["id", "contractServiceID", "serviceID", "units", "price", "cost", "internalCurrencyPrice", "startDate", "endDate", "approveAndPostDate"], "startDate");
|
|
5078
|
+
const unitRows = units.rows;
|
|
4955
5079
|
let chargesPart;
|
|
4956
5080
|
let charges = [];
|
|
4957
5081
|
let chargesCond = [];
|
|
4958
5082
|
if (v.includeRecurringCosts) {
|
|
4959
|
-
chargesCond = [{ field: "contractID", op: "eq", value: id },
|
|
4960
|
-
const raw = await
|
|
4961
|
-
wires.issued += 1;
|
|
5083
|
+
chargesCond = [{ field: "contractID", op: "eq", value: id }, ...windowOn("datePurchased")];
|
|
5084
|
+
const raw = await readPart("/V1.0/ContractCharges/query", chargesCond, ["id", "chargeType", "billableAmount", "extendedCost", "unitCost", "unitPrice", "unitQuantity", "datePurchased", "isBilled", "status", "contractServiceID"]);
|
|
4962
5085
|
charges = raw.items;
|
|
4963
5086
|
chargesPart = stampPart("contract_charges", raw, chargesCond);
|
|
4964
5087
|
} else {
|
|
4965
5088
|
chargesPart = skipPart("skipped: includeRecurringCosts is false (zero wire)");
|
|
4966
5089
|
}
|
|
4967
5090
|
const roleCostsCond = [{ field: "contractID", op: "eq", value: id }];
|
|
4968
|
-
const roleCostsRaw = await
|
|
4969
|
-
wires.issued += 1;
|
|
5091
|
+
const roleCostsRaw = await readPart("/V1.0/ContractRoleCosts/query", roleCostsCond, ["id", "rate", "resourceID", "roleID"]);
|
|
4970
5092
|
const ratesCond = [{ field: "contractID", op: "eq", value: id }];
|
|
4971
|
-
const ratesRaw = await
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
let billingItems;
|
|
4978
|
-
let billingRows = [];
|
|
4979
|
-
if (!billingFirst.hasMore) {
|
|
4980
|
-
const st = unbilledWorkPartStatus("billing_items", billingCond(), billingFirst.items);
|
|
4981
|
-
billingRows = billingFirst.items;
|
|
4982
|
-
billingItems = { items: billingFirst.items, capped: billingFirst.capped, hasMore: false, status: st.status, blindSurface: st.blindSurface, note: st.note, emptyReason: billingFirst.items.length === 0 && st.status === "ok" ? EMPTY_REASON_STAMPS.billing_items?.query : void 0 };
|
|
5093
|
+
const ratesRaw = await readPart("/V1.0/ContractRates/query", ratesCond, ["id", "roleID", "contractHourlyRate", "internalCurrencyContractHourlyRate"]);
|
|
5094
|
+
let blocksPart;
|
|
5095
|
+
if (!isBlockHours) {
|
|
5096
|
+
blocksPart = skipPart(
|
|
5097
|
+
contractTypeKnown ? `skipped: not a Block Hours contract (contractType ${contractType} "${contractTypeLabel}") \u2014 included hours apply to hour blocks only (zero wire)` : `skipped: the contract type is unknown (contractType ${contractType || "absent"}; label unresolved) \u2014 included hours are not evaluable (zero wire)`
|
|
5098
|
+
);
|
|
4983
5099
|
} else {
|
|
4984
|
-
const
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
}
|
|
5100
|
+
const blocksCond = [{ field: "contractID", op: "eq", value: id }];
|
|
5101
|
+
const raw = await readPart("/V1.0/ContractBlocks/query", blocksCond, ["id", "datePurchased", "startDate", "endDate", "hours", "hoursApproved", "status", "isPaid"]);
|
|
5102
|
+
blocksPart = stampPart("contract_blocks", raw, blocksCond);
|
|
5103
|
+
}
|
|
5104
|
+
const billingCond = [{ field: "contractID", op: "eq", value: id }, ...windowOn("itemDate")];
|
|
5105
|
+
const billingFields = ["id", "invoiceID", "timeEntryID", "ticketID", "ourCost", "quantity", "rate", "extendedPrice", "internalCurrencyExtendedPrice", "itemDate", "postedDate", "nonBillable", "billingCodeID", "billingItemType", "expenseItemID", ...SERVICE_LINE_FKS, ...CHARGE_LINE_FKS];
|
|
5106
|
+
const billing = await windowedPart("billingItems", "billing_items", "/V1.0/BillingItems/query", billingCond, billingFields, "itemDate");
|
|
5107
|
+
const billingItems = billing.part;
|
|
5108
|
+
const billingRows = billing.rows;
|
|
4988
5109
|
const invoiceIds = [...new Set(billingRows.map((b) => num2(b["invoiceID"])).filter((n) => n > 0))];
|
|
4989
5110
|
let invoicesPart;
|
|
4990
5111
|
let invoices = [];
|
|
@@ -4998,38 +5119,25 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
4998
5119
|
invoices = fb.items;
|
|
4999
5120
|
invoicesPart = { items: fb.items, capped: capped.truncated, hasMore: capped.truncated, status: "ok", note: fb.notes.join(" | ") || void 0 };
|
|
5000
5121
|
}
|
|
5001
|
-
const timeCond =
|
|
5122
|
+
const timeCond = [{ field: "contractID", op: "eq", value: id }, ...windowOn("dateWorked")];
|
|
5002
5123
|
const timeFields = ["id", "ticketID", "taskID", "resourceID", "roleID", "hoursWorked", "hoursToBill", "dateWorked", "startDateTime", "endDateTime", "isNonBillable", "timeEntryType", "billingCodeID", "createDateTime"];
|
|
5003
|
-
const
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
let entries = [];
|
|
5007
|
-
if (!timeFirst.hasMore) {
|
|
5008
|
-
const st = unbilledWorkPartStatus("time_entries", timeCond(), timeFirst.items);
|
|
5009
|
-
entries = timeFirst.items;
|
|
5010
|
-
timeEntries = { items: timeFirst.items, capped: timeFirst.capped, hasMore: false, status: st.status, blindSurface: st.blindSurface, note: st.note, emptyReason: timeFirst.items.length === 0 && st.status === "ok" ? EMPTY_REASON_STAMPS.time_entries?.query : void 0 };
|
|
5011
|
-
} else {
|
|
5012
|
-
const wp = await expandByMonth(http, "/V1.0/TimeEntries/query", timeCond(), v.limit, OP2, timeFields, "dateWorked", v.dateFrom, v.dateTo, wires);
|
|
5013
|
-
entries = wp.items;
|
|
5014
|
-
timeEntries = { ...wp, items: wp.items, note: wp.note };
|
|
5015
|
-
}
|
|
5124
|
+
const time = await windowedPart("timeEntries", "time_entries", "/V1.0/TimeEntries/query", timeCond, timeFields, "dateWorked");
|
|
5125
|
+
const timeEntries = time.part;
|
|
5126
|
+
const entries = time.rows;
|
|
5016
5127
|
const ticketFields = ["id", "ticketNumber", "title", "status", "ticketCategory", "contractID", "createDate", "resolvedDateTime", "completedDate", "lastActivityDate", "dueDateTime"];
|
|
5017
|
-
const
|
|
5018
|
-
const
|
|
5019
|
-
|
|
5128
|
+
const createdInWindow = [{ field: "contractID", op: "eq", value: id }, ...windowOn("createDate")];
|
|
5129
|
+
const refTicketIds = capJoinSet2(entries.map((e) => num2(e["ticketID"])).filter((n) => n > 0), v.maxTicketsForJoin);
|
|
5130
|
+
const ticketsActCond = refTicketIds.ids.length === 0 ? createdInWindow : [{ op: "or", items: [{ op: "and", items: createdInWindow }, { field: "id", op: "in", value: refTicketIds.ids }] }];
|
|
5131
|
+
const ticketsActRaw = await readPart("/V1.0/Tickets/query", ticketsActCond, ticketFields);
|
|
5020
5132
|
let openStatusSet = null;
|
|
5021
5133
|
let statusNote;
|
|
5134
|
+
wires.issued += 1;
|
|
5022
5135
|
try {
|
|
5023
|
-
const
|
|
5024
|
-
|
|
5025
|
-
const body = typeof res === "object" && res !== null ? res : {};
|
|
5026
|
-
const fields = Array.isArray(body["fields"]) ? body["fields"] : [];
|
|
5027
|
-
const statusRow = fields.find((f) => f["name"] === "status" && f["isPickList"] === true);
|
|
5028
|
-
const values = statusRow && Array.isArray(statusRow["picklistValues"]) ? statusRow["picklistValues"] : [];
|
|
5029
|
-
if (values.length === 0) {
|
|
5136
|
+
const values = (await readPicklists(http, "/V1.0/Tickets", OP2)).get("status");
|
|
5137
|
+
if (values === void 0 || values.size === 0) {
|
|
5030
5138
|
statusNote = "the tickets.status picklist is not exposed to this API user \u2014 the open-status labels are unavailable";
|
|
5031
5139
|
} else {
|
|
5032
|
-
const open = values.filter((
|
|
5140
|
+
const open = [...values].filter(([, label]) => !CLOSED_STATUS_LABEL_RE.test(label)).map(([v2]) => v2);
|
|
5033
5141
|
if (open.length === 0) {
|
|
5034
5142
|
statusNote = "every tickets.status picklist label is closed-class \u2014 no open statuses exist in this tenant";
|
|
5035
5143
|
} else {
|
|
@@ -5037,7 +5145,6 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5037
5145
|
}
|
|
5038
5146
|
}
|
|
5039
5147
|
} catch (err) {
|
|
5040
|
-
wires.issued += 1;
|
|
5041
5148
|
statusNote = `the tickets.status field catalog read failed (${err instanceof AutotaskError ? err.code : "SERVER_ERROR"}: ${err instanceof Error ? err.message : String(err)}) \u2014 the open-status labels are unavailable`;
|
|
5042
5149
|
}
|
|
5043
5150
|
let ticketsOpenPart;
|
|
@@ -5046,8 +5153,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5046
5153
|
ticketsOpenPart = skipPart(`skipped: ${statusNote} (zero wire)`);
|
|
5047
5154
|
} else {
|
|
5048
5155
|
const openCond = [{ field: "contractID", op: "eq", value: id }, { field: "status", op: "in", value: [...openStatusSet] }];
|
|
5049
|
-
const raw = await
|
|
5050
|
-
wires.issued += 1;
|
|
5156
|
+
const raw = await readPart("/V1.0/Tickets/query", openCond, ticketFields);
|
|
5051
5157
|
openTickets = raw.items;
|
|
5052
5158
|
ticketsOpenPart = stampPart("tickets", raw, openCond, ["openStatusHeuristic: true (the open set is the tenant picklist minus the closed-class labels)"]);
|
|
5053
5159
|
}
|
|
@@ -5071,16 +5177,16 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5071
5177
|
const k = num2(t["id"]);
|
|
5072
5178
|
if (k > 0 && !fetchedTickets.has(k)) fetchedTickets.set(k, t);
|
|
5073
5179
|
}
|
|
5074
|
-
const
|
|
5180
|
+
const contractTickets = [...fetchedTickets.values()].filter((t) => num2(t["contractID"]) === idNum);
|
|
5181
|
+
const ticketJoin = capJoinSet2(contractTickets.map((t) => num2(t["id"])), v.maxTicketsForJoin);
|
|
5075
5182
|
const joinNote = ticketJoin.truncated ? `the ticket join set is capped at ${v.maxTicketsForJoin} of ${ticketJoin.total} ids \u2014 the expenses + the miscoding probe cover the returned join set only` : void 0;
|
|
5076
5183
|
let expensesPart;
|
|
5077
5184
|
let expenses = [];
|
|
5078
5185
|
if (ticketJoin.ids.length === 0) {
|
|
5079
5186
|
expensesPart = skipPart("skipped: the contract has no fetched tickets to join against (an empty join set is NOT evidence of no expenses \u2014 zero wire)");
|
|
5080
5187
|
} else {
|
|
5081
|
-
const expCond = [{ field: "ticketID", op: "in", value: ticketJoin.ids },
|
|
5082
|
-
const raw = await
|
|
5083
|
-
wires.issued += 1;
|
|
5188
|
+
const expCond = [{ field: "ticketID", op: "in", value: ticketJoin.ids }, ...windowOn("expenseDate")];
|
|
5189
|
+
const raw = await readPart("/V1.0/ExpenseItems/query", expCond, ["id", "expenseDate", "expenseCategory", "paymentType", "expenseCurrencyExpenseAmount", "internalCurrencyExpenseAmount", "ticketID"]);
|
|
5084
5190
|
expenses = raw.items;
|
|
5085
5191
|
expensesPart = stampPart("expense_items", raw, expCond, joinNote === void 0 ? void 0 : [joinNote]);
|
|
5086
5192
|
}
|
|
@@ -5092,45 +5198,69 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5092
5198
|
const probeCond = [
|
|
5093
5199
|
{ field: "ticketID", op: "in", value: ticketJoin.ids },
|
|
5094
5200
|
{ op: "or", items: [{ field: "contractID", op: "noteq", value: id }, { field: "contractID", op: "notExist" }] },
|
|
5095
|
-
|
|
5096
|
-
{ field: "dateWorked", op: "lte", value: v.dateTo }
|
|
5201
|
+
...windowOn("dateWorked")
|
|
5097
5202
|
];
|
|
5098
|
-
const raw = await
|
|
5099
|
-
wires.issued += 1;
|
|
5203
|
+
const raw = await readPart("/V1.0/TimeEntries/query", probeCond, ["id", "ticketID", "contractID", "resourceID", "hoursWorked", "dateWorked"]);
|
|
5100
5204
|
probeRows = raw.items;
|
|
5101
5205
|
probePart = stampPart("time_entries", raw, probeCond, joinNote === void 0 ? void 0 : [joinNote]);
|
|
5102
5206
|
}
|
|
5103
5207
|
let configurationItemsPart;
|
|
5104
5208
|
let cis = [];
|
|
5105
|
-
let ciCond = [];
|
|
5106
5209
|
if (!v.includeRecurringCosts) {
|
|
5107
5210
|
configurationItemsPart = skipPart("skipped: includeRecurringCosts is false (zero wire)");
|
|
5108
|
-
} else if (companyID <= 0) {
|
|
5109
|
-
configurationItemsPart = skipPart("skipped: the contract carries no companyID \u2014 the CI join has no root (zero wire)");
|
|
5110
5211
|
} else {
|
|
5111
|
-
ciCond = [{ field: "
|
|
5112
|
-
const raw = await
|
|
5113
|
-
wires.issued += 1;
|
|
5212
|
+
const ciCond = [{ field: "contractID", op: "eq", value: id }];
|
|
5213
|
+
const raw = await readPart("/V1.0/ConfigurationItems/query", ciCond, ["id", "configurationItemName", "status", "contractID"]);
|
|
5114
5214
|
cis = raw.items;
|
|
5115
5215
|
configurationItemsPart = stampPart("configuration_items", raw, ciCond);
|
|
5116
5216
|
}
|
|
5117
5217
|
let subscriptionsPart;
|
|
5118
5218
|
let subscriptions = [];
|
|
5219
|
+
const subFactor = /* @__PURE__ */ new Map();
|
|
5220
|
+
const oneTimeSubs = /* @__PURE__ */ new Set();
|
|
5221
|
+
const cancelledSubs = /* @__PURE__ */ new Set();
|
|
5119
5222
|
if (cis.length === 0) {
|
|
5120
5223
|
subscriptionsPart = skipPart("skipped: the CI join found no configuration items \u2014 the subscriptions part is not evaluable (zero wire)");
|
|
5121
5224
|
} else {
|
|
5122
5225
|
const ciIds = capJoinSet2(cis.map((c) => num2(c["id"])).filter((n) => n > 0), FETCH_BY_IDS_MAX_ITEMS);
|
|
5123
5226
|
const subCond = [{ field: "configurationItemID", op: "in", value: ciIds.ids }];
|
|
5124
|
-
const raw = await
|
|
5125
|
-
wires.issued += 1;
|
|
5227
|
+
const raw = await readPart("/V1.0/Subscriptions/query", subCond, ["id", "subscriptionName", "configurationItemID", "periodCost", "periodPrice", "periodType", "effectiveDate", "expirationDate", "status", "totalCost"]);
|
|
5126
5228
|
subscriptions = raw.items;
|
|
5127
|
-
|
|
5229
|
+
let labels = null;
|
|
5230
|
+
if (subscriptions.length > 0) {
|
|
5231
|
+
wires.issued += 1;
|
|
5232
|
+
try {
|
|
5233
|
+
labels = await readPicklists(http, "/V1.0/Subscriptions", OP2);
|
|
5234
|
+
} catch (err) {
|
|
5235
|
+
if (!(err instanceof AutotaskError)) throw err;
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
for (const x of subscriptions) {
|
|
5239
|
+
const sid = num2(x["id"]);
|
|
5240
|
+
const periodLabel = labels?.get("periodType")?.get(num2(x["periodType"]));
|
|
5241
|
+
subFactor.set(sid, periodMonthlyFactor(x["periodType"], periodLabel));
|
|
5242
|
+
if (isOneTimePeriod(x["periodType"], periodLabel)) oneTimeSubs.add(sid);
|
|
5243
|
+
const statusLabel = labels?.get("status")?.get(num2(x["status"]));
|
|
5244
|
+
const notActive = statusLabel !== void 0 ? NOT_ACTIVE_LABEL_RE.test(statusLabel) : labels === null && SUBSCRIPTION_NOT_ACTIVE_STATUS_IDS.has(num2(x["status"]));
|
|
5245
|
+
if (notActive) cancelledSubs.add(sid);
|
|
5246
|
+
}
|
|
5247
|
+
const unknownPeriods = [...new Set(subscriptions.filter((x) => subFactor.get(num2(x["id"])) === null && !oneTimeSubs.has(num2(x["id"]))).map((x) => String(x["periodType"] ?? "absent")))];
|
|
5248
|
+
subscriptionsPart = stampPart("subscriptions", raw, subCond, [
|
|
5249
|
+
"scope: the subscriptions on the CIs this contract covers \u2014 periodCost prorated to a month by periodType into tooling_cost.vendor, REPORTED but NOT folded into total_cost/profit/margin (subscriptions bill outside the contract)",
|
|
5250
|
+
...subscriptions.length > 0 && labels === null ? ["the Subscriptions field catalog read failed \u2014 the verified id maps apply (periodType 1 One-Time, 2 Monthly, 3 Quarterly, 4 Semi-Annual, 5 Yearly; status 2 Cancelled / 3 Inactive excluded)"] : [],
|
|
5251
|
+
...cancelledSubs.size > 0 ? [`${cancelledSubs.size} not-active (cancelled/inactive) subscription(s) excluded from tooling_cost.vendor`] : [],
|
|
5252
|
+
...oneTimeSubs.size > 0 ? [`${oneTimeSubs.size} one-time subscription(s): periodCost booked whole in the effectiveDate month, never prorated`] : [],
|
|
5253
|
+
...unknownPeriods.length > 0 ? [`unrecognised periodType ${unknownPeriods.join(", ")} \u2014 those subscriptions are excluded from tooling_cost.vendor (not prorated)`] : []
|
|
5254
|
+
]);
|
|
5128
5255
|
}
|
|
5129
5256
|
const servicesPart = stampPart("contract_services", servicesRaw, servicesCond);
|
|
5130
|
-
const unitsPart =
|
|
5257
|
+
const unitsPart = units.part;
|
|
5131
5258
|
const roleCostsPart = stampPart("contract_role_costs", roleCostsRaw, roleCostsCond);
|
|
5132
5259
|
const ratesPart = stampPart("contract_rates", ratesRaw, ratesCond);
|
|
5133
|
-
const ticketsActivityPart = stampPart("tickets", ticketsActRaw, ticketsActCond,
|
|
5260
|
+
const ticketsActivityPart = stampPart("tickets", ticketsActRaw, ticketsActCond, [
|
|
5261
|
+
...ticketsActRaw.hasMore ? [`${fetchedTickets.size} ticket(s) returned of possibly more \u2014 the checks cover the returned set only`] : [],
|
|
5262
|
+
...refTicketIds.truncated ? [`the time-referenced ticket ids are capped at ${v.maxTicketsForJoin} of ${refTicketIds.total} \u2014 the rest read as ticket_not_in_fetched_set`] : []
|
|
5263
|
+
]);
|
|
5134
5264
|
const billedByEntryId = /* @__PURE__ */ new Map();
|
|
5135
5265
|
for (const b of billingRows) {
|
|
5136
5266
|
const te = num2(b["timeEntryID"]);
|
|
@@ -5171,56 +5301,92 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5171
5301
|
bucket.hours += hoursOf(e);
|
|
5172
5302
|
bucket.cost += r.cost ?? 0;
|
|
5173
5303
|
}
|
|
5304
|
+
for (const b of Object.values(sources)) b.cost = cents(b.cost);
|
|
5174
5305
|
const unresolvedHours = sources.missing.hours;
|
|
5175
5306
|
if (unresolvedHours > 0) resolutionNotes.push(`${fmtHours(unresolvedHours)} hours unresolved (missing rate) \u2014 total_cost is a lower bound`);
|
|
5176
5307
|
const costRateResolution = { method: v.method, sources, unresolvedHours, notes: resolutionNotes };
|
|
5177
5308
|
const months = monthsBetween(v.dateFrom, v.dateTo);
|
|
5178
5309
|
const monthOf = (date) => typeof date === "string" && DATE_RE2.test(`${date}`.slice(0, 10)) ? date.slice(0, 7) : null;
|
|
5179
5310
|
const inMonth = (date, m) => monthOf(date) === m;
|
|
5180
|
-
const
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
};
|
|
5185
|
-
const serviceById = new Map(servicesRaw.items.map((s) => [num2(s["id"]), s]));
|
|
5186
|
-
const expectedBasis = servicesRaw.items.length > 0 || unitsRaw.items.length > 0;
|
|
5187
|
-
const expectedIn = (m) => {
|
|
5188
|
-
let total = 0;
|
|
5189
|
-
for (const u of unitsRaw.items) {
|
|
5190
|
-
if (!unitEffectiveIn(u, m)) continue;
|
|
5191
|
-
const svc = serviceById.get(num2(u["contractServiceID"]));
|
|
5192
|
-
if (svc === void 0) continue;
|
|
5193
|
-
total += num2(svc["unitPrice"]) * num2(u["units"]);
|
|
5194
|
-
}
|
|
5195
|
-
return total;
|
|
5196
|
-
};
|
|
5311
|
+
const unitIn = (unit, m) => inMonth(unit["startDate"], m);
|
|
5312
|
+
const lineRevenue = (b) => internalAmount(b, "internalCurrencyExtendedPrice", "extendedPrice");
|
|
5313
|
+
const expectedBasis = unitsPart.status !== "absent" && (servicesRaw.items.length > 0 || unitRows.length > 0);
|
|
5314
|
+
const expectedIn = (m) => unitRows.filter((u) => unitIn(u, m)).reduce((a, u) => a + internalAmount(u, "internalCurrencyPrice", "price"), 0);
|
|
5197
5315
|
const recurringIn = (m) => {
|
|
5198
5316
|
if (!v.includeRecurringCosts) return 0;
|
|
5199
5317
|
let total = 0;
|
|
5200
|
-
for (const c of charges) if (inMonth(c["datePurchased"], m)) total +=
|
|
5201
|
-
for (const u of
|
|
5318
|
+
for (const c of charges) if (inMonth(c["datePurchased"], m)) total += chargeCost(c);
|
|
5319
|
+
for (const u of unitRows) if (unitIn(u, m)) total += num2(u["cost"]);
|
|
5202
5320
|
return total;
|
|
5203
5321
|
};
|
|
5204
5322
|
const invoicesById = new Map(invoices.map((i) => [num2(i["id"]), i]));
|
|
5323
|
+
const billedServiceIn = (m) => {
|
|
5324
|
+
let billed = 0;
|
|
5325
|
+
const ids = [];
|
|
5326
|
+
for (const b of billingRows) {
|
|
5327
|
+
if (!inMonth(b["itemDate"], m) || billingLineType(b) !== "recurring") continue;
|
|
5328
|
+
const invId = num2(b["invoiceID"]);
|
|
5329
|
+
const inv = invId > 0 ? invoicesById.get(invId) : void 0;
|
|
5330
|
+
if (inv === void 0 || inv["isVoided"] === true) continue;
|
|
5331
|
+
billed += lineRevenue(b);
|
|
5332
|
+
ids.push(num2(b["id"]));
|
|
5333
|
+
}
|
|
5334
|
+
return { billed, ids };
|
|
5335
|
+
};
|
|
5336
|
+
const contractRateByRole = /* @__PURE__ */ new Map();
|
|
5337
|
+
for (const r of ratesRaw.items) {
|
|
5338
|
+
const rate = typeof r["internalCurrencyContractHourlyRate"] === "number" ? r["internalCurrencyContractHourlyRate"] : r["contractHourlyRate"];
|
|
5339
|
+
if (num2(r["roleID"]) > 0 && typeof rate === "number" && Number.isFinite(rate)) contractRateByRole.set(num2(r["roleID"]), rate);
|
|
5340
|
+
}
|
|
5341
|
+
const monthlyParts = [
|
|
5342
|
+
["contractServiceUnits", unitsPart],
|
|
5343
|
+
["contractCharges", chargesPart],
|
|
5344
|
+
["contractRoleCosts", roleCostsPart],
|
|
5345
|
+
["billingItems", billingItems],
|
|
5346
|
+
["timeEntries", timeEntries],
|
|
5347
|
+
["expenses", expensesPart],
|
|
5348
|
+
["subscriptions", subscriptionsPart]
|
|
5349
|
+
];
|
|
5350
|
+
const coverageGaps = (m) => {
|
|
5351
|
+
const gaps = [];
|
|
5352
|
+
for (const [name, p] of monthlyParts) {
|
|
5353
|
+
if (p.status === "absent") gaps.push(`${name}: absent (${p.absent})`);
|
|
5354
|
+
else if (truncatedMonths[name] !== void 0) {
|
|
5355
|
+
if (truncatedMonths[name].includes(m)) gaps.push(`${name}: truncated at limit ${v.limit}`);
|
|
5356
|
+
} else if (p.hasMore) gaps.push(`${name}: capped at limit ${v.limit}`);
|
|
5357
|
+
}
|
|
5358
|
+
return gaps;
|
|
5359
|
+
};
|
|
5205
5360
|
const monthly = [];
|
|
5206
5361
|
for (let mi = 0; mi < months.length; mi++) {
|
|
5207
5362
|
const m = months[mi];
|
|
5208
5363
|
const mBilling = billingRows.filter((b) => inMonth(b["itemDate"], m));
|
|
5209
5364
|
let revenueInvoiced = 0;
|
|
5210
5365
|
let revenueVoided = 0;
|
|
5211
|
-
|
|
5366
|
+
let postedUninvoiced = 0;
|
|
5367
|
+
const byType = { recurring: 0, labour: 0, charges: 0, expenses: 0, other: 0 };
|
|
5368
|
+
let unresolvedInvoice = 0;
|
|
5369
|
+
let unresolvedLines = 0;
|
|
5212
5370
|
for (const b of mBilling) {
|
|
5213
5371
|
const invId = num2(b["invoiceID"]);
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5372
|
+
if (invId <= 0) {
|
|
5373
|
+
postedUninvoiced += lineRevenue(b);
|
|
5374
|
+
continue;
|
|
5375
|
+
}
|
|
5376
|
+
const inv = invoicesById.get(invId);
|
|
5377
|
+
if (inv === void 0) {
|
|
5378
|
+
unresolvedInvoice += lineRevenue(b);
|
|
5379
|
+
unresolvedLines += 1;
|
|
5380
|
+
continue;
|
|
5381
|
+
}
|
|
5382
|
+
if (inv["isVoided"] === true) revenueVoided += lineRevenue(b);
|
|
5217
5383
|
else {
|
|
5218
|
-
revenueInvoiced +=
|
|
5219
|
-
|
|
5384
|
+
revenueInvoiced += lineRevenue(b);
|
|
5385
|
+
byType[billingLineType(b)] += lineRevenue(b);
|
|
5220
5386
|
}
|
|
5221
5387
|
}
|
|
5222
5388
|
const expected = v.includeUnbilledRevenue && expectedBasis ? expectedIn(m) : null;
|
|
5223
|
-
const expectedUnbilled = expected === null ? 0 : Math.max(0, expected -
|
|
5389
|
+
const expectedUnbilled = expected === null ? 0 : Math.max(0, expected - billedServiceIn(m).billed);
|
|
5224
5390
|
const mEntries = entries.filter((e) => inMonth(e["dateWorked"], m));
|
|
5225
5391
|
let labourHours = 0;
|
|
5226
5392
|
let labourTotal = 0;
|
|
@@ -5230,7 +5396,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5230
5396
|
rateSources.missing = { entries: 0, hours: 0, cost: 0 };
|
|
5231
5397
|
for (const e of mEntries) {
|
|
5232
5398
|
const r = entryResolution.get(num2(e["id"]));
|
|
5233
|
-
labourHours +=
|
|
5399
|
+
labourHours += hoursOf(e);
|
|
5234
5400
|
const bucket = rateSources[r.source];
|
|
5235
5401
|
bucket.entries += 1;
|
|
5236
5402
|
bucket.hours += hoursOf(e);
|
|
@@ -5238,39 +5404,71 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5238
5404
|
if (r.source === "missing") unresolved += hoursOf(e);
|
|
5239
5405
|
else labourTotal += r.cost ?? 0;
|
|
5240
5406
|
}
|
|
5241
|
-
|
|
5407
|
+
const unposted = mEntries.filter((e) => e["isNonBillable"] !== true && !billedByEntryId.has(num2(e["id"])));
|
|
5408
|
+
const hoursMissing = unposted.some((e) => typeof e["hoursToBill"] !== "number" || !Number.isFinite(e["hoursToBill"]));
|
|
5409
|
+
const hoursPart = hoursMissing ? { entries: unposted.length, hours: null, hoursAbsent: "HOURS_TO_BILL_MISSING" } : { entries: unposted.length, hours: unposted.reduce((a, e) => a + Math.max(0, num2(e["hoursToBill"])), 0) };
|
|
5410
|
+
let unpostedTime;
|
|
5411
|
+
if (!contractTypeKnown) {
|
|
5412
|
+
unpostedTime = { ...hoursPart, value: null, absent: "CONTRACT_TYPE_UNKNOWN" };
|
|
5413
|
+
} else if (!isTimeAndMaterials) {
|
|
5414
|
+
unpostedTime = { ...hoursPart, value: null, absent: "NOT_TIME_AND_MATERIALS_CONTRACT" };
|
|
5415
|
+
} else if (unposted.some((e) => !contractRateByRole.has(num2(e["roleID"])))) {
|
|
5416
|
+
unpostedTime = { ...hoursPart, value: null, absent: "NO_CONTRACT_RATE" };
|
|
5417
|
+
} else if (hoursMissing) {
|
|
5418
|
+
unpostedTime = { ...hoursPart, value: null, absent: "HOURS_TO_BILL_MISSING" };
|
|
5419
|
+
} else {
|
|
5420
|
+
const value = unposted.reduce((a, e) => a + Math.max(0, num2(e["hoursToBill"])) * contractRateByRole.get(num2(e["roleID"])), 0);
|
|
5421
|
+
unpostedTime = { ...hoursPart, value };
|
|
5422
|
+
}
|
|
5423
|
+
let toolingVendor = subscriptionsPart.status === "skipped" || subscriptionsPart.status === "absent" ? null : 0;
|
|
5242
5424
|
if (toolingVendor !== null) {
|
|
5243
5425
|
for (const s of subscriptions) {
|
|
5244
5426
|
const eff = monthOf(s["effectiveDate"]) ?? "0000-00";
|
|
5245
5427
|
const exp = monthOf(s["expirationDate"]) ?? "9999-99";
|
|
5246
|
-
|
|
5428
|
+
const sid = num2(s["id"]);
|
|
5429
|
+
if (cancelledSubs.has(sid)) continue;
|
|
5430
|
+
if (oneTimeSubs.has(sid)) {
|
|
5431
|
+
if (eff === m) toolingVendor += num2(s["periodCost"]);
|
|
5432
|
+
continue;
|
|
5433
|
+
}
|
|
5434
|
+
const factor = subFactor.get(sid) ?? null;
|
|
5435
|
+
if (factor !== null && eff <= m && m <= exp) toolingVendor += num2(s["periodCost"]) * factor;
|
|
5247
5436
|
}
|
|
5248
5437
|
}
|
|
5249
5438
|
const callerSupplied = v.externalMonthlyCosts[mi] ?? 0;
|
|
5250
5439
|
const recurring = recurringIn(m);
|
|
5251
5440
|
let expenseCost = 0;
|
|
5252
|
-
for (const x of expenses) if (inMonth(x["expenseDate"], m)) expenseCost += num2(x["
|
|
5253
|
-
const totalCost = labourTotal +
|
|
5441
|
+
for (const x of expenses) if (inMonth(x["expenseDate"], m)) expenseCost += num2(x["internalCurrencyExpenseAmount"]);
|
|
5442
|
+
const totalCost = labourTotal + callerSupplied + recurring + expenseCost;
|
|
5254
5443
|
const profit = revenueInvoiced - totalCost;
|
|
5255
5444
|
const margin = revenueInvoiced > 0 ? profit / revenueInvoiced : null;
|
|
5256
|
-
const
|
|
5445
|
+
const nonLabour = totalCost - labourTotal;
|
|
5257
5446
|
const resolvedHours = mEntries.filter((e) => entryResolution.get(num2(e["id"])).source !== "missing").reduce((a, e) => a + hoursOf(e), 0);
|
|
5258
|
-
const
|
|
5447
|
+
const avgRate = resolvedHours > 0 ? labourTotal / resolvedHours : 0;
|
|
5448
|
+
const breakEven = avgRate > 0 ? Math.max(0, (revenueInvoiced - nonLabour) / avgRate) : null;
|
|
5449
|
+
for (const b of Object.values(rateSources)) b.cost = cents(b.cost);
|
|
5450
|
+
const gaps = coverageGaps(m);
|
|
5451
|
+
if (unresolvedLines > 0) gaps.push(`invoices: ${unresolvedLines} line(s) on unresolved invoices`);
|
|
5259
5452
|
monthly.push({
|
|
5260
5453
|
month: m,
|
|
5261
|
-
revenue_invoiced: revenueInvoiced,
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5454
|
+
revenue_invoiced: cents(revenueInvoiced),
|
|
5455
|
+
revenue_by_type: { recurring: cents(byType.recurring), labour: cents(byType.labour), charges: cents(byType.charges), expenses: cents(byType.expenses), other: cents(byType.other) },
|
|
5456
|
+
revenue_voided_excluded: cents(revenueVoided),
|
|
5457
|
+
revenue_posted_uninvoiced: cents(postedUninvoiced),
|
|
5458
|
+
revenue_unresolved_invoice: cents(unresolvedInvoice),
|
|
5459
|
+
revenue_expected: cents(expected),
|
|
5460
|
+
revenue_expected_unbilled: cents(expectedUnbilled),
|
|
5461
|
+
unposted_time: { ...unpostedTime, value: cents(unpostedTime.value) },
|
|
5265
5462
|
labour_hours: labourHours,
|
|
5266
|
-
labour_cost: { total: labourTotal, unresolvedHours: unresolved, rateSources },
|
|
5267
|
-
tooling_cost: { vendor: toolingVendor, caller_supplied: callerSupplied },
|
|
5268
|
-
recurring_cost: recurring,
|
|
5269
|
-
expense_cost: expenseCost,
|
|
5270
|
-
total_cost: totalCost,
|
|
5271
|
-
profit,
|
|
5463
|
+
labour_cost: { total: cents(labourTotal), unresolvedHours: unresolved, rateSources },
|
|
5464
|
+
tooling_cost: { vendor: cents(toolingVendor), caller_supplied: cents(callerSupplied) },
|
|
5465
|
+
recurring_cost: cents(recurring),
|
|
5466
|
+
expense_cost: cents(expenseCost),
|
|
5467
|
+
total_cost: cents(totalCost),
|
|
5468
|
+
profit: cents(profit),
|
|
5272
5469
|
margin,
|
|
5273
|
-
break_even_hours: breakEven
|
|
5470
|
+
break_even_hours: breakEven,
|
|
5471
|
+
coverage_gaps: gaps
|
|
5274
5472
|
});
|
|
5275
5473
|
}
|
|
5276
5474
|
const warnings = [];
|
|
@@ -5334,13 +5532,13 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5334
5532
|
const mA = months[i - 1];
|
|
5335
5533
|
const mB = months[i];
|
|
5336
5534
|
for (const c of charges) {
|
|
5337
|
-
const cA = inMonth(c["datePurchased"], mA) ?
|
|
5338
|
-
const cB = inMonth(c["datePurchased"], mB) ?
|
|
5535
|
+
const cA = inMonth(c["datePurchased"], mA) ? chargeCost(c) : 0;
|
|
5536
|
+
const cB = inMonth(c["datePurchased"], mB) ? chargeCost(c) : 0;
|
|
5339
5537
|
if (Math.abs(cB - cA) > 0) driftIds.add(num2(c["id"]));
|
|
5340
5538
|
}
|
|
5341
|
-
for (const u of
|
|
5342
|
-
const cA =
|
|
5343
|
-
const cB =
|
|
5539
|
+
for (const u of unitRows) {
|
|
5540
|
+
const cA = unitIn(u, mA) ? num2(u["cost"]) : 0;
|
|
5541
|
+
const cB = unitIn(u, mB) ? num2(u["cost"]) : 0;
|
|
5344
5542
|
if (Math.abs(cB - cA) > 0) driftIds.add(num2(u["id"]));
|
|
5345
5543
|
}
|
|
5346
5544
|
}
|
|
@@ -5348,22 +5546,10 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5348
5546
|
warnings.push({ code: "recurring_cost_drift", count: driftCount, sample_ids: dedupeSortCap([...driftIds], v.limit) });
|
|
5349
5547
|
let expectedUnbilledCount = 0;
|
|
5350
5548
|
const expectedUnbilledSamples = [];
|
|
5351
|
-
if (v.includeUnbilledRevenue) {
|
|
5352
|
-
for (
|
|
5353
|
-
const
|
|
5354
|
-
|
|
5355
|
-
if (expected === null) continue;
|
|
5356
|
-
const mBilling = billingRows.filter((b) => inMonth(b["itemDate"], m));
|
|
5357
|
-
let billed = 0;
|
|
5358
|
-
const ids = [];
|
|
5359
|
-
for (const b of mBilling) {
|
|
5360
|
-
const invId = num2(b["invoiceID"]);
|
|
5361
|
-
const inv = invId > 0 ? invoicesById.get(invId) : void 0;
|
|
5362
|
-
if (inv === void 0 || inv["isVoided"] === true) continue;
|
|
5363
|
-
billed += num2(b["extendedPrice"]);
|
|
5364
|
-
ids.push(num2(b["id"]));
|
|
5365
|
-
}
|
|
5366
|
-
if (expected > billed + 5e-3) {
|
|
5549
|
+
if (v.includeUnbilledRevenue && expectedBasis) {
|
|
5550
|
+
for (const m of months) {
|
|
5551
|
+
const { billed, ids } = billedServiceIn(m);
|
|
5552
|
+
if (expectedIn(m) > billed + 5e-3) {
|
|
5367
5553
|
expectedUnbilledCount += 1;
|
|
5368
5554
|
for (const i of ids) expectedUnbilledSamples.push(i);
|
|
5369
5555
|
}
|
|
@@ -5379,7 +5565,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5379
5565
|
});
|
|
5380
5566
|
const stale = [];
|
|
5381
5567
|
if (openStatusSet !== null) {
|
|
5382
|
-
for (const t of
|
|
5568
|
+
for (const t of contractTickets) {
|
|
5383
5569
|
const status = t["status"];
|
|
5384
5570
|
if (typeof status !== "number" || !openStatusSet.has(status)) continue;
|
|
5385
5571
|
const lastDay = dateToDay(t["lastActivityDate"]);
|
|
@@ -5431,7 +5617,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5431
5617
|
for (const x of expenses) {
|
|
5432
5618
|
const t = num2(x["ticketID"]);
|
|
5433
5619
|
if (t <= 0) continue;
|
|
5434
|
-
expenseByTicket.set(t, (expenseByTicket.get(t) ?? 0) + num2(x["
|
|
5620
|
+
expenseByTicket.set(t, (expenseByTicket.get(t) ?? 0) + num2(x["internalCurrencyExpenseAmount"]));
|
|
5435
5621
|
}
|
|
5436
5622
|
const ticketCost = (t) => {
|
|
5437
5623
|
const tid = num2(t["id"]);
|
|
@@ -5440,7 +5626,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5440
5626
|
for (const e of es) cost += entryResolution.get(num2(e["id"])).cost ?? 0;
|
|
5441
5627
|
return cost;
|
|
5442
5628
|
};
|
|
5443
|
-
const ticketHours = (t) => (entriesByTicket.get(num2(t["id"])) ?? []).reduce((a, e) => a +
|
|
5629
|
+
const ticketHours = (t) => (entriesByTicket.get(num2(t["id"])) ?? []).reduce((a, e) => a + hoursOf(e), 0);
|
|
5444
5630
|
const allTickets = [...fetchedTickets.values()];
|
|
5445
5631
|
const activityCategories = [];
|
|
5446
5632
|
if (v.groupByActivity) {
|
|
@@ -5458,7 +5644,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5458
5644
|
for (const t of tickets) {
|
|
5459
5645
|
const es = entriesByTicket.get(num2(t["id"])) ?? [];
|
|
5460
5646
|
entryCount += es.length;
|
|
5461
|
-
hours += es.reduce((a, e) => a +
|
|
5647
|
+
hours += es.reduce((a, e) => a + hoursOf(e), 0);
|
|
5462
5648
|
cost += ticketCost(t);
|
|
5463
5649
|
}
|
|
5464
5650
|
activityCategories.push({
|
|
@@ -5466,7 +5652,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5466
5652
|
ticketCount: tickets.length,
|
|
5467
5653
|
entryCount,
|
|
5468
5654
|
hours,
|
|
5469
|
-
cost,
|
|
5655
|
+
cost: cents(cost),
|
|
5470
5656
|
sample_ticketNumbers: dedupeSortCap(tickets.map((t) => num2(t["ticketNumber"])).filter((n) => n > 0), v.limit)
|
|
5471
5657
|
});
|
|
5472
5658
|
}
|
|
@@ -5478,7 +5664,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5478
5664
|
title: typeof t["title"] === "string" ? t["title"] : "",
|
|
5479
5665
|
status: typeof t["status"] === "number" ? t["status"] : null,
|
|
5480
5666
|
hours: ticketHours(t),
|
|
5481
|
-
cost: ticketCost(t),
|
|
5667
|
+
cost: cents(ticketCost(t)),
|
|
5482
5668
|
lastActivityDate: typeof t["lastActivityDate"] === "string" ? t["lastActivityDate"] : null
|
|
5483
5669
|
})).sort((a, b) => b.cost - a.cost || a.ticketId - b.ticketId).slice(0, v.limit);
|
|
5484
5670
|
const miscoding = /* @__PURE__ */ new Map();
|
|
@@ -5494,7 +5680,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5494
5680
|
const possibleMiscoding = [...miscoding.values()].sort((a, b) => a.entryId - b.entryId).slice(0, v.limit);
|
|
5495
5681
|
const openRenewal = [];
|
|
5496
5682
|
if (openStatusSet !== null) {
|
|
5497
|
-
for (const t of
|
|
5683
|
+
for (const t of contractTickets) {
|
|
5498
5684
|
const status = t["status"];
|
|
5499
5685
|
if (typeof status !== "number" || !openStatusSet.has(status)) continue;
|
|
5500
5686
|
const last = t["lastActivityDate"];
|
|
@@ -5515,7 +5701,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5515
5701
|
id: num2(s["id"]),
|
|
5516
5702
|
label: typeof s["subscriptionName"] === "string" && s["subscriptionName"] !== "" ? s["subscriptionName"] : `subscription ${num2(s["id"])}`,
|
|
5517
5703
|
date: typeof s["expirationDate"] === "string" ? s["expirationDate"].slice(0, 10) : null,
|
|
5518
|
-
detail: `periodType: ${typeof s["periodType"] === "
|
|
5704
|
+
detail: `periodType: ${typeof s["periodType"] === "number" ? s["periodType"] : "unknown"} (period cost ${fmtNum(num2(s["periodCost"]))}${cancelledSubs.has(num2(s["id"])) ? " \u2014 not active, not in tooling_cost.vendor" : oneTimeSubs.has(num2(s["id"])) ? " \u2014 one-time, booked in its effective month" : subFactor.get(num2(s["id"])) == null ? " \u2014 unrecognised period, not in tooling_cost.vendor" : `, ${fmtNum(num2(s["periodCost"]) * subFactor.get(num2(s["id"])))}/month`})`
|
|
5519
5705
|
});
|
|
5520
5706
|
}
|
|
5521
5707
|
const contractEndDay = dateToDay(contractRow["endDate"]);
|
|
@@ -5528,31 +5714,37 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5528
5714
|
detail: `the contract ends ${String(contractRow["endDate"]).slice(0, 10)}`
|
|
5529
5715
|
});
|
|
5530
5716
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
const
|
|
5536
|
-
if (included === 0 && worked === 0) continue;
|
|
5537
|
-
const left = included - worked;
|
|
5538
|
-
const my = Number(m.slice(0, 4));
|
|
5539
|
-
const mm = Number(m.slice(5, 7));
|
|
5540
|
-
const lastDay = new Date(Date.UTC(my, mm, 0)).getUTCDate();
|
|
5717
|
+
const blocks = blocksPart.items;
|
|
5718
|
+
for (const b of blocks) {
|
|
5719
|
+
const purchased = num2(b["hours"]);
|
|
5720
|
+
const approved = num2(b["hoursApproved"]);
|
|
5721
|
+
const left = purchased - approved;
|
|
5541
5722
|
openRenewal.push({
|
|
5542
5723
|
kind: "included_hours_burn",
|
|
5543
|
-
id:
|
|
5544
|
-
label: `
|
|
5545
|
-
date:
|
|
5546
|
-
detail: `${fmtHours(
|
|
5724
|
+
id: num2(b["id"]),
|
|
5725
|
+
label: `hour block ${num2(b["id"])}`,
|
|
5726
|
+
date: typeof b["endDate"] === "string" ? b["endDate"].slice(0, 10) : null,
|
|
5727
|
+
detail: `${fmtHours(approved)} h approved of ${fmtHours(purchased)} h purchased (${left >= 0 ? `${fmtHours(left)} h left` : `${fmtHours(Math.abs(left))} h over`})`
|
|
5547
5728
|
});
|
|
5548
5729
|
}
|
|
5730
|
+
const purchasedHours = blocks.reduce((a, b) => a + num2(b["hours"]), 0);
|
|
5731
|
+
const approvedHours = blocks.reduce((a, b) => a + num2(b["hoursApproved"]), 0);
|
|
5732
|
+
const includedHours = !contractTypeKnown ? { value: null, absent: "CONTRACT_TYPE_UNKNOWN" } : !isBlockHours ? { value: null, absent: "NOT_BLOCK_HOURS_CONTRACT" } : blocksPart.status === "absent" ? { value: null, absent: blocksPart.absent } : { value: { purchased: purchasedHours, approved: approvedHours, remaining: purchasedHours - approvedHours } };
|
|
5733
|
+
const rows = (part) => {
|
|
5734
|
+
if (v.includeRows) return part;
|
|
5735
|
+
const { items, ...stamps } = part;
|
|
5736
|
+
return { ...stamps, rows_fetched: items.length };
|
|
5737
|
+
};
|
|
5549
5738
|
return {
|
|
5550
5739
|
contract,
|
|
5551
5740
|
customer,
|
|
5741
|
+
...customerAbsent !== void 0 ? { customerAbsent } : {},
|
|
5742
|
+
contract_type: contractTypeKnown ? { id: contractType, label: contractTypeLabel } : { id: contractType > 0 ? contractType : null, label: null, absent: "CONTRACT_TYPE_UNKNOWN" },
|
|
5743
|
+
included_hours: includedHours,
|
|
5552
5744
|
period: { dateFrom: v.dateFrom, dateTo: v.dateTo, months, asOf },
|
|
5553
5745
|
currency: {
|
|
5554
5746
|
companyCurrencyID: customer && typeof customer["currencyID"] === "number" ? customer["currencyID"] : null,
|
|
5555
|
-
note: "
|
|
5747
|
+
note: "single-currency P&L in the INTERNAL currency: revenue is billing items internalCurrencyExtendedPrice and service units internalCurrencyPrice (each falling back to the local field when absent), unposted value the internalCurrencyContractHourlyRate; costs are internal (ourCost/role cost/internalCost, charge extendedCost, unit cost, internalCurrencyExpenseAmount). companyCurrencyID is the customer context stamp only"
|
|
5556
5748
|
},
|
|
5557
5749
|
monthly,
|
|
5558
5750
|
activity_categories: activityCategories,
|
|
@@ -5569,6 +5761,7 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5569
5761
|
contractCharges: chargesPart.status,
|
|
5570
5762
|
contractRoleCosts: roleCostsPart.status,
|
|
5571
5763
|
contractRates: ratesPart.status,
|
|
5764
|
+
contractBlocks: blocksPart.status,
|
|
5572
5765
|
billingItems: billingItems.status,
|
|
5573
5766
|
invoices: invoicesPart.status,
|
|
5574
5767
|
timeEntries: timeEntries.status,
|
|
@@ -5580,22 +5773,23 @@ async function profitability(resource, contractId, opts, http) {
|
|
|
5580
5773
|
configurationItems: configurationItemsPart.status,
|
|
5581
5774
|
subscriptions: subscriptionsPart.status
|
|
5582
5775
|
},
|
|
5583
|
-
contractServices: servicesPart,
|
|
5584
|
-
contractServiceUnits: unitsPart,
|
|
5585
|
-
contractCharges: chargesPart,
|
|
5586
|
-
contractRoleCosts: roleCostsPart,
|
|
5587
|
-
contractRates: ratesPart,
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5776
|
+
contractServices: rows(servicesPart),
|
|
5777
|
+
contractServiceUnits: rows(unitsPart),
|
|
5778
|
+
contractCharges: rows(chargesPart),
|
|
5779
|
+
contractRoleCosts: rows(roleCostsPart),
|
|
5780
|
+
contractRates: rows(ratesPart),
|
|
5781
|
+
contractBlocks: rows(blocksPart),
|
|
5782
|
+
billingItems: rows(billingItems),
|
|
5783
|
+
invoices: rows(invoicesPart),
|
|
5784
|
+
timeEntries: rows(timeEntries),
|
|
5785
|
+
ticketsActivity: rows(ticketsActivityPart),
|
|
5786
|
+
ticketsOpen: rows(ticketsOpenPart),
|
|
5787
|
+
resources: rows(resourcesPart),
|
|
5788
|
+
expenses: rows(expensesPart),
|
|
5789
|
+
miscodingProbe: rows(probePart),
|
|
5790
|
+
configurationItems: rows(configurationItemsPart),
|
|
5791
|
+
subscriptions: rows(subscriptionsPart),
|
|
5792
|
+
wires: { issued: wires.issued, requests: wires.requests, budget: wires.budget, note: wires.note }
|
|
5599
5793
|
};
|
|
5600
5794
|
}
|
|
5601
5795
|
|