diff --git a/accounting/gst.js b/accounting/gst.js index db99ca0a..136110e6 100644 --- a/accounting/gst.js +++ b/accounting/gst.js @@ -5,6 +5,46 @@ import { DateTime } from 'luxon'; import { saveExportData } from '../reports/commonExporter'; import { getSavePath } from '../src/utils'; +// prettier-ignore +export const stateCodeMap = { + 'JAMMU AND KASHMIR': '1', + 'HIMACHAL PRADESH': '2', + 'PUNJAB': '3', + 'CHANDIGARH': '4', + 'UTTARAKHAND': '5', + 'HARYANA': '6', + 'DELHI': '7', + 'RAJASTHAN': '8', + 'UTTAR PRADESH': '9', + 'BIHAR': '10', + 'SIKKIM': '11', + 'ARUNACHAL PRADESH': '12', + 'NAGALAND': '13', + 'MANIPUR': '14', + 'MIZORAM': '15', + 'TRIPURA': '16', + 'MEGHALAYA': '17', + 'ASSAM': '18', + 'WEST BENGAL': '19', + 'JHARKHAND': '20', + 'ODISHA': '21', + 'CHATTISGARH': '22', + 'MADHYA PRADESH': '23', + 'GUJARAT': '24', + 'DADRA AND NAGAR HAVELI AND DAMAN AND DIU': '26', + 'MAHARASHTRA': '27', + 'KARNATAKA': '29', + 'GOA': '30', + 'LAKSHADWEEP': '31', + 'KERALA': '32', + 'TAMIL NADU': '33', + 'PUDUCHERRY': '34', + 'ANDAMAN AND NICOBAR ISLANDS': '35', + 'TELANGANA': '36', + 'ANDHRA PRADESH': '37', + 'LADAKH': '38', +}; + const GST = { 'GST-0': 0, 'GST-0.25': 0.25, @@ -112,7 +152,7 @@ async function generateB2bData(rows) { items.forEach((item) => { const itemRecord = { - num: item.hsnCode || 0, + num: item.hsnCode, itm_det: { txval: item.baseAmount, rt: GST[item.tax], @@ -140,9 +180,75 @@ async function generateB2bData(rows) { } async function generateB2clData(invoices) { - return []; + 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: [], + }; + + let items = await frappe.db + .knex('SalesInvoiceItem') + .where('parent', invRecord.inum); + + items.forEach((item) => { + const itemRecord = { + num: item.hsnCode, + itm_det: { + txval: item.baseAmount, + rt: GST[item.tax], + csamt: 0, + iamt: ((invoice.rate || 0) * item.baseAmount) / 100, + }, + }; + + 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) { - return []; + 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; } diff --git a/models/doctype/Address/Address.js b/models/doctype/Address/Address.js index 4767270a..585cf2a0 100644 --- a/models/doctype/Address/Address.js +++ b/models/doctype/Address/Address.js @@ -1,6 +1,20 @@ +import { stateCodeMap } from '../../../accounting/gst'; +import countryList from '../../../fixtures/countryInfo.json'; +import { titleCase } from '../../../src/utils'; + +function getStates(doc) { + switch (doc.country) { + case 'India': + return Object.keys(stateCodeMap).map(titleCase).sort(); + default: + return []; + } +} + export default { name: 'Address', doctype: 'DocType', + regional: 1, isSingle: 0, keywordFields: [ 'addressLine1', @@ -8,7 +22,7 @@ export default { 'city', 'state', 'country', - 'postalCode' + 'postalCode', ], fields: [ { @@ -22,69 +36,71 @@ export default { fieldname: 'addressLine2', label: 'Address Line 2', placeholder: 'Address Line 2', - fieldtype: 'Data' + fieldtype: 'Data', }, { fieldname: 'city', label: 'City / Town', placeholder: 'City / Town', fieldtype: 'Data', - required: 1 + required: 1, }, { fieldname: 'state', label: 'State', placeholder: 'State', - fieldtype: 'Data' + fieldtype: 'AutoComplete', + getList: getStates, }, { fieldname: 'country', label: 'Country', placeholder: 'Country', - fieldtype: 'Data', - required: 1 + fieldtype: 'AutoComplete', + getList: () => Object.keys(countryList).sort(), + required: 1, }, { fieldname: 'postalCode', label: 'Postal Code', placeholder: 'Postal Code', - fieldtype: 'Data' + fieldtype: 'Data', }, { fieldname: 'emailAddress', label: 'Email Address', placeholder: 'Email Address', - fieldtype: 'Data' + fieldtype: 'Data', }, { fieldname: 'phone', label: 'Phone', placeholder: 'Phone', - fieldtype: 'Data' + fieldtype: 'Data', }, { fieldname: 'fax', label: 'Fax', - fieldtype: 'Data' + fieldtype: 'Data', }, { fieldname: 'addressDisplay', fieldtype: 'Text', label: 'Address Display', readOnly: true, - formula: doc => { + formula: (doc) => { return [ doc.addressLine1, doc.addressLine2, doc.city, doc.state, doc.country, - doc.postalCode + doc.postalCode, ] .filter(Boolean) .join(', '); - } - } + }, + }, ], quickEditFields: [ 'addressLine1', @@ -92,7 +108,7 @@ export default { 'city', 'state', 'country', - 'postalCode' + 'postalCode', ], - inlineEditDisplayField: 'addressDisplay' + inlineEditDisplayField: 'addressDisplay', }; diff --git a/models/doctype/Address/RegionalChanges.js b/models/doctype/Address/RegionalChanges.js new file mode 100644 index 00000000..26ca0bf7 --- /dev/null +++ b/models/doctype/Address/RegionalChanges.js @@ -0,0 +1,30 @@ +import { cloneDeep } from 'lodash'; +import { stateCodeMap } from '../../../accounting/gst'; +import { titleCase } from '../../../src/utils'; +import AddressOriginal from './Address'; + +export default function getAugmentedAddress({ country }) { + const Address = cloneDeep(AddressOriginal); + if (!country) { + return Address; + } + + const stateList = Object.keys(stateCodeMap).map(titleCase).sort(); + if (country === 'India') { + Address.fields = [ + ...Address.fields, + { + fieldname: 'pos', + label: 'Place of Supply', + fieldtype: 'AutoComplete', + placeholder: 'Place of Supply', + formula: (doc) => (stateList.includes(doc.state) ? doc.state : ''), + getList: () => stateList, + }, + ]; + + Address.quickEditFields = [...Address.quickEditFields, 'pos']; + } + + return Address; +} diff --git a/package.json b/package.json index b3c0df35..1b8e51a6 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "knex": "^0.95.12", "lodash": "^4.17.21", "luxon": "^2.0.2", - "portal-vue": "^2.1.7", "sqlite3": "^5.0.2", "vue": "^2.6.14", "vue-router": "^3.5.3" diff --git a/reports/BalanceSheet/viewConfig.js b/reports/BalanceSheet/viewConfig.js index 15f91ff8..922d172e 100644 --- a/reports/BalanceSheet/viewConfig.js +++ b/reports/BalanceSheet/viewConfig.js @@ -27,7 +27,7 @@ export default { }, ], actions: getCommonExportActions('balance-sheet'), - getColumns(data) { + getColumns({ data }) { const columns = [ { label: 'Account', fieldtype: 'Data', fieldname: 'account', width: 2 }, ]; diff --git a/reports/GoodsAndServiceTax/BaseGSTR.js b/reports/GoodsAndServiceTax/BaseGSTR.js index 8c14ab30..118aff0a 100644 --- a/reports/GoodsAndServiceTax/BaseGSTR.js +++ b/reports/GoodsAndServiceTax/BaseGSTR.js @@ -1,22 +1,27 @@ import frappe from 'frappejs'; +import { stateCodeMap } from '../../accounting/gst'; class BaseGSTR { async getCompleteReport(gstrType, filters) { if (['GSTR-1', 'GSTR-2'].includes(gstrType)) { + const place = filters.place; + delete filters.place; let entries = await frappe.db.getAll({ doctype: gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice', - filters + filters, }); + filters.place = place; let tableData = []; for (let entry of entries) { - entry.doctype = gstrType === 'GSTR-1' ? 'SalesInvoice' : 'PurchaseInvoice'; + 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 => { + tableData = tableData.filter((row) => { if (filters.account) return row.account === filters.account; if (filters.transferType) return row.transferType === filters.transferType; @@ -24,6 +29,7 @@ class BaseGSTR { return true; }); } + return tableData; } else { return []; @@ -31,36 +37,55 @@ class BaseGSTR { } async getRow(ledgerEntry) { - let row = {}; ledgerEntry = await frappe.getDoc(ledgerEntry.doctype, ledgerEntry.name); + + const row = {}; + const { gstin } = frappe.AccountingSettings; + let party = await frappe.getDoc( 'Party', ledgerEntry.customer || ledgerEntry.supplier ); + if (party.address) { let addressDetails = await frappe.getDoc('Address', party.address); - row.place = addressDetails.state || ''; + 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 = true; + row.inState = + gstin && gstin.substring(0, 2) === stateCodeMap[row.place?.toUpperCase()]; row.reverseCharge = !party.gstin ? 'Y' : 'N'; - ledgerEntry.taxes?.forEach(tax => { + + ledgerEntry.taxes?.forEach((tax) => { row.rate += tax.rate; const taxAmt = (tax.rate * ledgerEntry.netTotal) / 100; - if (tax.account === 'IGST') row.igstAmt = taxAmt; - if (tax.account === 'IGST') row.inState = false; - if (tax.account === 'CGST') row.cgstAmt = taxAmt; - if (tax.account === 'SGST') row.sgstAmt = taxAmt; - if (tax.account === 'Nil Rated') row.nilRated = true; - if (tax.account === 'Exempt') row.exempt = true; - if (tax.account === 'Non GST') row.nonGST = true; + + 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; } } diff --git a/reports/GoodsAndServiceTax/BaseViewConfig.js b/reports/GoodsAndServiceTax/BaseViewConfig.js index 2fed4496..f1e84d9a 100644 --- a/reports/GoodsAndServiceTax/BaseViewConfig.js +++ b/reports/GoodsAndServiceTax/BaseViewConfig.js @@ -1,5 +1,6 @@ import { DateTime } from 'luxon'; -import { generateGstr1Json } from '../../accounting/gst'; +import { generateGstr1Json, stateCodeMap } from '../../accounting/gst'; +import { titleCase } from '../../src/utils'; const transferTypeMap = { B2B: 'B2B', @@ -7,6 +8,7 @@ const transferTypeMap = { B2CS: 'B2C-Small', NR: 'Nil Rated, Exempted and Non GST supplies', }; +const stateList = Object.keys(stateCodeMap).map(titleCase).sort(); export default { filterFields: [ @@ -21,11 +23,12 @@ export default { size: 'small', }, { - fieldtype: 'Data', + fieldtype: 'AutoComplete', label: 'Place', size: 'small', placeholder: 'Place', fieldname: 'place', + getList: () => stateList, }, { fieldtype: 'Date', @@ -55,14 +58,8 @@ export default { }, ], - getColumns() { - return [ - { - label: 'GSTIN No.', - fieldname: 'gstin', - fieldtype: 'Data', - width: 1.5, - }, + getColumns({ filters }) { + const columns = [ { label: 'Party', fieldtype: 'Data', @@ -121,5 +118,17 @@ export default { fieldtype: 'Currency', }, ]; + + const transferType = filters.transferType || 'B2B'; + if (transferType === 'B2B') { + columns.unshift({ + label: 'GSTIN No.', + fieldname: 'gstin', + fieldtype: 'Data', + width: 1.5, + }); + } + + return columns; }, }; diff --git a/reports/GoodsAndServiceTax/GSTR1.js b/reports/GoodsAndServiceTax/GSTR1.js index 57e8e8de..8759da5e 100644 --- a/reports/GoodsAndServiceTax/GSTR1.js +++ b/reports/GoodsAndServiceTax/GSTR1.js @@ -8,6 +8,8 @@ class GSTR1 extends BaseGSTR { 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); } @@ -18,7 +20,7 @@ class GSTR1 extends BaseGSTR { const conditions = { 'B2B': row => row.gstin, 'B2CL': row => !row.gstin && !row.inState && row.invAmt >= 250000, - 'B2CS': row => !row.gstin && (row.inState || (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 }; diff --git a/reports/ProfitAndLoss/viewConfig.js b/reports/ProfitAndLoss/viewConfig.js index 04878d51..fb34074f 100644 --- a/reports/ProfitAndLoss/viewConfig.js +++ b/reports/ProfitAndLoss/viewConfig.js @@ -41,7 +41,7 @@ export default { }, ], actions: getCommonExportActions('profit-and-loss'), - getColumns(data) { + getColumns({ data }) { const columns = [ { label: 'Account', fieldtype: 'Data', fieldname: 'account', width: 2 }, ]; diff --git a/reports/TrialBalance/viewConfig.js b/reports/TrialBalance/viewConfig.js index 083f66a9..ff16dac3 100644 --- a/reports/TrialBalance/viewConfig.js +++ b/reports/TrialBalance/viewConfig.js @@ -32,7 +32,7 @@ export default { }, ], actions: getCommonExportActions('trial-balance'), - getColumns(data) { + getColumns() { const columns = [ { label: 'Account', fieldtype: 'Data', fieldname: 'account', width: 2 }, { diff --git a/src/App.vue b/src/App.vue index 25f1842b..67feed74 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,7 +18,6 @@ @setup-complete="showSetupWizardOrDesk(true)" @setup-canceled="setupCanceled" /> -
diff --git a/src/components/Controls/AutoComplete.vue b/src/components/Controls/AutoComplete.vue index f7926223..671cc81e 100644 --- a/src/components/Controls/AutoComplete.vue +++ b/src/components/Controls/AutoComplete.vue @@ -59,6 +59,9 @@ export default { }, }, }, + inject: { + doc: { default: null }, + }, computed: {}, methods: { async updateSuggestions(e) { @@ -81,7 +84,7 @@ export default { keyword = keyword.toLowerCase(); let list = this.df.getList - ? await this.df.getList() + ? await this.df.getList(this.doc) : this.df.options || []; let items = list.map((d) => { @@ -118,6 +121,12 @@ export default { async onBlur(value) { if (value === '' || value == null) { this.triggerChange(''); + return; + } + + if (value && this.suggestions.length === 0) { + this.triggerChange(value); + return; } if ( diff --git a/src/components/Controls/Select.vue b/src/components/Controls/Select.vue index e29bd942..4d8e29ae 100644 --- a/src/components/Controls/Select.vue +++ b/src/components/Controls/Select.vue @@ -31,6 +31,7 @@ v-for="option in options" :key="option.value" :value="option.value" + class="text-black" > {{ option.label }} diff --git a/src/components/Popover.vue b/src/components/Popover.vue index 0fee8124..9e7da5dc 100644 --- a/src/components/Popover.vue +++ b/src/components/Popover.vue @@ -3,17 +3,15 @@
- -
-
- -
-
+
+
+ +
@@ -25,17 +23,17 @@ export default { props: { hideArrow: { type: Boolean, - default: false + default: false, }, showPopup: { - default: null + default: null, }, right: Boolean, placement: { type: String, - default: 'bottom-start' + default: 'bottom-start', }, - popoverClass: [String, Object, Array] + popoverClass: [String, Object, Array], }, watch: { showPopup(value) { @@ -45,18 +43,18 @@ export default { if (value === false) { this.close(); } - } + }, }, data() { return { - isOpen: false + isOpen: false, }; }, mounted() { - let listener = e => { + let listener = (e) => { let $els = [this.$refs.reference, this.$refs.popover]; let insideClick = $els.some( - $el => $el && (e.target === $el || $el.contains(e.target)) + ($el) => $el && (e.target === $el || $el.contains(e.target)) ); if (insideClick) { return; @@ -83,17 +81,17 @@ export default { { name: 'arrow', options: { - element: this.$refs['popover-arrow'] - } + element: this.$refs['popover-arrow'], + }, }, { name: 'offset', options: { - offset: [0, 10] - } - } + offset: [0, 10], + }, + }, ] - : [] + : [], }); } else { this.popper.update(); @@ -126,8 +124,8 @@ export default { } this.isOpen = false; this.$emit('close'); - } - } + }, + }, };