import { Component, OnInit, ViewEncapsulation } from '@angular/core'; import { FormGroup, FormArray, FormBuilder, Validators, AbstractControl } from '@angular/forms'; import { newBillDefaults } from '../../../schemas/Bills/new-bill-defaults'; import { savedBillPayload } from '../../../schemas/Bills/savedBill'; import { BillsService } from '../../../services/bills.service'; import { ActivatedRoute, Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; import { BsModalService, BsModalRef } from 'ngx-bootstrap'; import { AlertModalComponent } from '../../common/alert-modal/alert-modal.component'; type vendorsData = { display_name: string, gst_reg_type: number, reg_type: string, gstin_uin: string, location_code_id: number } const INTRA_TAX_TYPE = 'GST'; const INTER_TAX_TYPE = 'IGST'; const TAX_INCLUSIVE = 'Tax Inclusive'; const TAX_EXCLUSIVE = 'Tax Exclusive'; const DBT = 'before_tax'; const DAT = 'after_tax'; const NON_TAXABLE_TYPE = 'Non Taxable' @Component({ selector: 'app-edit-bill', templateUrl: './edit-bill.component.html', styleUrls: ['./edit-bill.component.css'], encapsulation: ViewEncapsulation.None }) export class EditBillComponent implements OnInit { editBillForm: FormGroup; addedItems: FormArray; taxSlabs: FormArray; defDataForNewBill: newBillDefaults initalFormValues: JSON; billId: number; inventoryAsset: { account_name_id: number, account_name: string }[] = []; /*Below details has to be gathered from services, usage of the below variable may need to be changed with json reference */ itemPriceType: string[]; itemAccountOptions: any[]; /* all the predefined-tax options available in db */ // DBT, DAT itcOptions: any[] locationOptions: any[]; /*Above details has to be gathered from services, usage of the above variable may need to be changed with json reference */ // made member as it is needed to update totalAmount when adjustments change totalTaxAmountOnAddedItems = 0.00; calcDiscAmount = 0.00; sumOfAllItemsOrginalAmt = 0.00; /*It is used to store the prepending string('GST' or 'IGST') for tax options */ taxType: string; /*Displayed discountAmount on html*/ discountAmount = 0.00; /*Displayed totalAmount on html*/ totalAmount = 0.00; /*Displayed subTotal on html*/ subTotal: number; /*Displayed taxamount on html*/ tdsAmount = 0.00; /*Displayed itemTaxSlabOptions on html*/ itemTaxSlabOptions: string[]; gst_reg_type = ''; gstin_uin = ''; vendorName = ''; errorOnVendor: boolean = false; isTaxOnItemDisabled: boolean; ErrorAdjustTitle = false; errorOnTotalAmt = false; tdsOptions: { tds_id: number; description: string; percentage: number; }[]; paymentTerms: { payment_terms_id: number; terms_code: string; number_of_days?: number; }[]; vendorNames: { vendor_id: number, display_name: string, gst_reg_type: number, reg_type: string, gstin_uin: string, location_code_id: number }[]; interTaxOptions: { tax_slab_id: number; tax_slab_name: string; percentage: number; tax_slab_type: string }[]; nonTaxableOptions: { /* all the predefined-tax options available in db */ tax_slab_id: number; tax_slab_name: string; percentage: number; tax_slab_type: string; }[]; itemTaxOptions: { tax_slab_id: number; tax_slab_name: string; percentage: number; tax_slab_type: string; }[]; intraTaxOptions: { tax_slab_id: number; tax_slab_name: string; percentage: number; tax_slab_type: string; }[]; itemDetailsOptions: { variantId: number; itemName: string; taxable: boolean; costPrice: number; taxSlabId: number; taxGroupId: number; hsnCode: number; sacCode: number; accountNameId: number; isInventory: boolean; }[]; discountTaxOptions: { value: string; text: string; }[]; dueDateError = false; billDateError = false; isSEZVendor: any; savedBillValues: savedBillPayload; bill_state: string; errMsgonTotalAmt: string; bsModalRef: BsModalRef; billDateValue: any; dueDateValue: any; nonTaxableGroups: { tax_group_id: number; tax_group_name: string; tax_group_type: string; tax_slabs: { tax_slab_id: number; tax_slab_name: string; percentage: number; }[]; }[]; enableAccountForDiscount: boolean; constructor(private fb: FormBuilder, private billService: BillsService, private _route: ActivatedRoute, private _router: Router, private toastr: ToastrService, private modalService: BsModalService) { this.itemPriceType = [TAX_INCLUSIVE, TAX_EXCLUSIVE]; this.billService.sendNewBillDefaultsGetRequest().subscribe((defualtsFromDB: newBillDefaults) => { console.warn(defualtsFromDB); this.defDataForNewBill = defualtsFromDB; // Initializing different search dropdown options this.vendorNames = defualtsFromDB.data.vendorsData; this.locationOptions = defualtsFromDB.data.locationCodesData.map((location) => { return { location_code: location.location_id, location_state: location.location_state_code + ' - ' + location.location_state } }); this.paymentTerms = defualtsFromDB.data.paymentTerms; this.interTaxOptions = [...defualtsFromDB.data.interTaxSlabDetails.filter((taxSlab) => { return taxSlab.tax_slab_type === NON_TAXABLE_TYPE }), ...defualtsFromDB.data.interTaxSlabDetails.filter((taxSlab) => { return taxSlab.tax_slab_type !== NON_TAXABLE_TYPE })]; let taxGroupOptions = defualtsFromDB.data.intraTaxGrpDetails.map((taxGroup) => { return { tax_slab_id: taxGroup.tax_group_id, tax_slab_name: taxGroup.tax_group_name, tax_slab_type: taxGroup.tax_group_type, percentage: 0 } }); this.intraTaxOptions = [...taxGroupOptions.filter((taxGroup) => { return taxGroup.tax_slab_type === NON_TAXABLE_TYPE }), ...taxGroupOptions.filter((taxGroup) => { return taxGroup.tax_slab_type !== NON_TAXABLE_TYPE })]; this.nonTaxableGroups = defualtsFromDB.data.intraTaxGrpDetails.filter((taxgroup) => { return taxgroup.tax_group_type === NON_TAXABLE_TYPE }); // At the intial state, definetly there won't be any vendor assigned by default. So there should be any tax options available. this.nonTaxableOptions = defualtsFromDB.data.interTaxSlabDetails.filter((taxOption) => { return taxOption.tax_slab_type === NON_TAXABLE_TYPE }); this.itemTaxOptions = this.nonTaxableOptions; this.discountTaxOptions = [{ value: DBT, text: 'Discount Before Tax' }, { value: DAT, text: 'Discount After Tax' }]; this.itcOptions = defualtsFromDB.data.itcDetails; this.itemAccountOptions = defualtsFromDB.data.accountNamesData; this.inventoryAsset[0] = defualtsFromDB.data.inventoryAccountData; // get the names of all the items provided in SERVICE. this.itemDetailsOptions = defualtsFromDB.data.itemsDetails.map((dbItem) => { return { variantId: dbItem.variant_id, itemName: dbItem.item_name + "-" + dbItem.variant + " " + dbItem.uom_code + " " + dbItem.cost_price, taxable: dbItem.taxable, costPrice: dbItem.cost_price, taxSlabId: dbItem.inter_state_tax, taxGroupId: dbItem.intra_state_tax, hsnCode: dbItem.hsn_code, sacCode: dbItem.sac_code, accountNameId: dbItem.cost_account, isInventory: dbItem.is_inventory } }); //this.itemDetailsOptions = defualtsFromDB.data.itemsDetails; this.tdsOptions = defualtsFromDB.data.tdsDetails; if (this.editBillForm) { // this.editBillForm.get('destOfSupply').patchValue(defualtsFromDB.data.orgData.location_id ); } this._route.paramMap.subscribe(parametereMap => { const id = +parametereMap.get('id'); this.billService.getBill(id).subscribe((savedBillValuesFromDB: savedBillPayload) => { this.savedBillValues = savedBillValuesFromDB; this.setValuesToAllControls(); this.billId = savedBillValuesFromDB.data.bill.bill_id; this.applySubscribptionsonFormData(); }); }, (error) => { this.toastr.error("Something Went Wrong! Please try to refresh"); console.log("Error: " + error); }); }, (error) => { this.toastr.error("Something Went Wrong! Please try to refresh"); console.log("Error: " + error); }); } ngOnInit(): void { this.enableAccountForDiscount = false; // initialize subtotal to 0 as no item is default item. this.subTotal = 0.00; this.editBillForm = this.fb.group({ // validates vendorId only on focusout - validates for not empty and minlength of 9 Eg: 212634745 vendorId: ['', { validators: [Validators.required], updateOn: 'blur' }], sourceOfSupply: ['', { validators: [Validators.required], updateOn: 'blur' }], destOfSupply: ['', { validators: [Validators.required], updateOn: 'blur' }], referenceNumber: [''], // default value for terms is Due on Receipt. paymentTerm: [1], transRevCharge: [{ value: false, disabled: true }], itemPriceType: [TAX_EXCLUSIVE], isTaxGroupSelectedOnItems: Boolean(), totalTaxAmount: Number(0), addedItems: this.fb.array([]), /*Pricing section*/ // dynamically add taxslabs(tax or taxgroup) controls to UI with there cummulatve amounts calculated taxSlabs: this.fb.array([]), // Default value shoudl be discount After tax discountTaxType: [DAT], // Non mandatory field discountValue: [, { validators: [this.validateNonNegative], updateOn: 'blur' }], // by default value is % discountTypeisPercent: [true], accountForDiscount: [{ value: '', disabled: true }, [Validators.required]], tdsValue: [], // Non mandatory fields used for any adjustments in the bills. adjustmentTitle: [], adjustments: [, { validators: [this.validateNumeric], updateOn: 'blur' }], // trackingInfo // Default state is draft // billState: [1], }); // Add one empty row to item table at the intial stage. this.addItemToBill(); this.initalFormValues = this.editBillForm.value; } // Need a getter for Validations. get discountValue() { return this.editBillForm.get('discountValue'); } get adjustments() { return this.editBillForm.get('adjustments'); } get vendorId() { return this.editBillForm.get('vendorId'); } get sourceOfSupply() { return this.editBillForm.get('sourceOfSupply'); } get destOfSupply() { return this.editBillForm.get('destOfSupply'); } get accountForDiscount() { return this.editBillForm.get('accountForDiscount'); } get referenceNumber() { return this.editBillForm.get('referenceNumber'); } applySubscribptionsonFormData() { // calculate and update DueDate when payment Terms changes baseed on the billDate. this.editBillForm.get('paymentTerm').valueChanges.subscribe(value => this.calculateDueDate()); // re-calculated pricing details like, discount, taxslabs, totalamount when there is a change in itemPriceType this.editBillForm.get('itemPriceType').valueChanges.subscribe(value => { this.enableOrDisableTransRevCharge(); this.calculatePricing() }); this.editBillForm.get('discountTaxType').valueChanges.subscribe(value => this.calculatePricing()); this.editBillForm.get('discountTypeisPercent').valueChanges.subscribe(value => this.calculatePricing()); } // whenever billDate changes, calculate and update dueDate Based n Payment Terms billDateChanged(billDateStr) { this.billDateError = billDateStr && billDateStr != 'Invalid Date' ? false : true; this.calculateDueDate(billDateStr); } // validate dueDateValue dueDateChanged(dueDatestr) { const billDate = this.billDateValue; //new Date(this.newBillForm.get('billDate').value); if (!dueDatestr || dueDatestr == 'Invalid Date' || dueDatestr.getTime() < billDate.getTime() ) this.dueDateError = true; else this.dueDateError = false; } setValuesToAllControls() { console.warn("Setting values of bill"); this.bill_state = this.savedBillValues.data.bill.workflow_state.flow_state this.editBillForm.get('vendorId').setValue(Number(this.savedBillValues.data.bill.vendor_id)); const vendorOfBill = this.defDataForNewBill.data.vendorsData.find((vendor) => { return vendor.vendor_id === Number(this.savedBillValues.data.bill.vendor_id) }) this.gst_reg_type = vendorOfBill.reg_type; this.gstin_uin = vendorOfBill.gstin_uin; this.editBillForm.get('sourceOfSupply').patchValue(this.savedBillValues.data.bill.source_location); this.editBillForm.get('destOfSupply').patchValue(this.savedBillValues.data.bill.delivery_location); this.editBillForm.get('referenceNumber').patchValue(this.savedBillValues.data.bill.reference_number); this.editBillForm.get('paymentTerm').patchValue(this.savedBillValues.data.bill.payment_terms); this.billDateValue = new Date(this.savedBillValues.data.bill.bill_date); this.dueDateValue = new Date(this.savedBillValues.data.bill.due_date); if (vendorOfBill.reg_type.includes("Unregistered") || vendorOfBill.reg_type.includes("Regular") || vendorOfBill.reg_type.includes("SEZ") && (this.savedBillValues.data.bill.item_price_are !== TAX_INCLUSIVE)) { this.editBillForm.get('transRevCharge').enable(); this.editBillForm.get('transRevCharge').patchValue(this.savedBillValues.data.bill.reverse_charge); } this.editBillForm.get('itemPriceType').patchValue(this.savedBillValues.data.bill.item_price_are); this.editBillForm.get('totalTaxAmount').patchValue(this.savedBillValues.data.bill.tax_value); this.addedItems = this.editBillForm.get('addedItems') as FormArray; if (this.savedBillValues.data.addedItems.length > 0) { this.addedItems.clear(); } if (this.savedBillValues.data.bill.source_location === this.savedBillValues.data.bill.delivery_location && !vendorOfBill.reg_type.includes('SEZ')) { this.editBillForm.get('isTaxGroupSelectedOnItems').patchValue(true); this.itemTaxOptions = this.intraTaxOptions; this.taxType = INTRA_TAX_TYPE } else { this.editBillForm.get('isTaxGroupSelectedOnItems').patchValue(false); this.itemTaxOptions = this.interTaxOptions; this.taxType = INTER_TAX_TYPE } for (const addedItem of this.savedBillValues.data.addedItems) { this.addItemToBill(addedItem.item_code, addedItem.item_variant.item.hsn_code, addedItem.item_variant.item.sac_code, addedItem.account, addedItem.item_price, addedItem.quantity, this.editBillForm.value.isTaxGroupSelectedOnItems ? addedItem.tax_group : addedItem.tax_slab, addedItem.bill_itc, addedItem.item_variant.inter_state_tax, addedItem.item_variant.intra_state_tax, addedItem.item_variant.item.is_inventory, addedItem.item_variant.taxable, addedItem.item_amount) } this.taxSlabs = this.editBillForm.get('taxSlabs') as FormArray; this.taxSlabs.clear(); for (const taxSlab of this.savedBillValues.data.taxSlabs) { //let createdTaxSlab = this.createTaxControl(taxSlab.tax_slab_id, taxSlab.tax_slab.tax_slab_name, Number(taxSlab.amount)); this.taxSlabs.push(this.createTaxControl(taxSlab.tax_slab_id, taxSlab.tax_slab.tax_slab_name, Number(taxSlab.amount))) } this.editBillForm.get('discountTaxType').patchValue(this.savedBillValues.data.bill.discount_type); this.editBillForm.get('discountValue').patchValue(this.savedBillValues.data.bill.discount_value); if (this.savedBillValues.data.bill.discount_value) { this.enableAccountForDiscount = true; this.editBillForm.get('accountForDiscount').enable(); this.editBillForm.get('accountForDiscount').patchValue(this.savedBillValues.data.bill.discount_account); } this.editBillForm.get('discountTypeisPercent').patchValue(this.savedBillValues.data.bill.ispercentage); this.editBillForm.get('tdsValue').patchValue(this.savedBillValues.data.bill.tds); this.editBillForm.get('adjustmentTitle').patchValue(this.savedBillValues.data.bill.adjustment_reason); this.editBillForm.get('adjustments').patchValue(+this.savedBillValues.data.bill.adjustment); this.tdsAmount = -1 * (+this.savedBillValues.data.bill.tds_value); this.totalAmount = +this.savedBillValues.data.bill.total_amount; this.discountAmount = -1 * (+this.savedBillValues.data.bill.discount_amount); this.subTotal = +this.savedBillValues.data.bill.sub_total; } validateNeeded() { if (this.editBillForm.get('adjustments').value) { if (this.editBillForm.get('adjustmentTitle').value) { this.ErrorAdjustTitle = false; } else if (this.editBillForm.get("adjustmentTitle").touched) { this.ErrorAdjustTitle = true; } } else { this.ErrorAdjustTitle = false; } } validateNumeric(control: AbstractControl) { if (control.value !== undefined && isNaN(control.value)) { control.setValue(0); } } validateNonNegative(control: AbstractControl) { if (control.value !== undefined && control.value !== null && (control.value < 0)) { control.setValue(null); } } adjustmentsOnBlur() { this.validateNeeded(); this.calculateTotalAmount(); } /* used to dynamically create a item formGroup in a formArray, This adds an row to items table in UI. */ private _createItem(itemDetail?: number, hsncode?: string, saccode?: string, accountId?: number, price?: number, quantity?: number, tax?: number, itc?: number, interTaxSlab?: number, intraTaxGroup?: number, isInventory?: boolean, taxable?: boolean, amount?: number): FormGroup { return this.fb.group({ itemId: [itemDetail], hsnCode: [hsncode], sacCode: [saccode], accountNameId: [accountId], quantity: [quantity || 1], price: [price], taxOnItem: [tax], itc: [itc], // initalize the amount to zero when a new item record is created, this is needed when calculating the amount changed on item. amount: [amount || 0], interTaxSlab: Number(interTaxSlab), intraTaxGroup: Number(intraTaxGroup), isInventory: Boolean(isInventory), taxable: Boolean(taxable), originalAmt: Number(0) }); } /* @usage: called when a new item row needs to be added dynamically @purpose: adds required item fromgroup to formArray. */ addItemToBill(itemDetail?: number, hsncode?: string, saccode?: string, accountId?: number, price?: number, quantity?: number, tax?: number, itc?: number, interTaxSlab?: number, intraTaxGroup?: number, isInventory?: boolean, taxable?: boolean, amount?: number) { this.addedItems = this.editBillForm.get('addedItems') as FormArray; this.addedItems.push(this._createItem(itemDetail, hsncode, saccode, accountId, price, quantity, tax, itc, interTaxSlab, intraTaxGroup, isInventory, taxable, amount)); } /* @usage: called when a item row needs to be removed dynamically @param removeItemIndex : index of the item to be removed. @purpose: removes item fromgroup to formArray at provided index. */ removeItemFromBill(removeItemIndex: number) { this.subTotal -= this.editBillForm.value.addedItems[removeItemIndex].amount; this.addedItems.removeAt(removeItemIndex); this.calculatePricing(); /* when the only row in the item table is removed add another empty row */ if (this.editBillForm.value.addedItems.length === 0) { this.addItemToBill(); } } removeAllItemFromBill() { this.addedItems = this.editBillForm.get('addedItems') as FormArray; this.addedItems.clear(); this.addItemToBill(); // bring all pricings to inital state. this.subTotal = 0.00; this.calcDiscAmount = 0.00; this.discountAmount = 0.00; this.tdsAmount = 0.00; this.totalAmount = 0.00; this.totalTaxAmountOnAddedItems = 0.00; this.taxSlabs = this.editBillForm.get('taxSlabs') as FormArray; // clear all taxes if (this.taxSlabs.length > 0) { this.taxSlabs.clear(); this.editBillForm.get('totalTaxAmount').setValue(0); } } /* used to dynamically create a Tax Slab controls formGroup in a formArray, creates an new TaxSlab dynamically when tax on item changes or vendor location changes or on item CRUD ops. */ createTaxControl(tax_slab_id?: number, slabName?: string, amt?: number) { return this.fb.group({ taxSlabId: Number(tax_slab_id), slabName: [slabName], cumAmount: [amt] }); } removeAllTaxesOnItems() { for (const addedItem of this.editBillForm.value.addedItems) { addedItem.taxOnItem = null; // addedItem.itc = null; } this.editBillForm.get('addedItems').setValue(this.editBillForm.value.addedItems); this.calculatePricing(); } /* DueDate needs to be calculated whenever there is a chnage in bill Date or Payments Terms. Based on the payments Terms values received from service, due date is calculated using bill date as ref. */ calculateDueDate(billDateValue?) { let billDate; if(!billDateValue) billDate = this.billDateValue; else billDate = billDateValue if (!billDate) return; const paytermId = this.editBillForm.get('paymentTerm').value; const payterm = this.defDataForNewBill.data.paymentTerms.find((payterm) => { return payterm.payment_terms_id === paytermId }).terms_code; let duedate = new Date(billDate); switch (payterm.toLowerCase()) { case 'due on receipt': duedate = new Date(billDate); break; case 'end of month': duedate = new Date(billDate.getFullYear(), billDate.getMonth() + 1, 1); break; case 'end of next month': duedate = new Date(billDate.getFullYear(), billDate.getMonth() + 2, 1); break; case 'quaterly': duedate = new Date((new Date(billDate)).setMonth(billDate.getMonth() + 3)); break; default: for (const term of this.defDataForNewBill.data.paymentTerms) { //atleast one terms_code matches with payTerm selected, as the payterm options are mapped from term_code. if (term.terms_code === payterm) { duedate = new Date((new Date(billDate)).setDate(billDate.getDate() + term.number_of_days)); break; } } break; } // provides date string in the format DD/MM/YYYY this.dueDateValue = duedate; } /* validate the vendor */ validateVendor(vendorId): vendorsData { // applying linear search, may need to apply proper search according to requirement inputs. return this.defDataForNewBill.data.vendorsData.find((vendor) => { return vendor.vendor_id === vendorId; }); } transRevValChange(event) { if (event.currentTarget.checked) { this.removeAllTaxesOnItems(); this.isTaxOnItemDisabled = false; this.discountTaxOptions = [{ text: 'Discount Before Tax', value: DBT }]; this.editBillForm.get('discountTaxType').setValue(DBT); } else { this.removeAllItemFromBill(); let vendor = this.validateVendor(this.editBillForm.value.vendorId); this.discountTaxOptions = [{ text: 'Discount Before Tax', value: DBT }, { text: 'Discount After Tax', value: DAT }] if (vendor.reg_type.includes("Composition") || vendor.reg_type.includes("Overseas") || vendor.reg_type.includes("Unregistered")) { this.isTaxOnItemDisabled = true; } else { this.isTaxOnItemDisabled = false; } } } /* @usage: called whenever there is any change in the vendor. @purpose: Based on the updated vendor location, the taxations may need to be re-calculated. */ vendorChanged(vendorObj) { const vendor: vendorsData = this.validateVendor(vendorObj.vendor_id); if (vendor) { this.vendorName = vendor.display_name; let vendorlocationCode = this.defDataForNewBill.data.locationCodesData.find((location) => { return location.location_id === vendor.location_code_id }).location_state_code; if (vendor.reg_type.includes('Composition') && vendorlocationCode !== this.defDataForNewBill.data.orgData.location_state_code) { this.toastr.error("Vendor Registered under Composite GST Scheme from different state cannot be selected."); this.editBillForm.get('vendorId').setValue(null); this.removeAllItemFromBill(); this.itemTaxOptions = [...this.nonTaxableOptions]; this.editBillForm.get('sourceOfSupply').setValue(null); return; } else { // the vendor value is just entered if (this.taxType) { // vendor Changed this.removeAllItemFromBill(); } if (vendor.reg_type.includes('Composition') || vendor.reg_type.includes("Overseas") || vendor.reg_type.includes("Unregistered Business")) { this.removeAllTaxesOnItems(); this.isTaxOnItemDisabled = true; } else { this.isTaxOnItemDisabled = false; } if (vendor.reg_type.includes('SEZ')) { this.isSEZVendor = true; } else this.isSEZVendor = false if (vendor.gst_reg_type) { this.gst_reg_type = vendor.reg_type; } else { this.gst_reg_type = ''; } if (vendor.gstin_uin) { this.gstin_uin = vendor.gstin_uin; } else { this.gstin_uin = ''; } this.enableOrDisableTransRevCharge(); if (vendor.location_code_id) { this.editBillForm.get('sourceOfSupply').setValue(vendor.location_code_id); } this.locationChanged(); } } else { // No such Scenario as vendor is a dropdown populated with options from service. // Invalid Vendor or no Vendor Info } } enableOrDisableTransRevCharge() { const vendor: vendorsData = this.validateVendor(this.editBillForm.get('vendorId').value); if (vendor) { this.editBillForm.get('transRevCharge').setValue(false); if ((vendor.reg_type.includes("Unregistered") || vendor.reg_type.includes("Regular") || vendor.reg_type.includes("SEZ")) && (this.editBillForm.get('itemPriceType').value !== TAX_INCLUSIVE)) { this.editBillForm.get('transRevCharge').enable(); } else { //if (vendor.reg_type.includes("Composition") || vendor.reg_type.includes("Overseas") || vendor.reg_type.includes("Deemed")) { this.editBillForm.get('transRevCharge').disable(); } } else { this.editBillForm.get('transRevCharge').disable(); } this.discountTaxOptions = [{ text: 'Discount Before Tax', value: DBT }, { text: 'Discount After Tax', value: DAT }] } locationChanged() { if (this.editBillForm.get('vendorId').value) { let sourceOfSupply: string; const sourceOfSupplyObj = this.defDataForNewBill.data.locationCodesData.find((location) => location.location_id === this.editBillForm.get('sourceOfSupply').value) if (sourceOfSupplyObj) sourceOfSupply = sourceOfSupplyObj.location_state_code; else return; const destOfSupply = this.defDataForNewBill.data.locationCodesData.find((location) => location.location_id === this.editBillForm.get('destOfSupply').value).location_state_code; if (sourceOfSupply !== destOfSupply || this.isSEZVendor) { this.taxType = INTER_TAX_TYPE; this.editBillForm.get('isTaxGroupSelectedOnItems').setValue(false); if (!this.isTaxOnItemDisabled) { for (const addeditem of this.editBillForm.value.addedItems) { /* Item table will have one item group at intial stage without any itemdetails. so, update item only when itemNam is not undefined or null */ if (addeditem.itemId && (addeditem.taxable || addeditem.taxOnItem)) { // updated taxonitem with default tax value. addeditem.taxOnItem = addeditem.interTaxSlab; } } } this.itemTaxOptions = [...this.interTaxOptions]; } else if (sourceOfSupply === destOfSupply && !this.isSEZVendor) { this.taxType = INTRA_TAX_TYPE; this.editBillForm.get('isTaxGroupSelectedOnItems').setValue(true); if (!this.isTaxOnItemDisabled) { for (const addeditem of this.editBillForm.value.addedItems) { /* Item table will have one item group at intial stage without any itemdetails. so, update item only when itemId is neither undefined nor null */ if (addeditem.itemId && (addeditem.taxable || addeditem.taxOnItem)) { addeditem.taxOnItem = addeditem.intraTaxGroup; } } } this.itemTaxOptions = [...this.intraTaxOptions]; } // if(this.editBillForm.value.addedItems.length > 0 && this.editBillForm.value.addedItems.find((addedItem) => !addedItem.itemId)){ this.editBillForm.get('addedItems').setValue(this.editBillForm.value.addedItems); this.calculatePricing(); // } } } /* @usage : called when a different item is selected from dropdown. @param itemindex: index of changed item in the array of items added to the bill. @param selectedItemIndex: selected item index from dropdown. @purpose: updtes the row contents based on the newly selected item index and re-calculates subTotal, discount(if applicable), taxing (if applicable) and total amount. */ itemChanged(itemIndex, selectedItem) { if (selectedItem) { // update form control's values with the item values from service. // update tax based on the inter-taxation or intra-taxation or select a tax when no vendor is assigned. if (!this.isTaxOnItemDisabled) { if (this.taxType && selectedItem.taxable) { if (this.taxType === INTRA_TAX_TYPE) { this.editBillForm.value.addedItems[itemIndex].taxOnItem = selectedItem.taxGroupId; } else if (this.taxType === INTER_TAX_TYPE) { this.editBillForm.value.addedItems[itemIndex].taxOnItem = selectedItem.taxSlabId; } } else { this.editBillForm.value.addedItems[itemIndex].taxOnItem = null; } this.editBillForm.value.addedItems[itemIndex].itc = 1; } else { this.editBillForm.value.addedItems[itemIndex].taxOnItem = null; this.editBillForm.value.addedItems[itemIndex].itc = null; } if (selectedItem.isInventory) { this.editBillForm.value.addedItems[itemIndex].accountNameId = this.inventoryAsset[0].account_name_id; // "Inventory Asset" ID } else { this.editBillForm.value.addedItems[itemIndex].accountNameId = selectedItem.accountNameId; } this.editBillForm.value.addedItems[itemIndex].price = Number(selectedItem.costPrice); // store/update intraTaxGroup and interTaxSlab, which reduces the iteration on items from service while calculating the taxes. this.editBillForm.value.addedItems[itemIndex].intraTaxGroup = selectedItem.taxGroupId; this.editBillForm.value.addedItems[itemIndex].interTaxSlab = selectedItem.taxSlabId; this.editBillForm.value.addedItems[itemIndex].hsnCode = selectedItem.hsnCode; this.editBillForm.value.addedItems[itemIndex].sacCode = selectedItem.sacCode; this.editBillForm.value.addedItems[itemIndex].isInventory = selectedItem.isInventory; this.editBillForm.value.addedItems[itemIndex].taxable = selectedItem.taxable; this.amountChanged(itemIndex); } else { // this is unusal , as the items list mentioned in the dropdown are provided in same order with items from service reponse. Sothey never miss match. this.toastr.error("Something went wrong"); } } /* @usage: called when there is any change in items table. i.e, change in item amount by selecting different item, adding or removing item or change in price or quantity. @param itemIndex: row index of the item which got changed in bill item table. @purpose: calculates the difference of the amount changed and adds it to subTotal, re-calculates discount(if applicable), taxing (if applicable) and total amount. */ amountChanged(itemIndex?: number) { let difference = 0; if (itemIndex) { // initialize difference with previous amount of the item at the provided index. If the item is added newly, the value will be zero. difference = this.editBillForm.value.addedItems[itemIndex].amount; // calculate the new amount and update amount. this.editBillForm.value.addedItems[itemIndex].amount = Number(this.editBillForm.value.addedItems[itemIndex].price) * Number(this.editBillForm.value.addedItems[itemIndex].quantity); // calculate the difference between previous and current amount and add it to subtotal. difference = this.editBillForm.value.addedItems[itemIndex].amount - difference; this.subTotal += difference; } else { this.editBillForm.value.addedItems.forEach(addedItem => { addedItem.amount = addedItem.price * addedItem.quantity; difference += addedItem.amount; }); this.subTotal = difference; } // update formControl values. this.editBillForm.controls['addedItems'].setValue(this.editBillForm.value.addedItems); this.calculatePricing(); } /* @usage: called when a item is added/deleted/updated or when vendor is changed. @purpose: calculates discount(if applicable), taxing (if applicable) and total amount */ calculatePricing() { this.taxSlabs = this.editBillForm.get('taxSlabs') as FormArray; // re-calcuate taxes , so clear all taxes if (this.taxSlabs.length > 0) { this.taxSlabs.clear(); this.editBillForm.get('totalTaxAmount').setValue(0); } // re-calculate discount and totalAmount. this.calcDiscAmount = 0; this.totalTaxAmountOnAddedItems = 0; // vendor is or not selected . this._calcSumOfAllItems(); if (!this.taxType) { this.editBillForm.get('discountValue').setValue(null); this.calculateTotalAmount(); return; } this._enableOrDisableAccforDiscount(); this.editBillForm.value.addedItems.forEach((item) => this._calcTaxandDiscountForItem(item)); this.editBillForm.get('totalTaxAmount').setValue(this.totalTaxAmountOnAddedItems); this.discountAmount = Math.round(this.calcDiscAmount * -100) / 100; this.calculateTotalAmount() } private _calcSumOfAllItems() { this.sumOfAllItemsOrginalAmt = 0; this.editBillForm.value.addedItems.forEach(item => { this._calcItemOiginalPrice(item); this.sumOfAllItemsOrginalAmt += item.originalAmt; }); } private _correctRoundOffItemPrice(item, totalTaxAmtOnItem) { this.sumOfAllItemsOrginalAmt -= item.originalAmt; item.originalAmt = item.amount - totalTaxAmtOnItem; this.sumOfAllItemsOrginalAmt += item.originalAmt; } private _calcTaxandDiscountForItem(item) { const discountValue: number = parseFloat(this.editBillForm.get('discountValue').value); let applydiscountBeforeTax = (this.editBillForm.get('discountTaxType').value === DBT) || (this.editBillForm.get('transRevCharge').value) let itemTaxSlabs: { tax_slab_id: number, tax_slab_name: string, percentage: number }[] = []; let totalTaxAmtOnItem = 0, itemCostPostDiscount = item.originalAmt; if (item.itemId && item.taxOnItem && ((this.taxType === INTER_TAX_TYPE && !this.nonTaxableOptions.find((nonTaxableSlab) => nonTaxableSlab.tax_slab_id === item.taxOnItem)) || (this.taxType === INTRA_TAX_TYPE) && !this.nonTaxableGroups.find((taxgroup) => { return taxgroup.tax_group_id === item.taxOnItem }))) { totalTaxAmtOnItem = 0; itemTaxSlabs = this._findItemTaxSlabs(item); if (discountValue && applydiscountBeforeTax) itemCostPostDiscount = this._calcDiscountBeforeTaxOnItem(item, discountValue); totalTaxAmtOnItem = this._generateTaxSlabs(itemTaxSlabs, itemCostPostDiscount); if (this.taxType === INTRA_TAX_TYPE && this.editBillForm.get('itemPriceType').value == TAX_INCLUSIVE && (!discountValue || (discountValue && !applydiscountBeforeTax))) { this._correctRoundOffItemPrice(item, totalTaxAmtOnItem); } if (discountValue && !applydiscountBeforeTax) this._calcDiscountAfterTaxOnItem(item, totalTaxAmtOnItem, discountValue); this.totalTaxAmountOnAddedItems = Number((this.totalTaxAmountOnAddedItems + totalTaxAmtOnItem).toFixed(2)); } else { this._calcDiscountWithoutTax(item, discountValue); } } private _calcDiscountWithoutTax(item, discountValue) { if (discountValue) { if (this.editBillForm.value.discountTypeisPercent) { this.calcDiscAmount += Math.round(((item.amount * discountValue) / 100) * 100) / 100; } else { this.calcDiscAmount = discountValue; } } } private _generateTaxSlabs(itemTaxSlabs, itemCostPostDiscount) { let taxOnItemTaxSlab = 0, totalTaxAmtOnItem = 0; itemTaxSlabs.forEach((taxSlab) => { taxOnItemTaxSlab = Math.round(((itemCostPostDiscount * Number(taxSlab.percentage)) / 100) * 100) / 100; let tempTaxControl = this.createTaxControl(taxSlab.tax_slab_id, taxSlab.tax_slab_name, taxOnItemTaxSlab); this._addOrUpdateTaxSlab(tempTaxControl); totalTaxAmtOnItem += taxOnItemTaxSlab; }); return totalTaxAmtOnItem; } private _calcDiscountBeforeTaxOnItem(item, discountValue): number { let itemCost = item.originalAmt, discountOnItem = 0; if (discountValue) { if (this.editBillForm.value.discountTypeisPercent) { discountOnItem = Math.round(((item.originalAmt * discountValue) / 100) * 100) / 100; this.calcDiscAmount += discountOnItem; itemCost -= discountOnItem; } else { discountOnItem = Math.round((itemCost / this.sumOfAllItemsOrginalAmt) * discountValue * 100) / 100; itemCost -= discountOnItem; this.calcDiscAmount = discountValue; } } return itemCost; } private _calcDiscountAfterTaxOnItem(item, totalTaxAmtOnItem, discountValue) { if (discountValue) { if (this.editBillForm.value.discountTypeisPercent) { if (this.editBillForm.get('itemPriceType').value === "Tax Exclusive") { this.calcDiscAmount += Math.round((((item.originalAmt + totalTaxAmtOnItem) * discountValue) / 100) * 100) / 100; } else { this.calcDiscAmount += Math.round(((item.amount * discountValue) / 100) * 100) / 100; } } else { this.calcDiscAmount = discountValue; } } } private _calcItemOiginalPrice(item) { let taxPercentOnItem = 0; let itemCost = 0; if (item.taxOnItem) { if (this.editBillForm.get('itemPriceType').value === TAX_INCLUSIVE) { if (this.taxType === INTRA_TAX_TYPE) { taxPercentOnItem = this.defDataForNewBill.data.intraTaxGrpDetails.find((taxGroup) => { return taxGroup.tax_group_id === item.taxOnItem; }).tax_slabs.reduce((totalTaxPercent, taxslab) => totalTaxPercent + Number(taxslab.percentage), 0); } else { taxPercentOnItem = Number(this.defDataForNewBill.data.interTaxSlabDetails.find((taxSlab) => { return taxSlab.tax_slab_id == item.taxOnItem; }).percentage) } itemCost = (item.amount * 100) / (100 + taxPercentOnItem); } else { itemCost = item.amount; } } else { itemCost = item.amount; } item.originalAmt = Math.round(itemCost * 100) / 100; } private _findItemTaxSlabs(item) { let itemTaxSlabs: { tax_slab_id: number, tax_slab_name: string, percentage: number }[] = []; if (this.taxType == INTRA_TAX_TYPE) { itemTaxSlabs = this.defDataForNewBill.data.intraTaxGrpDetails.find(taxGroup => { return taxGroup.tax_group_id === item.taxOnItem; }).tax_slabs; } else if (this.taxType === INTER_TAX_TYPE) { itemTaxSlabs[0] = this.defDataForNewBill.data.interTaxSlabDetails.find((taxSlab) => { return taxSlab.tax_slab_id === item.taxOnItem; }) } return itemTaxSlabs; } private _enableOrDisableAccforDiscount() { const discountValue: number = parseFloat(this.editBillForm.get('discountValue').value); if (discountValue) { this.enableAccountForDiscount = true; this.editBillForm.get('accountForDiscount').enable(); } else { this.editBillForm.get('accountForDiscount').setValue(null); this.enableAccountForDiscount = false; this.editBillForm.get('accountForDiscount').disable(); this.editBillForm.get('accountForDiscount').markAsUntouched(); } } /* @usage: called whenever there is a pricing re-calculation @purpose: seggregates taxes and calculates it cummulative amount. */ private _addOrUpdateTaxSlab(newTaxSlab) { let found = false; let foundIndex = -1; if (this.taxSlabs.length === 0) { this.taxSlabs.push(newTaxSlab); } else { for (const taxSlab in this.taxSlabs.value) { if (this.taxSlabs.value[taxSlab].slabName === newTaxSlab.get('slabName').value) { const cumAmt = Math.round((this.taxSlabs.value[taxSlab].cumAmount + newTaxSlab.get('cumAmount').value) * 100) / 100; newTaxSlab.get('cumAmount').setValue(cumAmt); foundIndex = Number(taxSlab); found = true; break; } } if (!found) { this.taxSlabs.push(newTaxSlab); } else { this.taxSlabs.removeAt(foundIndex); this.taxSlabs.push(newTaxSlab); } } } /* @usage: called whenever pricing is re-calculated. @purpose: Based on Item price type( tax inclusive or exclusive) total amount is calculated including discount, and adjustments */ calculateTotalAmount() { let adjustmentValue = this.editBillForm.get('adjustments').value; let selectedTds = this.editBillForm.value.tdsValue; if (selectedTds) { let tdsPercent = this.defDataForNewBill.data.tdsDetails.find((tds) => { return tds.tds_id === selectedTds }).percentage; if (!this.editBillForm.get('transRevCharge').value && (!this.editBillForm.value.discountValue || this.editBillForm.value.discountTaxType === "after_tax")) { this.tdsAmount = Math.round(((this.sumOfAllItemsOrginalAmt * Number(tdsPercent)) / 100) * -100) / 100; } else { this.tdsAmount = Math.round((((this.sumOfAllItemsOrginalAmt - this.calcDiscAmount) * tdsPercent) / 100) * -100) / 100; } } if (!this.editBillForm.get('transRevCharge').value) { this.totalAmount = Math.round((this.sumOfAllItemsOrginalAmt + adjustmentValue + this.totalTaxAmountOnAddedItems + this.discountAmount + this.tdsAmount) * 100) / 100; } else { this.totalAmount = Math.round((this.sumOfAllItemsOrginalAmt + adjustmentValue + this.discountAmount + this.tdsAmount) * 100) / 100; } if (this.totalAmount < 0) { this.errorOnTotalAmt = true; this.errMsgonTotalAmt = 'Total amount cannot be negative.' } else if ((this.bill_state === "PARTIALLY PAID" || this.bill_state === "PAID") && this.savedBillValues.data.bill.amount_paid > this.totalAmount) { this.errMsgonTotalAmt = 'Total amount should not be less than already paid amount.' this.errorOnTotalAmt = true; } else { this.errorOnTotalAmt = false; } } onSubmit() { this.calculatePricing(); let arrOfIndexOfEmty: number[] = []; this.addedItems = this.editBillForm.get('addedItems') as FormArray; for (let index in this.editBillForm.value.addedItems) { if (!this.editBillForm.value.addedItems[index].itemId) { arrOfIndexOfEmty.push(Number(index)); } } let tempArr = []; tempArr.push(...arrOfIndexOfEmty); for (let index in tempArr) { this.addedItems.removeAt(arrOfIndexOfEmty.pop()); } if (this.editBillForm.value.addedItems.length < 1) { this.toastr.error("Cannot save a bill without items"); this.addItemToBill(); return; } this.editBillForm.controls['vendorId'].markAsTouched(); this.editBillForm.controls['sourceOfSupply'].markAsTouched(); this.editBillForm.controls['destOfSupply'].markAsTouched(); if (this.editBillForm.value.discountValue) this.editBillForm.controls['accountForDiscount'].markAsTouched(); this.validateNeeded(); if (this.editBillForm.valid && !this.billDateError && !this.dueDateError && !this.ErrorAdjustTitle && !this.errorOnTotalAmt) { console.warn('Form Submitted'); this.editBillForm.value.subTotal = Number(this.subTotal); if (this.bill_state === 'DRAFT' || this.bill_state === 'CONFIRMED' || (this.bill_state === 'PARTIALLY PAID' && this.totalAmount > this.savedBillValues.data.bill.amount_paid) || (this.bill_state === 'PAID' && this.totalAmount === this.savedBillValues.data.bill.amount_paid)) { this.editBillForm.value.billState = this.bill_state; } else if (this.bill_state === 'PARTIALLY PAID' && this.totalAmount === this.savedBillValues.data.bill.amount_paid) { this.editBillForm.value.billState = 'PAID'; } else if (this.bill_state === 'PAID' && this.totalAmount > this.savedBillValues.data.bill.amount_paid) { this.editBillForm.value.billState = 'PARTIALLY PAID'; } this.editBillForm.value.billDate = this.billDateValue; this.editBillForm.value.dueDate = this.dueDateValue; this.editBillForm.value.discountAmount = -1 * this.discountAmount; this.editBillForm.value.totalAmount = this.totalAmount; this.editBillForm.value.tdsAmount = -1 * this.tdsAmount; this.editBillForm.value.vendorName = this.vendorName; // this.editBillForm.value.billId = this.billId; console.warn(this.editBillForm.value); let updattingBillId = this.billId //API Call for Create this.billService.editBill(this.editBillForm.value, updattingBillId).subscribe((CreationResult: { status: string, message: string, data }) => { if (CreationResult.status === "success") { this.toastr.success("Bill with bill id: " + updattingBillId + " is successfully updated") this._router.navigate(['/admin/bills']); } else { this.toastr.error("Something Went Wrong! Please try to refresh"); } }, (error) => { this.toastr.error("Something Went Wrong! Please try to refresh"); console.log("Error: " + error) }) } else { //Logger implementation or remove all console logs console.warn("please fill needed fields") } } resetPage() { this.bsModalRef = this.modalService.show(AlertModalComponent, { backdrop: 'static', keyboard: false, animated: true, ignoreBackdropClick: true, initialState: { title: 'Alert', data: 'Are you want to leave the page without saving!', buttonText: 'Stay on Page', deleteBtn: 'Leave' } }); this.bsModalRef.content.event.subscribe(modelResult => { this._router.navigate(['/admin/bills']); }); } }