Added primitive support to send bills to LHDN.

This commit is contained in:
Ali Arya 2025-07-22 00:26:21 +03:30
parent 38d67f5f63
commit 822b7e908c
8 changed files with 741 additions and 592 deletions

View File

@ -12,5 +12,12 @@
"COBOL",
"ACUCOBOL"
],
"workbench.colorCustomizations": {
"editorCursor.foreground": "#ef857d",
"activityBarBadge.background": "#e9dbb7",
"activityBarBadge.foreground": "#262626",
"statusBar.background": "#9f5a51",
"statusBar.foreground": "#e9dbb7"
},
}
}

View File

@ -2,7 +2,8 @@ import axios from 'axios'
import router from '@/router'
import store from './store/store';
const baseURL = 'https://api.ira-lex.com/'
// const baseURL = 'https://api.ira-lex.com/'
const baseURL = 'https://newback.ira-lex.com/'
// const baseURL = 'http://192.168.2.125:8000/'
// const baseURL = 'https://test.ira-lex.com/'
// 'Cache-Control': 'no-cache',

File diff suppressed because it is too large Load Diff

View File

@ -148,6 +148,17 @@
<a-menu-item key="6" @click="InvoiceDisplay(text.record,true)" v-if="hasPermission('billing.fund.html')"><feather-icon icon="MonitorIcon" svg-classes="text-warning w-4 h-4" class="pr-2"/>View</a-menu-item>
<a-menu-item key="7" @click="goToRecordPaymentOrSendFund(text.record)" v-if="hasPermission('billing.payment') && approveNotDueDate(text.record)"><feather-icon icon="DollarSignIcon" svg-classes="text-primary w-4 h-4" class="pr-2"/>Record Payment</a-menu-item>
<a-menu-item key="8" @click="goToRecordPaymentOrSendFund(text.record,'sendFund')" v-if="hasPermission('billing.fund-request.send') && text.record.status === 2"><feather-icon icon="MessageCircleIcon" svg-classes="text-warning w-4 h-4" class="pr-2"/>Send Payment Reminder</a-menu-item>
<!-- Only visible for bill/invoice (i.e. not fund request
or official receipt), and when the bill is either unpaid or
paid. -->
<a-menu-item
key="9"
@click="sendToLhdn(text.record)"
v-if="(hasPermission('billing.fund-request.send') || hasPermission('billing.fund-request.pdf')) && (text.record.status === 3 || text.record.status === 4) && (text.record.type === BILLING_TYPE_BILL)"
>
<feather-icon icon="MailIcon" svg-classes="text-secondary w-4 h-4" class="pr-2"/>Send to LHDN
</a-menu-item>
</a-menu>
<a-button style="margin-left: 8px" @click="actionStatusBill(text.record)"> {{ statusBill(text.record) }} <a-icon type="down" /> </a-button>
</a-dropdown>
@ -587,6 +598,19 @@
</XTable>
</a-tab-pane>
</a-tabs>
<div id="billing-error-messages">
<div v-if="lhdnErrors.length" class="mt-8">
<a-alert
v-for="(error, index) in lhdnErrors"
:message="`${error.name}: ${error.msg}`"
type="error"
show-icon
closable
class="mb-4"
/>
</div>
</div>
</vx-card>
<deleteItem
@ -1039,7 +1063,6 @@
</div>
</template>
<script>
import * as TableCol from "./billTbl";
import * as TableColReceipt from "./receiptTbl";
@ -1078,8 +1101,11 @@ export default {
BillingViewModal,
},
data(){
return{
data() {
return {
BILLING_TYPE_BILL : 1,
BILLING_TYPE_FUND_REQUEST : 2,
model : null,
modelReceipt : null,
openQuickBill: false,
@ -1106,12 +1132,12 @@ export default {
tab_bill: '1',
template_data: {},
loading:false,
loadingReceipt: false
loadingReceipt: false,
lhdnErrors: [],
}
},
methods:{
async handlerChangeReceipt(value) {
methods: {
async handlerChangeReceipt(value) {
if (value) {
try {
this.loadingReceipt = true
@ -1648,9 +1674,38 @@ export default {
this.edit_mode = false;
this.hasReceiptClient = false
this.formReceipt.resetFields()
}
},
async sendToLhdn(record) {
try {
this.$vs.loading()
const { data } = await axios.get(`billing/lhdn/${record.id}`)
this.$notification['success']({
message: 'Success',
description: 'Your bill was submitted to LHDN. Check your LHDN panel to make sure there aren\'t any issues with the submitted invoice.',
})
} catch (e) {
if (e.status === 422) {
this.lhdnErrors = e.data.details
this.$nextTick(() => {
const errorsEl = document.getElementById('billing-error-messages')
if (errorsEl) {
errorsEl.scrollIntoView({ behavior: 'smooth', block: "start" })
}
})
} else {
this.$notification['error']({
message: "Error",
description: "Failed to submit the document to LHDN. This feature is fairly new and might not work correctly. Please contact support to resolve your problem.",
})
// this.$message.error(`${err.name}: ${err.msg}`)
}
} finally {
this.$vs.loading.close()
}
}
},
computed: {
currentDateFormat() {
return moment().format('YYYY-MM-DD')
@ -1659,10 +1714,12 @@ export default {
return localStorage.getItem('currency_symbol')
},
},
created() {
this.model = TableCol.default
this.modelReceipt = TableColReceipt.default
},
async mounted() {
this.is_click_add_time = true
if (this.$route.params.query) {

View File

@ -1,6 +1,8 @@
<template>
<div class="container">
<a-form :form="form" @submit.prevent="handleSubmit" autocomplete="off">
<!-- Contacts Card -->
<div class="flex flex-col md:flex-row">
<div class="w-full md:w-1/6">
<h4 class="text-gray-900 text-lg">Contacts</h4>
@ -28,7 +30,9 @@
</vs-card>
</div>
</div>
<a-divider/>
<!-- Contact Information -->
<a-divider />
<div class="flex flex-col md:flex-row">
<div class="w-full md:w-1/6">
<h4 class="text-gray-900 text-lg">Contact Information</h4>
@ -280,9 +284,8 @@
</div>
</div>
<a-divider/>
<!-- ADDRESS SECTION -->
<a-divider />
<section class="flex flex-col md:flex-row scoll_bottom">
<!-- TITLE -->
<div class="w-full md:w-1/6">
@ -379,6 +382,62 @@
</div>
</section>
<!-- E-Invoicing Information -->
<a-divider/>
<div class="flex flex-col md:flex-row">
<div class="w-full md:w-1/6">
<h4 class="text-gray-900 text-lg">E-Invoicing Information</h4>
</div>
<div class="w-full md:w-5/6">
<vs-card class="p-5">
<div class="grid sm:grid-cols-1 md:grid-cols-2 gap-x-4">
<template v-if="radioType === CONTACT_TYPE_PERSON">
<a-form-item label="BRN" has-feedback>
<a-input
v-decorator="['person.brn']"
placeholder="Business Registration Number"
/>
</a-form-item>
<a-form-item label="SST Registration Number" has-feedback>
<a-input
v-decorator="['person.sst_registeration_number']"
placeholder="SST Registration Number"
/>
</a-form-item>
<a-form-item label="TIN" has-feedback>
<a-input
v-decorator="['person.tin']"
placeholder="Tax Identification Number"
/>
</a-form-item>
</template>
<template v-else-if="radioType === CONTACT_TYPE_COMPANY">
<a-form-item label="BRN" has-feedback>
<a-input
v-decorator="['company.brn']"
placeholder="Business Registration Number"
/>
</a-form-item>
<a-form-item label="SST Registration Number" has-feedback>
<a-input
v-decorator="['company.sst_registeration_number']"
placeholder="SST Registration Number"
/>
</a-form-item>
<a-form-item label="TIN" has-feedback>
<a-input
v-decorator="['company.tin']"
placeholder="Tax Identification Number"
/>
</a-form-item>
</template>
</div>
</vs-card>
</div>
</div>
<br>
<br>
<br>
@ -399,13 +458,20 @@ import moment from 'moment'
export default {
name: "ContactAdd",
mixins: [Mixin],
components:{
components: {
Xupload,
Footer
},
data(){
return{
data() {
return {
// CONSTANTS
CONTACT_TYPE_PERSON: 1,
CONTACT_TYPE_COMPANY: 2,
imageUrl: '',
loading: false,
cities:[],
@ -425,8 +491,8 @@ export default {
radioType: 1
}
},
methods:{
methods:{
handleRadio(e){
this.radioType = e.target.value
},

View File

@ -45,71 +45,74 @@
</template>
<script>
import axios from "@/axios"
import Mixin from '@/views/mixin/mixin'
export default {
name: "SelectOption",
mixins: [Mixin],
components:{
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
},
},
props:{
url : { type : String,default:'/account/list/' },
label : { type : String },
field : { type : String },
method : { type: String , default: 'post' },
required : {type: Boolean , default: false},
is_initial_value : {type: Boolean , default: false},
mode : { type: String, default: 'default' },
placeholder : { type: String , default:'notification' },
is_add_extra_footer: { type: Boolean , default:false }
},
data(){
return{
data:[],
loading: false,
}
},
computed:{
getUser(){
return JSON.parse(localStorage.getItem('user'))
}
},
methods:{
async getMatter(){
try{
this.loading = true
if(this.method == 'post'){
const { data } = await axios.post(this.url,{limit:100})
this.data = data.rows
}else{
const { data } = await axios.get(this.url)
this.data = data
}
}catch (e) {
throw e
}finally {
this.loading = false
}
},
handleSelect(value){
this.$emit('SelectUser',value)
},
emitClicker(){
this.$emit('emitClicker')
},
handlerChange(value) {
this.$emit('change', value)
}
import axios from "@/axios"
import Mixin from '@/views/mixin/mixin'
},
mounted() {
this.getMatter()
},
export default {
name: "SelectOption",
mixins: [Mixin],
components:{
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
},
},
props:{
url : { type : String,default:'/account/list/' },
label : { type : String },
field : { type : String },
method : { type: String , default: 'post' },
required : {type: Boolean , default: false},
is_initial_value : {type: Boolean , default: false},
mode : { type: String, default: 'default' },
placeholder : { type: String , default:'notification' },
is_add_extra_footer: { type: Boolean , default:false }
},
data(){
return{
data:[],
loading: false,
}
},
computed:{
getUser(){
return JSON.parse(localStorage.getItem('user'))
}
},
methods:{
async getMatter() {
try {
this.loading = true
if (this.method == 'post') {
const { data } = await axios.post(this.url, { limit: 100 })
this.data = data.rows
this.$emit('loaded', data.rows)
} else {
const { data } = await axios.get(this.url)
this.data = data
this.$emit('loaded', data)
}
} catch (e) {
throw e
} finally {
this.loading = false
}
},
handleSelect(value){
this.$emit('SelectUser',value)
},
emitClicker(){
this.$emit('emitClicker')
},
handlerChange(value) {
this.$emit('change', value)
}
},
mounted() {
this.getMatter()
},
}
</script>
<style scoped>

View File

@ -37,18 +37,23 @@
autocomplete="none"
/>
</a-form-item>
<a-form-item label="Registration Number">
<a-form-item label="BRN">
<a-input
v-decorator="['brn']"
autocomplete="none"
placeholder="Business Registration Number"
/>
</a-form-item>
<a-form-item label="SST Registration Number">
<a-input
v-decorator="['sst_registeration_number']"
autocomplete="none"
placeholder="SST Registration Number"
/>
</a-form-item>
<a-form-item label="MSIC Code" help="Malaysia Standard Industrial Classification">
<a-input
v-decorator="['msic_code']"
autocomplete="none"
/>
</a-form-item>

View File

@ -12,10 +12,10 @@
<template slot="footer">
<div v-if="result.length" class="grid grid-cols-5">
<div class="font-bold">Total</div>
<div class="place-self-center mr-18">{{ sumCol(result,'complete') }}</div>
<div class="place-self-center mr-20">{{ sumCol(result,'in_progress') }}</div>
<div class="place-self-start ml-20">{{ sumCol(result,'overdue') }}</div>
<div class="place-self-start ml-10">{{ sumCol(result,'upcoming') }}</div>
<div class="place-self-center mr-18">{{ sumCol(result,'complete').slice(0, -3) }}</div>
<div class="place-self-center mr-20">{{ sumCol(result,'in_progress').slice(0, -3) }}</div>
<div class="place-self-start ml-20">{{ sumCol(result,'overdue').slice(0, -3) }}</div>
<div class="place-self-start ml-10">{{ sumCol(result,'upcoming').slice(0, -3) }}</div>
</div>
</template>
</a-table>