Luciano
10/7/2019 - 7:31 PM

VUE JS Parent component for Invoices CRUD

<template>
    <div class="order-form">

        <div class="row">

            <h4 class="font-weight-bold py-3 col-md">
                <div>
                    <span class="text-muted font-weight-light"><router-link tag="a" :to="{ name: 'invoice-list'}" >Facturas</router-link> /</span>

                    <span v-if="invoice.details.number">
                        {{ invoice.details.number }}
                        <span v-if="invoice.details.status.id == 8" class="badge ml-4 d-inline-block align-bottom badge-success">{{ invoice.details.status.name }}</span>
                        <span v-else-if="invoice.details.status.id == 1" class="badge ml-4 d-inline-block align-bottom badge-warning">{{ invoice.details.status.name }}</span>
                        <span v-else class="badge ml-4 d-inline-block align-bottom badge-default">{{ invoice.details.status.name }}</span>
                    </span>

                    <span v-else>Nuevo Registro</span>
                </div>
            </h4>

        </div>

        <b-form @submit.prevent="validateSubmit()" autocomplete="off">

            <b-card body-class="p-4" footer-class="text-right">

                <!-- HEADER FORM -->
                <div v-show="!invoice.details.id" class="row mb-3">

                    <!-- INVOICE DATA -->
                    <div class="col p-4 bg-lighter m-2">

                        <b-form-group class="mb-0">
                            <h5 class="mb-2">Datos de la Factura</h5>
                            <hr class="alert-primary mt-0">
                        </b-form-group>

                        <b-form-group label="Fecha">
                            <flat-pickr
                                v-model="invoice.details.date"
                                name="date"
                                :config="dateConfig"
                                :placeholder="'Seleccione...'"
                                data-vv-as=" "
                                v-validate="'required'"
                                class="d-inline-block"
                                :class="{ 'is-invalid': veeErrors.has('date') }" />
                            <label class="small text-danger" v-if="veeErrors.first('date')">{{ veeErrors.first('date') }}</label>
                        </b-form-group>

                        <b-form-group label="Tipo de Comprobante">

                            <multiselect
                                :allow-empty="false"
                                :multiple="false"
                                :close-on-select="true"
                                :searchable="false"
                                :show-labels="false"
                                :options="books"
                                placeholder="Comprobante..."
                                label="full_name"
                                track-by="id"
                                name=book
                                v-model="invoice.details.book"
                                data-vv-as=" "
                                v-validate="'required'"
                                :class="{ 'is-invalid': veeErrors.has('book') }">

                            </multiselect>
                            <label class="small text-danger" v-if="veeErrors.first('book')">{{ veeErrors.first('book') }}</label>
                        </b-form-group>

                        <b-form-group label="IVA Imputado">

                            <multiselect
                                :allow-empty="false"
                                :multiple="false"
                                :close-on-select="true"
                                :searchable="false"
                                :show-labels="false"
                                :options="iva_values"
                                placeholder="IVA..."
                                label="value"
                                track-by="id"
                                name=iva_value
                                v-model="invoice.details.iva_value"
                                data-vv-as=" "
                                v-validate="'required'"
                                :class="{ 'is-invalid': veeErrors.has('iva_value') }">


                                <template slot="singleLabel" slot-scope="props"><span v-if="props.option.value">{{props.option.value.replace(/\.0+$/,'')}} %</span></template>
                                <template slot="option" slot-scope="props"><span v-if="props.option.value">{{props.option.value.replace(/\.*0+$/,'')}} %</span></template>
                            </multiselect>
                            <label class="small text-danger" v-if="veeErrors.first('iva_value')">{{ veeErrors.first('iva_value') }}</label>
                        </b-form-group>
                    </div>

                    <!-- CUSTOMER DATA -->
                    <div class="col p-4 bg-lighter m-2">

                        <b-form-group class="mb-0">
                            <h5 class="mb-2">Datos del Cliente</h5>
                            <hr class="alert-primary mt-0">
                        </b-form-group>

                        <b-form-group>
                            <span slot="label">
                                Cliente

                                <small class="text-primary pl-4" v-if="!invoice.details.id && invoice.details.customer && invoice.details.customer.id">
                                    Se asociará la Factura al cliente <router-link tag="a" :to="{name:'customer-edit', params:{ customer_id: invoice.details.customer.id } }" target= "_blank">{{ invoice.details.customer.full_name }}</router-link>
                                </small>

                                <small class="text-primary pl-4" v-else-if="invoice.details.id && invoice.details.customer.id">
                                    Factura asociada a <router-link tag="a" :to="{name:'customer-edit', params:{ customer_id: invoice.details.customer.id } }" target= "_blank">{{ invoice.details.customer.full_name }}</router-link>
                                </small>

                            </span>
                            <multiselect
                                selectLabel="Presione Enter para seleccionar"
                                deselectLabel="Presione Enter para deseleccionar"
                                selectedLabel="Seleccionado"
                                tagPlaceholder="Presione Enter para agregar"
                                placeholder="Buscar un Cliente..."
                                open-direction="bottom"
                                :options="customers"
                                :loading="isLoading"
                                :multiple="false"
                                :taggable="true"
                                label="full_name"
                                track-by="id"
                                name="customer"
                                v-model="invoice.details.customer"
                                @search-change="getCustomers"
                                @tag="addCustomer"
                                data-vv-as=" "
                                v-validate="'required'"
                                :class="{ 'is-invalid': veeErrors.has('customer') }">

                                    <template slot="noOptions"><small class="text-muted ml-1">Ingrese texto para buscar</small></template>
                                    <template slot="singleLabel" slot-scope="{ option }">{{ option.full_name }}</template>
                                    <template slot="noResult">No se encontraron coincidencias.</template>
                            </multiselect>
                            <label class="small text-danger" v-if="veeErrors.first('customer')">{{ veeErrors.first('customer') }}</label>
                        </b-form-group>

                        <b-form-group label="Condición frente al IVA">

                            <multiselect
                                :allow-empty="true"
                                :multiple="false"
                                :close-on-select="true"
                                :searchable="false"
                                :show-labels="false"
                                :options="iva_conditions"
                                placeholder="Seleccione..."
                                label="name"
                                track-by="id"
                                name="iva_condition"
                                v-model="invoice.details.customer_iva_condition"
                                data-vv-as=" "
                                v-validate="'required'"
                                :class="{ 'is-invalid': veeErrors.has('iva_condition') }">
                            </multiselect>
                            <label class="small text-danger" v-if="veeErrors.first('iva_condition')">{{ veeErrors.first('iva_condition') }}</label>

                        </b-form-group>

                        <b-form-group label="Tipo/Nro Documento">

                            <div class="row">
                                <multiselect
                                    class="col-sm-3"
                                    :allow-empty="true"
                                    :multiple="false"
                                    :close-on-select="true"
                                    :searchable="false"
                                    :show-labels="false"
                                    :options="document_types"
                                    placeholder="Seleccione..."
                                    label="name"
                                    track-by="id"
                                    name="doc_type"
                                    v-validate="{required: invoice.details.book && invoice.details.book.letter != 'X' ? true : false}"
                                    v-model="invoice.details.customer_doc_type"
                                    :class="{ 'is-invalid': veeErrors.has('doc_type') }">
                                </multiselect>

                                <masked-input
                                    type="text"
                                    class="form-control col-sm-8 pl-2"
                                    name="doc_number"
                                    data-vv-as=" "
                                    v-validate="{required: invoice.details.book && invoice.details.book.letter != 'X' ? true : false}"
                                    v-model="invoice.details.customer_doc_number"
                                    :mask="invoice.details.customer_doc_type && (invoice.details.customer_doc_type.id == 7 || invoice.details.customer_doc_type.id == 8) ? cuitMask : noMask"
                                    :class="{ 'is-invalid': veeErrors.has('doc_number') }"/>
                            </div>

                            <label class="small text-danger" v-if="veeErrors.first('doc_number') || veeErrors.first('doc_type')">
                                {{ veeErrors.first('doc_number') || veeErrors.first('doc_type') }}
                            </label>
                        </b-form-group>
                    </div>

                    <!-- CUSTOMER INFO -->
                    <div class="col p-4 bg-lighter m-2">
                        <customer-info-component
                            :customer="invoice.details.customer">
                        </customer-info-component>
                    </div>
                </div>

                <!-- HEADER VIEW -->
                <div v-if="invoice.details.id" class="mb-3">
                    <formality-header-view-component :formality="invoice"></formality-header-view-component>
                </div>

                <!-- FORMALITIES, CONCEPTS -->
                <div class="p-3 bg-lighter mb-3">

                    <!-- FORMALITIES -->
                    <imputable-formalities-component
                        :customer="invoice.details.customer || null"
                        :formalities="invoice.formalities"
                        :actions="true"
                        :imputable_books="imputable_books"
                        :imputable_statuses="imputable_statuses"
                        :hideColumns="false"
                        class="mb-4">
                    </imputable-formalities-component>

                    <!-- CONCEPTS -->
                    <concepts-component
                        :customer="invoice.details.customer"
                        :concepts="invoice.concepts"
                        :actions="true">
                    </concepts-component>
                </div>

                <!-- DISCOUNTS -->
                <div class="p-3 bg-lighter mb-3">

                    <!-- CUSTOM DISCOUNTS -->
                    <discounts-component
                        :customer="invoice.details.customer"
                        :discounts="invoice.custom_discounts"
                        :actions="true"
                        class="mb-4">
                    </discounts-component>

                    <!-- IMPUTABLE DISCOUNTS -->
                    <imputable-discounts-component
                        :customer="invoice.details.customer || null"
                        :formalities="invoice.discounts"
                        :actions="true"
                        :imputable_books="imputable_discount_books"
                        :imputable_statuses="imputable_discount_statuses"
                        :hideColumns="false">
                    </imputable-discounts-component>
                </div>

                <!-- TOTALS -->
                <b-form-row class="alert-primary my-4 pt-3 mx-0">

                    <b-form-group class="col-md-10 text-right py-2 mb-0">
                        Total comprobantes:<br>
                        Total conceptos:<br>
                        Total descuentos:<br>
                        Total a cuenta:<br>
                        <span class="font-weight-bold mb-2 d-block">Subtotal factura:</span>
                        <span v-if="invoice.details.iva_value">IVA({{ parseFloat(invoice.details.iva_value.value).toFixed(1) }}%):<br></span>
                        <span class="text-big mt-2 font-weight-bold"
                             :class="(parseFloat(invoiceSubtotal) + parseFloat(invoiceTax)).toFixed(2) != invoiceTotal.toFixed(2) ? 'text-danger' : ''"
                             >Total factura:

                            <small class="small text-default" v-if="total_difference > 0">(+{{ total_difference }})</small>
                            <small class="small text-default" v-if="total_difference < 0">({{ total_difference }})</small>
                         </span><br><br>
                    </b-form-group>

                    <b-form-group class="col-md-1 py-2 mb-0 text-right">
                        <span>{{ formatPriceWithSymbol(invoiceSubtotalOrders) }}</span><br>
                        <span>{{ formatPriceWithSymbol(invoiceSubtotalConcepts) }}</span><br>
                        <span>- {{ formatPriceWithSymbol(invoiceSubtotalCustomDiscounts) }}</span><br>
                        <span>- {{ formatPriceWithSymbol(invoiceSubtotalDiscounts) }}</span><br>
                        <span class="font-weight-bold mb-2 d-block">{{ formatPriceWithSymbol(invoiceSubtotal) }}</span>
                        <span>{{ formatPriceWithSymbol(invoiceTax) }}</span><br>
                        <strong class="text-big mt-2"
                             :class="(parseFloat(invoiceSubtotal) + parseFloat(invoiceTax)).toFixed(2) != invoiceTotal.toFixed(2) ? 'text-danger' : ''">
                         {{ formatPriceWithSymbol(invoiceTotal) }}</strong><br><br>
                    </b-form-group>
                </b-form-row>



                <div slot="footer">

                    <ladda-btn type="submit"
                        v-if="$store.getters.user.can.create_invoice || (invoice.id && $store.getters.user.can.update_invoice)"
                        :loading="saveLoading"
                        data-style="zoom-out"
                        class="btn btn-primary">Guardar
                    </ladda-btn>

                    <b-btn variant="default" class="ml-2 d-inline" @click="$router.go(-1)">Cancelar</b-btn>

                </div>

            </b-card>
        </b-form>

        <!-- CONFIRM SUBMIT MODAL -->
        <b-modal
            v-model="confirmSubmit"
            size="md"
            ok-title="continuar"
            ok-variant="success"
            cancel-title="cancelar"
            @ok="submit()">

            <div slot="modal-title">Confirmar <span class="font-weight-light">Acción</span></div>

            Está seguro que desea crear la Factura? Los cambios no podrán revertirse.
        </b-modal>

        <!-- CONFIRM LEAVE MODAL -->
        <b-modal
            v-model="confirmLeave.modal"
            size="sm"
            ok-title="continuar"
            cancel-title="cancelar"
            @ok="confirmLeave.confirmed = true; $router.push({ path: confirmLeave.path.fullPath })"
            @hide="confirmLeave.confirmed = false">

            <div slot="modal-title">Confirmar <span class="font-weight-light">Acción</span></div>

            Existen cambios que no han sido guardados. Desea continuar de todos modos?
        </b-modal>

    </div>

