test(materials per day): work on getting this running better
This commit is contained in:
@@ -9,11 +9,14 @@ import {
|
||||
import { db } from "../../../../../database/dbclient.js";
|
||||
import { invHistoricalData } from "../../../../../database/schema/historicalINV.js";
|
||||
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||
import { createLog } from "../../../logger/logger.js";
|
||||
import { query } from "../../../sqlServer/prodSqlServer.js";
|
||||
import {
|
||||
materialPerDay,
|
||||
materialPurchasesPerDay,
|
||||
} from "../../../sqlServer/querys/dataMart/materialPerDay.js";
|
||||
import { singleArticle } from "../../../sqlServer/querys/misc/singleArticle.js";
|
||||
import { sendEmail } from "../sendMail.js";
|
||||
import { materialPurchases } from "./materialPurchases.js";
|
||||
import { buildInventoryTimeline } from "./materialWithInv.js";
|
||||
|
||||
@@ -99,43 +102,77 @@ export default async function materialPerDayCheck() {
|
||||
|
||||
// purchases
|
||||
|
||||
const { data: p, error: pe } = (await tryCatch(
|
||||
query(
|
||||
materialPurchasesPerDay
|
||||
.replace("[startDate]", startDate)
|
||||
.replace("[endDate]", endDate),
|
||||
"material check",
|
||||
),
|
||||
)) as any;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Error getting the material data",
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
if (!data.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: data.message,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
const pOrders = (await materialPurchases({ startDate, endDate })) as any;
|
||||
|
||||
//console.log(pOrders);
|
||||
const openingInventory: Record<string, number> = {};
|
||||
const inventoryRows = await getInv();
|
||||
for (const row of inventoryRows) {
|
||||
openingInventory[row.article] = Number(row.total_QTY) || 0;
|
||||
}
|
||||
|
||||
const materialsDemand = buildInventoryTimeline(
|
||||
sumByMaterialAndWeek(data.data) as any,
|
||||
openingInventory,
|
||||
pOrders,
|
||||
);
|
||||
|
||||
const { data: av, error: eav } = await tryCatch(
|
||||
query(singleArticle.replace("[av]", "107"), "single article"),
|
||||
);
|
||||
|
||||
const formattedMaterials = materialsDemand
|
||||
.filter((n) => n.MaterialHumanReadableId === `${av?.data[0].article}`)
|
||||
.map((i) => ({
|
||||
...i,
|
||||
OpeningInventory: i.OpeningInventory?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
Purchases: i.Purchases?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
Consumption: i.Consumption?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
ClosingInventory: i.ClosingInventory?.toLocaleString("en-US", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}),
|
||||
}));
|
||||
|
||||
// send the email stuff
|
||||
const emailSetup = {
|
||||
email: "blake.matthes@alpla.com",
|
||||
subject: `Material Week.`,
|
||||
template: "materialPerDay",
|
||||
context: {
|
||||
items: formattedMaterials,
|
||||
article: av?.data[0].combined,
|
||||
},
|
||||
};
|
||||
|
||||
const { data: sentEmail, error: sendEmailError } = await tryCatch(
|
||||
sendEmail(emailSetup),
|
||||
);
|
||||
if (sendEmailError) {
|
||||
createLog(
|
||||
"error",
|
||||
"blocking",
|
||||
"notify",
|
||||
"Failed to send email, will try again on next interval",
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to send email, will try again on next interval",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "material data",
|
||||
data: buildInventoryTimeline(
|
||||
sumByMaterialAndWeek(data.data) as any,
|
||||
openingInventory,
|
||||
),
|
||||
data: formattedMaterials,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { formatISO, parseISO, startOfWeek } from "date-fns";
|
||||
import { tryCatch } from "../../../../globalUtils/tryCatch.js";
|
||||
import { query } from "../../../sqlServer/prodSqlServer.js";
|
||||
import { materialPurchasesPerDay } from "../../../sqlServer/querys/dataMart/materialPerDay.js";
|
||||
|
||||
function toDate(val: any) {
|
||||
if (val instanceof Date) return val;
|
||||
@@ -6,20 +9,52 @@ function toDate(val: any) {
|
||||
return new Date(val);
|
||||
}
|
||||
|
||||
export const materialPurchases = async (data: any) => {
|
||||
export const materialPurchases = async ({
|
||||
startDate,
|
||||
endDate,
|
||||
}: {
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}) => {
|
||||
/** @type {Record<string, Record<string, number>>} */
|
||||
const grouped: any = {};
|
||||
|
||||
for (const r of data) {
|
||||
const { data: p, error } = (await tryCatch(
|
||||
query(
|
||||
materialPurchasesPerDay
|
||||
.replace("[startDate]", startDate)
|
||||
.replace("[endDate]", endDate),
|
||||
"material check",
|
||||
),
|
||||
)) as any;
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: "Error getting the material data",
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
if (!p.success) {
|
||||
return {
|
||||
success: false,
|
||||
message: p.message,
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
|
||||
for (const r of p.data) {
|
||||
const pOrder = String(r.purhcaseOrder);
|
||||
const mat = String(r.MaterialHumanReadableId);
|
||||
const d = toDate(r.CalDate);
|
||||
const d = toDate(r.deliveryDate);
|
||||
const week = formatISO(startOfWeek(d, { weekStartsOn: 1 }), {
|
||||
representation: "date",
|
||||
});
|
||||
|
||||
grouped[mat] ??= {};
|
||||
grouped[mat][week] ??= 0;
|
||||
grouped[mat][week] += Number(r.DailyMaterialDemand) || 0;
|
||||
grouped[mat][week] += Number(r.qty) || 0;
|
||||
}
|
||||
|
||||
const result = [];
|
||||
@@ -29,7 +64,7 @@ export const materialPurchases = async (data: any) => {
|
||||
result.push({
|
||||
MaterialHumanReadableId: mat,
|
||||
WeekStart: week,
|
||||
WeeklyDemand: Number(total).toFixed(2),
|
||||
WeeklyPurchase: Number(total).toFixed(2),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,25 +5,51 @@ export const buildInventoryTimeline = (
|
||||
WeeklyDemand: number;
|
||||
}>,
|
||||
opening: Record<string, number>,
|
||||
weeklyPurchases?: Array<{
|
||||
MaterialHumanReadableId: string;
|
||||
WeekStart: string;
|
||||
WeeklyPurchase: number;
|
||||
}>,
|
||||
) => {
|
||||
// group weekly demand by material
|
||||
const grouped: Record<
|
||||
const groupedDemand: Record<
|
||||
string,
|
||||
Array<{ WeekStart: string; Demand: number }>
|
||||
> = {};
|
||||
|
||||
for (const d of weeklyDemand) {
|
||||
const mat = d.MaterialHumanReadableId;
|
||||
grouped[mat] ??= [];
|
||||
grouped[mat].push({
|
||||
groupedDemand[mat] ??= [];
|
||||
groupedDemand[mat].push({
|
||||
WeekStart: d.WeekStart,
|
||||
Demand: Number(d.WeeklyDemand),
|
||||
});
|
||||
}
|
||||
|
||||
// sort weeks chronologically per material
|
||||
for (const mat of Object.keys(grouped)) {
|
||||
grouped[mat].sort(
|
||||
// group weekly purchases by material
|
||||
const groupedPurchases: Record<
|
||||
string,
|
||||
Array<{ WeekStart: string; Purchase: number }>
|
||||
> = {};
|
||||
if (weeklyPurchases) {
|
||||
for (const p of weeklyPurchases) {
|
||||
const mat = p.MaterialHumanReadableId;
|
||||
groupedPurchases[mat] ??= [];
|
||||
groupedPurchases[mat].push({
|
||||
WeekStart: p.WeekStart,
|
||||
Purchase: Number(p.WeeklyPurchase),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// sort both chronologically
|
||||
for (const mat of Object.keys(groupedDemand)) {
|
||||
groupedDemand[mat].sort(
|
||||
(a, b) =>
|
||||
new Date(a.WeekStart).getTime() - new Date(b.WeekStart).getTime(),
|
||||
);
|
||||
}
|
||||
for (const mat of Object.keys(groupedPurchases)) {
|
||||
groupedPurchases[mat].sort(
|
||||
(a, b) =>
|
||||
new Date(a.WeekStart).getTime() - new Date(b.WeekStart).getTime(),
|
||||
);
|
||||
@@ -33,25 +59,30 @@ export const buildInventoryTimeline = (
|
||||
MaterialHumanReadableId: string;
|
||||
WeekStart: string;
|
||||
OpeningInventory: number;
|
||||
Purchases: number;
|
||||
Consumption: number;
|
||||
ClosingInventory: number;
|
||||
}> = [];
|
||||
|
||||
for (const [mat, weeks] of Object.entries(grouped)) {
|
||||
// get starting inventory from the ERP result
|
||||
for (const [mat, weeks] of Object.entries(groupedDemand)) {
|
||||
let inv = opening[mat] ?? 0;
|
||||
const purchasesForMaterial = groupedPurchases[mat] ?? [];
|
||||
|
||||
for (const w of weeks) {
|
||||
const week = w.WeekStart;
|
||||
const demand = Number(w.Demand);
|
||||
for (const week of weeks) {
|
||||
const demand = Number(week.Demand);
|
||||
const purchase =
|
||||
purchasesForMaterial.find((p) => p.WeekStart === week.WeekStart)
|
||||
?.Purchase ?? 0;
|
||||
|
||||
const openingInv = inv;
|
||||
const closingInv = openingInv - demand;
|
||||
const adjustedInv = openingInv + purchase;
|
||||
const closingInv = adjustedInv - demand;
|
||||
|
||||
result.push({
|
||||
MaterialHumanReadableId: mat,
|
||||
WeekStart: week,
|
||||
WeekStart: week.WeekStart,
|
||||
OpeningInventory: Number(openingInv.toFixed(2)),
|
||||
Purchases: Number(purchase.toFixed(2)),
|
||||
Consumption: Number(demand.toFixed(2)),
|
||||
ClosingInventory: Number(closingInv.toFixed(2)),
|
||||
});
|
||||
|
||||
@@ -1,149 +1,152 @@
|
||||
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
||||
import { db } from "../../../../database/dbclient.js";
|
||||
import { settings } from "../../../../database/schema/settings.js";
|
||||
import nodemailer from "nodemailer";
|
||||
import Handlebars from "handlebars";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
|
||||
import nodemailer from "nodemailer";
|
||||
import type Mail from "nodemailer/lib/mailer/index.js";
|
||||
import type { Address } from "nodemailer/lib/mailer/index.js";
|
||||
import type SMTPTransport from "nodemailer/lib/smtp-transport/index.js";
|
||||
import hbs from "nodemailer-express-handlebars";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import hbs from "nodemailer-express-handlebars";
|
||||
import { promisify } from "util";
|
||||
import { createLog } from "../../logger/logger.js";
|
||||
import { db } from "../../../../database/dbclient.js";
|
||||
import { settings } from "../../../../database/schema/settings.js";
|
||||
import { tryCatch } from "../../../globalUtils/tryCatch.js";
|
||||
import { installed } from "../../../index.js";
|
||||
import { createLog } from "../../logger/logger.js";
|
||||
|
||||
interface HandlebarsMailOptions extends Mail.Options {
|
||||
template: string;
|
||||
context: Record<string, unknown>; // Use a generic object for context
|
||||
template: string;
|
||||
context: Record<string, unknown>; // Use a generic object for context
|
||||
}
|
||||
|
||||
interface EmailData {
|
||||
email: string;
|
||||
subject: string;
|
||||
template: string;
|
||||
context: [];
|
||||
email: string;
|
||||
subject: string;
|
||||
template: string;
|
||||
context: [];
|
||||
}
|
||||
|
||||
export const sendEmail = async (data: any): Promise<any> => {
|
||||
if (!installed) {
|
||||
createLog("error", "notify", "notify", "server not installed.");
|
||||
return;
|
||||
}
|
||||
let transporter: Transporter;
|
||||
let fromEmail: string | Address;
|
||||
const { data: settingData, error: settingError } = await tryCatch(
|
||||
db.select().from(settings)
|
||||
);
|
||||
if (!installed) {
|
||||
createLog("error", "notify", "notify", "server not installed.");
|
||||
return;
|
||||
}
|
||||
let transporter: Transporter;
|
||||
let fromEmail: string | Address;
|
||||
const { data: settingData, error: settingError } = await tryCatch(
|
||||
db.select().from(settings),
|
||||
);
|
||||
|
||||
if (settingError) {
|
||||
return {
|
||||
success: false,
|
||||
message: "There was an error getting the settings.",
|
||||
settingError,
|
||||
};
|
||||
}
|
||||
// get the plantToken
|
||||
const server = settingData.filter((n) => n.name === "server");
|
||||
if (settingError) {
|
||||
return {
|
||||
success: false,
|
||||
message: "There was an error getting the settings.",
|
||||
settingError,
|
||||
};
|
||||
}
|
||||
// get the plantToken
|
||||
const server = settingData.filter((n) => n.name === "server");
|
||||
|
||||
if (
|
||||
server[0].value === "localhost" &&
|
||||
process.env.EMAIL_USER &&
|
||||
process.env.EMAIL_PASSWORD
|
||||
) {
|
||||
transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
//debug: true,
|
||||
});
|
||||
if (
|
||||
server[0].value === "localhostx" &&
|
||||
process.env.EMAIL_USER &&
|
||||
process.env.EMAIL_PASSWORD
|
||||
) {
|
||||
transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
host: "smtp.gmail.com",
|
||||
port: 465,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASSWORD,
|
||||
},
|
||||
//debug: true,
|
||||
});
|
||||
|
||||
// update the from email
|
||||
fromEmail = process.env.EMAIL_USER;
|
||||
} else {
|
||||
// convert to the correct plant token.
|
||||
const plantToken = settingData.filter((s) => s.name === "plantToken");
|
||||
// update the from email
|
||||
fromEmail = process.env.EMAIL_USER;
|
||||
} else {
|
||||
// convert to the correct plant token.
|
||||
const plantToken = settingData.filter((s) => s.name === "plantToken");
|
||||
|
||||
let host = `${plantToken[0].value}-smtp.alpla.net`;
|
||||
let host = `${plantToken[0].value}-smtp.alpla.net`;
|
||||
|
||||
const testServers = ["test1", "test2", "test3"];
|
||||
const testServers = ["test1", "test2", "test3"];
|
||||
|
||||
if (testServers.includes(plantToken[0].value)) {
|
||||
host = "USMCD1-smtp.alpla.net";
|
||||
}
|
||||
if (testServers.includes(plantToken[0].value)) {
|
||||
host = "USMCD1-smtp.alpla.net";
|
||||
}
|
||||
|
||||
if (plantToken[0].value === "usiow2") {
|
||||
host = "USIOW1-smtp.alpla.net";
|
||||
}
|
||||
if (plantToken[0].value === "usiow2") {
|
||||
host = "USIOW1-smtp.alpla.net";
|
||||
}
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: 25,
|
||||
rejectUnauthorized: false,
|
||||
//secure: false,
|
||||
// auth: {
|
||||
// user: "alplaprod",
|
||||
// pass: "obelix",
|
||||
// },
|
||||
debug: true,
|
||||
} as SMTPTransport.Options);
|
||||
transporter = nodemailer.createTransport({
|
||||
host: host,
|
||||
port: 25,
|
||||
rejectUnauthorized: false,
|
||||
//secure: false,
|
||||
// auth: {
|
||||
// user: "alplaprod",
|
||||
// pass: "obelix",
|
||||
// },
|
||||
debug: true,
|
||||
} as SMTPTransport.Options);
|
||||
|
||||
// update the from email
|
||||
fromEmail = `noreply@alpla.com`;
|
||||
}
|
||||
// update the from email
|
||||
fromEmail = `noreply@alpla.com`;
|
||||
}
|
||||
|
||||
// creating the handlbar options
|
||||
const viewPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../utils/views/"
|
||||
);
|
||||
// creating the handlbar options
|
||||
const viewPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../utils/views/",
|
||||
);
|
||||
|
||||
const handlebarOptions = {
|
||||
viewEngine: {
|
||||
extname: ".hbs",
|
||||
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
|
||||
defaultLayout: "", // Specify the default layout
|
||||
partialsDir: viewPath,
|
||||
},
|
||||
viewPath: viewPath,
|
||||
extName: ".hbs", // File extension for Handlebars templates
|
||||
};
|
||||
const handlebarOptions = {
|
||||
viewEngine: {
|
||||
extname: ".hbs",
|
||||
//layoutsDir: path.resolve(viewPath, "layouts"), // Path to layouts directory
|
||||
defaultLayout: "", // Specify the default layout
|
||||
partialsDir: viewPath,
|
||||
},
|
||||
viewPath: viewPath,
|
||||
extName: ".hbs", // File extension for Handlebars templates
|
||||
};
|
||||
|
||||
transporter.use("compile", hbs(handlebarOptions));
|
||||
transporter.use("compile", hbs(handlebarOptions));
|
||||
|
||||
const mailOptions: HandlebarsMailOptions = {
|
||||
from: fromEmail,
|
||||
to: data.email,
|
||||
subject: data.subject,
|
||||
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
||||
//html: emailTemplate("BlakesTest", "This is an example with css"),
|
||||
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||
context: data.context,
|
||||
};
|
||||
const mailOptions: HandlebarsMailOptions = {
|
||||
from: fromEmail,
|
||||
to: data.email,
|
||||
subject: data.subject,
|
||||
//text: "You will have a reset token here and only have 30min to click the link before it expires.",
|
||||
//html: emailTemplate("BlakesTest", "This is an example with css"),
|
||||
template: data.template, // Name of the Handlebars template (e.g., 'welcome.hbs')
|
||||
context: data.context,
|
||||
};
|
||||
|
||||
// now verify and send the email
|
||||
const sendMailPromise = promisify(transporter.sendMail).bind(transporter);
|
||||
// now verify and send the email
|
||||
const sendMailPromise = promisify(transporter.sendMail).bind(transporter);
|
||||
|
||||
try {
|
||||
// Send email and await the result
|
||||
const info = await sendMailPromise(mailOptions);
|
||||
createLog(
|
||||
"info",
|
||||
"notification",
|
||||
"system",
|
||||
`Email was sent to: ${data.email}`
|
||||
);
|
||||
return { success: true, message: "Email sent.", data: info };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createLog(
|
||||
"error",
|
||||
"notification",
|
||||
"system",
|
||||
`Error sending Email: ${JSON.stringify(err)}`
|
||||
);
|
||||
return { success: false, message: "Error sending email.", error: err };
|
||||
}
|
||||
try {
|
||||
// Send email and await the result
|
||||
const info = await sendMailPromise(mailOptions);
|
||||
createLog(
|
||||
"info",
|
||||
"notification",
|
||||
"system",
|
||||
`Email was sent to: ${data.email}`,
|
||||
);
|
||||
return { success: true, message: "Email sent.", data: info };
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createLog(
|
||||
"error",
|
||||
"notification",
|
||||
"system",
|
||||
`Error sending Email: ${JSON.stringify(err)}`,
|
||||
);
|
||||
return { success: false, message: "Error sending email.", error: err };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user