Compare commits
4 Commits
2af78c76f4
...
850c721867
| Author | SHA1 | Date | |
|---|---|---|---|
| 850c721867 | |||
| a7b613c88b | |||
| 1f679825df | |||
| edb8282c41 |
@ -38,4 +38,16 @@ describe('parseFilter number fields', () => {
|
||||
const result = await parseFilter('_reference', 'JOB*', jobModel);
|
||||
expect(result._reference).toEqual({ $regex: '^JOB.*$', $options: 'i' });
|
||||
});
|
||||
|
||||
it('strips model prefix from equality expressions', async () => {
|
||||
await expect(parseFilter('_reference', 'GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({
|
||||
_reference: { $regex: '^EJX3EXUZYW2U$', $options: 'i' },
|
||||
});
|
||||
});
|
||||
|
||||
it('strips model prefix from not-equal expressions', async () => {
|
||||
await expect(parseFilter('_reference', '<>GCF:EJX3EXUZYW2U', jobModel)).resolves.toEqual({
|
||||
_reference: { $ne: 'EJX3EXUZYW2U' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
37
src/database/__tests__/tax.test.js
Normal file
37
src/database/__tests__/tax.test.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { amountWithTax, effectiveMarginPrice } from '../tax.js';
|
||||
|
||||
describe('amountWithTax', () => {
|
||||
it('returns 0 for zero amount', () => {
|
||||
expect(amountWithTax(0, { rateType: 'percentage', rate: 20 })).toBe(0);
|
||||
});
|
||||
|
||||
it('applies percentage tax', () => {
|
||||
expect(amountWithTax(100, { rateType: 'percentage', rate: 20 })).toBe(120);
|
||||
});
|
||||
|
||||
it('applies fixed amount tax', () => {
|
||||
expect(amountWithTax(100, { rateType: 'amount', rate: 5 })).toBe(105);
|
||||
});
|
||||
|
||||
it('applies fixed rate type from API schema', () => {
|
||||
expect(amountWithTax(100, { rateType: 'fixed', rate: 5 })).toBe(105);
|
||||
});
|
||||
|
||||
it('returns base amount when tax rate is missing', () => {
|
||||
expect(amountWithTax(100, null)).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('effectiveMarginPrice', () => {
|
||||
it('computes price from margin mode', () => {
|
||||
expect(
|
||||
effectiveMarginPrice({ priceMode: 'margin', price: 50, cost: 100, margin: 20 })
|
||||
).toBe(120);
|
||||
});
|
||||
|
||||
it('returns explicit price for amount mode', () => {
|
||||
expect(
|
||||
effectiveMarginPrice({ priceMode: 'amount', price: 150, cost: 100, margin: 20 })
|
||||
).toBe(150);
|
||||
});
|
||||
});
|
||||
@ -1,7 +1,9 @@
|
||||
import mongoose from 'mongoose';
|
||||
import { generateId } from '../../utils.js';
|
||||
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(
|
||||
{
|
||||
@ -140,23 +142,41 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
|
||||
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
|
||||
let totalAmount = 0;
|
||||
for (const item of invoice.invoiceOrderItems || []) {
|
||||
for (const item of invoiceOrderItems) {
|
||||
totalAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
||||
}
|
||||
let totalAmountWithTax = 0;
|
||||
for (const item of invoice.invoiceOrderItems || []) {
|
||||
for (const item of invoiceOrderItems) {
|
||||
totalAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
||||
}
|
||||
|
||||
// Calculate shipping totals from invoiceShipments
|
||||
let shippingAmount = 0;
|
||||
for (const item of invoice.invoiceShipments || []) {
|
||||
for (const item of invoiceShipments) {
|
||||
shippingAmount += Number.parseFloat(item.invoiceAmount) || 0;
|
||||
}
|
||||
let shippingAmountWithTax = 0;
|
||||
for (const item of invoice.invoiceShipments || []) {
|
||||
for (const item of invoiceShipments) {
|
||||
shippingAmountWithTax += Number.parseFloat(item.invoiceAmountWithTax) || 0;
|
||||
}
|
||||
|
||||
@ -168,6 +188,8 @@ invoiceSchema.statics.recalculate = async function (invoice, user) {
|
||||
(parseFloat(shippingAmountWithTax) - parseFloat(shippingAmount));
|
||||
|
||||
const updateData = {
|
||||
invoiceOrderItems,
|
||||
invoiceShipments,
|
||||
totalAmount: parseFloat(totalAmount).toFixed(2),
|
||||
totalAmountWithTax: parseFloat(totalAmountWithTax).toFixed(2),
|
||||
shippingAmount: parseFloat(shippingAmount).toFixed(2),
|
||||
|
||||
@ -15,9 +15,10 @@ const filamentStockSchema = new Schema(
|
||||
{
|
||||
_reference: { type: String, default: () => generateId()() },
|
||||
state: {
|
||||
type: { type: String, required: true },
|
||||
type: { type: String, required: true, default: 'draft' },
|
||||
progress: { type: Number, required: false },
|
||||
},
|
||||
postedAt: { type: Date, required: false },
|
||||
startingWeight: {
|
||||
net: { type: Number, required: true },
|
||||
gross: { type: Number, required: true },
|
||||
@ -65,6 +66,11 @@ const rollupConfigs = [
|
||||
filter: {},
|
||||
rollups: [{ name: 'totalCurrentWeight', property: 'currentWeight.net', operation: 'sum' }],
|
||||
},
|
||||
{
|
||||
name: 'draft',
|
||||
filter: { 'state.type': 'draft' },
|
||||
rollups: [{ name: 'draft', property: 'state.type', operation: 'count' }],
|
||||
},
|
||||
{
|
||||
name: 'unconsumed',
|
||||
filter: { 'state.type': 'unconsumed' },
|
||||
@ -114,6 +120,8 @@ filamentStockSchema.statics.recalculate = async function (filamentStock, user) {
|
||||
stockLocationId,
|
||||
user,
|
||||
});
|
||||
|
||||
if (filamentStock.state?.type === 'draft' || filamentStock.state?.type === 'consumed') return;
|
||||
};
|
||||
|
||||
// Add virtual id getter
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
getObject,
|
||||
} from '../../database.js';
|
||||
import { generateId } from '../../utils.js';
|
||||
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||
const { Schema } = mongoose;
|
||||
|
||||
const skuModelsByItemType = {
|
||||
@ -194,17 +195,10 @@ orderItemSchema.statics.recalculate = async function (orderItem, user) {
|
||||
}
|
||||
}
|
||||
|
||||
let taxRate = orderItem.taxRate;
|
||||
if (orderItem.taxRate?._id && Object.keys(orderItem.taxRate).length === 1) {
|
||||
taxRate = await getObject({
|
||||
model: taxRateModel,
|
||||
id: orderItem.taxRate._id,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
const taxRate = await resolveTaxRate(orderItem.taxRate, getObject, taxRateModel);
|
||||
|
||||
const orderTotalAmount = effectiveItemAmount * orderItem.quantity;
|
||||
const orderTotalAmountWithTax = orderTotalAmount * (1 + (taxRate?.rate || 0) / 100);
|
||||
const orderTotalAmountWithTax = amountWithTax(orderTotalAmount, taxRate);
|
||||
|
||||
const orderItemUpdateData = {
|
||||
totalAmount: orderTotalAmount,
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
editObject,
|
||||
getObject,
|
||||
} from '../../database.js';
|
||||
import { amountWithTax, resolveTaxRate } from '../../tax.js';
|
||||
|
||||
const shipmentSchema = new Schema(
|
||||
{
|
||||
@ -102,26 +103,17 @@ shipmentSchema.statics.recalculate = async function (shipment, user) {
|
||||
return;
|
||||
}
|
||||
|
||||
var taxRate = shipment.taxRate;
|
||||
const taxRate = await resolveTaxRate(shipment.taxRate, getObject, taxRateModel);
|
||||
|
||||
if (shipment.taxRate?._id && Object.keys(shipment.taxRate).length == 1) {
|
||||
taxRate = await getObject({
|
||||
model: taxRateModel,
|
||||
id: shipment.taxRate._id,
|
||||
cached: true,
|
||||
});
|
||||
}
|
||||
|
||||
const amountWithTax = parseFloat(
|
||||
(shipment.amount || 0) * (1 + (taxRate?.rate || 0) / 100)
|
||||
).toFixed(2);
|
||||
const amountWithTaxValue = amountWithTax(shipment.amount || 0, taxRate);
|
||||
await editObject({
|
||||
model: shipmentModel,
|
||||
id: shipment._id,
|
||||
updateData: {
|
||||
amountWithTax: amountWithTax,
|
||||
amountWithTax: amountWithTaxValue,
|
||||
invoicedAmountRemaining: shipment.amount - (shipment.invoicedAmount || 0),
|
||||
invoicedAmountWithTaxRemaining: amountWithTax - (shipment.invoicedAmountWithTax || 0),
|
||||
invoicedAmountWithTaxRemaining:
|
||||
amountWithTaxValue - (shipment.invoicedAmountWithTax || 0),
|
||||
},
|
||||
user,
|
||||
recalculate: false,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
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 marketplaceMappingSchema = new mongoose.Schema(
|
||||
@ -39,4 +42,29 @@ courierServiceSchema.virtual('id').get(function () {
|
||||
|
||||
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);
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
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;
|
||||
|
||||
// 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.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 skus = await filamentSkuModel.find({ filament: filament._id }).select('_id').lean();
|
||||
for (const sku of skus) {
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
import mongoose from 'mongoose';
|
||||
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;
|
||||
|
||||
// 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.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 skuId = filamentSku._id;
|
||||
const draftOrderItems = await orderItemModel
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
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;
|
||||
|
||||
// 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.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 skus = await partSkuModel.find({ part: part._id }).select('_id').lean();
|
||||
for (const sku of skus) {
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import mongoose from 'mongoose';
|
||||
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;
|
||||
|
||||
// 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.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 skuId = partSku._id;
|
||||
const draftOrderItems = await orderItemModel
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import mongoose from 'mongoose';
|
||||
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;
|
||||
|
||||
// Define the main product schema
|
||||
@ -35,6 +38,34 @@ productSchema.virtual('id').get(function () {
|
||||
productSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
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 itemId = product._id;
|
||||
const draftOrderItems = await orderItemModel
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import mongoose from 'mongoose';
|
||||
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 partSkuUsageSchema = new Schema({
|
||||
@ -43,6 +51,54 @@ productSkuSchema.virtual('id').get(function () {
|
||||
productSkuSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
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 skuId = productSku._id;
|
||||
const draftOrderItems = await orderItemModel
|
||||
|
||||
54
src/database/tax.js
Normal file
54
src/database/tax.js
Normal 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;
|
||||
}
|
||||
@ -12,10 +12,6 @@ const listAllowedFilters = [
|
||||
'state',
|
||||
'vendor._id',
|
||||
'client._id',
|
||||
'from',
|
||||
'from._id',
|
||||
'to',
|
||||
'to._id',
|
||||
'order',
|
||||
'order._id',
|
||||
'orderType',
|
||||
@ -27,10 +23,6 @@ const listAllowedSorters = ['createdAt', 'state', 'updatedAt', 'invoiceDate', 'd
|
||||
const propertiesAllowedFilters = [
|
||||
'vendor',
|
||||
'client',
|
||||
'from',
|
||||
'from._id',
|
||||
'to',
|
||||
'to._id',
|
||||
'orderType',
|
||||
'order',
|
||||
'order._id',
|
||||
|
||||
@ -21,13 +21,12 @@ const router = express.Router();
|
||||
const listAllowedFilters = [
|
||||
'name',
|
||||
'immediatePay',
|
||||
'marketplaces.marketplace',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
];
|
||||
const listAllowedSorters = ['name', 'immediatePay', 'createdAt', '_id', 'updatedAt'];
|
||||
const propertiesAllowedFilters = ['name', 'immediatePay', 'marketplaces.marketplace'];
|
||||
const propertiesAllowedFilters = ['name', 'immediatePay'];
|
||||
|
||||
router.get('/', isAuthenticated, checkPermissions('paymentPolicy', 'list'), async (req, res) => {
|
||||
const { page, limit, property, search, sortProperty, sortOrder } = req.query;
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
editMultipleFilamentStocksRouteHandler,
|
||||
newFilamentStockRouteHandler,
|
||||
deleteFilamentStockRouteHandler,
|
||||
postFilamentStockRouteHandler,
|
||||
listFilamentStocksByPropertiesRouteHandler,
|
||||
getFilamentStockStatsRouteHandler,
|
||||
getFilamentStockHistoryRouteHandler,
|
||||
@ -121,4 +122,8 @@ router.delete('/:id', isAuthenticated, async (req, res) => {
|
||||
deleteFilamentStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
router.post('/:id/post', isAuthenticated, checkPermissions('filamentStock', 'post'), async (req, res) => {
|
||||
postFilamentStockRouteHandler(req, res);
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@ -6,6 +6,8 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||
const router = express.Router();
|
||||
|
||||
const listAllowedFilters = [
|
||||
'part',
|
||||
'part._id',
|
||||
'partSku',
|
||||
'partSku._id',
|
||||
'state',
|
||||
@ -16,7 +18,14 @@ const listAllowedFilters = [
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
];
|
||||
const listAllowedSorters = ['partSku', 'currentQuantity', 'state', 'createdAt', 'updatedAt'];
|
||||
const listAllowedSorters = [
|
||||
'part',
|
||||
'partSku',
|
||||
'currentQuantity',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
];
|
||||
const propertiesAllowedFilters = ['part', 'state.type'];
|
||||
import {
|
||||
listPartStocksRouteHandler,
|
||||
|
||||
@ -6,6 +6,8 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||
const router = express.Router();
|
||||
|
||||
const listAllowedFilters = [
|
||||
'product',
|
||||
'product._id',
|
||||
'productSku',
|
||||
'productSku._id',
|
||||
'state',
|
||||
@ -16,8 +18,16 @@ const listAllowedFilters = [
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
];
|
||||
const listAllowedSorters = ['productSku', 'currentQuantity', 'state', 'createdAt', 'updatedAt'];
|
||||
const propertiesAllowedFilters = ['productSku', 'state.type'];
|
||||
const listAllowedSorters = [
|
||||
'product',
|
||||
'productSku',
|
||||
'currentQuantity',
|
||||
'state',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'stockLocation',
|
||||
];
|
||||
const propertiesAllowedFilters = ['product', 'productSku', 'state.type'];
|
||||
import {
|
||||
listProductStocksRouteHandler,
|
||||
getProductStockRouteHandler,
|
||||
|
||||
@ -8,6 +8,8 @@ const router = express.Router();
|
||||
const listAllowedFilters = [
|
||||
'state',
|
||||
'state.type',
|
||||
'fromLocation',
|
||||
'toLocation',
|
||||
'postedAt',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
|
||||
@ -15,7 +15,8 @@ const listAllowedFilters = [
|
||||
'deliveryTime',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'marketplaces.marketplace',
|
||||
'additionalCost',
|
||||
'additionalCostWithTax',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -25,8 +26,11 @@ const listAllowedSorters = [
|
||||
'courier',
|
||||
'active',
|
||||
'tracked',
|
||||
'deliveryTime',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'additionalCost',
|
||||
'additionalCostWithTax',
|
||||
'estimatedDeliveryTime',
|
||||
'createdAt',
|
||||
'_id',
|
||||
@ -42,7 +46,8 @@ const propertiesAllowedFilters = [
|
||||
'deliveryTime',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'marketplaces.marketplace',
|
||||
'additionalCost',
|
||||
'additionalCostWithTax',
|
||||
];
|
||||
import {
|
||||
listCourierServicesRouteHandler,
|
||||
|
||||
@ -11,11 +11,20 @@ const listAllowedFilters = [
|
||||
'active',
|
||||
'isGlobal',
|
||||
'state',
|
||||
'connection.port',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
];
|
||||
const listAllowedSorters = ['name', 'documentSize', 'connectedAt', 'updatedAt', 'state', 'createdAt'];
|
||||
const listAllowedSorters = [
|
||||
'name',
|
||||
'documentSize',
|
||||
'connectedAt',
|
||||
'connection.port',
|
||||
'updatedAt',
|
||||
'state',
|
||||
'createdAt',
|
||||
];
|
||||
const propertiesAllowedFilters = ['tags'];
|
||||
import {
|
||||
listDocumentPrintersRouteHandler,
|
||||
|
||||
@ -11,7 +11,10 @@ const listAllowedFilters = [
|
||||
'material._id',
|
||||
'diameter',
|
||||
'name',
|
||||
'density',
|
||||
'emptySpoolWeight',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -21,6 +24,9 @@ const listAllowedSorters = [
|
||||
'createdAt',
|
||||
'vendor',
|
||||
'material',
|
||||
'diameter',
|
||||
'density',
|
||||
'emptySpoolWeight',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'updatedAt',
|
||||
|
||||
@ -5,13 +5,26 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const listAllowedFilters = ['product._id', '_id', 'name', 'createdAt', 'updatedAt', '_reference'];
|
||||
const listAllowedFilters = [
|
||||
'product._id',
|
||||
'_id',
|
||||
'name',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'margin',
|
||||
'priceWithTax',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
];
|
||||
const listAllowedSorters = [
|
||||
'name',
|
||||
'priceMode',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'margin',
|
||||
'priceWithTax',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
@ -59,24 +72,15 @@ router.get('/properties', checkPermissions('part', 'list'), isAuthenticated, asy
|
||||
listPartsByPropertiesRouteHandler(req, res, properties, filter, masterFilter);
|
||||
});
|
||||
|
||||
router.get(
|
||||
'/values',
|
||||
checkPermissions('part', 'list'),
|
||||
isAuthenticated,
|
||||
async (req, res) => {
|
||||
const { property } = req.query;
|
||||
const filter = await getFilter(req.query, listAllowedFilters, true);
|
||||
var masterFilter = {};
|
||||
if (req.query.masterFilter) {
|
||||
masterFilter = await getFilter(
|
||||
JSON.parse(req.query.masterFilter),
|
||||
listAllowedFilters,
|
||||
true
|
||||
);
|
||||
}
|
||||
getPartPropertyValuesRouteHandler(req, res, property, filter, masterFilter);
|
||||
router.get('/values', checkPermissions('part', 'list'), isAuthenticated, async (req, res) => {
|
||||
const { property } = req.query;
|
||||
const filter = await getFilter(req.query, listAllowedFilters, true);
|
||||
var masterFilter = {};
|
||||
if (req.query.masterFilter) {
|
||||
masterFilter = await getFilter(JSON.parse(req.query.masterFilter), listAllowedFilters, true);
|
||||
}
|
||||
);
|
||||
getPartPropertyValuesRouteHandler(req, res, property, filter, masterFilter);
|
||||
});
|
||||
|
||||
router.get('/search', checkPermissions('part', 'list'), isAuthenticated, async (req, res) => {
|
||||
const { search } = req.query;
|
||||
|
||||
@ -12,7 +12,10 @@ const listAllowedFilters = [
|
||||
'part._id',
|
||||
'name',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'priceWithTax',
|
||||
'margin',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -25,6 +28,7 @@ const listAllowedSorters = [
|
||||
'costWithTax',
|
||||
'price',
|
||||
'priceWithTax',
|
||||
'margin',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
@ -11,6 +11,11 @@ const listAllowedFilters = [
|
||||
'globalPrice',
|
||||
'productCategory',
|
||||
'productCategory._id',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'margin',
|
||||
'priceWithTax',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -24,6 +29,7 @@ const listAllowedSorters = [
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'margin',
|
||||
'priceWithTax',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
@ -12,7 +12,10 @@ const listAllowedFilters = [
|
||||
'product._id',
|
||||
'name',
|
||||
'cost',
|
||||
'costWithTax',
|
||||
'price',
|
||||
'priceWithTax',
|
||||
'margin',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -25,6 +28,7 @@ const listAllowedSorters = [
|
||||
'costWithTax',
|
||||
'price',
|
||||
'priceWithTax',
|
||||
'margin',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
@ -6,7 +6,7 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
||||
const router = express.Router();
|
||||
|
||||
const listAllowedFilters = ['name', 'createdAt', 'updatedAt', '_reference', 'tags'];
|
||||
const listAllowedSorters = ['name', 'createdAt', '_id', 'updatedAt', 'tags'];
|
||||
const listAllowedSorters = ['name', 'createdAt', 'updatedAt'];
|
||||
const propertiesAllowedFilters = ['tags'];
|
||||
|
||||
import {
|
||||
|
||||
@ -12,7 +12,6 @@ const listAllowedFilters = [
|
||||
'active',
|
||||
'country',
|
||||
'jurisdiction',
|
||||
'marketplaces.marketplace',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
|
||||
@ -21,12 +21,8 @@ const router = express.Router();
|
||||
const listAllowedFilters = [
|
||||
'name',
|
||||
'handlingTime',
|
||||
'localPickup',
|
||||
'globalShipping',
|
||||
'freightShipping',
|
||||
'pickupDropOff',
|
||||
'courierServices',
|
||||
'marketplaces.marketplace',
|
||||
'localPickup',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -35,9 +31,8 @@ const listAllowedSorters = ['name', 'handlingTime', 'createdAt', '_id', 'updated
|
||||
const propertiesAllowedFilters = [
|
||||
'name',
|
||||
'handlingTime',
|
||||
'localPickup',
|
||||
'courierServices',
|
||||
'marketplaces.marketplace',
|
||||
'localPickup',
|
||||
];
|
||||
|
||||
router.get('/', isAuthenticated, checkPermissions('fulfillmentPolicy', 'list'), async (req, res) => {
|
||||
|
||||
@ -21,6 +21,7 @@ const listAllowedFilters = [
|
||||
'returnPolicy',
|
||||
'state',
|
||||
'state.type',
|
||||
'condition',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -48,6 +49,7 @@ const propertiesAllowedFilters = [
|
||||
'returnPolicy',
|
||||
'state',
|
||||
'state.type',
|
||||
'condition',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
];
|
||||
|
||||
@ -24,7 +24,6 @@ const listAllowedFilters = [
|
||||
'returnPeriodDays',
|
||||
'returnShippingCostPayer',
|
||||
'refundMethod',
|
||||
'marketplaces.marketplace',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'_reference',
|
||||
@ -42,7 +41,6 @@ const propertiesAllowedFilters = [
|
||||
'returnsAccepted',
|
||||
'returnShippingCostPayer',
|
||||
'refundMethod',
|
||||
'marketplaces.marketplace',
|
||||
];
|
||||
|
||||
router.get('/', isAuthenticated, checkPermissions('returnPolicy', 'list'), async (req, res) => {
|
||||
|
||||
@ -6,6 +6,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
|
||||
listObjects: jest.fn(),
|
||||
getObject: jest.fn(),
|
||||
editObject: jest.fn(),
|
||||
editObject: jest.fn(),
|
||||
editObjects: jest.fn(),
|
||||
newObject: jest.fn(),
|
||||
deleteObject: jest.fn(),
|
||||
@ -13,6 +14,7 @@ jest.unstable_mockModule('../../../database/database.js', () => ({
|
||||
getModelStats: jest.fn(),
|
||||
getModelHistory: jest.fn(),
|
||||
getObjectNeighbors: jest.fn(),
|
||||
checkStates: jest.fn(),
|
||||
aggregateRollups: jest.fn(),
|
||||
aggregateRollupsHistory: jest.fn(),
|
||||
}));
|
||||
@ -41,9 +43,12 @@ const {
|
||||
listFilamentStocksRouteHandler,
|
||||
getFilamentStockRouteHandler,
|
||||
newFilamentStockRouteHandler,
|
||||
postFilamentStockRouteHandler,
|
||||
} = await import('../filamentstocks.js');
|
||||
|
||||
const { listObjects, getObject, newObject } = await import('../../../database/database.js');
|
||||
const { listObjects, getObject, newObject, checkStates, editObject } = await import(
|
||||
'../../../database/database.js'
|
||||
);
|
||||
const { filamentStockModel } = await import(
|
||||
'../../../database/schemas/inventory/filamentstock.schema.js'
|
||||
);
|
||||
@ -80,21 +85,79 @@ describe('Filament Stock Service Route Handlers', () => {
|
||||
});
|
||||
|
||||
describe('newFilamentStockRouteHandler', () => {
|
||||
it('should create a new filament stock', async () => {
|
||||
it('should create a new draft filament stock without a stock event', async () => {
|
||||
req.body = {
|
||||
filament: 'filament123',
|
||||
startingWeight: { net: 1000, gross: 1100 },
|
||||
currentWeight: { net: 1000, gross: 1100 },
|
||||
};
|
||||
const mockStock = { _id: '456', ...req.body };
|
||||
const mockStockEvent = { _id: '789' };
|
||||
newObject.mockResolvedValueOnce(mockStock).mockResolvedValueOnce(mockStockEvent);
|
||||
const mockStock = { _id: '456', ...req.body, state: { type: 'draft' } };
|
||||
newObject.mockResolvedValueOnce(mockStock);
|
||||
|
||||
await newFilamentStockRouteHandler(req, res);
|
||||
|
||||
expect(newObject).toHaveBeenCalledTimes(2);
|
||||
expect(newObject).toHaveBeenCalledTimes(1);
|
||||
expect(newObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
newData: expect.objectContaining({ state: { type: 'draft' } }),
|
||||
})
|
||||
);
|
||||
expect(res.send).toHaveBeenCalledWith(mockStock);
|
||||
});
|
||||
});
|
||||
|
||||
describe('postFilamentStockRouteHandler', () => {
|
||||
it('should post a draft filament stock and create an initial stock event', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
req.user = { _id: 'test-user-id' };
|
||||
checkStates.mockResolvedValue(true);
|
||||
getObject.mockResolvedValue({
|
||||
_id: '507f1f77bcf86cd799439011',
|
||||
startingWeight: { net: 1000, gross: 1100 },
|
||||
});
|
||||
newObject.mockResolvedValue({ _id: '789' });
|
||||
editObject.mockResolvedValue({
|
||||
_id: '507f1f77bcf86cd799439011',
|
||||
state: { type: 'unconsumed' },
|
||||
postedAt: expect.any(Date),
|
||||
});
|
||||
|
||||
await postFilamentStockRouteHandler(req, res);
|
||||
|
||||
expect(checkStates).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ states: ['draft'] })
|
||||
);
|
||||
expect(newObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
newData: expect.objectContaining({
|
||||
value: 1000,
|
||||
unit: 'g',
|
||||
parentType: 'filamentStock',
|
||||
}),
|
||||
recalculate: true,
|
||||
})
|
||||
);
|
||||
expect(editObject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updateData: expect.objectContaining({
|
||||
state: { type: 'unconsumed' },
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail if filament stock is not in draft state', async () => {
|
||||
req.params.id = '507f1f77bcf86cd799439011';
|
||||
checkStates.mockResolvedValue(false);
|
||||
|
||||
await postFilamentStockRouteHandler(req, res);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ error: 'Filament stock is not in draft state.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
listObjectsByProperties,
|
||||
getModelStats,
|
||||
getModelHistory,
|
||||
checkStates,
|
||||
searchObjects,
|
||||
getPropertyValues,
|
||||
getObjectNeighbors,
|
||||
@ -20,6 +21,12 @@ import { stockEventModel } from '../../database/schemas/inventory/stockevent.sch
|
||||
const logger = log4js.getLogger('Filament Stocks');
|
||||
logger.level = config.server.logLevel;
|
||||
|
||||
const FILAMENT_STOCK_POPULATE = [
|
||||
{ path: 'filament' },
|
||||
{ path: 'filamentSku', populate: 'filament' },
|
||||
{ path: 'stockLocation' },
|
||||
];
|
||||
|
||||
export const listFilamentStocksRouteHandler = async (
|
||||
req,
|
||||
res,
|
||||
@ -130,19 +137,33 @@ export const editFilamentStockRouteHandler = async (req, res) => {
|
||||
|
||||
logger.trace(`Filament Stock with ID: ${id}`);
|
||||
|
||||
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
logger.error('Error checking filament stock states:', checkStatesResult.error);
|
||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Filament stock is not in draft state.');
|
||||
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
stockLocation: req.body.stockLocation,
|
||||
filament: req.body?.filament,
|
||||
filamentSku: req.body?.filamentSku,
|
||||
stockLocation: req.body?.stockLocation,
|
||||
startingWeight: req.body?.startingWeight,
|
||||
currentWeight: req.body?.currentWeight ?? req.body?.startingWeight,
|
||||
};
|
||||
const result = await editObject({
|
||||
model: filamentStockModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
populate: [
|
||||
{ path: 'filament' },
|
||||
{ path: 'filamentSku', populate: 'filament' },
|
||||
{ path: 'stockLocation' },
|
||||
],
|
||||
populate: FILAMENT_STOCK_POPULATE,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@ -183,13 +204,14 @@ export const editMultipleFilamentStocksRouteHandler = async (req, res) => {
|
||||
};
|
||||
|
||||
export const newFilamentStockRouteHandler = async (req, res) => {
|
||||
const startingWeight = req.body.startingWeight;
|
||||
const newData = {
|
||||
updatedAt: new Date(),
|
||||
startingWeight: req.body.startingWeight,
|
||||
currentWeight: req.body.currentWeight,
|
||||
startingWeight,
|
||||
currentWeight: req.body.currentWeight ?? startingWeight,
|
||||
filament: req.body.filament,
|
||||
filamentSku: req.body.filamentSku,
|
||||
state: req.body.state,
|
||||
state: req.body.state ?? { type: 'draft' },
|
||||
stockLocation: req.body.stockLocation,
|
||||
};
|
||||
const result = await newObject({
|
||||
@ -204,28 +226,6 @@ export const newFilamentStockRouteHandler = async (req, res) => {
|
||||
|
||||
logger.debug(`New filament stock with ID: ${result._id}`);
|
||||
|
||||
const netStockEventData = {
|
||||
updatedAt: new Date(),
|
||||
value: req.body.startingWeight.net,
|
||||
owner: req.user,
|
||||
ownerType: 'user',
|
||||
parent: result._id,
|
||||
parentType: 'filamentStock',
|
||||
unit: 'g',
|
||||
};
|
||||
|
||||
const stockEventResult = await newObject({
|
||||
model: stockEventModel,
|
||||
newData: netStockEventData,
|
||||
user: req.user,
|
||||
});
|
||||
if (stockEventResult.error) {
|
||||
logger.error('No stock event created:', stockEventResult.error);
|
||||
return res.status(stockEventResult.code).send(stockEventResult);
|
||||
}
|
||||
|
||||
logger.debug(`New stock event with ID: ${stockEventResult._id}`);
|
||||
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
@ -235,6 +235,20 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
|
||||
|
||||
logger.trace(`Filament Stock with ID: ${id}`);
|
||||
|
||||
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
logger.error('Error checking filament stock states:', checkStatesResult.error);
|
||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Filament stock is not in draft state.');
|
||||
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteObject({
|
||||
model: filamentStockModel,
|
||||
id,
|
||||
@ -250,6 +264,79 @@ export const deleteFilamentStockRouteHandler = async (req, res) => {
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const postFilamentStockRouteHandler = async (req, res) => {
|
||||
const id = new mongoose.Types.ObjectId(req.params.id);
|
||||
|
||||
logger.trace(`Filament Stock with ID: ${id}`);
|
||||
|
||||
const checkStatesResult = await checkStates({ model: filamentStockModel, id, states: ['draft'] });
|
||||
|
||||
if (checkStatesResult.error) {
|
||||
logger.error('Error checking filament stock states:', checkStatesResult.error);
|
||||
res.status(checkStatesResult.code).send(checkStatesResult);
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStatesResult === false) {
|
||||
logger.error('Filament stock is not in draft state.');
|
||||
res.status(400).send({ error: 'Filament stock is not in draft state.', code: 400 });
|
||||
return;
|
||||
}
|
||||
|
||||
const filamentStock = await getObject({
|
||||
model: filamentStockModel,
|
||||
id,
|
||||
populate: FILAMENT_STOCK_POPULATE,
|
||||
});
|
||||
if (filamentStock?.error) {
|
||||
logger.error('Error loading filament stock to post:', filamentStock.error);
|
||||
res.status(filamentStock.code || 500).send(filamentStock);
|
||||
return;
|
||||
}
|
||||
|
||||
const initialStockEventResult = await newObject({
|
||||
model: stockEventModel,
|
||||
newData: {
|
||||
value: filamentStock.startingWeight.net,
|
||||
unit: 'g',
|
||||
parent: { _id: id },
|
||||
parentType: 'filamentStock',
|
||||
owner: { _id: req.user._id },
|
||||
ownerType: 'user',
|
||||
},
|
||||
recalculate: true,
|
||||
user: req.user,
|
||||
});
|
||||
if (initialStockEventResult?.error) {
|
||||
logger.error('Error creating initial stock event:', initialStockEventResult.error);
|
||||
res.status(initialStockEventResult.code || 500).send(initialStockEventResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
updatedAt: new Date(),
|
||||
state: { type: 'unconsumed' },
|
||||
postedAt: new Date(),
|
||||
currentWeight: filamentStock.startingWeight,
|
||||
};
|
||||
const result = await editObject({
|
||||
model: filamentStockModel,
|
||||
id,
|
||||
updateData,
|
||||
user: req.user,
|
||||
populate: FILAMENT_STOCK_POPULATE,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
logger.error('Error posting filament stock:', result.error);
|
||||
res.status(result.code).send(result);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Posted filament stock with ID: ${id}`);
|
||||
res.send(result);
|
||||
};
|
||||
|
||||
export const getFilamentStockStatsRouteHandler = async (req, res) => {
|
||||
const result = await getModelStats({ model: filamentStockModel });
|
||||
if (result?.error) {
|
||||
|
||||
@ -51,6 +51,15 @@ const trimSpotlightObject = (object, objectType) => {
|
||||
online: object.online || undefined,
|
||||
amount: object.amount || undefined,
|
||||
unit: object.unit || undefined,
|
||||
currentWeight: object.currentWeight || undefined,
|
||||
currentQuantity: object.currentQuantity || undefined,
|
||||
grandTotalAmount: object.grandTotalAmount || undefined,
|
||||
totalAmount: object.totalAmount || undefined,
|
||||
totalAmountWithTax: object.totalAmountWithTax || undefined,
|
||||
cost: object.cost || undefined,
|
||||
costWithTax: object.costWithTax || undefined,
|
||||
price: object.price || undefined,
|
||||
priceWithTax: object.priceWithTax || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
45
src/utils.js
45
src/utils.js
@ -198,6 +198,26 @@ function parsePrefixedValue(value) {
|
||||
return { prefix: null, suffix: trimmed, hadPrefix: false };
|
||||
}
|
||||
|
||||
const FILTER_OPERATOR_PREFIXES = ['<>', '>=', '<=', '>', '<', '='];
|
||||
|
||||
function splitLeadingOperator(str) {
|
||||
const text = String(str).trim();
|
||||
for (const op of FILTER_OPERATOR_PREFIXES) {
|
||||
if (text.startsWith(op)) {
|
||||
return { operator: op, operand: text.slice(op.length).trim() };
|
||||
}
|
||||
}
|
||||
return { operator: '', operand: text };
|
||||
}
|
||||
|
||||
// Strips a model prefix (e.g. GCF:) from an expression, including after a leading operator.
|
||||
function stripModelPrefixFromExpression(expression) {
|
||||
const { operator, operand } = splitLeadingOperator(expression);
|
||||
const { suffix, hadPrefix } = parsePrefixedValue(operand);
|
||||
if (!hadPrefix) return expression;
|
||||
return operator + suffix;
|
||||
}
|
||||
|
||||
function buildRegexOp(pattern, useOptions = true) {
|
||||
const op = { $regex: pattern };
|
||||
if (useOptions) op.$options = 'i';
|
||||
@ -242,12 +262,12 @@ function getRefModelEntryFromPrefix(prefix) {
|
||||
}
|
||||
|
||||
function stripPrefixFromOperand(operand) {
|
||||
const { suffix, hadPrefix } = parsePrefixedValue(String(operand).trim());
|
||||
return hadPrefix ? suffix : String(operand).trim();
|
||||
return stripModelPrefixFromExpression(operand);
|
||||
}
|
||||
|
||||
function getRefModelEntryForToken(token, fallbackRefName) {
|
||||
const { prefix, hadPrefix } = parsePrefixedValue(String(token).trim());
|
||||
const { operand } = splitLeadingOperator(token);
|
||||
const { prefix, hadPrefix } = parsePrefixedValue(operand);
|
||||
if (hadPrefix) {
|
||||
const entry = getRefModelEntryFromPrefix(prefix);
|
||||
if (entry) return entry;
|
||||
@ -741,9 +761,15 @@ function combineAndRefIds(children) {
|
||||
}
|
||||
|
||||
async function resolveRefOperand(operand, refModelEntry, operator = 'eq') {
|
||||
const lookupOperand = String(operand).trim();
|
||||
const { prefix, suffix, hadPrefix } = parsePrefixedValue(lookupOperand);
|
||||
if (hadPrefix) {
|
||||
const prefixEntry = getRefModelEntryFromPrefix(prefix);
|
||||
if (prefixEntry) refModelEntry = prefixEntry;
|
||||
}
|
||||
|
||||
if (!refModelEntry?.model) return NO_MATCH_CONDITION;
|
||||
|
||||
const lookupOperand = String(operand).trim();
|
||||
const objectId = extractObjectIdFromOperand(lookupOperand);
|
||||
|
||||
if (objectId) {
|
||||
@ -752,14 +778,16 @@ async function resolveRefOperand(operand, refModelEntry, operator = 'eq') {
|
||||
return NO_MATCH_CONDITION;
|
||||
}
|
||||
|
||||
const expression = buildRefListExpression(operator, lookupOperand);
|
||||
const ids = await listRefModelIds(refModelEntry, expression);
|
||||
const expressionOperand = hadPrefix ? suffix : lookupOperand;
|
||||
|
||||
if (operator === 'ne') {
|
||||
const ids = await listRefModelIds(refModelEntry, expressionOperand);
|
||||
if (ids.length === 0) return { query: {} };
|
||||
return { op: { $nin: ids } };
|
||||
}
|
||||
|
||||
const expression = buildRefListExpression(operator, expressionOperand);
|
||||
const ids = await listRefModelIds(refModelEntry, expression);
|
||||
return idsToCondition(ids);
|
||||
}
|
||||
|
||||
@ -850,9 +878,8 @@ async function parseFilter(property, value, model = null) {
|
||||
}
|
||||
|
||||
let expression = trimmed;
|
||||
if (fieldKind.kind === 'default' && expression.charAt(3) === ':') {
|
||||
const afterColon = value.split(':')[1];
|
||||
expression = afterColon != null ? afterColon.trim() : '';
|
||||
if (fieldKind.kind === 'default') {
|
||||
expression = stripModelPrefixFromExpression(expression);
|
||||
}
|
||||
|
||||
const isDateField = looksLikeDateField(property);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user