</template>

<script>

import Multiselect from 'vue-multiselect'
import numeral from 'numeral'
import flatPickr from 'vue-flatpickr-component'
import {Spanish} from 'flatpickr/dist/l10n/es.js'
import moment from 'moment'
import VeeValidate, { Validator } from 'vee-validate'
import MaskedInput from 'vue-text-mask'
import * as textMaskAddons from 'text-mask-addons/dist/textMaskAddons'

import CustomerInfoComponent from '@/components/partials/CustomerInfoComponent'
import ImputableFormalitiesComponent from '@/components/formalities/ImputableFormalitiesComponent'
import ImputableDiscountsComponent from '@/components/discounts/ImputableDiscountsComponent'
import DiscountsComponent from '@/components/discounts/DiscountsComponent'
import ConceptsComponent from '@/components/concepts/ConceptsComponent'
import FormalityHeaderViewComponent from '@/components/partials/FormalityHeaderViewComponent' // Used in form

export default {
    name: 'invoice-component',

    metaInfo: {
        title: 'Factura - MH Carteles'
    },

    components: {
        Multiselect,
        flatPickr,
        MaskedInput,
        CustomerInfoComponent,
        ImputableFormalitiesComponent,
        ConceptsComponent,
        ImputableDiscountsComponent,
        DiscountsComponent,
        FormalityHeaderViewComponent,
    },

    data() {

        return {
            cuitMask: [/[1-9]/, /\d/, '-', /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/, '-', /\d/],
            noMask: [/[1-9]/,/\d/, /\d/, /\d/, /\d/, /\d/, /\d/, /\d/],

            moment:moment,

            /*******************************/
            saveLoading: false,
            isLoading: false,

            // Flatpicker
            dateConfig: {
                altInputClass: 'form-control w-100',
                altInput: true,
                altFormat: 'd/m/Y',
                allowInput: true,
                locale: Spanish,
                static: false
            },

            confirmSubmit: false,
            confirmLeave: {
                modal: false,
                path: null,
                confirmed: false
            },

            invoice_statuses: [],
            iva_values:[],
            customers: [],
            books: [],
            imputable_books:[],
            imputable_discount_books:[],
            imputable_statuses:[],
            imputable_discount_statuses:[],
            iva_conditions: [],
            document_types:[],

            title_status: '',

            originalinvoice: null,

            invoice: {

                details: {},
                concepts:[],
                formalities:[],
                discounts:[],
                custom_discounts:[],
            },
        }
    },

    beforeRouteLeave (to, from, next) {

        if (this.confirmLeave.confirmed || this.originalinvoice == JSON.stringify(this.invoice)) {
            next()
        } else {

            this.confirmLeave.path = to
            this.confirmLeave.modal = true
        }
    },

    beforeRouteEnter ( to, from, next ) {

        if ( to.params.invoice_id ){

            axios.get('/invoices/get/'+to.params.invoice_id)
                .then((response)=>{
                    next(vm => {

                        let invoice = {
                            details: response.data.details,
                            concepts: response.data.concepts,
                            formalities: response.data.formalities,
                            discounts: response.data.discounts,
                            custom_discounts: response.data.custom_discounts,
                            parents: response.data.parents
                        }

                        invoice.details.customer = response.data.details.customer || {id:null,full_name:response.data.details.customer_name}

                        vm.invoice = invoice
                        vm.originalinvoice = JSON.stringify(vm.invoice)
                    });
                }).catch((error)=>{
                    console.log(error.response.data);
                });

        } else {

            next(vm => {

                vm.invoice = {

                    details: {
                        id: '',
                        customer:null,
                        iva_value:null,
                        date: moment.utc(new Date()).format('YYYY-MM-DD')
                    },
                    concepts:[],
                    formalities:[],
                    discounts:[],
                    custom_discounts:[],
                    parents:[],
                }

                if (to.params.customer_id) vm.preselectCustomer(to.params.customer_id) // Preset customer if needed

                vm.originalinvoice = JSON.stringify(vm.invoice)
                vm.$validator.reset()
            })
        }
    },

    mounted(){
        this.getIvaValues()
        this.getBooks()
        this.getImputableBooks()
        this.getImputableStatuses()
        this.getImputableDiscountBooks()
        this.getImputableDiscountStatuses()
        this.getIvaConditions()
        this.getDocumentTypes()

        Validator.extend('notZero', {

            validate: function(value) {

                let valid = value > 0 ? true : false

                return {
                    valid: valid,
                    data: {
                        message: !valid ? 'El campo debe ser mayor a $0' : ''
                    }
                };
            }.bind(this),

            getMessage: (field, params, data) => {
                return data.message
            }
        })
    },

    computed: {

        invoiceSubtotalOrders(){
            return this.invoice.formalities.reduce((cnt, formality) => {
                cnt +=  parseFloat(formality.amount || 0)
                return cnt
            }, 0)
        },

        invoiceSubtotalConcepts(){
            return this.invoice.concepts.reduce((cnt, concept) => {
                cnt +=  parseFloat(concept.amount || 0)
                return cnt
            }, 0)
        },

        invoiceSubtotalDiscounts(){
            return this.invoice.discounts.reduce((cnt, discount) => {
                cnt +=  parseFloat(discount.amount || 0)
                return cnt
            }, 0)
        },

        invoiceSubtotalCustomDiscounts(){
            return this.invoice.custom_discounts.reduce((cnt, discount) => {
                cnt +=  parseFloat(discount.amount || 0)
                return cnt
            }, 0)
        },

        invoiceSubtotal () {
            return this.invoiceSubtotalOrders + this.invoiceSubtotalConcepts - this.invoiceSubtotalDiscounts - this.invoiceSubtotalCustomDiscounts
        },

        invoiceTax () {
            let total_iva = this.invoiceSubtotal * (this.invoice.details.iva_value ? this.invoice.details.iva_value.value : 0 ) / 100
            return total_iva
        },

        invoiceTotal () {
            return this.invoice && this.invoice.details.id ? parseFloat(this.invoice.details.total) : parseFloat(this.invoiceSubtotal + this.invoiceTax)
        },

        total_difference(){
            return ( parseFloat(this.invoiceSubtotal) + parseFloat(this.invoiceTax) - parseFloat(this.invoiceTotal)).toFixed(2)
        }
    },

    methods: {

        // Getters
        getIvaValues() {

            axios({
                method: 'get',
                url:    '/iva-values/all'
            })
            .then((response) => {

                this.iva_values = response.data
                if (!this.invoice.details.id) this.invoice.details.iva_value = response.data[0]
                this.originalinvoice = JSON.stringify(this.invoice)

            }).catch((error) => {
                console.log(error.response.data)
            })
        },

        getBooks() {

            axios({
                method: 'post',
                url:    '/books/by-group',
                data:   {
                    ids: [process.env.MIX_INVOICE_GROUP_ID]
                }
            })
            .then((response)=>{

                this.books = response.data
                this.invoice.details.book = response.data[0]
                this.originalinvoice = JSON.stringify(this.invoice)

            }).catch((error)=>{
                console.log(error.response.data)
            })
        },

        getImputableBooks() {

            axios({
                method: 'post',
                url:    '/books/by-group',
                data:   {
                    ids: [process.env.MIX_ORDER_GROUP_ID]
                }
            })
            .then((response)=>{

                this.imputable_books = response.data

            }).catch((error)=>{
                console.log(error.response.data)
            })
        },

        getImputableDiscountBooks() {

            axios({
                method: 'post',
                url:    '/books/by-group',
                data:   {
                    ids: [process.env.MIX_RECEIPT_GROUP_ID]
                }
            })
            .then((response)=>{

                this.imputable_discount_books = response.data

            }).catch((error)=>{
                console.log(error.response.data)
            })
        },

        getImputableStatuses() {

            axios({
                method: 'post',
                url:    '/statuses/by-group',
                data:   { ids: [1,2,3,4,5] }
            })
            .then((response)=>{

                this.imputable_statuses = response.data

            }).catch((error)=>{
                console.log(error.response.data)
            })
        },

        getImputableDiscountStatuses() {

            axios({
                method: 'post',
                url:    '/statuses/by-group',
                data:   { ids: [6] }
            })
            .then((response)=>{

                this.imputable_discount_statuses = response.data

            }).catch((error)=>{
                console.log(error.response.data)
            })
        },

        getIvaConditions() {

            axios({
                method: 'get',
                url:    '/iva-conditions/all'
            })
            .then((response) => {

                this.iva_conditions = response.data

            }).catch((error) => {
                console.log(error.response.data)
            })
        },

        getDocumentTypes() {

            axios({
                method: 'get',
                url:    '/document-types/all'
            })
            .then((response) => {

                this.document_types = response.data

            }).catch((error) => {
                console.log(error.response.data)
            })
        },

        getCustomers(querysearch) {
            this.isLoading = true

            axios({
                method: 'post',
                url:    '/customers/find',
                data: {querysearch:querysearch}
            })
            .then((response) => {

                this.customers = response.data
                this.isLoading = false

            }).catch((error) => {
                console.log(error.response.data)
            })
        },

        // Customers
        addCustomer(newTag) {
            const tag = {
                id: null,
                full_name: newTag
            }

            this.customers.push(tag)
            this.invoice.details.customer = tag
        },

        preselectCustomer(customer_id){
            axios.get('/customers/get/'+customer_id)
                .then((response) => {

                    this.invoice.details.customer = response.data
                    this.originalinvoice = JSON.stringify(this.invoice)

                }).catch((error) => {
                    console.log(error.response.data)
                })
        },

        // Submit
        validateSubmit() {

            this.$validator.validateAll()
            .then(result => {

                console.log(this.invoiceTotal)

                if (!result) {

                    this.saveLoading = false
                    this.$root.$emit('notifySecondary', 'Por favor, complete todos los campos obligatorios.')
                    return

                } else if ( !this.invoice.formalities.length && !this.invoice.concepts.length ){
                    this.saveLoading = false
                    this.$root.$emit('notifySecondary', 'Debe seleccionar al menos un Comprobante o Concepto a imputar.')
                    return

                } else if ( this.invoice.details.id && (parseFloat(this.invoiceSubtotal) + parseFloat(this.invoice.details.total_iva) != this.invoice.details.total) ){
                    this.saveLoading = false
                    this.$root.$emit('notifySecondary', 'El total de la Factura no se corresponde con lo imputado.')
                    return

                } else if ( this.invoiceTotal <= 0 ){
                    this.saveLoading = false
                    this.$root.$emit('notifySecondary', 'El total la Factura debe ser mayor a $0.')
                    return
                }

                this.confirmSubmit = true
            })
        },

        submit() {

            this.saveLoading = true

            axios({
                method: this.invoice.details.id ? 'patch' : 'post',
                url:    '/invoices',
                data:   this.invoice
            })
            .then((response)=>{

                this.confirmLeave.confirmed = true
                this.$router.push({ name: 'invoice-view', params: { invoice_id: response.data.invoice.id } })

                this.$root.$emit('notifySuccess', response.data.message)

            }).catch((error)=>{

                this.saveLoading = false
                if (error.response.status !== 401) this.$root.$emit('notifyError', error);
                console.log(error)

            })
        }

    }
}
</script>