From 5a633b9d07f96a7df40ec629ca0d835b8b7ced1b Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 18 May 2022 14:18:30 +0530 Subject: [PATCH] incr: add gstr exports --- fyo/core/converter.ts | 4 - reports/GoodsAndServiceTax/BaseGSTR.ts | 8 + reports/GoodsAndServiceTax/GSTR1.ts | 4 - reports/GoodsAndServiceTax/GSTR2.ts | 4 - reports/GoodsAndServiceTax/gstExporter.ts | 367 ++++++++++++++++++++++ reports/GoodsAndServiceTax/types.ts | 5 +- reports/commonExporter.ts | 33 +- reports/types.ts | 2 +- 8 files changed, 394 insertions(+), 33 deletions(-) create mode 100644 reports/GoodsAndServiceTax/gstExporter.ts diff --git a/fyo/core/converter.ts b/fyo/core/converter.ts index 4671ba30..d519427e 100644 --- a/fyo/core/converter.ts +++ b/fyo/core/converter.ts @@ -56,10 +56,6 @@ export class Converter { } static toDocValue(value: RawValue, field: Field, fyo: Fyo): DocValue { - if (!field?.fieldtype) { - console.log(value, field); - console.trace(); - } switch (field.fieldtype) { case FieldTypeEnum.Currency: return toDocCurrency(value, field, fyo); diff --git a/reports/GoodsAndServiceTax/BaseGSTR.ts b/reports/GoodsAndServiceTax/BaseGSTR.ts index 48a242d2..92e39afa 100644 --- a/reports/GoodsAndServiceTax/BaseGSTR.ts +++ b/reports/GoodsAndServiceTax/BaseGSTR.ts @@ -1,4 +1,5 @@ import { t } from 'fyo'; +import { Action } from 'fyo/model/types'; import { DateTime } from 'luxon'; import { Invoice } from 'models/baseModels/Invoice/Invoice'; import { Party } from 'models/regionalModels/in/Party'; @@ -8,6 +9,7 @@ import { Report } from 'reports/Report'; import { ColumnField, ReportData, ReportRow } from 'reports/types'; import { Field, OptionField } from 'schemas/types'; import { isNumeric } from 'src/utils'; +import getGSTRExportActions from './gstExporter'; import { GSTRRow, GSTRType, TransferType, TransferTypeEnum } from './types'; export abstract class BaseGSTR extends Report { @@ -16,6 +18,7 @@ export abstract class BaseGSTR extends Report { fromDate?: string; transferType?: TransferType; usePagination: boolean = true; + gstrRows?: GSTRRow[]; abstract gstrType: GSTRType; @@ -45,6 +48,7 @@ export abstract class BaseGSTR extends Report { async setReportData(): Promise { const gstrRows = await this.getGstrRows(); const filteredRows = this.filterGstrRows(gstrRows); + this.gstrRows = filteredRows; this.reportData = this.getReportDataFromGSTRRows(filteredRows); } @@ -337,4 +341,8 @@ export abstract class BaseGSTR extends Report { return columns; } + + getActions(): Action[] { + return getGSTRExportActions(this); + } } diff --git a/reports/GoodsAndServiceTax/GSTR1.ts b/reports/GoodsAndServiceTax/GSTR1.ts index 3f2316b1..9b8085e9 100644 --- a/reports/GoodsAndServiceTax/GSTR1.ts +++ b/reports/GoodsAndServiceTax/GSTR1.ts @@ -1,4 +1,3 @@ -import { Action } from 'fyo/model/types'; import { BaseGSTR } from './BaseGSTR'; import { GSTRType } from './types'; @@ -7,7 +6,4 @@ export class GSTR1 extends BaseGSTR { static reportName = 'gstr-1'; gstrType: GSTRType = 'GSTR-1'; - getActions(): Action[] { - return []; - } } diff --git a/reports/GoodsAndServiceTax/GSTR2.ts b/reports/GoodsAndServiceTax/GSTR2.ts index 861ec099..b1abe12e 100644 --- a/reports/GoodsAndServiceTax/GSTR2.ts +++ b/reports/GoodsAndServiceTax/GSTR2.ts @@ -1,4 +1,3 @@ -import { Action } from 'fyo/model/types'; import { BaseGSTR } from './BaseGSTR'; import { GSTRType } from './types'; @@ -7,7 +6,4 @@ export class GSTR2 extends BaseGSTR { static reportName = 'gstr-2'; gstrType: GSTRType = 'GSTR-2'; - getActions(): Action[] { - return []; - } } diff --git a/reports/GoodsAndServiceTax/gstExporter.ts b/reports/GoodsAndServiceTax/gstExporter.ts new file mode 100644 index 00000000..42011683 --- /dev/null +++ b/reports/GoodsAndServiceTax/gstExporter.ts @@ -0,0 +1,367 @@ +import { Action } from 'fyo/model/types'; +import { Verb } from 'fyo/telemetry/types'; +import { DateTime } from 'luxon'; +import { ModelNameEnum } from 'models/types'; +import { codeStateMap } from 'regional/in'; +import { ExportExtention } from 'reports/types'; +import { getSavePath } from 'src/utils/ipcCalls'; +import { showMessageDialog } from 'src/utils/ui'; +import { invertMap } from 'utils'; +import { getCsvData, saveExportData } from '../commonExporter'; +import { BaseGSTR } from './BaseGSTR'; +import { TransferTypeEnum } from './types'; + +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, +} as Record; + +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, +} as Record; + +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, +} as Record; + +interface GSTData { + version: string; + hash: string; + gstin: string; + fp: string; + b2b?: B2BCustomer[]; + b2cl?: B2CLStateInvoiceRecord[]; + b2cs?: B2CSInvRecord[]; +} + +interface B2BCustomer { + ctin: string; + inv: B2BInvRecord[]; +} + +interface B2BInvRecord { + inum: string; + idt: string; + val: number; + pos: string; + rchrg: 'Y' | 'N'; + inv_typ: string; + itms: B2BItmRecord[]; +} + +interface B2BItmRecord { + num: number; + itm_det: { + txval: number; + rt: number; + csamt: number; + camt: number; + samt: number; + iamt: number; + }; +} + +interface B2CLInvRecord { + inum: string; + idt: string; + val: number; + itms: B2CLItmRecord[]; +} + +interface B2CLItmRecord { + num: number; + itm_det: { + txval: number; + rt: number; + csamt: 0; + iamt: number; + }; +} + +interface B2CLStateInvoiceRecord { + pos: string; + inv: B2CLInvRecord[]; +} + +interface B2CSInvRecord { + sply_ty: 'INTRA' | 'INTER'; + pos: string; + typ: 'OE'; // "OE" - Errors and omissions excepted. + txval: number; + rt: number; + iamt: number; + camt: number; + samt: number; + csamt: number; +} + +export default function getGSTRExportActions(report: BaseGSTR): Action[] { + const exportExtention = ['csv', 'json'] as ExportExtention[]; + + return exportExtention.map((ext) => ({ + group: `Export`, + label: ext.toUpperCase(), + type: 'primary', + action: async () => { + await exportReport(ext, report); + }, + })); +} + +async function exportReport(extention: ExportExtention, report: BaseGSTR) { + const canExport = await getCanExport(report); + if (!canExport) { + return; + } + + const { filePath, canceled } = await getSavePath( + report.reportName, + extention + ); + + if (canceled || !filePath) { + return; + } + + let data = ''; + + if (extention === 'csv') { + data = getCsvData(report); + } else if (extention === 'json') { + data = await getGstrJsonData(report); + } + + if (!data.length) { + return; + } + + await saveExportData(data, filePath, report.fyo); + report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention }); +} + +async function getCanExport(report: BaseGSTR) { + const gstin = await report.fyo.getValue( + ModelNameEnum.AccountingSettings, + 'gstin' + ); + if (gstin) { + return true; + } + + showMessageDialog({ + message: 'Cannot Export', + detail: 'Please set GSTIN in General Settings.', + }); + + return false; +} + +export async function getGstrJsonData(report: BaseGSTR): Promise { + const toDate = report.toDate!; + const transferType = report.transferType!; + const gstin = await report.fyo.getValue( + ModelNameEnum.AccountingSettings, + 'gstin' + ); + + const gstData: GSTData = { + version: 'GST3.0.4', + hash: 'hash', + gstin: gstin as string, + fp: DateTime.fromISO(toDate).toFormat('MMyyyy'), + }; + + if (transferType === TransferTypeEnum.B2B) { + gstData.b2b = await generateB2bData(report); + } else if (transferType === TransferTypeEnum.B2CL) { + gstData.b2cl = await generateB2clData(report); + } else if (transferType === TransferTypeEnum.B2CS) { + gstData.b2cs = await generateB2csData(report); + } + + return JSON.stringify(gstData); +} + +async function generateB2bData(report: BaseGSTR): Promise { + const fyo = report.fyo; + const b2b: B2BCustomer[] = []; + + const schemaName = + report.gstrType === 'GSTR-1' + ? ModelNameEnum.SalesInvoiceItem + : ModelNameEnum.PurchaseInvoiceItem; + + for (const row of report.gstrRows ?? []) { + const invRecord: B2BInvRecord = { + inum: row.invNo, + idt: DateTime.fromJSDate(row.invDate).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(schemaName, { + fields: ['baseAmount', 'tax', 'hsnCode'], + filters: { parent: invRecord.inum as string }, + }); + + items.forEach((item) => { + const hsnCode = item.hsnCode as number; + const tax = item.tax as string; + const baseAmount = (item.baseAmount ?? 0) as string; + + const itemRecord: B2BItmRecord = { + num: hsnCode, + itm_det: { + txval: fyo.pesa(baseAmount).float, + rt: GST[tax], + csamt: 0, + camt: fyo + .pesa(CSGST[tax] ?? 0) + .mul(baseAmount) + .div(100).float, + samt: fyo + .pesa(CSGST[tax] ?? 0) + .mul(baseAmount) + .div(100).float, + iamt: fyo + .pesa(IGST[tax] ?? 0) + .mul(baseAmount) + .div(100).float, + }, + }; + + invRecord.itms.push(itemRecord); + }); + + const customerRecord = b2b.find((b) => b.ctin === row.gstin); + const customer = { + ctin: row.gstin, + inv: [], + } as B2BCustomer; + + if (customerRecord) { + customerRecord.inv.push(invRecord); + } else { + customer.inv.push(invRecord); + b2b.push(customer); + } + } + + return b2b; +} + +async function generateB2clData( + report: BaseGSTR +): Promise { + const fyo = report.fyo; + const b2cl: B2CLStateInvoiceRecord[] = []; + const stateCodeMap = invertMap(codeStateMap); + + const schemaName = + report.gstrType === 'GSTR-1' + ? ModelNameEnum.SalesInvoiceItem + : ModelNameEnum.PurchaseInvoiceItem; + + for (const row of report.gstrRows ?? []) { + const invRecord: B2CLInvRecord = { + inum: row.invNo, + idt: DateTime.fromJSDate(row.invDate).toFormat('dd-MM-yyyy'), + val: row.invAmt, + itms: [], + }; + + const items = await fyo.db.getAllRaw(schemaName, { + fields: ['hsnCode', 'tax', 'baseAmount'], + filters: { parent: invRecord.inum }, + }); + + items.forEach((item) => { + const hsnCode = item.hsnCode as number; + const tax = item.tax as string; + const baseAmount = (item.baseAmount ?? 0) as string; + + const itemRecord: B2CLItmRecord = { + num: hsnCode, + itm_det: { + txval: fyo.pesa(baseAmount).float, + rt: GST[tax] ?? 0, + csamt: 0, + iamt: fyo + .pesa(row.rate ?? 0) + .mul(baseAmount) + .div(100).float, + }, + }; + + invRecord.itms.push(itemRecord); + }); + + const stateRecord = b2cl.find((b) => b.pos === stateCodeMap[row.place]); + const stateInvoiceRecord: B2CLStateInvoiceRecord = { + pos: stateCodeMap[row.place], + inv: [], + }; + + if (stateRecord) { + stateRecord.inv.push(invRecord); + } else { + stateInvoiceRecord.inv.push(invRecord); + b2cl.push(stateInvoiceRecord); + } + } + + return b2cl; +} + +function generateB2csData(report: BaseGSTR): B2CSInvRecord[] { + const stateCodeMap = invertMap(codeStateMap); + const b2cs: B2CSInvRecord[] = []; + + for (const row of report.gstrRows ?? []) { + const invRecord: B2CSInvRecord = { + sply_ty: row.inState ? 'INTRA' : 'INTER', + pos: stateCodeMap[row.place], + typ: 'OE', + txval: row.taxVal, + rt: row.rate, + iamt: !row.inState ? (row.taxVal * row.rate) / 100 : 0, + camt: row.inState ? row.cgstAmt ?? 0 : 0, + samt: row.inState ? row.sgstAmt ?? 0 : 0, + csamt: 0, + }; + + b2cs.push(invRecord); + } + + return b2cs; +} diff --git a/reports/GoodsAndServiceTax/types.ts b/reports/GoodsAndServiceTax/types.ts index 50468748..3971f829 100644 --- a/reports/GoodsAndServiceTax/types.ts +++ b/reports/GoodsAndServiceTax/types.ts @@ -1,8 +1,7 @@ - export enum TransferTypeEnum { 'B2B' = 'B2B', -'B2CL' = 'B2C', - 'B2CS' = 'B2C', + 'B2CL' = 'B2CL', + 'B2CS' = 'B2CS', 'NR' = 'NR', } diff --git a/reports/commonExporter.ts b/reports/commonExporter.ts index 17de0728..e8001e54 100644 --- a/reports/commonExporter.ts +++ b/reports/commonExporter.ts @@ -5,9 +5,7 @@ import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls'; import { getIsNullOrUndef } from 'utils'; import { generateCSV } from 'utils/csvParser'; import { Report } from './Report'; -import { ReportCell } from './types'; - -type ExportExtention = 'csv' | 'json'; +import { ExportExtention, ReportCell } from './types'; interface JSONExport { columns: { fieldname: string; label: string }[]; @@ -42,21 +40,23 @@ async function exportReport(extention: ExportExtention, report: Report) { return; } - switch (extention) { - case 'csv': - await exportCsv(report, filePath); - break; - case 'json': - await exportJson(report, filePath); - break; - default: - return; + let data = ''; + + if (extention === 'csv') { + data = getCsvData(report); + } else if (extention === 'json') { + data = getJsonData(report); } + if (!data.length) { + return; + } + + await saveExportData(data, filePath, report.fyo); report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention }); } -async function exportJson(report: Report, filePath: string) { +function getJsonData(report: Report): string { const exportObject: JSONExport = { columns: [], rows: [], @@ -117,13 +117,12 @@ async function exportJson(report: Report, filePath: string) { exportObject.softwareName = 'Frappe Books'; exportObject.softwareVersion = report.fyo.store.appVersion; - await saveExportData(JSON.stringify(exportObject), filePath, report.fyo); + return JSON.stringify(exportObject); } -export async function exportCsv(report: Report, filePath: string) { +export function getCsvData(report: Report): string { const csvMatrix = convertReportToCSVMatrix(report); - const csvString = generateCSV(csvMatrix); - saveExportData(csvString, filePath, report.fyo); + return generateCSV(csvMatrix); } function convertReportToCSVMatrix(report: Report): unknown[][] { diff --git a/reports/types.ts b/reports/types.ts index 92548bb2..416df57b 100644 --- a/reports/types.ts +++ b/reports/types.ts @@ -2,7 +2,7 @@ import { DateTime } from 'luxon'; import { AccountRootType } from 'models/baseModels/Account/types'; import { BaseField, RawValue } from 'schemas/types'; -export type ExportExtension = 'csv' | 'json'; +export type ExportExtention = 'csv' | 'json'; export interface ReportCell { bold?: boolean;