diff --git a/src/database/schemas/management/documentjob.schema.js b/src/database/schemas/management/documentjob.schema.js index 8fbaf7c..eed28c5 100644 --- a/src/database/schemas/management/documentjob.schema.js +++ b/src/database/schemas/management/documentjob.schema.js @@ -14,8 +14,15 @@ const documentJobSchema = new Schema( object: { type: Schema.Types.ObjectId, refPath: 'objectType', - required: true, + required: false, }, + objects: [ + { + type: Schema.Types.ObjectId, + refPath: 'objectType', + required: false, + }, + ], state: { type: { type: String, required: true, default: 'queued' }, progress: { type: Number, required: false }, diff --git a/src/database/schemas/management/emailmessage.schema.js b/src/database/schemas/management/emailmessage.schema.js index ee0f7a3..5c4f066 100644 --- a/src/database/schemas/management/emailmessage.schema.js +++ b/src/database/schemas/management/emailmessage.schema.js @@ -13,7 +13,8 @@ const emailMessageSchema = new Schema( required: true, }, objectType: { type: String, required: true }, - object: { type: Schema.Types.ObjectId, refPath: 'objectType', required: true }, + object: { type: Schema.Types.ObjectId, refPath: 'objectType', required: false }, + objects: [{ type: Schema.Types.ObjectId, refPath: 'objectType', required: false }], emailAccount: { type: Schema.Types.ObjectId, ref: 'emailAccount', diff --git a/src/routes/management/emailmessages.js b/src/routes/management/emailmessages.js index f9c2b1a..46f4ad4 100644 --- a/src/routes/management/emailmessages.js +++ b/src/routes/management/emailmessages.js @@ -21,7 +21,7 @@ const filters = [ 'emailTemplate', 'emailAccount', 'objectType', - 'object', + 'objects', 'recipientEmail', 'recipientType', 'recipient', diff --git a/src/services/management/__tests__/emailmessages.test.js b/src/services/management/__tests__/emailmessages.test.js index 4df08f7..5fad49b 100644 --- a/src/services/management/__tests__/emailmessages.test.js +++ b/src/services/management/__tests__/emailmessages.test.js @@ -175,7 +175,7 @@ describe('email message creation', () => { expect.objectContaining({ properties: ['state', 'emailAccount'], filter: { read: false }, - populate: ['emailTemplate', 'emailAccount', 'object', 'recipient', 'attachments'], + populate: ['emailTemplate', 'emailAccount', 'objects', 'object', 'recipient', 'attachments'], }) ); }); @@ -267,7 +267,7 @@ describe('email message creation', () => { expect(templateManager.renderDownload).toHaveBeenCalledWith( 'doc-1', undefined, - invoice, + { ...invoice, objects: [invoice] }, 'pdf' ); expect(newObject).toHaveBeenCalledWith( diff --git a/src/services/management/documentjobs.js b/src/services/management/documentjobs.js index 0f99a95..a4fabf3 100644 --- a/src/services/management/documentjobs.js +++ b/src/services/management/documentjobs.js @@ -16,6 +16,7 @@ import { getObjectNeighbors, } from '../../database/database.js'; import { createDocumentJobUserNotifier } from '../../utils.js'; +import { relatedObjectIdsFromBody, withLegacyObjects } from '../../utils/relatedObjects.js'; const logger = log4js.getLogger('Document Jobs'); logger.level = config.server.logLevel; @@ -48,7 +49,7 @@ export const listDocumentJobsRouteHandler = async ( } logger.debug(`List of document jobs (Page ${page}, Limit ${limit}). Count: ${result.length}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const listDocumentJobsByPropertiesRouteHandler = async ( @@ -72,7 +73,7 @@ export const listDocumentJobsByPropertiesRouteHandler = async ( } logger.debug(`List of document jobs. Count: ${result.length}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const getDocumentJobPropertyValuesRouteHandler = async ( @@ -95,7 +96,7 @@ export const searchDocumentJobsRouteHandler = async (req, res, search) => { model: documentJobModel, search, }); - res.send(result); + res.send(withLegacyObjects(result)); }; export const getDocumentJobRouteHandler = async (req, res) => { @@ -103,14 +104,14 @@ export const getDocumentJobRouteHandler = async (req, res) => { const result = await getObject({ model: documentJobModel, id, - populate: ['documentTemplate', 'documentPrinter', 'object'], + populate: ['documentTemplate', 'documentPrinter', 'objects', 'object'], }); if (result?.error) { logger.warn(`Document Job not found with supplied id.`); return res.status(result.code).send(result); } logger.debug(`Retreived document job with ID: ${id}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const editDocumentJobRouteHandler = async (req, res) => { @@ -129,7 +130,7 @@ export const editDocumentJobRouteHandler = async (req, res) => { id, updateData, user: req.user, - populate: ['documentTemplate', 'documentPrinter', 'object'], + populate: ['documentTemplate', 'documentPrinter', 'objects', 'object'], }); if (result.error) { @@ -140,7 +141,7 @@ export const editDocumentJobRouteHandler = async (req, res) => { logger.debug(`Edited document job with ID: ${id}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const newDocumentJobRouteHandler = async (req, res) => { @@ -150,7 +151,7 @@ export const newDocumentJobRouteHandler = async (req, res) => { documentPrinter: req.body.documentPrinter, documentTemplate: req.body.documentTemplate, objectType: req.body.objectType, - object: req.body.object, + objects: relatedObjectIdsFromBody(req.body), content: req.body.content, quantity: req.body.quantity ?? 1, state: { type: 'draft' }, @@ -161,6 +162,7 @@ export const newDocumentJobRouteHandler = async (req, res) => { model: documentJobModel, newData, user: req.user, + populate: ['documentTemplate', 'documentPrinter', 'objects', 'object'], }); if (result.error) { logger.error('No document job created:', result.error); @@ -171,7 +173,7 @@ export const newDocumentJobRouteHandler = async (req, res) => { logger.debug(`New document job with ID: ${result._id}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const deleteDocumentJobRouteHandler = async (req, res) => { diff --git a/src/services/management/emailmessages.js b/src/services/management/emailmessages.js index 9f9c449..effaa65 100644 --- a/src/services/management/emailmessages.js +++ b/src/services/management/emailmessages.js @@ -27,11 +27,17 @@ import { import { BUCKETS, downloadFile, uploadFile } from '../../database/ceph.js'; import { distributeNew, getFileMeta, notfiyObjectUserNotifiers } from '../../utils.js'; import { templateManager } from '../../templates/templatemanager.js'; +import { + relatedObjectIdsFromBody, + relatedObjectRefs, + templateDataFromObjects, + withLegacyObjects, +} from '../../utils/relatedObjects.js'; const logger = log4js.getLogger('Email Messages'); logger.level = config.server.logLevel; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const populate = ['emailTemplate', 'emailAccount', 'object', 'recipient', 'attachments']; +const populate = ['emailTemplate', 'emailAccount', 'objects', 'object', 'recipient', 'attachments']; let worker; const pending = new Map(); @@ -167,14 +173,18 @@ async function resolveEmailAttachments(emailMessage, user) { if (!modelEntry?.model) { throw new Error(`Unknown object type for email attachment: ${emailMessage.objectType}`); } - const object = await getObject({ - model: modelEntry.model, - id: emailMessage.object?._id || emailMessage.object, - }); + const objects = []; + for (const ref of relatedObjectRefs(emailMessage)) { + const object = await getObject({ + model: modelEntry.model, + id: ref?._id || ref, + }); + objects.push(object); + } const rendered = await templateManager.renderDownload( templateId, undefined, - object, + templateDataFromObjects(objects), attachment.fileType || 'pdf' ); if (rendered?.error) throw new Error(rendered.error); @@ -265,7 +275,7 @@ export const processEmailMessage = async (id, account, user) => { emailMessage.emailTemplate, undefined, undefined, - emailMessage.object, + templateDataFromObjects(relatedObjectRefs(emailMessage)), {}, false, String(id) @@ -350,7 +360,7 @@ export const listEmailMessagesRouteHandler = async ( } logger.debug(`List of email messages (Page ${page}, Limit ${limit}). Count: ${result.length}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const listEmailMessagesByPropertiesRouteHandler = async ( @@ -375,7 +385,7 @@ export const listEmailMessagesByPropertiesRouteHandler = async ( } logger.debug(`List of email messages. Count: ${result.length}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const getEmailMessagePropertyValuesRouteHandler = async ( @@ -398,7 +408,7 @@ export const searchEmailMessagesRouteHandler = async (req, res, search) => { model: emailMessageModel, search, }); - res.send(result); + res.send(withLegacyObjects(result)); }; export const getEmailMessageRouteHandler = async (req, res) => { @@ -413,7 +423,7 @@ export const getEmailMessageRouteHandler = async (req, res) => { return res.status(result.code).send(result); } logger.debug(`Retreived email message with ID: ${id}`); - res.send(result); + res.send(withLegacyObjects(result)); }; export const newEmailMessageRouteHandler = async (req, res) => { @@ -441,7 +451,7 @@ export const newEmailMessageRouteHandler = async (req, res) => { name: req.body.name, emailTemplate: req.body.emailTemplate?._id ?? req.body.emailTemplate, objectType: req.body.objectType, - object: req.body.object?._id ?? req.body.object, + objects: relatedObjectIdsFromBody(req.body), emailAccount: account._id, recipientEmail: req.body.recipientEmail, recipientType: req.body.recipientType, @@ -472,7 +482,7 @@ export const newEmailMessageRouteHandler = async (req, res) => { logger.debug(`New email message with ID: ${result._id}`); - res.send(result); + res.send(withLegacyObjects(result)); setImmediate(() => processEmailMessage(result._id, account, req.user)); }; @@ -557,5 +567,5 @@ export const markEmailMessageAsReadRouteHandler = async (req, res) => { logger.debug(`Marked email message as read with ID: ${id}`); - res.send(result); + res.send(withLegacyObjects(result)); }; diff --git a/src/services/misc/export.js b/src/services/misc/export.js index e0b8b45..a8ef556 100644 --- a/src/services/misc/export.js +++ b/src/services/misc/export.js @@ -39,7 +39,7 @@ export const EXPORT_FILTER_BY_TYPE = { stockTransfer: ['state.type', 'postedAt'], stockAudit: ['auditLevel._id', 'stockLocation._id', 'state.type', 'postedAt'], stockAuditLevel: ['name', 'tags'], - documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'], + documentJob: ['documentTemplate', 'documentPrinter', 'objects._id', 'objectType'], documentTemplate: ['parent._id', 'documentSize._id'], salesOrder: ['client'], invoice: ['to._id', 'from._id', 'order._id', 'orderType'], diff --git a/src/templates/__tests__/templatemanager.test.js b/src/templates/__tests__/templatemanager.test.js index 63d32c3..367f20b 100644 --- a/src/templates/__tests__/templatemanager.test.js +++ b/src/templates/__tests__/templatemanager.test.js @@ -433,6 +433,50 @@ describe('TemplateManager', () => { ); }); + it('renders each object and joins them with a document page break', async () => { + const mockTemplate = { + documentSize: { width: 100, height: 100, infiniteHeight: false }, + global: false, + objectType: 'printer', + }; + + getObject.mockImplementation(async ({ id }) => { + if (id === 'temp-id') return mockTemplate; + if (id === 'printer-a') return { _id: 'printer-a', name: 'Printer A', status: 'online' }; + if (id === 'printer-b') return { _id: 'printer-b', name: 'Printer B', status: 'offline' }; + return null; + }); + + await templateManager.renderTemplate( + 'temp-id', + 'some content', + { objects: [{ _id: 'printer-a' }, { _id: 'printer-b' }] }, + 1, + {}, + true + ); + + const objectRenders = ejs.render.mock.calls.filter((call) => call[0] === 'some content'); + expect(objectRenders).toHaveLength(2); + expect(objectRenders[0][1]).toEqual( + expect.objectContaining({ + name: 'Printer A', + status: 'online', + }) + ); + expect(objectRenders[1][1]).toEqual( + expect.objectContaining({ + name: 'Printer B', + status: 'offline', + }) + ); + expect( + ejs.render.mock.calls.some((call) => + String(call[1]?.content || '').includes('class="documentPageBreak"') + ) + ).toBe(true); + }); + it('does not hydrate a saved testObject when not previewing', async () => { getObject.mockResolvedValue({ documentSize: { width: 100, height: 100, infiniteHeight: false }, diff --git a/src/templates/templatemanager.js b/src/templates/templatemanager.js index 84d29a0..5fac0eb 100644 --- a/src/templates/templatemanager.js +++ b/src/templates/templatemanager.js @@ -500,6 +500,9 @@ function isLegacyPreviewSnapshot(data) { if (typeof data.toHexString === 'function') { return false; } + if (Array.isArray(data.objects)) { + return false; + } if (extractObjectId(data) != null) { return false; } @@ -529,6 +532,28 @@ function resolveNestedTemplateObject(object) { return omitTemplateFc(object); } +function isObjectSnapshot(value) { + if (value == null || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + if (typeof value.toHexString === 'function') { + return false; + } + return Object.keys(value).some((key) => key !== '_id' && key !== 'id' && key !== 'objects'); +} + +function getRenderObjects(resolvedData) { + if (Array.isArray(resolvedData?.objects) && resolvedData.objects.length > 0) { + return resolvedData.objects.filter(Boolean); + } + if (resolvedData != null && typeof resolvedData === 'object' && !Array.isArray(resolvedData)) { + return [resolvedData]; + } + return [{}]; +} + +const OBJECT_PAGE_BREAK = '