mirror of
https://github.com/frappe/books.git
synced 2025-01-03 07:12:21 +00:00
incr: add code to generate a dummy instance
This commit is contained in:
parent
d5d40dd807
commit
0701c56429
@ -4,6 +4,22 @@ import { BespokeFunction } from './types';
|
||||
export class BespokeQueries {
|
||||
[key: string]: BespokeFunction;
|
||||
|
||||
static async getLastInserted(
|
||||
db: DatabaseCore,
|
||||
schemaName: string
|
||||
): Promise<number> {
|
||||
const lastInserted = (await db.knex!.raw(
|
||||
'select cast(name as int) as num from ?? order by num desc limit 1',
|
||||
[schemaName]
|
||||
)) as { num: number }[];
|
||||
|
||||
const num = lastInserted?.[0]?.num;
|
||||
if (num === undefined) {
|
||||
return 0;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
static async getTopExpenses(
|
||||
db: DatabaseCore,
|
||||
fromDate: string,
|
||||
|
7
dummy/README.md
Normal file
7
dummy/README.md
Normal 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
dummy/helpers.ts
Normal file
54
dummy/helpers.ts
Normal 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;
|
||||
}
|
417
dummy/index.ts
417
dummy/index.ts
@ -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 @@
|
||||
[{"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"}]
|
File diff suppressed because one or more lines are too long
@ -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);
|
||||
});
|
||||
|
@ -156,6 +156,10 @@ function toDocString(value: RawValue, field: Field) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
@ -362,6 +366,10 @@ function toRawString(value: DocValue, field: Field): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { SingleValue } from 'backend/database/types';
|
||||
import { Fyo } from 'fyo';
|
||||
import { DatabaseDemux } from 'fyo/demux/db';
|
||||
import { ValueError } from 'fyo/utils/errors';
|
||||
import Observable from 'fyo/utils/observable';
|
||||
import { Field, RawValue, SchemaMap } from 'schemas/types';
|
||||
import { getMapFromList } from 'utils';
|
||||
@ -208,6 +209,20 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
*
|
||||
* The query logic for these is in backend/database/bespoke.ts
|
||||
*/
|
||||
|
||||
async getLastInserted(schemaName: string): Promise<number> {
|
||||
if (this.schemaMap[schemaName]?.naming !== 'autoincrement') {
|
||||
throw new ValueError(
|
||||
`invalid schema, ${schemaName} does not have autoincrement naming`
|
||||
);
|
||||
}
|
||||
|
||||
return (await this.#demux.callBespoke(
|
||||
'getLastInserted',
|
||||
schemaName
|
||||
)) as number;
|
||||
}
|
||||
|
||||
async getTopExpenses(fromDate: string, toDate: string): Promise<TopExpenses> {
|
||||
return (await this.#demux.callBespoke(
|
||||
'getTopExpenses',
|
||||
|
@ -63,6 +63,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
_dirty: boolean = true;
|
||||
_notInserted: boolean = true;
|
||||
|
||||
_syncing = false;
|
||||
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
|
||||
super();
|
||||
this.fyo = markRaw(fyo);
|
||||
@ -117,6 +118,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return !!this.submitted && !!this.cancelled;
|
||||
}
|
||||
|
||||
get syncing() {
|
||||
return this._syncing;
|
||||
}
|
||||
|
||||
_setValuesWithoutChecks(data: DocValueMap) {
|
||||
for (const field of this.schema.fields) {
|
||||
const fieldname = field.fieldname;
|
||||
@ -628,6 +633,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async sync(): Promise<Doc> {
|
||||
this._syncing = true;
|
||||
await this.trigger('beforeSync');
|
||||
let doc;
|
||||
if (this.notInserted) {
|
||||
@ -638,6 +644,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
this._notInserted = false;
|
||||
await this.trigger('afterSync');
|
||||
this.fyo.doc.observer.trigger(`sync:${this.schemaName}`, this.name);
|
||||
this._syncing = false;
|
||||
return doc;
|
||||
}
|
||||
|
||||
|
@ -33,54 +33,33 @@ export async function setName(doc: Doc, fyo: Fyo) {
|
||||
}
|
||||
|
||||
if (doc.schema.naming === 'autoincrement') {
|
||||
doc.name = await getNextId(doc.schemaName, fyo);
|
||||
return;
|
||||
return (doc.name = await getNextId(doc.schemaName, fyo));
|
||||
}
|
||||
|
||||
// Current, per doc number series
|
||||
const numberSeries = doc.numberSeries as string | undefined;
|
||||
if (numberSeries !== undefined) {
|
||||
doc.name = await getSeriesNext(numberSeries, doc.schemaName, fyo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (doc.name) {
|
||||
return;
|
||||
if (doc.numberSeries !== undefined) {
|
||||
return (doc.name = await getSeriesNext(
|
||||
doc.numberSeries as string,
|
||||
doc.schemaName,
|
||||
fyo
|
||||
));
|
||||
}
|
||||
|
||||
// name === schemaName for Single
|
||||
if (doc.schema.isSingle) {
|
||||
doc.name = doc.schemaName;
|
||||
return;
|
||||
return (doc.name = doc.schemaName);
|
||||
}
|
||||
|
||||
// assign a random name by default
|
||||
// override doc to set a name
|
||||
// Assign a random name by default
|
||||
if (!doc.name) {
|
||||
doc.name = getRandomString();
|
||||
}
|
||||
|
||||
return doc.name;
|
||||
}
|
||||
|
||||
export async function getNextId(schemaName: string, fyo: Fyo) {
|
||||
// get the last inserted row
|
||||
const lastInserted = await getLastInserted(schemaName, fyo);
|
||||
let name = 1;
|
||||
if (lastInserted) {
|
||||
let lastNumber = parseInt(lastInserted.name as string);
|
||||
if (isNaN(lastNumber)) lastNumber = 0;
|
||||
name = lastNumber + 1;
|
||||
}
|
||||
return (name + '').padStart(9, '0');
|
||||
}
|
||||
|
||||
export async function getLastInserted(schemaName: string, fyo: Fyo) {
|
||||
const lastInserted = await fyo.db.getAll(schemaName, {
|
||||
fields: ['name'],
|
||||
limit: 1,
|
||||
orderBy: 'created',
|
||||
order: 'desc',
|
||||
});
|
||||
return lastInserted && lastInserted.length ? lastInserted[0] : null;
|
||||
export async function getNextId(schemaName: string, fyo: Fyo): Promise<string> {
|
||||
const lastInserted = await fyo.db.getLastInserted(schemaName);
|
||||
return String(lastInserted + 1).padStart(9, '0');
|
||||
}
|
||||
|
||||
export async function getSeriesNext(
|
||||
|
@ -13,12 +13,6 @@ export function slug(str: string) {
|
||||
.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
export function range(n: number) {
|
||||
return Array(n)
|
||||
.fill(null)
|
||||
.map((_, i) => i);
|
||||
}
|
||||
|
||||
export function unique<T>(list: T[], key = (it: T) => String(it)) {
|
||||
const seen: Record<string, boolean> = {};
|
||||
return list.filter((item) => {
|
||||
|
@ -20,6 +20,8 @@ import { LedgerPosting } from './LedgerPosting';
|
||||
*/
|
||||
|
||||
export abstract class Transactional extends Doc {
|
||||
date?: Date;
|
||||
|
||||
get isTransactional() {
|
||||
return true;
|
||||
}
|
||||
@ -27,25 +29,25 @@ export abstract class Transactional extends Doc {
|
||||
abstract getPosting(): Promise<LedgerPosting>;
|
||||
|
||||
async validate() {
|
||||
super.validate();
|
||||
await super.validate();
|
||||
const posting = await this.getPosting();
|
||||
posting.validate();
|
||||
}
|
||||
|
||||
async afterSubmit(): Promise<void> {
|
||||
super.afterSubmit();
|
||||
await super.afterSubmit();
|
||||
const posting = await this.getPosting();
|
||||
await posting.post();
|
||||
}
|
||||
|
||||
async afterCancel(): Promise<void> {
|
||||
super.afterCancel();
|
||||
await super.afterCancel();
|
||||
const posting = await this.getPosting();
|
||||
await posting.postReverse();
|
||||
}
|
||||
|
||||
async afterDelete(): Promise<void> {
|
||||
super.afterDelete();
|
||||
await super.afterDelete();
|
||||
const ledgerEntryIds = (await this.fyo.db.getAll(
|
||||
ModelNameEnum.AccountingLedgerEntry,
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ import { Invoice } from '../Invoice/Invoice';
|
||||
|
||||
export abstract class InvoiceItem extends Doc {
|
||||
account?: string;
|
||||
amount?: Money;
|
||||
baseAmount?: Money;
|
||||
exchangeRate?: number;
|
||||
parentdoc?: Invoice;
|
||||
|
@ -17,7 +17,7 @@ export function getInvoiceActions(
|
||||
condition: (doc: Doc) =>
|
||||
doc.isSubmitted && !(doc.outstandingAmount as Money).isZero(),
|
||||
action: async function makePayment(doc: Doc) {
|
||||
const payment = await fyo.doc.getNewDoc('Payment');
|
||||
const payment = fyo.doc.getNewDoc('Payment');
|
||||
payment.once('afterSync', async () => {
|
||||
await payment.submit();
|
||||
});
|
||||
|
@ -29,3 +29,5 @@ export enum ModelNameEnum {
|
||||
SingleValue = 'SingleValue',
|
||||
SystemSettings = 'SystemSettings',
|
||||
}
|
||||
|
||||
export type ModelName = keyof typeof ModelNameEnum
|
@ -1,5 +1,6 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { App as VueApp, createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
@ -105,3 +106,6 @@ function setOnWindow() {
|
||||
window.fyo = fyo;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
window.DateTime = DateTime;
|
||||
|
Loading…
Reference in New Issue
Block a user