diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx
index 419de25fa..5de4231f7 100644
--- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx
+++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx
@@ -25,6 +25,9 @@ export default function OwnerDetailFormComponent({ form, loading }) {
+
+
+
diff --git a/client/src/graphql/owners.queries.js b/client/src/graphql/owners.queries.js
index e28fc8aa2..d169263f4 100644
--- a/client/src/graphql/owners.queries.js
+++ b/client/src/graphql/owners.queries.js
@@ -48,6 +48,7 @@ export const QUERY_OWNER_BY_ID = gql`
query QUERY_OWNER_BY_ID($id: uuid!) {
owners_by_pk(id: $id) {
id
+ accountingid
allow_text_message
ownr_addr1
ownr_addr2
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index e86e9936e..337047e1f 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -2394,6 +2394,7 @@
"selectexistingornew": "Select an existing owner record or create a new one. "
},
"fields": {
+ "accountingid": "Accounting ID",
"address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "Production not in Production Status",
"production_over_time": "Production Level over Time",
"psr_by_make": "Percent of Sales by Vehicle Make",
+ "purchase_return_ratio_excel": "Purchase & Return Ratio - Excel",
"purchase_return_ratio_grouped_by_vendor_detail": "Purchase & Return Ratio by Vendor (Detail)",
"purchase_return_ratio_grouped_by_vendor_summary": "Purchase & Return Ratio by Vendor (Summary)",
"purchases_by_cost_center_detail": "Purchases by Cost Center (Detail)",
@@ -3082,6 +3084,7 @@
"timetickets": "Time Tickets",
"timetickets_employee": "Employee Time Tickets",
"timetickets_summary": "Time Tickets Summary",
+ "total_loss_jobs": "Jobs Marked as Total Loss",
"unclaimed_hrs": "Unflagged Hours",
"void_ros": "Void ROs",
"work_in_progress_committed_labour": "Work in Progress - Committed Labor",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 8b9026489..02c3dd2dd 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -2394,6 +2394,7 @@
"selectexistingornew": ""
},
"fields": {
+ "accountingid": "",
"address": "Dirección",
"allow_text_message": "Permiso de texto?",
"name": "Nombre",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "",
"production_over_time": "",
"psr_by_make": "",
+ "purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "",
@@ -3082,6 +3084,7 @@
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": "",
+ "total_loss_jobs": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_committed_labour": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 8f9330ea0..7ea7ef02c 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -2394,6 +2394,7 @@
"selectexistingornew": ""
},
"fields": {
+ "accountingid": "",
"address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom",
@@ -3057,6 +3058,7 @@
"production_not_production_status": "",
"production_over_time": "",
"psr_by_make": "",
+ "purchase_return_ratio_excel": "",
"purchase_return_ratio_grouped_by_vendor_detail": "",
"purchase_return_ratio_grouped_by_vendor_summary": "",
"purchases_by_cost_center_detail": "",
@@ -3082,6 +3084,7 @@
"timetickets": "",
"timetickets_employee": "",
"timetickets_summary": "",
+ "total_loss_jobs": "",
"unclaimed_hrs": "",
"void_ros": "",
"work_in_progress_committed_labour": "",
diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js
index 9f80986d2..9e40624c0 100644
--- a/client/src/utils/TemplateConstants.js
+++ b/client/src/utils/TemplateConstants.js
@@ -2184,6 +2184,30 @@ export const TemplateList = (type, context) => {
},
group: "payroll",
adp_payroll: true
+ },
+ purchase_return_ratio_excel: {
+ title: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
+ subject: i18n.t("reportcenter.templates.purchase_return_ratio_excel"),
+ key: "purchase_return_ratio_excel",
+ //idtype: "vendor",
+ reporttype: "excel",
+ disabled: false,
+ rangeFilter: {
+ object: i18n.t("reportcenter.labels.objects.bills"),
+ field: i18n.t("bills.fields.date")
+ },
+ group: "purchases"
+ },
+ total_loss_jobs: {
+ title: i18n.t("reportcenter.templates.total_loss_jobs"),
+ subject: i18n.t("reportcenter.templates.total_loss_jobs"),
+ key: "total_loss_jobs",
+ disabled: false,
+ rangeFilter: {
+ object: i18n.t("reportcenter.labels.objects.jobs"),
+ field: i18n.t("jobs.fields.date_open")
+ },
+ group: "jobs"
}
}
: {}),
diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js
index f57c6711a..c7c74928b 100644
--- a/server/accounting/qbo/qbo-receivables.js
+++ b/server/accounting/qbo/qbo-receivables.js
@@ -328,6 +328,7 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st
},
+ ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
...(isThreeTier
? {
Job: true,
@@ -395,7 +396,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
PostalCode: job.ownr_zip,
CountrySubDivisionCode: job.ownr_st
},
-
+ ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {}),
Job: true,
ParentRef: {
value: parentTierRef.Id
@@ -556,7 +557,8 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
- }
+ },
+ ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
};
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
@@ -673,7 +675,8 @@ async function InsertInvoiceMultiPayerInvoice(
Line3: `${job.ownr_city || ""}, ${job.ownr_st || ""} ${job.ownr_zip || ""}`.trim(),
Line2: job.ownr_addr1 || "",
Line1: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`
- }
+ },
+ ...(job.ownr_ea ? { BillEmail: { Address: job.ownr_ea.trim() } } : {})
};
logger.log("qbo-receivable-objectlog", "DEBUG", req.user.email, job.id, {
diff --git a/server/render/canvas-handler.js b/server/render/canvas-handler.js
index b35926935..946b6d082 100644
--- a/server/render/canvas-handler.js
+++ b/server/render/canvas-handler.js
@@ -5,6 +5,9 @@ const Chart = require("chart.js/auto");
const { backgroundColors, borderColors } = require("./canvas-colors");
const { defaultsDeep, isNumber } = require("lodash");
+let isProcessing = false;
+const requestQueue = [];
+
try {
FontLibrary.use("Montserrat", [
"/usr/share/fonts/Montserrat-Regular.ttf",
@@ -60,69 +63,81 @@ const getChartConfiguration = (keys, values, override) => {
return defaultsDeep(override || {}, defaultConfiguration);
};
-exports.canvastest = function (req, res) {
- res.status(200).send("OK");
-};
-
-exports.canvas = function (req, res) {
- const { logger } = req;
-
- const { w, h, values, keys, override } = req.body;
- logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
-
- // Set the default Width and Height
- let [width, height] = [500, 275];
-
- // Allow for custom width and height
- if (isNumber(w)) {
- width = w;
- }
- if (isNumber(h)) {
- height = h;
- }
-
- const configuration = getChartConfiguration(keys, values, override);
-
- res.status(200).send(
- (() => {
- const canvas = createCanvas(width, height);
- const ctx = canvas.getContext("2d");
- new Chart(ctx, configuration);
- return canvas.toDataURL();
- })()
- );
-};
-
-exports.canvasSkia = async function (req, res) {
+const processCanvasRequest = async (req, res, isSkia = false) => {
const { logger } = req;
const { w, h, values, keys, override } = req.body;
- // Log incoming request for debugging
logger.log("inbound-canvas-creation", "debug", "jsr", null, { w, h, values, keys, override });
// Default width and height
- const width = typeof w === "number" && w > 0 ? w : 500;
- const height = typeof h === "number" && h > 0 ? h : 275;
+ const width = isNumber(w) && w > 0 ? w : 500;
+ const height = isNumber(h) && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override);
try {
- // Create a canvas and get the 2D rendering context
- const canvas = new Canvas(width, height);
+ const canvas = isSkia ? new Canvas(width, height) : createCanvas(width, height);
const ctx = canvas.getContext("2d");
// Render the chart
- new Chart(ctx, configuration);
+ const chart = new Chart(ctx, configuration);
- // Convert the canvas to a Base64-encoded image
- const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
- const dataURL = `data:image/png;base64,${chartImage}`;
+ // Generate and send the image
+ const chartImage = isSkia
+ ? (await canvas.toBuffer("image/png")).toString("base64")
+ : canvas.toDataURL();
- // Send the Base64-encoded image as the response
- res.status(200).send(dataURL);
+ chart.destroy();
+ res.status(200).send(isSkia ? `data:image/png;base64,${chartImage}` : chartImage);
} catch (error) {
- // Log and handle rendering errors
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
res.status(500).send("Failed to generate canvas.");
}
};
+
+const processNextInQueue = async () => {
+ if (requestQueue.length === 0) {
+ isProcessing = false;
+ return;
+ }
+
+ const { req, res, isSkia } = requestQueue.shift();
+ await processCanvasRequest(req, res, isSkia);
+ processNextInQueue();
+};
+
+exports.canvastest = function (req, res) {
+ res.status(200).send("OK");
+};
+
+exports.canvas = async function (req, res) {
+ if (isProcessing) {
+ if (requestQueue.length >= 100) {
+ // Set a maximum queue size
+ return res.status(503).send("Server is busy. Please try again later.");
+ }
+ requestQueue.push({ req, res, isSkia: false });
+ req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
+ return;
+ }
+
+ isProcessing = true;
+ await processCanvasRequest(req, res, false);
+ processNextInQueue();
+};
+
+exports.canvasSkia = async function (req, res) {
+ if (isProcessing) {
+ if (requestQueue.length >= 100) {
+ // Set a maximum queue size
+ return res.status(503).send("Server is busy. Please try again later.");
+ }
+ requestQueue.push({ req, res, isSkia: true });
+ req.logger.log("inbound-canvas-creation-queue", "debug", "jsr", null, { queue: requestQueue.length });
+ return;
+ }
+
+ isProcessing = true;
+ await processCanvasRequest(req, res, true);
+ processNextInQueue();
+};