Add ensure-references script and update package.json
All checks were successful
farmcontrol/farmcontrol-ws/pipeline/head This commit looks good

- Introduced a new script `ensure-references.js` to ensure all documents have a `_reference` field, enhancing data integrity across models.
- Updated `package.json` to include the new script in the npm scripts section for easy execution.
- Refactored database utility functions to exclude additional models from audit logging, improving data privacy.
- Enhanced various schemas with new fields and methods for better tax calculations and inventory management.
This commit is contained in:
Tom Butcher 2026-09-02 20:55:38 +01:00
parent 5f84c7077d
commit c94dc058ef
21 changed files with 528 additions and 38 deletions

View File

@ -12,7 +12,8 @@
"lint:fix": "eslint src/ --fix", "lint:fix": "eslint src/ --fix",
"format": "prettier --write \"src/**/*.{js,json}\"", "format": "prettier --write \"src/**/*.{js,json}\"",
"format:check": "prettier --check \"src/**/*.{js,json}\"", "format:check": "prettier --check \"src/**/*.{js,json}\"",
"fix": "npm run lint:fix && npm run format" "fix": "npm run lint:fix && npm run format",
"ensure-references": "node scripts/ensure-references.js"
}, },
"author": "Tom Butcher", "author": "Tom Butcher",
"license": "ISC", "license": "ISC",

View File

@ -0,0 +1,104 @@
import { editObject } from '../src/database/database.js';
import { generateId } from '../src/database/utils.js';
import { models } from '../src/database/schemas/models.js';
import { mongoServer } from '../src/database/mongo.js';
import { natsServer } from '../src/database/nats.js';
import { redisServer } from '../src/database/redis.js';
const MISSING_REFERENCE_FILTER = {
$or: [
{ _reference: { $exists: false } },
{ _reference: null },
{ _reference: '' },
],
};
const dryRun = process.argv.includes('--dry-run');
async function ensureReferences() {
console.log(
dryRun
? 'Scanning for documents missing _reference (dry run)...'
: 'Ensuring all documents have a _reference...'
);
await mongoServer.connect();
await natsServer.connect();
await redisServer.connect();
let totalMissing = 0;
let totalUpdated = 0;
let totalFailed = 0;
for (const [prefix, entry] of Object.entries(models)) {
const model = entry.model;
if (!model?.schema?.path('_reference')) {
continue;
}
const missing = await model.find(MISSING_REFERENCE_FILTER).select('_id').lean();
if (missing.length === 0) {
continue;
}
console.log(`[${prefix}] ${entry.label}: ${missing.length} missing`);
totalMissing += missing.length;
if (dryRun) {
continue;
}
for (const doc of missing) {
const _reference = generateId()();
const result = await editObject({
model,
id: doc._id,
updateData: { _reference },
auditLog: false,
notify: false,
recalculate: false,
});
if (result?.error) {
totalFailed += 1;
console.error(
` Failed ${doc._id}: ${result.error}${result.code ? ` (${result.code})` : ''}`
);
continue;
}
totalUpdated += 1;
console.log(` ${doc._id} -> ${_reference}`);
}
}
console.log(
dryRun
? `Done. ${totalMissing} document(s) missing _reference.`
: `Done. ${totalUpdated} updated, ${totalFailed} failed, ${totalMissing} found.`
);
}
try {
await ensureReferences();
} catch (error) {
console.error('ensure-references failed:', error);
process.exitCode = 1;
} finally {
try {
await redisServer.disconnect();
} catch {
// ignore disconnect errors on shutdown
}
try {
await mongoServer.disconnect();
} catch {
// ignore disconnect errors on shutdown
}
try {
await natsServer.disconnect();
} catch {
// ignore disconnect errors on shutdown
}
process.exit(process.exitCode ?? 0);
}

View File

