mirror of
https://github.com/frappe/books.git
synced 2024-12-22 10:58:59 +00:00
feat: add shipment and purchase receipt
This commit is contained in:
parent
14138967c1
commit
9a510f1a63
@ -857,6 +857,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.trigger('beforeCancel');
|
||||
await this.trigger('beforeCancel');
|
||||
await this.setAndSync('cancelled', true);
|
||||
await this.trigger('afterCancel');
|
||||
@ -908,6 +909,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
if (convertToFloat) {
|
||||
return sum.float;
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
|
@ -26,28 +26,56 @@ export abstract class Transactional extends Doc {
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract getPosting(): Promise<LedgerPosting>;
|
||||
abstract getPosting(): Promise<LedgerPosting | null>;
|
||||
|
||||
async validate() {
|
||||
await super.validate();
|
||||
if (!this.isTransactional) {
|
||||
return;
|
||||
}
|
||||
|
||||
const posting = await this.getPosting();
|
||||
if (posting === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
posting.validate();
|
||||
}
|
||||
|
||||
async afterSubmit(): Promise<void> {
|
||||
await super.afterSubmit();
|
||||
if (!this.isTransactional) {
|
||||
return;
|
||||
}
|
||||
|
||||
const posting = await this.getPosting();
|
||||
if (posting === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await posting.post();
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
if (!this.isTransactional) {
|
||||
return;
|
||||
}
|
||||
|
||||
const posting = await this.getPosting();
|
||||
if (posting === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await posting.postReverse();
|
||||
}
|
||||
|
||||
async afterDelete(): Promise<void> {
|
||||
await super.afterDelete();
|
||||
if (!this.isTransactional) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ledgerEntryIds = (await this.fyo.db.getAll(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap } from 'fyo/model/types';
|
||||
import { AccountTypeEnum } from 'models/baseModels/Account/types';
|
||||
import { valuationMethod } from './types';
|
||||
import { ValuationMethod } from './types';
|
||||
|
||||
export class InventorySettings extends Doc {
|
||||
stockInHand?: string;
|
||||
valuationMethod?: valuationMethod;
|
||||
stockInReceivedButNotBilled?: string;
|
||||
valuationMethod?: ValuationMethod;
|
||||
stockReceivedButNotBilled?: string;
|
||||
costOfGoodsSold?: string;
|
||||
|
||||
static filters: FiltersMap = {
|
||||
stockInHand: () => ({
|
||||
@ -17,5 +18,9 @@ export class InventorySettings extends Doc {
|
||||
isGroup: false,
|
||||
accountType: AccountTypeEnum['Stock Received But Not Billed'],
|
||||
}),
|
||||
costOfGoodsSold: () => ({
|
||||
isGroup: false,
|
||||
accountType: AccountTypeEnum['Cost of Goods Sold'],
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { DateTime } from 'luxon';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { StockLedgerEntry } from './StockLedgerEntry';
|
||||
@ -53,6 +54,19 @@ export class StockManager {
|
||||
});
|
||||
}
|
||||
|
||||
async validateCancel(transferDetails: SMTransferDetails[]) {
|
||||
const reverseTransferDetails = transferDetails.map(
|
||||
({ item, rate, quantity, fromLocation, toLocation }) => ({
|
||||
item,
|
||||
rate,
|
||||
quantity,
|
||||
fromLocation: toLocation,
|
||||
toLocation: fromLocation,
|
||||
})
|
||||
);
|
||||
await this.validateTransfers(reverseTransferDetails);
|
||||
}
|
||||
|
||||
async #sync() {
|
||||
for (const item of this.items) {
|
||||
await item.sync();
|
||||
@ -117,15 +131,21 @@ export class StockManager {
|
||||
return;
|
||||
}
|
||||
|
||||
const quantityBefore =
|
||||
const date = details.date.toISOString();
|
||||
let quantityBefore =
|
||||
(await this.fyo.db.getStockQuantity(
|
||||
details.item,
|
||||
details.fromLocation,
|
||||
undefined,
|
||||
details.date.toISOString()
|
||||
date
|
||||
)) ?? 0;
|
||||
|
||||
const formattedDate = this.fyo.format(details.date, 'Datetime');
|
||||
|
||||
if (this.isCancelled) {
|
||||
quantityBefore += details.quantity;
|
||||
}
|
||||
|
||||
if (quantityBefore < details.quantity) {
|
||||
throw new ValidationError(
|
||||
[
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
DefaultMap,
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
ListViewSettings
|
||||
ListViewSettings,
|
||||
} from 'fyo/model/types';
|
||||
import { getDocStatusListColumn } from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { StockManager } from './StockManager';
|
||||
import { StockMovementItem } from './StockMovementItem';
|
||||
import { Transfer } from './Transfer';
|
||||
import { MovementType } from './types';
|
||||
|
||||
export class StockMovement extends Doc {
|
||||
export class StockMovement extends Transfer {
|
||||
name?: string;
|
||||
date?: Date;
|
||||
numberSeries?: string;
|
||||
@ -20,6 +20,14 @@ export class StockMovement extends Doc {
|
||||
items?: StockMovementItem[];
|
||||
amount?: Money;
|
||||
|
||||
override get isTransactional(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override async getPosting(): Promise<LedgerPosting | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
amount: {
|
||||
formula: () => {
|
||||
@ -46,23 +54,6 @@ export class StockMovement extends Doc {
|
||||
};
|
||||
}
|
||||
|
||||
async beforeSubmit(): Promise<void> {
|
||||
await super.beforeSubmit();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
await this._getStockManager().validateTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async afterSubmit(): Promise<void> {
|
||||
await super.afterSubmit();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
await this._getStockManager().createTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
await this._getStockManager().cancelTransfers();
|
||||
}
|
||||
|
||||
_getTransferDetails() {
|
||||
return (this.items ?? []).map((row) => ({
|
||||
item: row.item!,
|
||||
@ -72,16 +63,4 @@ export class StockMovement extends Doc {
|
||||
toLocation: row.toLocation,
|
||||
}));
|
||||
}
|
||||
|
||||
_getStockManager(): StockManager {
|
||||
return new StockManager(
|
||||
{
|
||||
date: this.date!,
|
||||
referenceName: this.name!,
|
||||
referenceType: this.schemaName,
|
||||
},
|
||||
this.isCancelled,
|
||||
this.fyo
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,136 @@
|
||||
import { t } from 'fyo';
|
||||
import { Attachment } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
|
||||
import { NotFoundError, ValidationError } from 'fyo/utils/errors';
|
||||
import { Defaults } from 'models/baseModels/Defaults/Defaults';
|
||||
import { getNumberSeries } from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { StockTransferItem } from './StockTransferItem';
|
||||
import { Transfer } from './Transfer';
|
||||
|
||||
export abstract class StockTransfer extends Doc {
|
||||
export abstract class StockTransfer extends Transfer {
|
||||
name?: string;
|
||||
date?: string;
|
||||
date?: Date;
|
||||
party?: string;
|
||||
terms?: string;
|
||||
attachment?: Attachment;
|
||||
grandTotal?: Money;
|
||||
items?: StockTransferItem[];
|
||||
|
||||
get isSales() {
|
||||
return this.schemaName === ModelNameEnum.Shipment;
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
grandTotal: {
|
||||
formula: () => this.getSum('items', 'amount', false),
|
||||
dependsOn: ['items'],
|
||||
},
|
||||
};
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
|
||||
terms: (doc) => {
|
||||
const defaults = doc.fyo.singles.Defaults as Defaults | undefined;
|
||||
if (doc.schemaName === ModelNameEnum.Shipment) {
|
||||
return defaults?.shipmentTerms ?? '';
|
||||
}
|
||||
|
||||
return defaults?.purchaseReceiptTerms ?? '';
|
||||
},
|
||||
date: () => new Date().toISOString().slice(0, 10),
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
party: (doc: Doc) => ({
|
||||
role: ['in', [doc.isSales ? 'Customer' : 'Supplier', 'Both']],
|
||||
}),
|
||||
numberSeries: (doc: Doc) => ({ referenceType: doc.schemaName }),
|
||||
};
|
||||
|
||||
override _getTransferDetails() {
|
||||
return (this.items ?? []).map((row) => {
|
||||
let fromLocation = undefined;
|
||||
let toLocation = undefined;
|
||||
|
||||
if (this.isSales) {
|
||||
fromLocation = row.location;
|
||||
} else {
|
||||
toLocation = row.location;
|
||||
}
|
||||
|
||||
return {
|
||||
item: row.item!,
|
||||
rate: row.rate!,
|
||||
quantity: row.quantity!,
|
||||
fromLocation,
|
||||
toLocation,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
override async getPosting(): Promise<LedgerPosting | null> {
|
||||
await this.validateAccounts();
|
||||
const stockInHand = (await this.fyo.getValue(
|
||||
ModelNameEnum.InventorySettings,
|
||||
'stockInHand'
|
||||
)) as string;
|
||||
|
||||
const amount = this.grandTotal ?? this.fyo.pesa(0);
|
||||
const posting = new LedgerPosting(this, this.fyo);
|
||||
|
||||
if (this.isSales) {
|
||||
const costOfGoodsSold = (await this.fyo.getValue(
|
||||
ModelNameEnum.InventorySettings,
|
||||
'costOfGoodsSold'
|
||||
)) as string;
|
||||
|
||||
await posting.debit(costOfGoodsSold, amount);
|
||||
await posting.credit(stockInHand, amount);
|
||||
} else {
|
||||
const stockReceivedButNotBilled = (await this.fyo.getValue(
|
||||
ModelNameEnum.InventorySettings,
|
||||
'stockReceivedButNotBilled'
|
||||
)) as string;
|
||||
|
||||
await posting.debit(stockInHand, amount);
|
||||
await posting.credit(stockReceivedButNotBilled, amount);
|
||||
}
|
||||
|
||||
await posting.makeRoundOffEntry()
|
||||
return posting;
|
||||
}
|
||||
|
||||
async validateAccounts() {
|
||||
const settings: string[] = ['stockInHand'];
|
||||
if (this.isSales) {
|
||||
settings.push('costOfGoodsSold');
|
||||
} else {
|
||||
settings.push('stockReceivedButNotBilled');
|
||||
}
|
||||
|
||||
const messages: string[] = [];
|
||||
for (const setting of settings) {
|
||||
const value = this.fyo.singles.InventorySettings?.[setting] as
|
||||
| string
|
||||
| undefined;
|
||||
const field = this.fyo.getField(ModelNameEnum.InventorySettings, setting);
|
||||
if (!value) {
|
||||
messages.push(t`${field.label} account not set in Inventory Settings.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const exists = await this.fyo.db.exists(ModelNameEnum.Account, value);
|
||||
if (!exists) {
|
||||
messages.push(t`Account ${value} does not exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length) {
|
||||
throw new ValidationError(messages.join(' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, FormulaMap } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { locationFilter } from './helpers';
|
||||
|
||||
export class StockTransferItem extends Doc {
|
||||
item?: string;
|
||||
@ -10,4 +13,98 @@ export class StockTransferItem extends Doc {
|
||||
unit?: string;
|
||||
description?: string;
|
||||
hsnCode?: number;
|
||||
|
||||
formulas: FormulaMap = {
|
||||
description: {
|
||||
formula: async () =>
|
||||
(await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
'description'
|
||||
)) as string,
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
unit: {
|
||||
formula: async () =>
|
||||
(await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
'unit'
|
||||
)) as string,
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
hsnCode: {
|
||||
formula: async () =>
|
||||
(await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
'hsnCode'
|
||||
)) as string,
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
amount: {
|
||||
formula: () => {
|
||||
return this.rate?.mul(this.quantity ?? 0) ?? this.fyo.pesa(0);
|
||||
},
|
||||
dependsOn: ['rate', 'quantity'],
|
||||
},
|
||||
rate: {
|
||||
formula: async (fieldname) => {
|
||||
const rate = (await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
'rate'
|
||||
)) as undefined | Money;
|
||||
|
||||
if (!rate?.float && this.rate?.float) {
|
||||
return this.rate;
|
||||
}
|
||||
|
||||
return rate ?? this.fyo.pesa(0);
|
||||
},
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
quantity: {
|
||||
formula: async () => {
|
||||
if (!this.item) {
|
||||
return this.quantity as number;
|
||||
}
|
||||
|
||||
const itemDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Item,
|
||||
this.item as string
|
||||
);
|
||||
|
||||
const unitDoc = itemDoc.getLink('unit');
|
||||
if (unitDoc?.isWhole) {
|
||||
return Math.round(this.quantity as number);
|
||||
}
|
||||
|
||||
return this.quantity as number;
|
||||
},
|
||||
dependsOn: ['quantity'],
|
||||
},
|
||||
account: {
|
||||
formula: () => {
|
||||
let accountType = 'expenseAccount';
|
||||
if (this.isSales) {
|
||||
accountType = 'incomeAccount';
|
||||
}
|
||||
return this.fyo.getValue('Item', this.item as string, accountType);
|
||||
},
|
||||
dependsOn: ['item'],
|
||||
},
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
item: (doc: Doc) => {
|
||||
let itemNotFor = 'Sales';
|
||||
if (doc.isSales) {
|
||||
itemNotFor = 'Purchases';
|
||||
}
|
||||
|
||||
return { for: ['not in', [itemNotFor]], trackItem: true };
|
||||
},
|
||||
location: locationFilter,
|
||||
};
|
||||
}
|
||||
|
51
models/inventory/Transfer.ts
Normal file
51
models/inventory/Transfer.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
import { StockManager } from './StockManager';
|
||||
import { SMTransferDetails } from './types';
|
||||
|
||||
export abstract class Transfer extends Transactional {
|
||||
date?: Date;
|
||||
|
||||
async beforeSubmit(): Promise<void> {
|
||||
await super.beforeSubmit();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
await this._getStockManager().validateTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async afterSubmit(): Promise<void> {
|
||||
await super.afterSubmit();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
await this._getStockManager().createTransfers(transferDetails);
|
||||
}
|
||||
|
||||
async beforeCancel(): Promise<void> {
|
||||
await super.beforeCancel();
|
||||
const transferDetails = this._getTransferDetails();
|
||||
const stockManager = this._getStockManager();
|
||||
stockManager.isCancelled = true;
|
||||
await stockManager.validateCancel(transferDetails);
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
await super.afterCancel();
|
||||
await this._getStockManager().cancelTransfers();
|
||||
}
|
||||
|
||||
_getStockManager(): StockManager {
|
||||
let date = this.date!;
|
||||
if (typeof date === 'string') {
|
||||
date = new Date(date);
|
||||
}
|
||||
|
||||
return new StockManager(
|
||||
{
|
||||
date,
|
||||
referenceName: this.name!,
|
||||
referenceType: this.schemaName,
|
||||
},
|
||||
this.isCancelled,
|
||||
this.fyo
|
||||
);
|
||||
}
|
||||
|
||||
abstract _getTransferDetails(): SMTransferDetails[];
|
||||
}
|
@ -1,8 +1,18 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { StockMovement } from '../StockMovement';
|
||||
import { StockTransfer } from '../StockTransfer';
|
||||
import { MovementType } from '../types';
|
||||
|
||||
type ALE = {
|
||||
date: string;
|
||||
account: string;
|
||||
party: string;
|
||||
debit: string;
|
||||
credit: string;
|
||||
reverted: number;
|
||||
};
|
||||
|
||||
type SLE = {
|
||||
date: string;
|
||||
name: string;
|
||||
@ -20,10 +30,28 @@ type Transfer = {
|
||||
rate: number;
|
||||
};
|
||||
|
||||
interface TransferTwo extends Omit<Transfer, 'from' | 'to'> {
|
||||
location: string;
|
||||
}
|
||||
|
||||
export function getItem(name: string, rate: number) {
|
||||
return { name, rate, trackItem: true };
|
||||
}
|
||||
|
||||
export async function getStockTransfer(
|
||||
schemaName: ModelNameEnum.PurchaseReceipt | ModelNameEnum.Shipment,
|
||||
party: string,
|
||||
date: Date,
|
||||
transfers: TransferTwo[],
|
||||
fyo: Fyo
|
||||
): Promise<StockTransfer> {
|
||||
const doc = fyo.doc.getNewDoc(schemaName, { party, date }) as StockTransfer;
|
||||
for (const { item, location, quantity, rate } of transfers) {
|
||||
await doc.append('items', { item, location, quantity, rate });
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function getStockMovement(
|
||||
movementType: MovementType,
|
||||
date: Date,
|
||||
@ -64,3 +92,14 @@ export async function getSLEs(
|
||||
fields: ['date', 'name', 'item', 'location', 'rate', 'quantity'],
|
||||
})) as SLE[];
|
||||
}
|
||||
|
||||
export async function getALEs(
|
||||
referenceName: string,
|
||||
referenceType: string,
|
||||
fyo: Fyo
|
||||
) {
|
||||
return (await fyo.db.getAllRaw(ModelNameEnum.AccountingLedgerEntry, {
|
||||
filters: { referenceName, referenceType },
|
||||
fields: ['date', 'account', 'party', 'debit', 'credit', 'reverted'],
|
||||
})) as ALE[];
|
||||
}
|
||||
|
288
models/inventory/tests/testStockTransfer.spec.ts
Normal file
288
models/inventory/tests/testStockTransfer.spec.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import {
|
||||
assertDoesNotThrow,
|
||||
assertThrows
|
||||
} from 'backend/database/tests/helpers';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { RawValue } from 'schemas/types';
|
||||
import test from 'tape';
|
||||
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||
import { InventorySettings } from '../InventorySettings';
|
||||
import { ValuationMethod } from '../types';
|
||||
import { getALEs, getItem, getSLEs, getStockTransfer } from './helpers';
|
||||
|
||||
const fyo = getTestFyo();
|
||||
setupTestFyo(fyo, __filename);
|
||||
|
||||
const item = 'Pen';
|
||||
const location = 'Common';
|
||||
const party = 'Someone';
|
||||
const testDocs = {
|
||||
Item: {
|
||||
[item]: getItem(item, 100),
|
||||
},
|
||||
Location: {
|
||||
[location]: { name: location },
|
||||
},
|
||||
Party: { [party]: { name: party, Role: 'Both' } },
|
||||
} as Record<string, Record<string, { name: string; [key: string]: RawValue }>>;
|
||||
|
||||
test('insert test docs', async (t) => {
|
||||
for (const schemaName in testDocs) {
|
||||
for (const name in testDocs[schemaName]) {
|
||||
await fyo.doc.getNewDoc(schemaName, testDocs[schemaName][name]).sync();
|
||||
}
|
||||
}
|
||||
|
||||
t.ok(await fyo.db.exists(ModelNameEnum.Party, party), 'party created');
|
||||
t.ok(
|
||||
await fyo.db.exists(ModelNameEnum.Location, location),
|
||||
'location created'
|
||||
);
|
||||
t.ok(await fyo.db.exists(ModelNameEnum.Item, item), 'item created');
|
||||
});
|
||||
|
||||
test('inventory settings', async (t) => {
|
||||
const doc = (await fyo.doc.getDoc(
|
||||
ModelNameEnum.InventorySettings
|
||||
)) as InventorySettings;
|
||||
|
||||
t.equal(doc.valuationMethod, ValuationMethod.FIFO, 'fifo valuation set');
|
||||
t.ok(doc.stockInHand, 'stock in hand set');
|
||||
t.ok(doc.stockReceivedButNotBilled, 'stock rec. but not billed set');
|
||||
});
|
||||
|
||||
test('PurchaseReceipt, create inward stock movement', async (t) => {
|
||||
const date = new Date('2022-01-01');
|
||||
const rate = (testDocs['Item'][item].rate as number) ?? 0;
|
||||
const quantity = 10;
|
||||
const doc = await getStockTransfer(
|
||||
ModelNameEnum.PurchaseReceipt,
|
||||
party,
|
||||
date,
|
||||
[
|
||||
{
|
||||
item,
|
||||
location,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await doc.sync();
|
||||
const grandTotal = quantity * rate;
|
||||
t.equal(doc.grandTotal?.float, quantity * rate);
|
||||
|
||||
await doc.submit();
|
||||
|
||||
t.equal(
|
||||
(await fyo.db.getAllRaw(ModelNameEnum.PurchaseReceipt)).length,
|
||||
1,
|
||||
'purchase receipt created'
|
||||
);
|
||||
t.equal(
|
||||
(await getSLEs(doc.name!, doc.schemaName, fyo)).length,
|
||||
1,
|
||||
'sle created'
|
||||
);
|
||||
t.equal(
|
||||
await fyo.db.getStockQuantity(item, location),
|
||||
quantity,
|
||||
'stock purchased'
|
||||
);
|
||||
t.ok(doc.name?.startsWith('PREC-'));
|
||||
|
||||
const ales = await getALEs(doc.name!, doc.schemaName, fyo);
|
||||
for (const ale of ales) {
|
||||
t.equal(ale.party, party, 'party matches');
|
||||
if (ale.account === 'Stock Received But Not Billed') {
|
||||
t.equal(parseFloat(ale.debit), 0);
|
||||
t.equal(parseFloat(ale.credit), grandTotal);
|
||||
} else {
|
||||
t.equal(parseFloat(ale.credit), 0);
|
||||
t.equal(parseFloat(ale.debit), grandTotal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Shipment, create outward stock movement', async (t) => {
|
||||
const date = new Date('2022-01-02');
|
||||
const rate = (testDocs['Item'][item].rate as number) ?? 0;
|
||||
const quantity = 5;
|
||||
const doc = await getStockTransfer(
|
||||
ModelNameEnum.Shipment,
|
||||
party,
|
||||
date,
|
||||
[
|
||||
{
|
||||
item,
|
||||
location,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await doc.sync();
|
||||
const grandTotal = quantity * rate;
|
||||
t.equal(doc.grandTotal?.float, grandTotal);
|
||||
|
||||
await doc.submit();
|
||||
|
||||
t.equal(
|
||||
(await fyo.db.getAllRaw(ModelNameEnum.Shipment)).length,
|
||||
1,
|
||||
'shipment created'
|
||||
);
|
||||
t.equal(
|
||||
(await getSLEs(doc.name!, doc.schemaName, fyo)).length,
|
||||
1,
|
||||
'sle created'
|
||||
);
|
||||
t.equal(
|
||||
await fyo.db.getStockQuantity(item, location),
|
||||
10 - quantity,
|
||||
'stock purchased'
|
||||
);
|
||||
t.ok(doc.name?.startsWith('SHPM-'));
|
||||
|
||||
const ales = await getALEs(doc.name!, doc.schemaName, fyo);
|
||||
for (const ale of ales) {
|
||||
t.equal(ale.party, party, 'party matches');
|
||||
if (ale.account === 'Cost of Goods Sold') {
|
||||
t.equal(parseFloat(ale.debit), grandTotal);
|
||||
t.equal(parseFloat(ale.credit), 0);
|
||||
} else {
|
||||
t.equal(parseFloat(ale.debit), 0);
|
||||
t.equal(parseFloat(ale.credit), grandTotal);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Shipment, invalid', async (t) => {
|
||||
const date = new Date('2022-01-03');
|
||||
const rate = (testDocs['Item'][item].rate as number) ?? 0;
|
||||
const quantity = 10;
|
||||
const doc = await getStockTransfer(
|
||||
ModelNameEnum.Shipment,
|
||||
party,
|
||||
date,
|
||||
[
|
||||
{
|
||||
item,
|
||||
location,
|
||||
quantity,
|
||||
rate,
|
||||
},
|
||||
],
|
||||
fyo
|
||||
);
|
||||
|
||||
await doc.sync();
|
||||
const grandTotal = quantity * rate;
|
||||
|
||||
t.equal(await fyo.db.getStockQuantity(item, location), 5, 'stock unchanged');
|
||||
t.equal(doc.grandTotal?.float, grandTotal);
|
||||
await assertThrows(async () => await doc.submit());
|
||||
|
||||
t.equal(
|
||||
(await getSLEs(doc.name!, doc.schemaName, fyo)).length,
|
||||
0,
|
||||
'sles not created'
|
||||
);
|
||||
t.equal(
|
||||
(await getALEs(doc.name!, doc.schemaName, fyo)).length,
|
||||
0,
|
||||
'ales not created'
|
||||
);
|
||||
});
|
||||
|
||||
test('Stock Transfer, invalid cancellation', async (t) => {
|
||||
const { name } =
|
||||
(
|
||||
(await fyo.db.getAllRaw(ModelNameEnum.PurchaseReceipt)) as {
|
||||
name: string;
|
||||
}[]
|
||||
)[0] ?? {};
|
||||
|
||||
t.ok(name?.startsWith('PREC-'));
|
||||
const doc = await fyo.doc.getDoc(ModelNameEnum.PurchaseReceipt, name);
|
||||
await assertThrows(async () => await doc.cancel());
|
||||
t.equal(await fyo.db.getStockQuantity(item, location), 5, 'stock unchanged');
|
||||
t.equal(
|
||||
(await getSLEs(name, doc.schemaName, fyo)).length,
|
||||
1,
|
||||
'sle unchanged'
|
||||
);
|
||||
const ales = await getALEs(name, doc.schemaName, fyo);
|
||||
t.ok(ales.every((i) => !i.reverted) && ales.length === 2, 'ale unchanged');
|
||||
});
|
||||
|
||||
test('Shipment, cancel and delete', async (t) => {
|
||||
const { name } =
|
||||
(
|
||||
(await fyo.db.getAllRaw(ModelNameEnum.Shipment, { order: 'asc' })) as {
|
||||
name: string;
|
||||
}[]
|
||||
)[0] ?? {};
|
||||
|
||||
t.ok(name?.startsWith('SHPM-'), 'number series matches');
|
||||
const doc = await fyo.doc.getDoc(ModelNameEnum.Shipment, name);
|
||||
t.ok(doc.isSubmitted, `doc ${name} is submitted`);
|
||||
await assertDoesNotThrow(async () => await doc.cancel());
|
||||
t.ok(doc.isCancelled), `doc is cancelled`;
|
||||
|
||||
t.equal(await fyo.db.getStockQuantity(item, location), 10, 'stock changed');
|
||||
t.equal((await getSLEs(name, doc.schemaName, fyo)).length, 0, 'sle deleted');
|
||||
const ales = await getALEs(name, doc.schemaName, fyo);
|
||||
t.ok(ales.every((i) => !!i.reverted) && ales.length === 4, 'ale reverted');
|
||||
|
||||
await doc.delete();
|
||||
t.equal((await getALEs(name, doc.schemaName, fyo)).length, 0, 'ales deleted');
|
||||
t.equal(
|
||||
(
|
||||
await fyo.db.getAllRaw(ModelNameEnum.Shipment, {
|
||||
filters: { name: name },
|
||||
})
|
||||
).length,
|
||||
0,
|
||||
'doc deleted'
|
||||
);
|
||||
});
|
||||
|
||||
test('Purchase Receipt, cancel and delete', async (t) => {
|
||||
const { name } =
|
||||
(
|
||||
(await fyo.db.getAllRaw(ModelNameEnum.PurchaseReceipt, {
|
||||
order: 'asc',
|
||||
})) as {
|
||||
name: string;
|
||||
}[]
|
||||
)[0] ?? {};
|
||||
|
||||
t.ok(name?.startsWith('PREC-'), 'number series matches');
|
||||
const doc = await fyo.doc.getDoc(ModelNameEnum.PurchaseReceipt, name);
|
||||
t.ok(doc.isSubmitted, `doc ${name} is submitted`);
|
||||
await assertDoesNotThrow(async () => await doc.cancel());
|
||||
t.ok(doc.isCancelled), `doc is cancelled`;
|
||||
|
||||
t.equal(await fyo.db.getStockQuantity(item, location), null, 'stock changed');
|
||||
t.equal((await getSLEs(name, doc.schemaName, fyo)).length, 0, 'sle deleted');
|
||||
const ales = await getALEs(name, doc.schemaName, fyo);
|
||||
t.ok(ales.every((i) => !!i.reverted) && ales.length === 4, 'ale reverted');
|
||||
|
||||
await doc.delete();
|
||||
t.equal((await getALEs(name, doc.schemaName, fyo)).length, 0, 'ales deleted');
|
||||
t.equal(
|
||||
(
|
||||
await fyo.db.getAllRaw(ModelNameEnum.Shipment, {
|
||||
filters: { name: name },
|
||||
})
|
||||
).length,
|
||||
0,
|
||||
'doc deleted'
|
||||
);
|
||||
});
|
||||
closeTestFyo(fyo, __filename);
|
@ -1,5 +1,10 @@
|
||||
import { Money } from 'pesa';
|
||||
|
||||
export enum ValuationMethod {
|
||||
'FIFO' = 'FIFO',
|
||||
'MovingAverage' = 'MovingAverage',
|
||||
}
|
||||
|
||||
export enum MovementType {
|
||||
'MaterialIssue' = 'MaterialIssue',
|
||||
'MaterialReceipt' = 'MaterialReceipt',
|
||||
|
@ -94,12 +94,10 @@ export class StockLedger extends Report {
|
||||
}
|
||||
|
||||
if (toDate && row.date > toDate) {
|
||||
console.log('here');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fromDate && row.date < fromDate) {
|
||||
console.log('here');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -25,15 +25,19 @@
|
||||
"fieldname": "stockInHand",
|
||||
"label": "Stock In Hand Acc.",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "stockReceivedButNotBilled",
|
||||
"label": "Stock Received But Not Billed Acc.",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"create": true
|
||||
"target": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "costOfGoodsSold",
|
||||
"label": "Cost Of Goods Sold",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -38,9 +38,6 @@ const components = {
|
||||
export default {
|
||||
name: 'FormControl',
|
||||
render() {
|
||||
if (!this.$attrs.df) {
|
||||
console.log(this);
|
||||
}
|
||||
const fieldtype = this.$attrs.df.fieldtype;
|
||||
const component = components[fieldtype] ?? Data;
|
||||
|
||||
|
@ -52,10 +52,6 @@ export default defineComponent({
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
console.log(
|
||||
'changed',
|
||||
this.doc?.schema.fields.map(({ fieldname }) => fieldname).join(',')
|
||||
);
|
||||
return this.doc?.schema.fields;
|
||||
},
|
||||
},
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
|
||||
import { numberSeriesDefaultsMap } from 'models/baseModels/Defaults/Defaults';
|
||||
import { InventorySettings } from 'models/inventory/InventorySettings';
|
||||
import { valuationMethod } from 'models/inventory/types';
|
||||
import { ValuationMethod } from 'models/inventory/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { createRegionalRecords } from 'src/regional';
|
||||
import {
|
||||
@ -346,32 +346,26 @@ async function updateInventorySettings(fyo: Fyo) {
|
||||
)) as InventorySettings;
|
||||
|
||||
if (!inventorySettings.valuationMethod) {
|
||||
await inventorySettings.set('valuationMethod', valuationMethod.FIFO);
|
||||
await inventorySettings.set('valuationMethod', ValuationMethod.FIFO);
|
||||
}
|
||||
const accountTypeDefaultMap = {
|
||||
[AccountTypeEnum.Stock]: 'stockInHand',
|
||||
[AccountTypeEnum['Stock Received But Not Billed']]:
|
||||
'stockReceivedButNotBilled',
|
||||
[AccountTypeEnum['Cost of Goods Sold']]: 'costOfGoodsSold',
|
||||
} as Record<string, string>;
|
||||
|
||||
const stockAccounts = (await fyo.db.getAllRaw('Account', {
|
||||
filters: { accountType: AccountTypeEnum.Stock, isGroup: false },
|
||||
})) as { name: string }[];
|
||||
for (const accountType in accountTypeDefaultMap) {
|
||||
const accounts = (await fyo.db.getAllRaw('Account', {
|
||||
filters: { accountType, isGroup: false },
|
||||
})) as { name: string }[];
|
||||
|
||||
if (stockAccounts.length && !inventorySettings.stockInHand) {
|
||||
await inventorySettings.set('stockInHand', stockAccounts[0].name);
|
||||
}
|
||||
if (!accounts.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stockReceivedButNotBilled = (await fyo.db.getAllRaw('Account', {
|
||||
filters: {
|
||||
accountType: AccountTypeEnum['Stock Received But Not Billed'],
|
||||
isGroup: false,
|
||||
},
|
||||
})) as { name: string }[];
|
||||
|
||||
if (
|
||||
stockReceivedButNotBilled.length &&
|
||||
!inventorySettings.stockInReceivedButNotBilled
|
||||
) {
|
||||
await inventorySettings.set(
|
||||
'stockInReceivedButNotBilled',
|
||||
stockAccounts[0].name
|
||||
);
|
||||
const settingName = accountTypeDefaultMap[accountType]!;
|
||||
inventorySettings.set(settingName, accounts[0].name);
|
||||
}
|
||||
|
||||
await inventorySettings.sync();
|
||||
|
Loading…
Reference in New Issue
Block a user