2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +00:00

incr: display gstr reports

This commit is contained in:
18alantom 2022-05-17 17:42:57 +05:30
parent 67f35b5f79
commit 496b2b77aa
25 changed files with 496 additions and 756 deletions

View File

@ -1,332 +0,0 @@
import { t } from 'fyo';
import { DateTime } from 'luxon';
import { fyo } from 'src/initFyo';
import { showMessageDialog } from 'src/utils';
import { stateCodeMap } from '../regional/in';
import { exportCsv, saveExportData } from '../reports/commonExporter';
import { getSavePath } from '../src/utils';
const GST = {
'GST-0': 0,
'GST-0.25': 0.25,
'GST-3': 3,
'GST-5': 5,
'GST-6': 6,
'GST-12': 12,
'GST-18': 18,
'GST-28': 28,
'IGST-0': 0,
'IGST-0.25': 0.25,
'IGST-3': 3,
'IGST-5': 5,
'IGST-6': 6,
'IGST-12': 12,
'IGST-18': 18,
'IGST-28': 28,
};
const CSGST = {
'GST-0': 0,
'GST-0.25': 0.125,
'GST-3': 1.5,
'GST-5': 2.5,
'GST-6': 3,
'GST-12': 6,
'GST-18': 9,
'GST-28': 14,
};
const IGST = {
'IGST-0.25': 0.25,
'IGST-3': 3,
'IGST-5': 5,
'IGST-6': 6,
'IGST-12': 12,
'IGST-18': 18,
'IGST-28': 28,
};
export async function generateGstr1Json(getReportData) {
const { gstin } = fyo.AccountingSettings;
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}
const {
rows,
filters: { transferType, toDate },
} = getReportData();
const { filePath, canceled } = await getSavePath('gstr-1', 'json');
if (canceled || !filePath) return;
const gstData = {
version: 'GST3.0.4',
hash: 'hash',
gstin: gstin,
// fp is the the MMYYYY for the last month of the report
// for example if you are extracting report for 1st July 2020 to 31st September 2020 then
// fb = 092020
fp: DateTime.fromISO(toDate).toFormat('MMyyyy'),
};
if (transferType === 'B2B') {
gstData.b2b = await generateB2bData(rows);
} else if (transferType === 'B2CL') {
gstData.b2cl = await generateB2clData(rows);
} else if (transferType === 'B2CS') {
gstData.b2cs = await generateB2csData(rows);
}
const jsonData = JSON.stringify(gstData);
await saveExportData(jsonData, filePath);
}
async function generateB2bData(rows) {
const b2b = [];
for (let row of rows) {
const customer = {
ctin: row.gstin,
inv: [],
};
const invRecord = {
inum: row.invNo,
idt: DateTime.fromFormat(row.invDate, 'yyyy-MM-dd').toFormat(
'dd-MM-yyyy'
),
val: row.invAmt,
pos: row.gstin && row.gstin.substring(0, 2),
rchrg: row.reverseCharge,
inv_typ: 'R',
itms: [],
};
const items = await fyo.db.getAllRaw('SalesInvoiceItem', {
fields: ['*'],
filters: { parent: invRecord.inum },
});
items.forEach((item) => {
const itemRecord = {
num: item.hsnCode,
itm_det: {
txval: fyo.pesa(item.baseAmount).float,
rt: GST[item.tax],
csamt: 0,
camt: fyo
.pesa(CSGST[item.tax] || 0)
.mul(item.baseAmount)
.div(100).float,
samt: fyo
.pesa(CSGST[item.tax] || 0)
.mul(item.baseAmount)
.div(100).float,
iamt: fyo
.pesa(IGST[item.tax] || 0)
.mul(item.baseAmount)
.div(100).float,
},
};
invRecord.itms.push(itemRecord);
});
const customerRecord = b2b.find((b) => b.ctin === row.gstin);
if (customerRecord) {
customerRecord.inv.push(invRecord);
} else {
customer.inv.push(invRecord);
b2b.push(customer);
}
}
return b2b;
}
async function generateB2clData(invoices) {
const b2cl = [];
for (let invoice of invoices) {
const stateInvoiceRecord = {
pos: stateCodeMap[invoice.place.toUpperCase()],
inv: [],
};
const invRecord = {
inum: invoice.invNo,
idt: DateTime.fromFormat(invoice.invDate, 'yyyy-MM-dd').toFormat(
'dd-MM-yyyy'
),
val: invoice.invAmt,
itms: [],
};
const items = await fyo.db.getAllRaw('SalesInvoiceItem', {
fields: ['*'],
filters: { parent: invRecord.inum },
});
items.forEach((item) => {
const itemRecord = {
num: item.hsnCode,
itm_det: {
txval: fyo.pesa(item.baseAmount).float,
rt: GST[item.tax],
csamt: 0,
iamt: fyo
.pesa(invoice.rate || 0)
.mul(item.baseAmount)
.div(100).float,
},
};
invRecord.itms.push(itemRecord);
});
const stateRecord = b2cl.find((b) => b.pos === stateCodeMap[invoice.place]);
if (stateRecord) {
stateRecord.inv.push(invRecord);
} else {
stateInvoiceRecord.inv.push(invRecord);
b2cl.push(stateInvoiceRecord);
}
}
return b2cl;
}
async function generateB2csData(invoices) {
const b2cs = [];
for (let invoice of invoices) {
const pos = invoice.place.toUpperCase();
const invRecord = {
sply_ty: invoice.inState ? 'INTRA' : 'INTER',
pos: stateCodeMap[pos],
// "OE" - Abbreviation for errors and omissions excepted.
typ: 'OE',
txval: invoice.taxVal,
rt: invoice.rate,
iamt: !invoice.inState ? (invoice.taxVal * invoice.rate) / 100 : 0,
camt: invoice.inState ? invoice.cgstAmt : 0,
samt: invoice.inState ? invoice.sgstAmt : 0,
csamt: 0,
};
b2cs.push(invRecord);
}
return b2cs;
}
export async function generateGstr2Csv(getReportData) {
const { gstin } = fyo.AccountingSettings;
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}
const {
rows,
columns,
filters: { transferType, toDate },
} = getReportData();
const { filePath, canceled } = await getSavePath('gstr-2', 'csv');
if (canceled || !filePath) return;
let gstData;
if (transferType === 'B2B') {
gstData = await generateB2bCsvGstr2(rows, columns);
}
await exportCsv(gstData.rows, gstData.columns, filePath);
}
async function generateB2bCsvGstr2(rows, columns) {
const csvColumns = [
{
label: t`GSTIN of Supplier`,
fieldname: 'gstin',
},
{
label: t`Invoice Number`,
fieldname: 'invNo',
},
{
label: t`Invoice Date`,
fieldname: 'invDate',
},
{
label: t`Invoice Value`,
fieldname: 'invAmt',
},
{
label: t`Place of supply`,
fieldname: 'place',
},
{
label: t`Reverse Charge`,
fieldname: 'reverseCharge',
},
{
label: t`Rate`,
fieldname: 'rate',
},
{
label: t`Taxable Value`,
fieldname: 'taxVal',
},
{
label: t`Intergrated Tax Paid`,
fieldname: 'igstAmt',
},
{
label: t`Central Tax Paid`,
fieldname: 'cgstAmt',
},
{
label: t`State/UT Tax Paid`,
fieldname: 'sgstAmt',
},
];
return {
columns: csvColumns || [],
rows: rows || [],
};
}
export async function generateGstr1Csv(getReportData) {
const { gstin } = fyo.AccountingSettings;
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}
const {
rows,
columns,
filters: { transferType, toDate },
} = getReportData();
const { filePath, canceled } = await getSavePath('gstr-1', 'csv');
if (canceled || !filePath) return;
await exportCsv(rows, columns, filePath);
}