@ -564,7 +564,8 @@ export const editObject = async ({
ownerType != undefined && ownerType != undefined &&
parentType !== 'notification' && parentType !== 'notification' &&
parentType !== 'auditLog' && parentType !== 'auditLog' &&
parentType !== 'userNotifier' parentType !== 'userNotifier' &&
parentType !== 'objectView'
) { ) {
await editNotification( await editNotification(
previousExpandedObject, previousExpandedObject,

View File

@ -24,11 +24,10 @@ jest.unstable_mockModule('../../utils.js', () => ({
})); }));
jest.unstable_mockModule('../inventory/stockaudit.schema.js', () => ({ jest.unstable_mockModule('../inventory/stockaudit.schema.js', () => ({
updateDraftStockAuditCurrents: jest.fn(), updateDraftStockAuditCurrents: jest.fn().mockResolvedValue(),
})); }));
const { aggregateRollups, editObject, newObject, deleteObject } = await import('../../database.js'); const { aggregateRollups, editObject, newObject, deleteObject } = await import('../../database.js');
const { updateDraftStockAuditCurrents } = await import('../inventory/stockaudit.schema.js');
const { listingModel } = await import('../sales/listing.schema.js'); const { listingModel } = await import('../sales/listing.schema.js');
const { listingVarientModel } = await import('../sales/listingvarient.schema.js'); const { listingVarientModel } = await import('../sales/listingvarient.schema.js');
const { productSkuModel } = await import('../management/productsku.schema.js'); const { productSkuModel } = await import('../management/productsku.schema.js');
@ -370,9 +369,7 @@ describe('productStock.recalculate', () => {
beforeEach(() => { beforeEach(() => {
aggregateRollups.mockReset(); aggregateRollups.mockReset();
editObject.mockReset(); editObject.mockReset();
updateDraftStockAuditCurrents.mockReset();
jest.restoreAllMocks(); jest.restoreAllMocks();
updateDraftStockAuditCurrents.mockResolvedValue(undefined);
}); });
it('writes the sku/location total onto matching listing varients', async () => { it('writes the sku/location total onto matching listing varients', async () => {

View File

@ -1,7 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
const { Schema } = mongoose; const { Schema } = mongoose;
import { aggregateRollups, aggregateRollupsHistory, editObject } from '../../database.js'; import { aggregateRollups, aggregateRollupsHistory, editObject, getObject } from '../../database.js';
import { taxRateModel } from '../management/taxrate.schema.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const invoiceOrderItemSchema = new Schema( const invoiceOrderItemSchema = new Schema(
{ {
@ -140,23 +142,41 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
return; return;
} }
const invoiceOrderItems = [];
for (const item of invoice.invoiceOrderItems || []) {
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
invoiceOrderItems.push({
...item,
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
});
}
const invoiceShipments = [];
for (const item of invoice.invoiceShipments || []) {
const taxRate = await resolveTaxRate(item.taxRate, getObject, taxRateModel);
invoiceShipments.push({
...item,
invoiceAmountWithTax: amountWithTax(item.invoiceAmount, taxRate),
});
}
// Calculate totals from invoiceOrderItems // Calculate totals from invoiceOrderItems
let totalAmount = 0; let totalAmount = 0;
for (const item of invoice.invoiceOrderItems || []) { for (const item of invoiceOrderItems) {
totalAmount += Number.parseFloat(item.invoiceAmount) || 0; totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
} }
let totalAmountWithTax = 0; let totalAmountWithTax = 0;
for (const item of invoice.invoiceOrderItems || []) { for (const item of invoiceOrderItems) {
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
} }
// Calculate shipping totals from invoiceShipments // Calculate shipping totals from invoiceShipments
let shippingAmount = 0; let shippingAmount = 0;
for (const item of invoice.invoiceShipments || []) { for (const item of invoiceShipments) {
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0; shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
} }
let shippingAmountWithTax = 0; let shippingAmountWithTax = 0;
for (const item of invoice.invoiceShipments || []) { for (const item of invoiceShipments) {
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0; shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
} }
@ -168,6 +188,8 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount)); (parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
const updateData = { const updateData = {
invoiceOrderItems,
invoiceShipments,
totalAmount: parseFloat(totalAmount).toFixed(2), totalAmount: parseFloat(totalAmount).toFixed(2),
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2), totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
shippingAmount: parseFloat(shippingAmount).toFixed(2), shippingAmount: parseFloat(shippingAmount).toFixed(2),

View File

@ -15,9 +15,10 @@ const filamentStockSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
state: { state: {
type: { type: String, required: true }, type: { type: String, required: true, default: 'draft' },
progress: { type: Number, required: false }, progress: { type: Number, required: false },
}, },
postedAt: { type: Date, required: false },
startingWeight: { startingWeight: {
net: { type: Number, required: true }, net: { type: Number, required: true },
gross: { type: Number, required: true }, gross: { type: Number, required: true },
@ -65,6 +66,11 @@ const rollupConfigs = [
filter: {}, filter: {},
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }], rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
}, },
{
name: 'draft',
filter: { 'state.type': 'draft' },
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
},
{ {
name: 'unconsumed', name: 'unconsumed',
filter: { 'state.type': 'unconsumed' }, filter: { 'state.type': 'unconsumed' },
@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
stockLocationId, stockLocationId,
user, user,
}); });
if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return;
}; };
// Add virtual id getter // Add virtual id getter

View File

@ -15,6 +15,7 @@ import {
getObject, getObject,
} from '../../database.js'; } from '../../database.js';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const skuModelsByItemType = { const skuModelsByItemType = {
@ -194,17 +195,10 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
} }
} }
let taxRate = orderItem.taxRate; const taxRate = await resolveTaxRate(orderItem.taxRate, getObject, taxRateModel);
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) {
taxRate = await getObject({
model: taxRateModel,
id: orderItem.taxRate._id,
cached: true,
});
}
const orderTotalAmount = effectiveItemAmount * orderItem.quantity; const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100); const orderTotalAmountWithTax = amountWithTax(orderTotalAmount, taxRate);
const orderItemUpdateData = { const orderItemUpdateData = {
totalAmount: orderTotalAmount, totalAmount: orderTotalAmount,

View File

@ -10,6 +10,7 @@ import {
editObject, editObject,
getObject, getObject,
} from '../../database.js'; } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const shipmentSchema = new Schema( const shipmentSchema = new Schema(
{ {
@ -102,26 +103,17 @@ shipmentSchema.statics.recalculate = async function (shipment, user) {
return; return;
} }
var taxRate = shipment.taxRate; const taxRate = await resolveTaxRate(shipment.taxRate, getObject, taxRateModel);
if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) { const amountWithTaxValue = amountWithTax(shipment.amount || 0, taxRate);
taxRate = await getObject({
model: taxRateModel,
id: shipment.taxRate._id,
cached: true,
});
}
const amountWithTax = parseFloat(
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
).toFixed(2);
await editObject({ await editObject({
model: shipmentModel, model: shipmentModel,
id: shipment._id, id: shipment._id,
updateData: { updateData: {
amountWithTax: amountWithTax, amountWithTax: amountWithTaxValue,
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0), invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0), invoicedAmountWithTaxRemaining:
amountWithTaxValue - (shipment.invoicedAmountWithTax || 0),
}, },
user, user,
recalculate: false, recalculate: false,

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { taxRateModel } from './taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const marketplaceMappingSchema = new mongoose.Schema( const marketplaceMappingSchema = new mongoose.Schema(
@ -39,4 +42,29 @@ courierServiceSchema.virtual('id').get(function () {
courierServiceSchema.set('toJSON', { virtuals: true }); courierServiceSchema.set('toJSON', { virtuals: true });
courierServiceSchema.statics.recalculate = async function (courierService, user) {
const costTaxRate = await resolveTaxRate(courierService.costTaxRate, getObject, taxRateModel);
const updateData = {};
if (courierService.cost != null) {
updateData.costWithTax = amountWithTax(courierService.cost, costTaxRate);
}
if (courierService.additionalCost != null) {
updateData.additionalCostWithTax = amountWithTax(
courierService.additionalCost,
costTaxRate
);
}
if (Object.keys(updateData).length > 0) {
await editObject({
model: this,
id: courierService._id,
updateData,
user,
recalculate: false,
});
}
};
export const courierServiceModel = mongoose.model('courierService', courierServiceSchema); export const courierServiceModel = mongoose.model('courierService', courierServiceSchema);

View File

@ -8,6 +8,8 @@ const connectionSchema = new Schema(
protocol: { type: String, required: true }, protocol: { type: String, required: true },
host: { type: String, required: true }, host: { type: String, required: true },
port: { type: Number, required: false }, port: { type: Number, required: false },
username: { type: String, required: false },
password: { type: String, required: false },
}, },
{ _id: false } { _id: false }
); );
@ -22,6 +24,8 @@ const documentPrinterSchema = new Schema(
}, },
connection: { type: connectionSchema, required: true }, connection: { type: connectionSchema, required: true },
currentDocumentSize: { type: Schema.Types.ObjectId, ref: 'documentSize', required: false }, currentDocumentSize: { type: Schema.Types.ObjectId, ref: 'documentSize', required: false },
supportedDocumentSizes: [{ type: Schema.Types.ObjectId, ref: 'documentSize', required: false }],
rotateOrientation: { type: Boolean, required: false, default: false },
tags: [{ type: String }], tags: [{ type: String }],
online: { type: Boolean, required: true, default: false }, online: { type: Boolean, required: true, default: false },
active: { type: Boolean, required: true, default: true }, active: { type: Boolean, required: true, default: true },
@ -30,6 +34,10 @@ const documentPrinterSchema = new Schema(
message: { type: String, required: false }, message: { type: String, required: false },
progress: { type: Number, required: false }, progress: { type: Number, required: false },
}, },
paperState: {
type: { type: String, required: true, default: 'unknown' },
message: { type: String, required: false },
},
connectedAt: { type: Date, default: null }, connectedAt: { type: Date, default: null },
host: { type: Schema.Types.ObjectId, ref: 'host', required: true }, host: { type: Schema.Types.ObjectId, ref: 'host', required: true },
vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false }, vendor: { type: Schema.Types.ObjectId, ref: 'vendor', required: false },

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { taxRateModel } from '../management/taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Filament base - cost and tax; color and cost override at FilamentSKU // Filament base - cost and tax; color and cost override at FilamentSKU
@ -28,6 +31,24 @@ filamentSchema.virtual('id').get(function () {
filamentSchema.set('toJSON', { virtuals: true }); filamentSchema.set('toJSON', { virtuals: true });
filamentSchema.statics.recalculate = async function (filament, user) { filamentSchema.statics.recalculate = async function (filament, user) {
const costTaxRate = await resolveTaxRate(filament.costTaxRate, getObject, taxRateModel);
const taxUpdateData = {};
if (filament.cost != null) {
taxUpdateData.costWithTax = amountWithTax(filament.cost, costTaxRate);
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: filament._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(filament, taxUpdateData);
}
const filamentSkuModel = mongoose.model('filamentSku'); const filamentSkuModel = mongoose.model('filamentSku');
const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean(); const skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean();
for (const sku of skus) { for (const sku of skus) {

View File

@ -1,5 +1,9 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { filamentModel } from './filament.schema.js';
import { taxRateModel } from './taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Define the main filament SKU schema - color and cost live at SKU level // Define the main filament SKU schema - color and cost live at SKU level
@ -30,6 +34,34 @@ filamentSkuSchema.virtual('id').get(function () {
filamentSkuSchema.set('toJSON', { virtuals: true }); filamentSkuSchema.set('toJSON', { virtuals: true });
filamentSkuSchema.statics.recalculate = async function (filamentSku, user) { filamentSkuSchema.statics.recalculate = async function (filamentSku, user) {
const parent = await getObject({
model: filamentModel,
id: filamentSku.filament?._id || filamentSku.filament,
cached: true,
});
const taxUpdateData = {};
if (filamentSku.overrideCost) {
const costTaxRate = await resolveTaxRate(filamentSku.costTaxRate, getObject, taxRateModel);
if (filamentSku.cost != null) {
taxUpdateData.costWithTax = amountWithTax(filamentSku.cost, costTaxRate);
}
} else if (parent?.costWithTax != null) {
taxUpdateData.costWithTax = parent.costWithTax;
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: filamentSku._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(filamentSku, taxUpdateData);
}
const orderItemModel = mongoose.model('orderItem'); const orderItemModel = mongoose.model('orderItem');
const skuId = filamentSku._id; const skuId = filamentSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { taxRateModel } from '../management/taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Define the main part schema - cost/price and tax; override at PartSku // Define the main part schema - cost/price and tax; override at PartSku
@ -32,6 +35,28 @@ partSchema.virtual('id').get(function () {
partSchema.set('toJSON', { virtuals: true }); partSchema.set('toJSON', { virtuals: true });
partSchema.statics.recalculate = async function (part, user) { partSchema.statics.recalculate = async function (part, user) {
const costTaxRate = await resolveTaxRate(part.costTaxRate, getObject, taxRateModel);
const priceTaxRate = await resolveTaxRate(part.priceTaxRate, getObject, taxRateModel);
const taxUpdateData = {};
if (part.cost != null) {
taxUpdateData.costWithTax = amountWithTax(part.cost, costTaxRate);
}
if (part.price != null) {
taxUpdateData.priceWithTax = amountWithTax(part.price, priceTaxRate);
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: part._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(part, taxUpdateData);
}
const partSkuModel = mongoose.model('partSku'); const partSkuModel = mongoose.model('partSku');
const skus = await partSkuModel.find({ part: part._id }).select('_id').lean(); const skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
for (const sku of skus) { for (const sku of skus) {

View File

@ -1,5 +1,13 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { partModel } from './part.schema.js';
import { taxRateModel } from './taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import {
amountWithTax,
effectiveMarginPrice,
resolveTaxRate,
} from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Define the main part SKU schema - pricing lives at SKU level // Define the main part SKU schema - pricing lives at SKU level
@ -36,6 +44,54 @@ partSkuSchema.virtual('id').get(function () {
partSkuSchema.set('toJSON', { virtuals: true }); partSkuSchema.set('toJSON', { virtuals: true });
partSkuSchema.statics.recalculate = async function (partSku, user) { partSkuSchema.statics.recalculate = async function (partSku, user) {
const parent = await getObject({
model: partModel,
id: partSku.part?._id || partSku.part,
cached: true,
});
const taxUpdateData = {};
if (partSku.overrideCost) {
const costTaxRate = await resolveTaxRate(partSku.costTaxRate, getObject, taxRateModel);
if (partSku.cost != null) {
taxUpdateData.costWithTax = amountWithTax(partSku.cost, costTaxRate);
}
} else if (parent?.costWithTax != null) {
taxUpdateData.costWithTax = parent.costWithTax;
}
if (partSku.overridePrice) {
const priceTaxRate = await resolveTaxRate(
partSku.priceTaxRate ?? parent?.priceTaxRate,
getObject,
taxRateModel
);
const cost = partSku.overrideCost ? partSku.cost : parent?.cost;
const price = effectiveMarginPrice({
priceMode: partSku.priceMode ?? parent?.priceMode,
price: partSku.price,
cost,
margin: partSku.margin ?? parent?.margin,
});
if (price != null) {
taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate);
}
} else if (parent?.priceWithTax != null) {
taxUpdateData.priceWithTax = parent.priceWithTax;
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: partSku._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(partSku, taxUpdateData);
}
const orderItemModel = mongoose.model('orderItem'); const orderItemModel = mongoose.model('orderItem');
const skuId = partSku._id; const skuId = partSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { taxRateModel } from '../management/taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import { amountWithTax, resolveTaxRate } from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
// Define the main product schema // Define the main product schema
@ -35,6 +38,34 @@ productSchema.virtual('id').get(function () {
productSchema.set('toJSON', { virtuals: true }); productSchema.set('toJSON', { virtuals: true });
productSchema.statics.recalculate = async function (product, user) { productSchema.statics.recalculate = async function (product, user) {
const costTaxRate = await resolveTaxRate(product.costTaxRate, getObject, taxRateModel);
const priceTaxRate = await resolveTaxRate(product.priceTaxRate, getObject, taxRateModel);
const taxUpdateData = {};
if (product.cost != null) {
taxUpdateData.costWithTax = amountWithTax(product.cost, costTaxRate);
}
if (product.price != null) {
taxUpdateData.priceWithTax = amountWithTax(product.price, priceTaxRate);
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: product._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(product, taxUpdateData);
}
const productSkuModel = mongoose.model('productSku');
const skus = await productSkuModel.find({ product: product._id }).select('_id').lean();
for (const sku of skus) {
await productSkuModel.recalculate(sku, user);
}
const orderItemModel = mongoose.model('orderItem'); const orderItemModel = mongoose.model('orderItem');
const itemId = product._id; const itemId = product._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -1,5 +1,13 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { generateId } from '../../utils.js'; import { generateId } from '../../utils.js';
import { productModel } from './product.schema.js';
import { taxRateModel } from './taxrate.schema.js';
import { editObject, getObject } from '../../database.js';
import {
amountWithTax,
effectiveMarginPrice,
resolveTaxRate,
} from '../../tax.js';
const { Schema } = mongoose; const { Schema } = mongoose;
const partSkuUsageSchema = new Schema({ const partSkuUsageSchema = new Schema({
@ -43,6 +51,54 @@ productSkuSchema.virtual('id').get(function () {
productSkuSchema.set('toJSON', { virtuals: true }); productSkuSchema.set('toJSON', { virtuals: true });
productSkuSchema.statics.recalculate = async function (productSku, user) { productSkuSchema.statics.recalculate = async function (productSku, user) {
const parent = await getObject({
model: productModel,
id: productSku.product?._id || productSku.product,
cached: true,
});
const taxUpdateData = {};
if (productSku.overrideCost) {
const costTaxRate = await resolveTaxRate(productSku.costTaxRate, getObject, taxRateModel);
if (productSku.cost != null) {
taxUpdateData.costWithTax = amountWithTax(productSku.cost, costTaxRate);
}
} else if (parent?.costWithTax != null) {
taxUpdateData.costWithTax = parent.costWithTax;
}
if (productSku.overridePrice) {
const priceTaxRate = await resolveTaxRate(
productSku.priceTaxRate ?? parent?.priceTaxRate,
getObject,
taxRateModel
);
const cost = productSku.overrideCost ? productSku.cost : parent?.cost;
const price = effectiveMarginPrice({
priceMode: productSku.priceMode ?? parent?.priceMode,
price: productSku.price,
cost,
margin: productSku.margin ?? parent?.margin,
});
if (price != null) {
taxUpdateData.priceWithTax = amountWithTax(price, priceTaxRate);
}
} else if (parent?.priceWithTax != null) {
taxUpdateData.priceWithTax = parent.priceWithTax;
}
if (Object.keys(taxUpdateData).length > 0) {
await editObject({
model: this,
id: productSku._id,
updateData: taxUpdateData,
user,
recalculate: false,
});
Object.assign(productSku, taxUpdateData);
}
const orderItemModel = mongoose.model('orderItem'); const orderItemModel = mongoose.model('orderItem');
const skuId = productSku._id; const skuId = productSku._id;
const draftOrderItems = await orderItemModel const draftOrderItems = await orderItemModel

View File

@ -13,8 +13,8 @@ const toId = (value) => {
export function normalizeAuditLevelLine(line) { export function normalizeAuditLevelLine(line) {
const allItems = Boolean(line?.allItems); const allItems = Boolean(line?.allItems);
const allSkus = allItems ? true : Boolean(line?.allSkus); const allSkus = allItems ? true : Boolean(line?.allSkus);
const item = allItems ? null : line?.item?._id ?? line?.item ?? null; const item = allItems ? null : (line?.item?._id ?? line?.item ?? null);
const itemSku = allItems || allSkus ? null : line?.itemSku?._id ?? line?.itemSku ?? null; const itemSku = allItems || allSkus ? null : (line?.itemSku?._id ?? line?.itemSku ?? null);
return { return {
_id: line?._id, _id: line?._id,
@ -62,7 +62,7 @@ const stockAuditLevelSchema = new Schema(
{ {
_reference: { type: String, default: () => generateId()() }, _reference: { type: String, default: () => generateId()() },
name: { type: String, required: true }, name: { type: String, required: true },
tags: [{ type: String }], tags: [{ type: String, required: true }],
auditLines: { type: [stockAuditLevelLineSchema], default: [] }, auditLines: { type: [stockAuditLevelLineSchema], default: [] },
}, },
{ timestamps: true } { timestamps: true }

View File

@ -0,0 +1,56 @@
import mongoose from 'mongoose';
import { generateId } from '../../utils.js';
const { Schema } = mongoose;
const objectViewSchema = new mongoose.Schema({
_reference: { type: String, default: () => generateId()() },
user: {
type: Schema.Types.ObjectId,
ref: 'user',
required: true,
},
objectType: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
color: {
type: String,
required: true,
default: '#3498DB',
},
private: {
type: Boolean,
required: true,
default: true,
},
filter: {
type: Schema.Types.Mixed,
default: () => ({}),
},
sort: {
type: Schema.Types.Mixed,
default: () => ({}),
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
required: true,
default: Date.now,
},
});
objectViewSchema.virtual('id').get(function () {
return this._id;
});
objectViewSchema.set('toJSON', { virtuals: true });
export const objectViewModel = mongoose.model('objectView', objectViewSchema);

View File

@ -32,6 +32,7 @@ import { noteTypeModel } from './management/notetype.schema.js';
import { noteModel } from './misc/note.schema.js'; import { noteModel } from './misc/note.schema.js';
import { notificationModel } from './misc/notification.schema.js'; import { notificationModel } from './misc/notification.schema.js';
import { userNotifierModel } from './misc/usernotifier.schema.js'; import { userNotifierModel } from './misc/usernotifier.schema.js';
import { objectViewModel } from './misc/objectview.schema.js';
import { documentSizeModel } from './management/documentsize.schema.js'; import { documentSizeModel } from './management/documentsize.schema.js';
import { documentTemplateModel } from './management/documenttemplate.schema.js'; import { documentTemplateModel } from './management/documenttemplate.schema.js';
import { hostModel } from './management/host.schema.js'; import { hostModel } from './management/host.schema.js';
@ -103,6 +104,7 @@ export const models = {
NTE: modelEntry(() => noteModel, 'note', 'Note'), NTE: modelEntry(() => noteModel, 'note', 'Note'),
NTF: modelEntry(() => notificationModel, 'notification', 'Notification'), NTF: modelEntry(() => notificationModel, 'notification', 'Notification'),
ONF: modelEntry(() => userNotifierModel, 'userNotifier', 'User Notifier'), ONF: modelEntry(() => userNotifierModel, 'userNotifier', 'User Notifier'),
OVW: modelEntry(() => objectViewModel, 'objectView', 'Object View'),
DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'), DSZ: modelEntry(() => documentSizeModel, 'documentSize', 'Document Size'),
DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'), DTP: modelEntry(() => documentTemplateModel, 'documentTemplate', 'Document Template'),
DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'), DPR: modelEntry(() => documentPrinterModel, 'documentPrinter', 'Document Printer'),

54
src/database/tax.js Normal file
View File

@ -0,0 +1,54 @@
/**
* Tax calculation helpers mirroring farmcontrol-ui model value functions.
*/
export function isPopulatedTaxRate(taxRate) {
return taxRate != null && taxRate.rateType != null;
}
export async function resolveTaxRate(taxRateRef, getObject, taxRateModel) {
if (!taxRateRef) {
return null;
}
if (isPopulatedTaxRate(taxRateRef)) {
return taxRateRef;
}
const id = taxRateRef._id ?? taxRateRef;
if (!id) {
return null;
}
if (
typeof taxRateRef === 'object' &&
taxRateRef._id &&
Object.keys(taxRateRef).length === 1
) {
return await getObject({ model: taxRateModel, id, cached: true });
}
return await getObject({ model: taxRateModel, id, cached: true });
}
export function amountWithTax(amount, taxRate) {
const base = Number.parseFloat(amount) || 0;
if (!base) {
return 0;
}
if (!taxRate) {
return Number.parseFloat(base.toFixed(2));
}
const rate = Number.parseFloat(taxRate.rate) || 0;
if (taxRate.rateType === 'percentage') {
return Number.parseFloat((base * (1 + rate / 100)).toFixed(2));
}
if (taxRate.rateType === 'amount' || taxRate.rateType === 'fixed') {
return Number.parseFloat((base + rate).toFixed(2));
}
return Number.parseFloat(base.toFixed(2));
}
export function effectiveMarginPrice({ priceMode, price, cost, margin }) {
if (priceMode === 'margin' && margin != null && cost != null) {
return cost * (1 + margin / 100);
}
return price;
}

View File

@ -9,11 +9,13 @@ import { customAlphabet } from 'nanoid';
const NOTIFICATION_EXCLUDED_MODELS = [ const NOTIFICATION_EXCLUDED_MODELS = [
'notification', 'notification',
'userNotifier', 'userNotifier',
'objectView',
'auditLog' 'auditLog'
]; ];
const AUDIT_EXCLUDED_MODELS = [ const AUDIT_EXCLUDED_MODELS = [
'notification', 'notification',
'userNotifier', 'userNotifier',
'objectView',
'marketplaceEvent' 'marketplaceEvent'
]; ];
const AUDIT_EXCLUDED_CHANGES = ['state.message']; const AUDIT_EXCLUDED_CHANGES = ['state.message'];