incr: add code to generate a dummy instance

This commit is contained in:
18alantom
2022-05-23 16:18:23 +05:30
parent d5d40dd807
commit 0701c56429
17 changed files with 570 additions and 53 deletions
+7
View File
@@ -0,0 +1,7 @@
# Dummy
This will be used to generate dummy data for the purposes of tests and to create
a demo instance.
There are a few `.json` files here (eg: `items.json`) which have been generated,
these are not to be edited.
+54
View File
@@ -0,0 +1,54 @@
import { DateTime } from 'luxon';
// prettier-ignore
export const partyPurchaseItemMap: Record<string, string[]> = {
'Janky Office Spaces': ['Office Rent', 'Office Cleaning'],
"Josféña's 611s": ['611 Jeans - PCH', '611 Jeans - SHR'],
'Lankness Feet Fomenters': ['Bominga Shoes', 'Jade Slippers'],
'The Overclothes Company': ['Jacket - RAW', 'Cryo Gloves', 'Cool Cloth'],
'Adani Electricity Mumbai Limited': ['Electricity'],
'Only Fulls': ['Full Sleeve - BLK', 'Full Sleeve - COL'],
'Just Epaulettes': ['Epaulettes - 4POR'],
'Le Socials': ['Social Ads'],
'Maxwell': ['Marketing - Video'],
};
export const purchaseItemPartyMap: Record<string, string> = Object.keys(
partyPurchaseItemMap
).reduce((acc, party) => {
for (const item of partyPurchaseItemMap[party]) {
acc[item] = party;
}
return acc;
}, {} as Record<string, string>);
export function getFlowConstant(months: number) {
// Jan to December
const flow = [
0.15, 0.1, 0.25, 0.1, 0.01, 0.01, 0.01, 0.05, 0, 0.15, 0.2, 0.4,
];
const d = DateTime.now().minus({ months });
return flow[d.month - 1];
}
export function getRandomDates(count: number, months: number): Date[] {
/**
* Returns `count` number of dates for a month, `months` back from the
* current date.
*/
let endDate = DateTime.now();
if (months !== 0) {
const back = endDate.minus({ months });
endDate = DateTime.local(endDate.year, back.month, back.daysInMonth);
}
const dates: Date[] = [];
for (let i = 0; i < count; i++) {
const day = Math.ceil(endDate.day * Math.random());
const date = DateTime.local(endDate.year, endDate.month, day);
dates.push(date.toJSDate());
}
return dates;
}
+414 -3
View File
@@ -1,10 +1,33 @@
import { Fyo } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { range, sample } from 'lodash';
import { DateTime } from 'luxon';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { Payment } from 'models/baseModels/Payment/Payment';
import { PurchaseInvoice } from 'models/baseModels/PurchaseInvoice/PurchaseInvoice';
import { SalesInvoice } from 'models/baseModels/SalesInvoice/SalesInvoice';
import { ModelNameEnum } from 'models/types';
import setupInstance from 'src/setup/setupInstance';
import { getMapFromList } from 'utils';
import { getFiscalYear } from 'utils/misc';
import {
getFlowConstant,
getRandomDates,
purchaseItemPartyMap,
} from './helpers';
import items from './items.json';
import parties from './parties.json';
export async function setupDummyInstance(dbPath: string, fyo: Fyo) {
type Notifier = (stage: string, count: number, total: number) => void;
export async function setupDummyInstance(
dbPath: string,
fyo: Fyo,
years: number = 1,
baseCount: number = 1000,
notifier?: Notifier
) {
notifier?.(fyo.t`Setting Up Instance`, 0, 0);
await setupInstance(
dbPath,
{
@@ -22,10 +45,385 @@ export async function setupDummyInstance(dbPath: string, fyo: Fyo) {
fyo
);
await generateEntries(fyo);
years = Math.floor(years);
notifier?.(fyo.t`Creating Items and Parties`, 0, 0);
await generateStaticEntries(fyo);
await generateDynamicEntries(fyo, years, baseCount, notifier);
}
async function generateEntries(fyo: Fyo) {
/**
* warning: long functions ahead!
*/
async function generateDynamicEntries(
fyo: Fyo,
years: number,
baseCount: number,
notifier?: Notifier
) {
notifier?.(fyo.t`Getting Sales Invoices`, 0, 0);
const salesInvoices = await getSalesInvoices(fyo, years, baseCount);
notifier?.(fyo.t`Getting Purchase Invoices`, 0, 0);
const purchaseInvoices = await getPurchaseInvoices(fyo, years, salesInvoices);
notifier?.(fyo.t`Getting Journal Entries`, 0, 0);
const journalEntries = await getJournalEntries(fyo, salesInvoices);
await syncAndSubmit(journalEntries, (count: number, total: number) =>
notifier?.(fyo.t`Syncing Journal Entries`, count, total)
);
const invoices = ([salesInvoices, purchaseInvoices].flat() as Invoice[]).sort(
(a, b) => +(a.date as Date) - +(b.date as Date)
);
await syncAndSubmit(invoices, (count: number, total: number) =>
notifier?.(fyo.t`Syncing Invoices`, count, total)
);
const payments = await getPayments(fyo, invoices);
await syncAndSubmit(payments, (count: number, total: number) =>
notifier?.(fyo.t`Syncing Payments`, count, total)
);
}
async function getJournalEntries(fyo: Fyo, salesInvoices: SalesInvoice[]) {
const entries = [];
const amount = salesInvoices
.map((i) => i.items!)
.flat()
.reduce((a, b) => a.add(b.amount!), fyo.pesa(0))
.percent(75)
.clip(0);
const lastInv = salesInvoices.sort((a, b) => +a.date! - +b.date!).at(-1)!
.date!;
const date = DateTime.fromJSDate(lastInv).minus({ months: 6 }).toJSDate();
// Bank Entry
let doc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
date,
entryType: 'Bank Entry',
});
await doc.append('accounts', {
account: 'Supreme Bank',
debit: amount,
credit: fyo.pesa(0),
});
await doc.append('accounts', {
account: 'Secured Loans',
credit: amount,
debit: fyo.pesa(0),
});
entries.push(doc);
// Cash Entry
doc = fyo.doc.getNewDoc(ModelNameEnum.JournalEntry, {
date,
entryType: 'Cash Entry',
});
await doc.append('accounts', {
account: 'Cash',
debit: amount.percent(30),
credit: fyo.pesa(0),
});
await doc.append('accounts', {
account: 'Supreme Bank',
credit: amount.percent(30),
debit: fyo.pesa(0),
});
entries.push(doc);
return entries;
}
async function getPayments(fyo: Fyo, invoices: Invoice[]) {
const payments = [];
for (const invoice of invoices) {
// Defaulters
if (invoice.isSales && Math.random() < 0.007) {
continue;
}
const doc = fyo.doc.getNewDoc(ModelNameEnum.Payment) as Payment;
doc.party = invoice.party as string;
doc.paymentType = invoice.isSales ? 'Receive' : 'Pay';
doc.paymentMethod = 'Cash';
doc.date = DateTime.fromJSDate(invoice.date as Date)
.plus({ hours: 1 })
.toJSDate();
if (doc.paymentType === 'Receive') {
doc.account = 'Debtors';
doc.paymentAccount = 'Cash';
} else {
doc.account = 'Cash';
doc.paymentAccount = 'Creditors';
}
doc.amount = invoice.outstandingAmount;
// Discount
if (invoice.isSales && Math.random() < 0.05) {
await doc.set('writeOff', invoice.outstandingAmount?.percent(15));
}
doc.push('for', {
referenceType: invoice.schemaName,
referenceName: invoice.name,
amount: invoice.outstandingAmount,
});
payments.push(doc);
}
return payments;
}
async function getSalesInvoices(fyo: Fyo, years: number, baseCount: number) {
const invoices: SalesInvoice[] = [];
const salesItems = items.filter((i) => i.for !== 'Purchases');
const customers = parties.filter((i) => i.role !== 'Supplier');
/**
* Get certain number of entries for each month of the count
* of years.
*/
for (const months of range(0, years * 12)) {
const flow = getFlowConstant(months);
const count = Math.ceil(flow * baseCount * (Math.random() * 0.25 + 0.75));
const dates = getRandomDates(count, months);
/**
* For each date create a Sales Invoice.
*/
for (const date of dates) {
const customer = sample(customers);
const doc = fyo.doc.getNewDoc(ModelNameEnum.SalesInvoice, {
date,
}) as SalesInvoice;
await doc.set('party', customer!.name);
if (!doc.account) {
doc.account = 'Debtors';
}
/**
* Add `numItems` number of items to the invoice.
*/
const numItems = Math.ceil(Math.random() * 5);
for (let i = 0; i < numItems; i++) {
const item = sample(salesItems);
if ((doc.items ?? []).find((i) => i.item === item)) {
continue;
}
let quantity = 1;
/**
* Increase quantity depending on the rate.
*/
if (item!.rate < 100 && Math.random() < 0.4) {
quantity = Math.ceil(Math.random() * 10);
} else if (item!.rate < 1000 && Math.random() < 0.2) {
quantity = Math.ceil(Math.random() * 4);
} else if (Math.random() < 0.01) {
quantity = Math.ceil(Math.random() * 3);
}
const rate = fyo.pesa(item!.rate * flow + 1).clip(0);
await doc.append('items', {});
await doc.items!.at(-1)!.set({
item: item!.name,
rate,
quantity,
account: item!.incomeAccount,
amount: rate.mul(quantity),
tax: item!.tax,
description: item!.description,
hsnCode: item!.hsnCode,
});
}
invoices.push(doc);
}
}
return invoices;
}
async function getPurchaseInvoices(
fyo: Fyo,
years: number,
salesInvoices: SalesInvoice[]
): Promise<PurchaseInvoice[]> {
return [
await getSalesPurchaseInvoices(fyo, salesInvoices),
await getNonSalesPurchaseInvoices(fyo, years),
].flat();
}
async function getSalesPurchaseInvoices(
fyo: Fyo,
salesInvoices: SalesInvoice[]
): Promise<PurchaseInvoice[]> {
const invoices = [] as PurchaseInvoice[];
/**
* Group all sales invoices by their YYYY-MM.
*/
const dateGrouped = salesInvoices
.map((si) => {
const date = DateTime.fromJSDate(si.date as Date);
const key = `${date.year}-${String(date.month).padStart(2, '0')}`;
return { key, si };
})
.reduce((acc, item) => {
acc[item.key] ??= [];
acc[item.key].push(item.si);
return acc;
}, {} as Record<string, SalesInvoice[]>);
/**
* Sort the YYYY-MM keys in ascending order.
*/
const dates = Object.keys(dateGrouped)
.map((k) => ({ key: k, date: new Date(k) }))
.sort((a, b) => +a.date - +b.date);
const purchaseQty: Record<string, number> = {};
/**
* For each date create a set of Purchase Invoices.
*/
for (const { key, date } of dates) {
/**
* Group items by name to get the total quantity used in a month.
*/
const itemGrouped = dateGrouped[key].reduce((acc, si) => {
for (const item of si.items!) {
if (item.item === 'Dry-Cleaning') {
continue;
}
acc[item.item as string] ??= 0;
acc[item.item as string] += item.quantity as number;
}
return acc;
}, {} as Record<string, number>);
/**
* Set order quantity for the first of the month.
*/
Object.keys(itemGrouped).forEach((name) => {
const quantity = itemGrouped[name];
purchaseQty[name] ??= 0;
let prevQty = purchaseQty[name];
if (prevQty <= quantity) {
prevQty = quantity - prevQty;
}
purchaseQty[name] = Math.ceil(prevQty / 10) * 10;
});
const supplierGrouped = Object.keys(itemGrouped).reduce((acc, item) => {
const supplier = purchaseItemPartyMap[item];
acc[supplier] ??= [];
acc[supplier].push(item);
return acc;
}, {} as Record<string, string[]>);
/**
* For each supplier create a Purchase Invoice
*/
for (const supplier in supplierGrouped) {
const doc = fyo.doc.getNewDoc(ModelNameEnum.PurchaseInvoice, {
date,
}) as PurchaseInvoice;
await doc.set('party', supplier);
if (!doc.account) {
doc.account = 'Creditors';
}
/**
* For each item create a row
*/
for (const item of supplierGrouped[supplier]) {
await doc.append('items', {});
const quantity = purchaseQty[item];
doc.items!.at(-1)!.set({ item, quantity });
}
invoices.push(doc);
}
}
return invoices;
}
async function getNonSalesPurchaseInvoices(
fyo: Fyo,
years: number
): Promise<PurchaseInvoice[]> {
const purchaseItems = items.filter((i) => i.for !== 'Sales');
const itemMap = getMapFromList(purchaseItems, 'name');
const periodic: Record<string, number> = {
'Marketing - Video': 2,
'Social Ads': 1,
Electricity: 1,
'Office Cleaning': 1,
'Office Rent': 1,
};
const invoices: SalesInvoice[] = [];
for (const months of range(0, years * 12)) {
/**
* All purchases on the first of the month.
*/
const temp = DateTime.now().minus({ months });
const date = DateTime.local(temp.year, temp.month, 1).toJSDate();
for (const name in periodic) {
if (months % periodic[name] !== 0) {
continue;
}
const doc = fyo.doc.getNewDoc(ModelNameEnum.PurchaseInvoice, {
date,
}) as PurchaseInvoice;
const party = purchaseItemPartyMap[name];
await doc.set('party', party);
if (!doc.account) {
doc.account = 'Creditors';
}
await doc.append('items', {});
const row = doc.items!.at(-1)!;
const item = itemMap[name];
let quantity = 1;
let rate = item.rate;
if (name === 'Social Ads') {
quantity = Math.ceil(Math.random() * 200);
} else if (name !== 'Office Rent') {
rate = rate * (Math.random() * 0.4 + 0.8);
}
await row.set({
item: item.name,
quantity,
rate: fyo.pesa(rate).clip(0),
});
invoices.push(doc);
}
}
return invoices;
}
async function generateStaticEntries(fyo: Fyo) {
await generateItems(fyo);
await generateParties(fyo);
}
@@ -43,3 +441,16 @@ async function generateParties(fyo: Fyo) {
await doc.sync();
}
}
async function syncAndSubmit(
docs: Doc[],
notifier: (count: number, total: number) => void
) {
const total = docs.length;
for (const i in docs) {
notifier(parseInt(i) + 1, total);
const doc = docs[i];
await doc.sync();
await doc.submit();
}
}
+1 -1
View File
@@ -1 +1 @@
[{"name": "611 Jeans - PCH", "description": "Peach coloured 611s", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 4499, "hsnCode": 62034990, "for": "Sales"}, {"name": "611 Jeans - SHR", "description": "Shark skin 611s", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 6499, "hsnCode": 62034990, "for": "Sales"}, {"name": "Bominga Shoes", "description": null, "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 4999, "hsnCode": 640291, "for": "Sales"}, {"name": "Cryo Gloves", "description": "Keeps hands cool", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 3499, "hsnCode": 611693, "for": "Sales"}, {"name": "Epaulettes - 4POR", "description": "Porcelain epaulettes", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 2499, "hsnCode": 62179090, "for": "Sales"}, {"name": "Full Sleeve - BLK", "description": "Black sleeved", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 599, "hsnCode": 100820, "for": "Sales"}, {"name": "Full Sleeve - COL", "description": "All color sleeved", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 499, "hsnCode": 100820, "for": "Sales"}, {"name": "Jacket - RAW", "description": "Raw baby skinned jackets", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 8999, "hsnCode": 100820, "for": "Sales"}, {"name": "Jade Slippers", "description": null, "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 2999, "hsnCode": 640520, "for": "Sales"}, {"name": "Dry-Cleaning", "description": null, "unit": "Unit", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 69, "hsnCode": 999712, "for": "Sales"}, {"name": "Electricity", "description": "Bzz Bzz", "unit": "Day", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Utility Expenses", "tax": "GST-0", "rate": 6000, "hsnCode": 271600, "for": "Purchases"}, {"name": "Marketing - Video", "description": "One single video", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Marketing Expenses", "tax": "GST-18", "rate": 15000, "hsnCode": 998371, "for": "Purchases"}, {"name": "Office Rent", "description": "Rent per day", "unit": "Day", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Office Rent", "tax": "GST-18", "rate": 100000, "hsnCode": 997212, "for": "Purchases"}, {"name": "Office Cleaning", "description": "Cleaning cost per day", "unit": "Day", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Office Maintenance Expenses", "tax": "GST-18", "rate": 100000, "hsnCode": 998533, "for": "Purchases"}, {"name": "Social Ads", "description": "Cost per click", "unit": "Unit", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Marketing Expenses", "tax": "GST-18", "rate": 50, "hsnCode": 99836, "for": "Purchases"}, {"name": "Cool Cloth", "description": "Some real \ud83c\udd92 cloth", "unit": "Meter", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 4000, "hsnCode": 59111000, "for": "Both"}]
[{"name": "Dry-Cleaning", "description": null, "unit": "Unit", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 69, "hsnCode": 999712, "for": "Sales"}, {"name": "Electricity", "description": "Bzz Bzz", "unit": "Day", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Utility Expenses", "tax": "GST-0", "rate": 6000, "hsnCode": 271600, "for": "Purchases"}, {"name": "Marketing - Video", "description": "One single video", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Marketing Expenses", "tax": "GST-18", "rate": 15000, "hsnCode": 998371, "for": "Purchases"}, {"name": "Office Rent", "description": "Rent per day", "unit": "Day", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Office Rent", "tax": "GST-18", "rate": 100000, "hsnCode": 997212, "for": "Purchases"}, {"name": "Office Cleaning", "description": "Cleaning cost per day", "unit": "Day", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Office Maintenance Expenses", "tax": "GST-18", "rate": 100000, "hsnCode": 998533, "for": "Purchases"}, {"name": "Social Ads", "description": "Cost per click", "unit": "Unit", "itemType": "Service", "incomeAccount": "Service", "expenseAccount": "Marketing Expenses", "tax": "GST-18", "rate": 50, "hsnCode": 99836, "for": "Purchases"}, {"name": "Cool Cloth", "description": "Some real \ud83c\udd92 cloth", "unit": "Meter", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 4000, "hsnCode": 59111000, "for": "Both"}, {"name": "611 Jeans - PCH", "description": "Peach coloured 611s", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 4499, "hsnCode": 62034990, "for": "Both"}, {"name": "611 Jeans - SHR", "description": "Shark skin 611s", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 6499, "hsnCode": 62034990, "for": "Both"}, {"name": "Bominga Shoes", "description": null, "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 4999, "hsnCode": 640291, "for": "Both"}, {"name": "Cryo Gloves", "description": "Keeps hands cool", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 3499, "hsnCode": 611693, "for": "Both"}, {"name": "Epaulettes - 4POR", "description": "Porcelain epaulettes", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 2499, "hsnCode": 62179090, "for": "Both"}, {"name": "Full Sleeve - BLK", "description": "Black sleeved", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 599, "hsnCode": 100820, "for": "Both"}, {"name": "Full Sleeve - COL", "description": "All color sleeved", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 499, "hsnCode": 100820, "for": "Both"}, {"name": "Jacket - RAW", "description": "Raw baby skinned jackets", "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-12", "rate": 8999, "hsnCode": 100820, "for": "Both"}, {"name": "Jade Slippers", "description": null, "unit": "Unit", "itemType": "Product", "incomeAccount": "Sales", "expenseAccount": "Cost of Goods Sold", "tax": "GST-18", "rate": 2999, "hsnCode": 640520, "for": "Both"}]
+1 -1
View File
File diff suppressed because one or more lines are too long
+19 -2
View File
@@ -1,12 +1,14 @@
import * as assert from 'assert';
import { DatabaseManager } from 'backend/database/manager';
import { assertDoesNotThrow } from 'backend/database/tests/helpers';
import { purchaseItemPartyMap } from 'dummy/helpers';
import { Fyo } from 'fyo';
import { DummyAuthDemux } from 'fyo/tests/helpers';
import 'mocha';
import { getTestDbPath } from 'tests/helpers';
import { setupDummyInstance } from '..';
describe('dummy', function () {
describe.skip('dummy', function () {
const dbPath = getTestDbPath();
let fyo: Fyo;
@@ -28,5 +30,20 @@ describe('dummy', function () {
await assertDoesNotThrow(async () => {
await setupDummyInstance(dbPath, fyo);
}, 'setup instance failed');
});
for (const item in purchaseItemPartyMap) {
assert.strictEqual(
await fyo.db.exists('Item', item),
true,
`not found ${item}`
);
const party = purchaseItemPartyMap[item];
assert.strictEqual(
await fyo.db.exists('Party', party),
true,
`not found ${party}`
);
}
}).timeout(120_000);
});