2
0
mirror of https://github.com/frappe/books.git synced 2024-11-08 14:50:56 +00:00

feat: add shipment and purchase receipt

This commit is contained in:
18alantom 2022-11-18 23:01:50 +05:30
parent 14138967c1
commit 9a510f1a63
16 changed files with 706 additions and 77 deletions

View File

@ -857,6 +857,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
return; return;
} }
await this.trigger('beforeCancel');
await this.trigger('beforeCancel'); await this.trigger('beforeCancel');
await this.setAndSync('cancelled', true); await this.setAndSync('cancelled', true);
await this.trigger('afterCancel'); await this.trigger('afterCancel');
@ -908,6 +909,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
if (convertToFloat) { if (convertToFloat) {
return sum.float; return sum.float;
} }
return sum; return sum;
} }

View File

@ -26,28 +26,56 @@ export abstract class Transactional extends Doc {
return true; return true;
} }
abstract getPosting(): Promise<LedgerPosting>; abstract getPosting(): Promise<LedgerPosting | null>;
async validate() { async validate() {
await super.validate(); await super.validate();
if (!this.isTransactional) {
return;
}
const posting = await this.getPosting(); const posting = await this.getPosting();
if (posting === null) {
return;
}
posting.validate(); posting.validate();
} }
async afterSubmit(): Promise<void> { async afterSubmit(): Promise<void> {
await super.afterSubmit(); await super.afterSubmit();
if (!this.isTransactional) {
return;
}
const posting = await this.getPosting(); const posting = await this.getPosting();
if (posting === null) {
return;
}
await posting.post(); await posting.post();
} }
async afterCancel(): Promise<void> { async afterCancel(): Promise<void> {
await super.afterCancel(); await super.afterCancel();
if (!this.isTransactional) {
return;
}
const posting = await this.getPosting(); const posting = await this.getPosting();
if (posting === null) {
return;
}
await posting.postReverse(); await posting.postReverse();
} }
async afterDelete(): Promise<void> { async afterDelete(): Promise<void> {
await super.afterDelete(); await super.afterDelete();
if (!this.isTransactional) {
return;
}
const ledgerEntryIds = (await this.fyo.db.getAll( const ledgerEntryIds = (await this.fyo.db.getAll(
ModelNameEnum.AccountingLedgerEntry, ModelNameEnum.AccountingLedgerEntry,
{ {

View File

@ -1,12 +1,13 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { FiltersMap } from 'fyo/model/types'; import { FiltersMap } from 'fyo/model/types';
import { AccountTypeEnum } from 'models/baseModels/Account/types'; import { AccountTypeEnum } from 'models/baseModels/Account/types';
import { valuationMethod } from './types'; import { ValuationMethod } from './types';
export class InventorySettings extends Doc { export class InventorySettings extends Doc {
stockInHand?: string; stockInHand?: string;
valuationMethod?: valuationMethod; valuationMethod?: ValuationMethod;
stockInReceivedButNotBilled?: string; stockReceivedButNotBilled?: string;
costOfGoodsSold?: string;
static filters: FiltersMap = { static filters: FiltersMap = {
stockInHand: () => ({ stockInHand: () => ({
@ -17,5 +18,9 @@ export class InventorySettings extends Doc {
isGroup: false, isGroup: false,
accountType: AccountTypeEnum['Stock Received But Not Billed'], accountType: AccountTypeEnum['Stock Received But Not Billed'],
}), }),
costOfGoodsSold: () => ({
isGroup: false,
accountType: AccountTypeEnum['Cost of Goods Sold'],
}),
}; };
} }

View File

@ -1,5 +1,6 @@
import { Fyo, t } from 'fyo'; import { Fyo, t } from 'fyo';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { DateTime } from 'luxon';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { StockLedgerEntry } from './StockLedgerEntry'; 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() { async #sync() {
for (const item of this.items) { for (const item of this.items) {
await item.sync(); await item.sync();
@ -117,15 +131,21 @@ export class StockManager {
return; return;
} }
const quantityBefore = const date = details.date.toISOString();
let quantityBefore =
(await this.fyo.db.getStockQuantity( (await this.fyo.db.getStockQuantity(
details.item, details.item,
details.fromLocation, details.fromLocation,
undefined, undefined,
details.date.toISOString() date
)) ?? 0; )) ?? 0;
const formattedDate = this.fyo.format(details.date, 'Datetime'); const formattedDate = this.fyo.format(details.date, 'Datetime');
if (this.isCancelled) {
quantityBefore += details.quantity;
}
if (quantityBefore < details.quantity) { if (quantityBefore < details.quantity) {
throw new ValidationError( throw new ValidationError(
[ [

View File

@ -1,18 +1,18 @@
import { Doc } from 'fyo/model/doc';
import { import {
DefaultMap, DefaultMap,
FiltersMap, FiltersMap,
FormulaMap, FormulaMap,
ListViewSettings ListViewSettings,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { getDocStatusListColumn } from 'models/helpers'; import { getDocStatusListColumn } from 'models/helpers';
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { StockManager } from './StockManager';
import { StockMovementItem } from './StockMovementItem'; import { StockMovementItem } from './StockMovementItem';
import { Transfer } from './Transfer';
import { MovementType } from './types'; import { MovementType } from './types';
export class StockMovement extends Doc { export class StockMovement extends Transfer {
name?: string; name?: string;
date?: Date; date?: Date;
numberSeries?: string; numberSeries?: string;
@ -20,6 +20,14 @@ export class StockMovement extends Doc {
items?: StockMovementItem[]; items?: StockMovementItem[];
amount?: Money; amount?: Money;
override get isTransactional(): boolean {
return false;
}
override async getPosting(): Promise<LedgerPosting | null> {
return null;
}
formulas: FormulaMap = { formulas: FormulaMap = {
amount: { amount: {
formula: () => { 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() { _getTransferDetails() {
return (this.items ?? []).map((row) => ({ return (this.items ?? []).map((row) => ({
item: row.item!, item: row.item!,
@ -72,16 +63,4 @@ export class StockMovement extends Doc {
toLocation: row.toLocation, toLocation: row.toLocation,
})); }));
} }
_getStockManager(): StockManager {
return new StockManager(
{
date: this.date!,
referenceName: this.name!,
referenceType: this.schemaName,
},
this.isCancelled,
this.fyo
);
}
} }

View File

@ -1,10 +1,136 @@
import { t } from 'fyo';
import { Attachment } from 'fyo/core/types'; import { Attachment } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; 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; name?: string;
date?: string; date?: Date;
party?: string; party?: string;
terms?: string; terms?: string;
attachment?: Attachment; 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(' '));
}
}
} }

View File

@ -1,5 +1,8 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { FiltersMap, FormulaMap } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { locationFilter } from './helpers';
export class StockTransferItem extends Doc { export class StockTransferItem extends Doc {
item?: string; item?: string;
@ -10,4 +13,98 @@ export class StockTransferItem extends Doc {
unit?: string; unit?: string;
description?: string; description?: string;
hsnCode?: number; 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,
};
} }

View 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[];
}

View File

@ -1,8 +1,18 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { StockMovement } from '../StockMovement'; import { StockMovement } from '../StockMovement';
import { StockTransfer } from '../StockTransfer';
import { MovementType } from '../types'; import { MovementType } from '../types';
type ALE = {
date: string;
account: string;
party: string;
debit: string;
credit: string;
reverted: number;
};
type SLE = { type SLE = {
date: string; date: string;
name: string; name: string;
@ -20,10 +30,28 @@ type Transfer = {
rate: number; rate: number;
}; };
interface TransferTwo extends Omit<Transfer, 'from' | 'to'> {
location: string;
}
export function getItem(name: string, rate: number) { export function getItem(name: string, rate: number) {
return { name, rate, trackItem: true }; 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( export async function getStockMovement(
movementType: MovementType, movementType: MovementType,
date: Date, date: Date,
@ -64,3 +92,14 @@ export async function getSLEs(
fields: ['date', 'name', 'item', 'location', 'rate', 'quantity'], fields: ['date', 'name', 'item', 'location', 'rate', 'quantity'],
})) as SLE[]; })) 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[];
}

View 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);

View File

@ -1,5 +1,10 @@
import { Money } from 'pesa'; import { Money } from 'pesa';
export enum ValuationMethod {
'FIFO' = 'FIFO',
'MovingAverage' = 'MovingAverage',
}
export enum MovementType { export enum MovementType {
'MaterialIssue' = 'MaterialIssue', 'MaterialIssue' = 'MaterialIssue',
'MaterialReceipt' = 'MaterialReceipt', 'MaterialReceipt' = 'MaterialReceipt',

View File

@ -94,12 +94,10 @@ export class StockLedger extends Report {
} }
if (toDate && row.date > toDate) { if (toDate && row.date > toDate) {
console.log('here');
continue; continue;
} }
if (fromDate && row.date < fromDate) { if (fromDate && row.date < fromDate) {
console.log('here');
continue; continue;
} }

View File

@ -25,15 +25,19 @@
"fieldname": "stockInHand", "fieldname": "stockInHand",
"label": "Stock In Hand Acc.", "label": "Stock In Hand Acc.",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Account", "target": "Account"
"create": true
}, },
{ {
"fieldname": "stockReceivedButNotBilled", "fieldname": "stockReceivedButNotBilled",
"label": "Stock Received But Not Billed Acc.", "label": "Stock Received But Not Billed Acc.",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Account", "target": "Account"
"create": true },
{
"fieldname": "costOfGoodsSold",
"label": "Cost Of Goods Sold",
"fieldtype": "Link",
"target": "Account"
} }
] ]
} }

View File

@ -38,9 +38,6 @@ const components = {
export default { export default {
name: 'FormControl', name: 'FormControl',
render() { render() {
if (!this.$attrs.df) {
console.log(this);
}
const fieldtype = this.$attrs.df.fieldtype; const fieldtype = this.$attrs.df.fieldtype;
const component = components[fieldtype] ?? Data; const component = components[fieldtype] ?? Data;

View File

@ -52,10 +52,6 @@ export default defineComponent({
}, },
computed: { computed: {
fields() { fields() {
console.log(
'changed',
this.doc?.schema.fields.map(({ fieldname }) => fieldname).join(',')
);
return this.doc?.schema.fields; return this.doc?.schema.fields;
}, },
}, },

View File

@ -15,7 +15,7 @@ import {
import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings'; import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings';
import { numberSeriesDefaultsMap } from 'models/baseModels/Defaults/Defaults'; import { numberSeriesDefaultsMap } from 'models/baseModels/Defaults/Defaults';
import { InventorySettings } from 'models/inventory/InventorySettings'; import { InventorySettings } from 'models/inventory/InventorySettings';
import { valuationMethod } from 'models/inventory/types'; import { ValuationMethod } from 'models/inventory/types';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { createRegionalRecords } from 'src/regional'; import { createRegionalRecords } from 'src/regional';
import { import {
@ -346,32 +346,26 @@ async function updateInventorySettings(fyo: Fyo) {
)) as InventorySettings; )) as InventorySettings;
if (!inventorySettings.valuationMethod) { 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', { for (const accountType in accountTypeDefaultMap) {
filters: { accountType: AccountTypeEnum.Stock, isGroup: false }, const accounts = (await fyo.db.getAllRaw('Account', {
})) as { name: string }[]; filters: { accountType, isGroup: false },
})) as { name: string }[];
if (stockAccounts.length && !inventorySettings.stockInHand) { if (!accounts.length) {
await inventorySettings.set('stockInHand', stockAccounts[0].name); continue;
} }
const stockReceivedButNotBilled = (await fyo.db.getAllRaw('Account', { const settingName = accountTypeDefaultMap[accountType]!;
filters: { inventorySettings.set(settingName, accounts[0].name);
accountType: AccountTypeEnum['Stock Received But Not Billed'],
isGroup: false,
},
})) as { name: string }[];
if (
stockReceivedButNotBilled.length &&
!inventorySettings.stockInReceivedButNotBilled
) {
await inventorySettings.set(
'stockInReceivedButNotBilled',
stockAccounts[0].name
);
} }
await inventorySettings.sync(); await inventorySettings.sync();