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 { BillsService } from '../../../services/bills.service'; 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-new-bill', templateUrl: './new-bill.component.html', styleUrls: ['./new-bill.component.css'], encapsulation: ViewEncapsulation.None }) export class NewBillComponent implements OnInit { newBillForm: FormGroup; addedItems: FormArray; taxSlabs: FormArray; defDataForNewBill: newBillDefaults initalFormValues: JSON; 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; bsModalRef: BsModalRef; 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; }[]; }[]; billDateValue: any; dueDateValue: any; enableAccountForDiscount: boolean; constructor(private fb: FormBuilder, private billService: BillsService, private toastr: ToastrService, private modalService: BsModalService) { this.itemPriceType = [TAX_INCLUSIVE, TAX_EXCLUSIVE]; this.billService.sendNewBillDefaultsGetRequest().subscribe((defualtsFromDB: newBillDefaults) => { console.warn(defualtsFromDB); this.defDataForNewBill = defualtsFromDB; this.billDateValue = new Date(); this.dueDateValue = new Date(); // 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.newBillForm) { this.newBillForm.get('destOfSupply').patchValue(defualtsFromDB.data.orgData.location_id); } }, (error) => { this.toastr.error("Something Went Wrong! Please try to refresh"); console.log("Error: " + error); }); } ngOnInit(): void { // initialize subtotal to 0 as no item is default item. this.subTotal = 0.00; this.newBillForm = 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, revisit: should be populated from common file of strings discountTaxType: [DAT], // Non mandatory field discountValue: [, { validators: [Validators.pattern('^[0-9]*\.?[0-9]*$'), this.validateNonNegative], updateOn: 'blur' }], // by default value is %, revisit: value can be store in common file. same data has to be used as values in HTML file as well 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' }], }); this.enableAccountForDiscount=false; // Add one empty row to item table at the intial stage. this.addItemToBill(); this.initalFormValues = this.newBillForm.value; // calculate and update DueDate when payment Terms changes baseed on the billDate. this.newBillForm.get('paymentTerm').valueChanges.subscribe(value => this.calculateDueDate()); // re-calculated pricing details like, discount, taxslabs, totalamount when there is a change in itemPriceType this.newBillForm.get('itemPriceType').valueChanges.subscribe(value => { this.enableOrDisableTransRevCharge(); this.calculatePricing() }); this.newBillForm.get('discountTaxType').valueChanges.subscribe(value => this.calculatePricing()); this.newBillForm.get('discountTypeisPercent').valueChanges.subscribe(value => this.calculatePricing()); } // Need a getter for Validations. get discountValue() { return this.newBillForm.get('discountValue'); } get adjustments() { return this.newBillForm.get('adjustments'); } get vendorId() { return this.newBillForm.get('vendorId'); } get sourceOfSupply() { return this.newBillForm.get('sourceOfSupply'); } get destOfSupply() { return this.newBillForm.get('destOfSupply'); } get accountForDiscount() { return this.newBillForm.get('accountForDiscount'); } get referenceNumber() { return this.newBillForm.get('referenceNumber'); } // 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; } validateNeeded() { if (this.newBillForm.get('adjustments').value) { if (this.newBillForm.get('adjustmentTitle').value) { this.ErrorAdjustTitle = false; } else if (this.newBillForm.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.hasError('pattern')) { 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(): FormGroup { return this.fb.group({ itemId: [''], hsnCode: [''], sacCode: [''], accountNameId: [''], quantity: [1], price: [0], taxOnItem: [], itc: [], // initalize the amount to zero when a new item record is created, this is needed when calculating the amount changed on item. amount: [0], // revisit: these are internally used values for reducing the search in items objects array from service again. interTaxSlab: Number(), intraTaxGroup: Number(), isInventory: Boolean(), taxable: Boolean(), originalAmt: Number(0) }); } /* @usage: called when a new item row needs to be added dynamically @purpose: adds required item fromgroup to formArray. */ addItemToBill() { this.addedItems = this.newBillForm.get('addedItems') as FormArray; this.addedItems.push(this._createItem()); } /* @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.newBillForm.value.addedItems[removeItemIndex].price * this.newBillForm.value.addedItems[removeItemIndex].quantity; this.addedItems.removeAt(removeItemIndex); this.calculatePricing(); /* when the only row in the item table is removed add another empty row */ if (this.newBillForm.value.addedItems.length === 0) { this.addItemToBill(); } } removeAllItemFromBill() { this.addedItems = this.newBillForm.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.newBillForm.get('taxSlabs') as FormArray; // clear all taxes if (this.taxSlabs.length > 0) { this.taxSlabs.clear(); this.newBillForm.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.newBillForm.value.addedItems) { addedItem.taxOnItem = null; // addedItem.itc = null; } this.newBillForm.get('addedItems').setValue(this.newBillForm.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.newBillForm.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.newBillForm.get('discountTaxType').setValue(DBT); } else { this.removeAllItemFromBill(); let vendor = this.validateVendor(this.newBillForm.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.newBillForm.get('vendorId').setValue(null); this.removeAllItemFromBill(); this.itemTaxOptions = [...this.nonTaxableOptions]; this.newBillForm.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.newBillForm.get('sourceOfSupply').setValue(vendor.location_code_id); } this.locationChanged(); this.calculatePricing(); } } 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.newBillForm.get('vendorId').value); if (vendor) { this.newBillForm.get('transRevCharge').setValue(false); if ((vendor.reg_type.includes("Unregistered") || vendor.reg_type.includes("Regular") || vendor.reg_type.includes("SEZ")) && (this.newBillForm.get('itemPriceType').value !== TAX_INCLUSIVE)) { this.newBillForm.get('transRevCharge').enable(); } else { //if (vendor.reg_type.includes("Composition") || vendor.reg_type.includes("Overseas") || vendor.reg_type.includes("Deemed")) { this.newBillForm.get('transRevCharge').disable(); } } else { this.newBillForm.get('transRevCharge').disable(); } this.discountTaxOptions = [{ text: 'Discount Before Tax', value: DBT }, { text: 'Discount After Tax', value: DAT }] } locationChanged() { if (this.newBillForm.get('vendorId').value) { let sourceOfSupply: string; const sourceOfSupplyObj = this.defDataForNewBill.data.locationCodesData.find((location) => location.location_id === this.newBillForm.get('sourceOfSupply').value) if (sourceOfSupplyObj) sourceOfSupply = sourceOfSupplyObj.location_state_code; else return; const destOfSupply = this.defDataForNewBill.data.locationCodesData.find((location) => location.location_id === this.newBillForm.get('destOfSupply').value).location_state_code; if (sourceOfSupply !== destOfSupply || this.isSEZVendor) { this.taxType = INTER_TAX_TYPE; this.newBillForm.get('isTaxGroupSelectedOnItems').setValue(false); if (!this.isTaxOnItemDisabled) { for (const addeditem of this.newBillForm.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.newBillForm.get('isTaxGroupSelectedOnItems').setValue(true); if (!this.isTaxOnItemDisabled) { for (const addeditem of this.newBillForm.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.newBillForm.value.addedItems.length > 0 && this.newBillForm.value.addedItems.find((addedItem) => !addedItem.itemId)){ this.newBillForm.get('addedItems').setValue(this.newBillForm.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.newBillForm.value.addedItems[itemIndex].taxOnItem = selectedItem.taxGroupId; } else if (this.taxType === INTER_TAX_TYPE) { this.newBillForm.value.addedItems[itemIndex].taxOnItem = selectedItem.taxSlabId; } } else { this.newBillForm.value.addedItems[itemIndex].taxOnItem = null; } this.newBillForm.value.addedItems[itemIndex].itc = 1; } else { this.newBillForm.value.addedItems[itemIndex].taxOnItem = null; this.newBillForm.value.addedItems[itemIndex].itc = null; } if (selectedItem.isInventory) { this.newBillForm.value.addedItems[itemIndex].accountNameId = this.inventoryAsset[0].account_name_id; // "Inventory Asset" ID } else { this.newBillForm.value.addedItems[itemIndex].accountNameId = selectedItem.accountNameId; } this.newBillForm.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.newBillForm.value.addedItems[itemIndex].intraTaxGroup = selectedItem.taxGroupId; this.newBillForm.value.addedItems[itemIndex].interTaxSlab = selectedItem.taxSlabId; this.newBillForm.value.addedItems[itemIndex].hsnCode = selectedItem.hsnCode; this.newBillForm.value.addedItems[itemIndex].sacCode = selectedItem.sacCode; this.newBillForm.value.addedItems[itemIndex].isInventory = selectedItem.isInventory; this.newBillForm.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.newBillForm.value.addedItems[itemIndex].amount; // calculate the new amount and update amount. this.newBillForm.value.addedItems[itemIndex].amount = Number(this.newBillForm.value.addedItems[itemIndex].price) * Number(this.newBillForm.value.addedItems[itemIndex].quantity); // calculate the difference between previous and current amount and add it to subtotal. difference = this.newBillForm.value.addedItems[itemIndex].amount - difference; this.subTotal += difference; } else { this.newBillForm.value.addedItems.forEach(addedItem => { addedItem.amount = addedItem.price * addedItem.quantity; difference += addedItem.amount; }); this.subTotal = difference; } // update formControl values. this.newBillForm.controls['addedItems'].setValue(this.newBillForm.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.newBillForm.get('taxSlabs') as FormArray; // re-calcuate taxes , so clear all taxes if (this.taxSlabs.length > 0) { this.taxSlabs.clear(); this.newBillForm.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.newBillForm.get('discountValue').setValue(null); this.calculateTotalAmount(); return; } this._enableOrDisableAccforDiscount(); this.newBillForm.value.addedItems.forEach((item) => this._calcTaxandDiscountForItem(item)); this.newBillForm.get('totalTaxAmount').setValue(this.totalTaxAmountOnAddedItems); this.discountAmount = Math.round(this.calcDiscAmount * -100) / 100; this.calculateTotalAmount() } private _calcSumOfAllItems() { this.sumOfAllItemsOrginalAmt = 0; this.newBillForm.value.addedItems.forEach(item => { this._calcItemOiginalPrice(item); this.sumOfAllItemsOrginalAmt += item.originalAmt; }); } private _correctRoundOffItemPrice(item, totalTaxAmtOnItem) { this.sumOfAllItemsOrginalAmt -= item.originalAmt; item.originalAmt = (item.price * item.quantity) - totalTaxAmtOnItem; this.sumOfAllItemsOrginalAmt += item.originalAmt; } private _calcTaxandDiscountForItem(item) { const discountValue: number = parseFloat(this.newBillForm.get('discountValue').value); let applydiscountBeforeTax = (this.newBillForm.get('discountTaxType').value === DBT) || (this.newBillForm.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.newBillForm.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.newBillForm.value.discountTypeisPercent) { this.calcDiscAmount += Math.round(((item.price * item.quantity * 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.newBillForm.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.newBillForm.value.discountTypeisPercent) { if (this.newBillForm.get('itemPriceType').value === "Tax Exclusive") { this.calcDiscAmount += Math.round((((item.originalAmt + totalTaxAmtOnItem) * discountValue) / 100) * 100) / 100; } else { this.calcDiscAmount += Math.round(((item.price * item.quantity * discountValue) / 100) * 100) / 100; } } else { this.calcDiscAmount = discountValue; } } } private _calcItemOiginalPrice(item) { let taxPercentOnItem = 0; let itemCost = 0; if (item.taxOnItem) { if (this.newBillForm.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.price * item.quantity * 100) / (100 + taxPercentOnItem); } else { itemCost = item.price * item.quantity; } } else { itemCost = item.price * item.quantity; } 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.newBillForm.get('discountValue').value); if (discountValue) { this.newBillForm.get('accountForDiscount').enable(); this.enableAccountForDiscount = true; } else { this.newBillForm.get('accountForDiscount').setValue(null); this.newBillForm.get('accountForDiscount').disable(); this.enableAccountForDiscount = false; this.newBillForm.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.newBillForm.get('adjustments').value; let selectedTds = this.newBillForm.value.tdsValue; if (selectedTds) { let tdsPercent = this.defDataForNewBill.data.tdsDetails.find((tds) => { return tds.tds_id === selectedTds }).percentage; if (!this.newBillForm.get('transRevCharge').value && (!this.newBillForm.value.discountValue || this.newBillForm.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.newBillForm.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; } else { this.errorOnTotalAmt = false; } } onSubmit(event) { let arrOfIndexOfEmty: number[] = []; this.addedItems = this.newBillForm.get('addedItems') as FormArray; for (let index in this.newBillForm.value.addedItems) { if (!this.newBillForm.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.newBillForm.value.addedItems.length < 1) { this.toastr.error("Cannot create a bill without items"); this.addItemToBill(); return; } this.newBillForm.controls['vendorId'].markAsTouched(); this.newBillForm.controls['sourceOfSupply'].markAsTouched(); this.newBillForm.controls['destOfSupply'].markAsTouched(); this.newBillForm.controls['adjustmentTitle'].markAllAsTouched(); if (this.newBillForm.value.discountValue) this.newBillForm.controls['accountForDiscount'].markAsTouched(); this.validateNeeded(); if (this.newBillForm.valid && !this.billDateError && !this.dueDateError && !this.ErrorAdjustTitle && !this.errorOnTotalAmt) { console.warn('Form Submitted'); this.newBillForm.value.billDate = this.billDateValue; this.newBillForm.value.dueDate = this.dueDateValue; this.newBillForm.value.subTotal = this.subTotal; this.newBillForm.value.billState = event.submitter.value; this.newBillForm.value.discountAmount = -1 * this.discountAmount; this.newBillForm.value.totalAmount = this.totalAmount; this.newBillForm.value.tdsAmount = -1 * this.tdsAmount; this.newBillForm.value.vendorName = this.vendorName; console.warn(this.newBillForm.value); //API Call for Create this.billService.createNewBill(this.newBillForm.value).subscribe((CreationResult: { status: string, message: string, data }) => { if (CreationResult.status === "success") { this.toastr.success("Bill got created Successfully with bill id: " + CreationResult.data.bill_id) this.resetPage(true); } 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(OnSubmit?: boolean) { if (!OnSubmit) { 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.resetAllFormDataAndMarkUntouched(); }); } else { this.resetAllFormDataAndMarkUntouched(); } } resetAllFormDataAndMarkUntouched() { this.newBillForm.controls['vendorId'].markAsUntouched(); this.newBillForm.controls['sourceOfSupply'].markAsUntouched(); this.newBillForm.controls['destOfSupply'].markAsUntouched(); if (this.newBillForm.value.discountValue) this.newBillForm.controls['accountForDiscount'].markAsUntouched(); this.ErrorAdjustTitle = false; this.billDateError = false; this.dueDateError = false; this.billDateValue = new Date(); // For temporary purpose resetting form to intial values. this.newBillForm.reset(this.initalFormValues); this.newBillForm.get('destOfSupply').patchValue(this.defDataForNewBill.data.orgData.location_id); this.calculateDueDate(); this.removeAllItemFromBill(); } }