View File

@ -22,9 +22,7 @@ export const purchaseItemPartyMap: Record<string, string> = Object.keys(
return acc; return acc;
}, {} as Record<string, string>); }, {} as Record<string, string>);
export const flow = [ export const flow = [0.25, 0.2, 0.35, 0.2, 0.1, 0.01, 0.01, 0.15, 0, 0.25, 0.3, 0.5];
0.15, 0.1, 0.25, 0.1, 0.01, 0.01, 0.01, 0.05, 0, 0.15, 0.2, 0.4,
];
export function getFlowConstant(months: number) { export function getFlowConstant(months: number) {
// Jan to December // Jan to December

View File

@ -1 +1 @@
[{"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"}] [{"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": 50000, "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": 7500, "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

View File

@ -1,8 +1,7 @@
import { t } from 'fyo'; import { t } from 'fyo';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types'; import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types';
import { stateCodeMap } from 'regional/in'; import { codeStateMap } from 'regional/in';
import { titleCase } from 'utils';
import { getCountryInfo } from 'utils/misc'; import { getCountryInfo } from 'utils/misc';
export class Address extends Doc { export class Address extends Doc {
@ -36,7 +35,7 @@ export class Address extends Doc {
const country = doc?.country as string | undefined; const country = doc?.country as string | undefined;
switch (country) { switch (country) {
case 'India': case 'India':
return Object.keys(stateCodeMap).map(titleCase).sort(); return Object.values(codeStateMap).sort();
default: default:
return [] as string[]; return [] as string[];
} }

View File

@ -19,6 +19,7 @@ export abstract class Invoice extends Transactional {
account?: string; account?: string;
currency?: string; currency?: string;
netTotal?: Money; netTotal?: Money;
grandTotal?: Money;
baseGrandTotal?: Money; baseGrandTotal?: Money;
outstandingAmount?: Money; outstandingAmount?: Money;
exchangeRate?: number; exchangeRate?: number;

View File

@ -1,7 +1,6 @@
import { FormulaMap, ListsMap } from 'fyo/model/types'; import { FormulaMap, ListsMap } from 'fyo/model/types';
import { Address as BaseAddress } from 'models/baseModels/Address/Address'; import { Address as BaseAddress } from 'models/baseModels/Address/Address';
import { stateCodeMap } from 'regional/in'; import { codeStateMap } from 'regional/in';
import { titleCase } from 'utils';
export class Address extends BaseAddress { export class Address extends BaseAddress {
formulas: FormulaMap = { formulas: FormulaMap = {
@ -30,7 +29,7 @@ export class Address extends BaseAddress {
pos: { pos: {
formula: async () => { formula: async () => {
const stateList = Object.keys(stateCodeMap).map(titleCase).sort(); const stateList = Object.values(codeStateMap).sort();
const state = this.state as string; const state = this.state as string;
if (stateList.includes(state)) { if (stateList.includes(state)) {
return state; return state;
@ -44,7 +43,7 @@ export class Address extends BaseAddress {
static lists: ListsMap = { static lists: ListsMap = {
...BaseAddress.lists, ...BaseAddress.lists,
pos: () => { pos: () => {
return Object.keys(stateCodeMap).map(titleCase).sort(); return Object.values(codeStateMap).sort();
}, },
}; };
} }

View File

@ -3,6 +3,9 @@ import { Party as BaseParty } from 'models/baseModels/Party/Party';
import { GSTType } from './types'; import { GSTType } from './types';
export class Party extends BaseParty { export class Party extends BaseParty {
gstin?: string;
gstType?: GSTType;
async beforeSync() { async beforeSync() {
const gstin = this.get('gstin') as string | undefined; const gstin = this.get('gstin') as string | undefined;
const gstType = this.get('gstType') as GSTType; const gstType = this.get('gstType') as GSTType;

View File

@ -1,39 +1,38 @@
// prettier-ignore export const codeStateMap = {
export const stateCodeMap = { '01': 'Jammu and Kashmir',
'JAMMU AND KASHMIR': '01', '02': 'Himachal Pradesh',
'HIMACHAL PRADESH': '02', '03': 'Punjab',
'PUNJAB': '03', '04': 'Chandigarh',
'CHANDIGARH': '04', '05': 'Uttarakhand',
'UTTARAKHAND': '05', '06': 'Haryana',
'HARYANA': '06', '07': 'Delhi',
'DELHI': '07', '08': 'Rajasthan',
'RAJASTHAN': '08', '09': 'Uttar Pradesh',
'UTTAR PRADESH': '09', '10': 'Bihar',
'BIHAR': '10', '11': 'Sikkim',
'SIKKIM': '11', '12': 'Arunachal Pradesh',
'ARUNACHAL PRADESH': '12', '13': 'Nagaland',
'NAGALAND': '13', '14': 'Manipur',
'MANIPUR': '14', '15': 'Mizoram',
'MIZORAM': '15', '16': 'Tripura',
'TRIPURA': '16', '17': 'Meghalaya',
'MEGHALAYA': '17', '18': 'Assam',
'ASSAM': '18', '19': 'West Bengal',
'WEST BENGAL': '19', '20': 'Jharkhand',
'JHARKHAND': '20', '21': 'Odisha',
'ODISHA': '21', '22': 'Chattisgarh',
'CHATTISGARH': '22', '23': 'Madhya Pradesh',
'MADHYA PRADESH': '23', '24': 'Gujarat',
'GUJARAT': '24', '26': 'Dadra and Nagar Haveli and Daman and Diu',
'DADRA AND NAGAR HAVELI AND DAMAN AND DIU': '26', '27': 'Maharashtra',
'MAHARASHTRA': '27', '29': 'Karnataka',
'KARNATAKA': '29', '30': 'Goa',
'GOA': '30', '31': 'Lakshadweep',
'LAKSHADWEEP': '31', '32': 'Kerala',
'KERALA': '32', '33': 'Tamil Nadu',
'TAMIL NADU': '33', '34': 'Puducherry',
'PUDUCHERRY': '34', '35': 'Andaman and Nicobar Islands',
'ANDAMAN AND NICOBAR ISLANDS': '35', '36': 'Telangana',
'TELANGANA': '36', '37': 'Andhra Pradesh',
'ANDHRA PRADESH': '37', '38': 'Ladakh',
'LADAKH': '38', } as Record<string, string>;
};

View File

@ -1,95 +0,0 @@
import { fyo } from 'src/initFyo';
import { stateCodeMap } from '../../accounting/gst';
import { convertPesaValuesToFloat } from '../../src/utils';
class BaseGSTR {
async getCompleteReport(gstrType, filters) {
if (['GSTR-1', 'GSTR-2'].includes(gstrType)) {
const place = filters.place;
delete filters.place;
let entries = await fyo.db.getAll({
doctype: gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice',
filters,
});
filters.place = place;
let tableData = [];
for (let entry of entries) {
entry.doctype =
gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice';
const row = await this.getRow(entry);
tableData.push(row);
}
if (Object.keys(filters).length != 0) {
tableData = tableData.filter((row) => {
if (filters.account) return row.account === filters.account;
if (filters.transferType)
return row.transferType === filters.transferType;
if (filters.place) return row.place === filters.place;
return true;
});
}
tableData.forEach(convertPesaValuesToFloat);
return tableData;
} else {
return [];
}
}
async getRow(ledgerEntry) {
ledgerEntry = await fyo.doc.getDoc(ledgerEntry.doctype, ledgerEntry.name);
const row = {};
const { gstin } = fyo.AccountingSettings;
let party = await fyo.doc.getDoc(
'Party',
ledgerEntry.customer || ledgerEntry.supplier
);
if (party.address) {
let addressDetails = await fyo.doc.getDoc('Address', party.address);
row.place = addressDetails.pos || '';
}
row.gstin = party.gstin;
row.partyName = ledgerEntry.customer || ledgerEntry.supplier;
row.invNo = ledgerEntry.name;
row.invDate = ledgerEntry.date;
row.rate = 0;
row.inState =
gstin && gstin.substring(0, 2) === stateCodeMap[row.place?.toUpperCase()];
row.reverseCharge = !party.gstin ? 'Y' : 'N';
ledgerEntry.taxes?.forEach((tax) => {
row.rate += tax.rate;
const taxAmt = ledgerEntry.netTotal.percent(tax.rate);
switch (tax.account) {
case 'IGST': {
row.igstAmt = taxAmt;
row.inState = false;
}
case 'CGST':
row.cgstAmt = taxAmt;
case 'SGST':
row.sgstAmt = taxAmt;
case 'Nil Rated':
row.nilRated = true;
case 'Exempt':
row.exempt = true;
case 'Non GST':
row.nonGST = true;
}
});
row.invAmt = ledgerEntry.grandTotal;
row.taxVal = ledgerEntry.netTotal;
return row;
}
}
export default BaseGSTR;

View File

@ -0,0 +1,340 @@
import { t } from 'fyo';
import { DateTime } from 'luxon';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
import { Party } from 'models/regionalModels/in/Party';
import { ModelNameEnum } from 'models/types';
import { codeStateMap } from 'regional/in';
import { Report } from 'reports/Report';
import { ColumnField, ReportData, ReportRow } from 'reports/types';
import { Field, OptionField } from 'schemas/types';
import { isNumeric } from 'src/utils';
import { GSTRRow, GSTRType, TransferType, TransferTypeEnum } from './types';
export abstract class BaseGSTR extends Report {
place?: string;
toDate?: string;
fromDate?: string;
transferType?: TransferType;
usePagination: boolean = true;
abstract gstrType: GSTRType;
get transferTypeMap(): Record<string, string> {
if (this.gstrType === 'GSTR-2') {
return {
B2B: 'B2B',
};
}
return {
B2B: 'B2B',
B2CL: 'B2C-Large',
B2CS: 'B2C-Small',
NR: 'Nil Rated, Exempted and Non GST supplies',
};
}
get schemaName() {
if (this.gstrType === 'GSTR-1') {
return ModelNameEnum.SalesInvoice;
}
return ModelNameEnum.PurchaseInvoice;
}
async setReportData(): Promise<void> {
const gstrRows = await this.getGstrRows();
const filteredRows = this.filterGstrRows(gstrRows);
this.reportData = this.getReportDataFromGSTRRows(filteredRows);
}
getReportDataFromGSTRRows(gstrRows: GSTRRow[]): ReportData {
const reportData: ReportData = [];
for (const row of gstrRows) {
const reportRow: ReportRow = { cells: [] };
for (const { fieldname, fieldtype, width } of this.columns) {
const align = isNumeric(fieldtype) ? 'right' : 'left';
const rawValue = row[fieldname as keyof GSTRRow];
let value = '';
if (rawValue !== undefined) {
value = this.fyo.format(rawValue, fieldtype);
}
reportRow.cells.push({
align,
rawValue,
value,
width: width ?? 1,
});
}
reportData.push(reportRow);
}
return reportData;
}
filterGstrRows(gstrRows: GSTRRow[]) {
return gstrRows.filter((row) => {
let allow = true;
if (this.place) {
allow &&= codeStateMap[this.place] === row.place;
}
this.place;
return (allow &&= this.transferFilterFunction(row));
});
}
get transferFilterFunction(): (row: GSTRRow) => boolean {
if (this.transferType === 'B2B') {
return (row) => !!row.gstin;
}
if (this.transferType === 'B2CL') {
return (row) => !row.gstin && !row.inState && row.invAmt >= 250000;
}
if (this.transferType === 'B2CS') {
return (row) => !row.gstin && (row.inState || row.invAmt < 250000);
}
if (this.transferType === 'NR') {
return (row) => row.rate === 0; // this takes care of both nil rated, exempted goods
}
return (_) => true;
}
async getEntries() {
const date: string[] = [];
if (this.toDate) {
date.push('<=', this.toDate);
}
if (this.fromDate) {
date.push('>=', this.fromDate);
}
return (await this.fyo.db.getAllRaw(this.schemaName, {
filters: { date, submitted: true, cancelled: false },
})) as { name: string }[];
}
async getGstrRows(): Promise<GSTRRow[]> {
const entries = await this.getEntries();
const gstrRows: GSTRRow[] = [];
for (const entry of entries) {
const gstrRow = await this.getGstrRow(entry.name as string);
gstrRows.push(gstrRow);
}
return gstrRows;
}
async getGstrRow(entryName: string): Promise<GSTRRow> {
const entry = (await this.fyo.doc.getDoc(
this.schemaName,
entryName
)) as Invoice;
const gstin = (await this.fyo.getValue(
ModelNameEnum.AccountingSettings,
'gstin'
)) as string | null;
const party = (await this.fyo.doc.getDoc('Party', entry.party!)) as Party;
let place = '';
if (party.address) {
const pos = (await this.fyo.getValue(
ModelNameEnum.Address,
party.address as string,
'pos'
)) as string | undefined;
place = pos ?? '';
} else if (party.gstin) {
const code = party.gstin.slice(0, 2);
place = codeStateMap[code] ?? '';
}
let inState = false;
if (gstin) {
inState = codeStateMap[gstin.slice(0, 2)] === place;
}
const gstrRow: GSTRRow = {
gstin: party.gstin ?? '',
partyName: entry.party!,
invNo: entry.name!,
invDate: entry.date!,
rate: 0,
reverseCharge: !party.gstin ? 'Y' : 'N',
inState,
place,
invAmt: entry.grandTotal?.float ?? 0,
taxVal: entry.netTotal?.float ?? 0,
};
for (const tax of entry.taxes ?? []) {
gstrRow.rate += tax.rate ?? 0;
}
this.setTaxValuesOnGSTRRow(entry, gstrRow);
return gstrRow;
}
setTaxValuesOnGSTRRow(entry: Invoice, gstrRow: GSTRRow) {
for (const tax of entry.taxes ?? []) {
const rate = tax.rate ?? 0;
gstrRow.rate += rate;
const taxAmt = entry.netTotal!.percent(rate).float;
switch (tax.account) {
case 'IGST': {
gstrRow.igstAmt = taxAmt;
gstrRow.inState = false;
}
case 'CGST':
gstrRow.cgstAmt = taxAmt;
case 'SGST':
gstrRow.sgstAmt = taxAmt;
case 'Nil Rated':
gstrRow.nilRated = true;
case 'Exempt':
gstrRow.exempt = true;
case 'Non GST':
gstrRow.nonGST = true;
}
}
}
async setDefaultFilters() {
if (!this.toDate) {
this.toDate = DateTime.local().toISODate();
}
if (!this.fromDate) {
this.fromDate = DateTime.local().minus({ months: 3 }).toISODate();
}
if (!this.transferType) {
this.transferType = 'B2B';
}
}
getFilters(): Field[] {
const transferTypeMap = this.transferTypeMap;
const options = Object.keys(transferTypeMap).map((k) => ({
value: k,
label: transferTypeMap[k],
}));
return [
{
fieldtype: 'Select',
label: t`Transfer Type`,
placeholder: t`Transfer Type`,
fieldname: 'transferType',
options,
} as OptionField,
{
fieldtype: 'AutoComplete',
label: t`Place`,
placeholder: t`Place`,
fieldname: 'place',
options: Object.keys(codeStateMap).map((code) => {
return {
value: code,
label: codeStateMap[code],
};
}),
} as OptionField,
{
fieldtype: 'Date',
label: t`From Date`,
placeholder: t`From Date`,
fieldname: 'fromDate',
},
{
fieldtype: 'Date',
label: t`To Date`,
placeholder: t`To Date`,
fieldname: 'toDate',
},
];
}
getColumns(): ColumnField[] | Promise<ColumnField[]> {
const columns = [
{
label: t`Party`,
fieldtype: 'Data',
fieldname: 'partyName',
width: 1.5,
},
{
label: t`Invoice No.`,
fieldname: 'invNo',
fieldtype: 'Data',
},
{
label: t`Invoice Value`,
fieldname: 'invAmt',
fieldtype: 'Currency',
},
{
label: t`Invoice Date`,
fieldname: 'invDate',
fieldtype: 'Date',
},
{
label: t`Place of supply`,
fieldname: 'place',
fieldtype: 'Data',
},
{
label: t`Rate`,
fieldname: 'rate',
width: 0.5,
},
{
label: t`Taxable Value`,
fieldname: 'taxVal',
fieldtype: 'Currency',
},
{
label: t`Reverse Chrg.`,
fieldname: 'reverseCharge',
fieldtype: 'Data',
},
{
label: t`Intergrated Tax`,
fieldname: 'igstAmt',
fieldtype: 'Currency',
},
{
label: t`Central Tax`,
fieldname: 'cgstAmt',
fieldtype: 'Currency',
},
{
label: t`State Tax`,
fieldname: 'sgstAmt',
fieldtype: 'Currency',
},
] as ColumnField[];
const transferType = this.transferType ?? TransferTypeEnum.B2B;
if (transferType === TransferTypeEnum.B2B) {
columns.unshift({
label: t`GSTIN No.`,
fieldname: 'gstin',
fieldtype: 'Data',
width: 1.5,
});
}
return columns;
}
}

View File

@ -1,106 +0,0 @@
import { t } from 'fyo';
import { DateTime } from 'luxon';
import { stateCodeMap } from '../../accounting/gst';
import { titleCase } from '../../src/utils';
export default {
filterFields: [
{
fieldtype: 'AutoComplete',
label: t`Place`,
size: 'small',
placeholder: t`Place`,
fieldname: 'place',
getList: () => Object.keys(stateCodeMap).map(titleCase).sort(),
},
{
fieldtype: 'Date',
label: t`From Date`,
size: 'small',
placeholder: t`From Date`,
fieldname: 'fromDate',
default: () => DateTime.local().minus({ months: 3 }).toISODate(),
},
{
fieldtype: 'Date',
label: t`To Date`,
size: 'small',
placeholder: t`To Date`,
fieldname: 'toDate',
default: () => DateTime.local().toISODate(),
},
],
getColumns({ filters }) {
const columns = [
{
label: t`Party`,
fieldtype: 'Data',
fieldname: 'partyName',
width: 1.5,
},
{
label: t`Invoice No.`,
fieldname: 'invNo',
fieldtype: 'Data',
},
{
label: t`Invoice Value`,
fieldname: 'invAmt',
fieldtype: 'Currency',
},
{
label: t`Invoice Date`,
fieldname: 'invDate',
fieldtype: 'Date',
},
{
label: t`Place of supply`,
fieldname: 'place',
fieldtype: 'Data',
},
{
label: t`Rate`,
fieldname: 'rate',
fieldtype: 'Data',
width: 0.5,
},
{
label: t`Taxable Value`,
fieldname: 'taxVal',
fieldtype: 'Currency',
},
{
label: t`Reverse Chrg.`,
fieldname: 'reverseCharge',
fieldtype: 'Data',
},
{
label: t`Intergrated Tax`,
fieldname: 'igstAmt',
fieldtype: 'Currency',
},
{
label: t`Central Tax`,
fieldname: 'cgstAmt',
fieldtype: 'Currency',
},
{
label: t`State Tax`,
fieldname: 'sgstAmt',
fieldtype: 'Currency',
},
];
const transferType = filters.transferType || 'B2B';
if (transferType === 'B2B') {
columns.unshift({
label: t`GSTIN No.`,
fieldname: 'gstin',
fieldtype: 'Data',
width: 1.5,
});
}
return columns;
},
};

View File

@ -1,32 +0,0 @@
import BaseGSTR from './BaseGSTR';
class GSTR1 extends BaseGSTR {
async run(params) {
if (!Object.keys(params).length) return [];
let filters = {};
filters.cancelled = 0;
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.place) filters.place = params.place;
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
const data = await this.getCompleteReport('GSTR-1', filters);
// prettier-ignore
const conditions = {
'B2B': row => row.gstin,
'B2CL': row => !row.gstin && !row.inState && row.invAmt >= 250000,
'B2CS': row => !row.gstin && (row.inState || row.invAmt < 250000),
'NR': row => (row.rate === 0), // this takes care of both nil rated, exempted goods
};
if (!params.transferType) return data;
return data.filter((row) => conditions[params.transferType](row));
}
}
export default GSTR1;

View File

@ -0,0 +1,13 @@
import { Action } from 'fyo/model/types';
import { BaseGSTR } from './BaseGSTR';
import { GSTRType } from './types';
export class GSTR1 extends BaseGSTR {
static title = 'GSTR1';
static reportName = 'gstr-1';
gstrType: GSTRType = 'GSTR-1';
getActions(): Action[] {
return [];
}
}

View File

@ -1,49 +0,0 @@
const title = 'GSTR 1';
import { t } from 'fyo';
import { generateGstr1Csv, generateGstr1Json } from '../../accounting/gst';
import baseConfig from './BaseViewConfig';
const transferTypeMap = {
B2B: 'B2B',
B2CL: 'B2C-Large',
B2CS: 'B2C-Small',
NR: 'Nil Rated, Exempted and Non GST supplies',
};
const transferType = {
fieldtype: 'Select',
label: t`Transfer Type`,
placeholder: t`Transfer Type`,
fieldname: 'transferType',
options: Object.keys(transferTypeMap),
map: transferTypeMap,
default: 'B2B',
size: 'small',
};
const actions = [
{
group: t`Export`,
label: t`JSON`,
type: 'primary',
action: async (report, filters) => {
generateGstr1Json(report, filters);
},
},
{
group: t`Export`,
label: t`CSV`,
type: 'primary',
action: async (report, filters) => {
generateGstr1Csv(report, filters);
},
},
];
export default {
title: title,
method: 'gstr-1',
filterFields: [ transferType, ...baseConfig.filterFields],
actions: actions,
getColumns: baseConfig.getColumns,
};

View File

@ -1,26 +0,0 @@
import BaseGSTR from './BaseGSTR';
class GSTR2 extends BaseGSTR {
async run(params) {
if (!Object.keys(params).length) return [];
let filters = {};
if (params.toDate || params.fromDate) {
filters.date = [];
if (params.toDate) filters.date.push('<=', params.toDate);
if (params.fromDate) filters.date.push('>=', params.fromDate);
}
const data = await this.getCompleteReport('GSTR-2', filters);
// prettier-ignore
const conditions = {
'B2B': row => row.gstin,
};
if (!params.transferType) return data;
return data.filter((row) => conditions[params.transferType](row));
}
}
export default GSTR2;

View File

@ -0,0 +1,13 @@
import { Action } from 'fyo/model/types';
import { BaseGSTR } from './BaseGSTR';
import { GSTRType } from './types';
export class GSTR2 extends BaseGSTR {
static title = 'GSTR2';
static reportName = 'gstr-2';
gstrType: GSTRType = 'GSTR-2';
getActions(): Action[] {
return [];
}
}

View File

@ -1,38 +0,0 @@
const title = 'GSTR 2';
import { t } from 'fyo';
import { generateGstr2Csv } from '../../accounting/gst';
import baseConfig from './BaseViewConfig';
const transferTypeMap = {
B2B: 'B2B',
};
const transferType = {
fieldtype: 'Select',
label: t`Transfer Type`,
placeholder: t`Transfer Type`,
fieldname: 'transferType',
options: Object.keys(transferTypeMap),
map: transferTypeMap,
default: 'B2B',
size: 'small',
};
const actions = [
{
group: t`Export`,
label: t`CSV`,
type: 'primary',
action: async (report, filters) => {
generateGstr2Csv(report, filters);
},
},
];
export default {
title: title,
method: 'gstr-2',
filterFields: [ transferType, ...baseConfig.filterFields],
actions: actions,
getColumns: baseConfig.getColumns,
};

View File

@ -0,0 +1,28 @@
export enum TransferTypeEnum {
'B2B' = 'B2B',
'B2CL' = 'B2C',
'B2CS' = 'B2C',
'NR' = 'NR',
}
export type TransferType = keyof typeof TransferTypeEnum;
export type GSTRType = 'GSTR-1' | 'GSTR-2';
export interface GSTRRow {
gstin: string;
partyName: string;
invNo: string;
invDate: Date;
rate: number;
reverseCharge: 'Y' | 'N';
inState: boolean;
place: string;
invAmt: number;
taxVal: number;
igstAmt?: number;
cgstAmt?: number;
sgstAmt?: number;
exempt?: boolean;
nonGST?: boolean;
nilRated?: boolean;
}

View File

@ -1,5 +1,7 @@
import { BalanceSheet } from './BalanceSheet/BalanceSheet'; import { BalanceSheet } from './BalanceSheet/BalanceSheet';
import { GeneralLedger } from './GeneralLedger/GeneralLedger'; import { GeneralLedger } from './GeneralLedger/GeneralLedger';
import { GSTR1 } from './GoodsAndServiceTax/GSTR1';
import { GSTR2 } from './GoodsAndServiceTax/GSTR2';
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss'; import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
import { TrialBalance } from './TrialBalance/TrialBalance'; import { TrialBalance } from './TrialBalance/TrialBalance';
@ -8,4 +10,6 @@ export const reports = {
ProfitAndLoss, ProfitAndLoss,
BalanceSheet, BalanceSheet,
TrialBalance, TrialBalance,
GSTR1,
GSTR2,
}; };

View File

@ -21,6 +21,8 @@
</template> </template>
<script> <script>
import { isNumeric } from 'src/utils';
export default { export default {
name: 'Base', name: 'Base',
props: { props: {
@ -105,9 +107,7 @@ export default {
parse(value) { parse(value) {
return value; return value;
}, },
isNumeric(df) { isNumeric,
return ['Int', 'Float', 'Currency'].includes(df.fieldtype);
},
}, },
}; };
</script> </script>

View File

@ -75,6 +75,7 @@
</template> </template>
<script> <script>
import { Report } from 'reports/Report'; import { Report } from 'reports/Report';
import { FieldTypeEnum } from 'schemas/types';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Paginator from '../Paginator.vue'; import Paginator from '../Paginator.vue';
import WithScroll from '../WithScroll.vue'; import WithScroll from '../WithScroll.vue';
@ -143,6 +144,17 @@ export default defineComponent({
styles['padding-left'] = '0px'; styles['padding-left'] = '0px';
} }
if (
!cell.align &&
[
FieldTypeEnum.Currency,
FieldTypeEnum.Int,
FieldTypeEnum.Float,
].includes(cell.fieldtype)
) {
styles['text-align'] = 'right';
}
if (i === this.report.columns.length - 1) { if (i === this.report.columns.length - 1) {
styles['padding-right'] = '0px'; styles['padding-right'] = '0px';
} }

View File

@ -11,24 +11,18 @@
<div class="w-1/2 flex flex-col gap-5 mt-8"> <div class="w-1/2 flex flex-col gap-5 mt-8">
<!-- Ledgend Item --> <!-- Ledgend Item -->
<div <div
class="flex justify-between items-center text-sm" class="flex items-center text-sm"
v-for="(d, i) in expenses" v-for="(d, i) in expenses"
:key="d.name" :key="d.name"
>
<div
class="flex items-center"
@mouseover="active = i" @mouseover="active = i"
@mouseleave="active = null" @mouseleave="active = null"
> >
<div class="w-3 h-3 rounded-sm flex-shrink-0" :class="d.class" /> <div class="w-3 h-3 rounded-sm flex-shrink-0" :class="d.class" />
<p <p class="ml-2 overflow-x-scroll whitespace-nowrap no-scrollbar w-28">
class="ml-2 w-24 overflow-x-scroll whitespace-nowrap no-scrollbar"
>
{{ d.account }} {{ d.account }}
</p> </p>
</div> <p class="whitespace-nowrap flex-shrink-0 ml-auto">
<p class="whitespace-nowrap"> {{ fyo.format(d?.total ?? 0, 'Currency') }}
{{ fyo.format(d.total, 'Currency') }}
</p> </p>
</div> </div>
</div> </div>
@ -116,7 +110,9 @@ export default {
{ class: 'bg-gray-200', hex: theme.backgroundColor.gray['200'] }, { class: 'bg-gray-200', hex: theme.backgroundColor.gray['200'] },
]; ];
topExpenses = topExpenses.map((d, i) => { topExpenses = topExpenses
.filter((e) => e.total > 0)
.map((d, i) => {
d.color = shades[i].hex; d.color = shades[i].hex;
d.class = shades[i].class; d.class = shades[i].class;
return d; return d;

View File

@ -6,6 +6,7 @@ import { Doc } from 'fyo/model/doc';
import { isPesa } from 'fyo/utils'; import { isPesa } from 'fyo/utils';
import { DuplicateEntryError, LinkValidationError } from 'fyo/utils/errors'; import { DuplicateEntryError, LinkValidationError } from 'fyo/utils/errors';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
export function stringifyCircular( export function stringifyCircular(
obj: unknown, obj: unknown,
@ -97,3 +98,17 @@ export function getErrorMessage(e: Error, doc?: Doc): string {
return errorMessage; return errorMessage;
} }
export function isNumeric(fieldtype: Field | FieldType): boolean {
if (typeof fieldtype !== 'string') {
fieldtype = fieldtype?.fieldtype;
}
const numericTypes: FieldType[] = [
FieldTypeEnum.Int,
FieldTypeEnum.Float,
FieldTypeEnum.Currency,
];
return numericTypes.includes(fieldtype);
}

View File

@ -158,20 +158,18 @@ function getCompleteSidebar(): SidebarConfig {
name: 'trial-balance', name: 'trial-balance',
route: '/report/TrialBalance', route: '/report/TrialBalance',
}, },
/*
{ {
label: t`GSTR1`, label: t`GSTR1`,
name: 'gstr1', name: 'gstr1',
route: '/report/gstr-1', route: '/report/GSTR1',
hidden: () => fyo.singles.AccountingSettings!.country !== 'India', hidden: () => fyo.singles.AccountingSettings!.country !== 'India',
}, },
{ {
label: t`GSTR2`, label: t`GSTR2`,
name: 'gstr2', name: 'gstr2',
route: '/report/gstr-2', route: '/report/GSTR2',
hidden: () => fyo.singles.AccountingSettings!.country !== 'India', hidden: () => fyo.singles.AccountingSettings!.country !== 'India',
}, },
*/
], ],
}, },
{ {