فهرست منبع

[ADD] Get Api

adrielso 6 سال پیش
والد
کامیت
7cee433092

+ 2 - 0
__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+import controllers

+ 1 - 1
__openerp__.py

@@ -22,6 +22,6 @@
         'eiru_topbar_toggler',
     ],
     'data': [
-        # 'views/template.xml',
+        'views/templates.xml',
     ],
 }

+ 2 - 0
controllers/__init__.py

@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+import main

+ 260 - 0
controllers/main.py

@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+from openerp import http
+from openerp.http import request
+from werkzeug.wrappers import Response
+from werkzeug.datastructures import Headers
+from datetime import datetime
+from dateutil.relativedelta import relativedelta as rd
+from dateutil.parser import parse
+from pytz import timezone
+from gzip import GzipFile
+from StringIO import StringIO as IO
+import simplejson as json
+import gzip
+import logging
+
+LOGGER = logging.getLogger(__name__)
+DATE_FORMAT = '%Y-%m-%d'
+DATETIME_FORMAT = '%Y-%m-%d %H:%m:%S'
+GZIP_COMPRESSION_LEVEL = 9
+
+class Purchases(http.Controller):
+
+    ''' Get timezone '''
+    def get_timezone(self):
+        return timezone(request.context['tz'])
+
+    '''  Get server date to send '''
+    def get_server_date(self):
+        return datetime.now(self.get_timezone()).strftime(DATE_FORMAT)
+
+    ''' Get current user information '''
+    def get_user(self):
+        user = request.env.user
+        return {
+            'id': user.id,
+            'name': user.name,
+            'displayName': user.display_name,
+            'currency': {
+                'id': user.company_id.currency_id.id,
+                'name': user.company_id.currency_id.name,
+                'displayName': user.company_id.currency_id.display_name,
+                'symbol': user.company_id.currency_id.symbol
+            },
+            'company': {
+                'id': user.company_id.id,
+                'name': user.company_id.name,
+                'displayName': user.company_id.display_name
+            }
+        }
+
+    ''' Get currencies from journals '''
+    def get_currencies_from_journal(self):
+        domain = [
+            ('type', 'in', ['bank', 'cash']),
+            ('default_debit_account_id.currency_id', '=', False),
+            ('active', '=', True)
+        ]
+
+        currencies = []
+
+        for j in request.env['account.journal'].search(domain):
+            c = j.currency or j.company_id.currency_id
+
+            currencies.append({
+                'id': c.id,
+                'name': c.display_name,
+                'base': c.base,
+                'symbol': c.symbol,
+                'position': c.position,
+                'rateSilent': c.rate_silent,
+                'decimalSeparator': c.decimal_separator,
+                'decimalPlaces': c.decimal_places,
+                'thousandsSeparator': c.thousands_separator
+            })
+
+        return {c['id']:c for c in currencies}.values()
+
+    '''  all active journals '''
+    def get_journals(self, type=None):
+        if (not type[0]):
+            type= ['bank', 'cash']
+
+        user = request.env.user
+        journal = []
+
+        for store in user.store_ids:
+            for journalID in store.journal_ids:
+                if (journalID.type in ['bank', 'cash']):
+                    journal.append(journalID.id)
+
+        domain = [('type', 'in', type), ('default_debit_account_id.currency_id', '=', False), ('active', '=', True)]
+
+        if (journal):
+            domain.append(('id', 'in', journal ))
+
+        return [{
+            'id': journal.id,
+            'name': journal.name,
+            'displayName': journal.display_name,
+            'code': journal.code,
+            'cashControl': journal.cash_control,
+            'type': journal.type,
+            'currency': {
+                'id': journal.currency.id,
+                'name': journal.currency.name,
+                'displayName': journal.currency.display_name
+            },
+            'defaultDebitAccount': {
+                'id': journal.default_debit_account_id.id,
+                'name': journal.default_debit_account_id.name,
+                'displayName': journal.default_debit_account_id.display_name,
+                'code': journal.default_debit_account_id.code,
+                'exchange_rate': journal.default_credit_account_id.exchange_rate,
+                'foreignBalance': journal.default_credit_account_id.foreign_balance,
+                'reconcile': journal.default_credit_account_id.reconcile,
+                'debit': journal.default_credit_account_id.debit,
+                'credit': journal.default_credit_account_id.credit,
+                'currencyMode': journal.default_credit_account_id.currency_mode,
+                'companyCurrency': {
+                    'id': journal.default_credit_account_id.company_currency_id.id,
+                    'name': journal.default_credit_account_id.company_currency_id.name,
+                    'displayName': journal.default_credit_account_id.company_currency_id.display_name,
+                },
+                'currency': {
+                    'id': journal.default_credit_account_id.currency_id.id,
+                    'name': journal.default_credit_account_id.currency_id.name,
+                    'displayName': journal.default_credit_account_id.currency_id.display_name
+                }
+            }
+        } for journal in request.env['account.journal'].search(domain, order='id')]
+
+    ''' account.bank.statement '''
+    def get_account_bank_statement(self, journalIds=None):
+        if (not journalIds):
+            return False
+
+        domain = [('journal_id.id', 'in', journalIds)]
+        # domain = [('journal_id.id', 'in', journalIds), ('user_id', '=', request.env.user.id)]
+        return [{
+            'id': statement.id,
+            'name': statement.name,
+            'date': statement.date,
+            'balanceEnd': statement.balance_end,
+            'user': {
+                'id': statement.user_id.id,
+                'name': statement.user_id.name,
+                'displayName': statement.user_id.display_name
+            },
+            'journal': {
+                'id': statement.journal_id.id,
+                'name': statement.journal_id.name,
+                'displayName': statement.journal_id.display_name,
+                'code': statement.journal_id.code,
+                'cashControl': statement.journal_id.cash_control,
+                'type': statement.journal_id.type,
+                'currency': {
+                    'id': statement.journal_id.currency.id,
+                    'name': statement.journal_id.currency.name,
+                    'displayName': statement.journal_id.currency.display_name
+                }
+            },
+            'line': [{
+                'id': line.id,
+                'date': line.date,
+                'name': line.name,
+                'ref': line.ref,
+                'amount': line.amount,
+                'patner':{
+                    'id': line.partner_id.id,
+                    'name': line.partner_id.name,
+                    'displayName': line.partner_id.display_name
+                },
+            } for line in statement.line_ids],
+            'typeStatement': {
+                'id': statement.type_statement.id,
+                'name': statement.type_statement.name,
+                'code': statement.type_statement.code
+            }
+        } for statement in request.env['account.bank.statement'].search(domain)]
+
+    ''' Configuracion de Caja '''
+    def get_account_bank_statement_config(self):
+        'account.bank.statement.config'
+        return [{
+            'transfer': {
+                'userIds': map(lambda x: x.id, config.transfer_user_ids),
+                'statementIds': map(lambda x: x.id, config.transfer_statement_ids) ,
+                'negativeAmount': config.transfer_negative_amount
+            },
+            'inputCashBox': {
+                'userIds': map(lambda x: x.id, config.input_cash_box_user_id),
+                'statementIds': map(lambda x: x.id, config.input_cash_box_statement_ids)
+            },
+            'outputCashBox': {
+                'userIds': map(lambda x: x.id, config.output_cash_box_user_id),
+                'statementIds': map(lambda x: x.id, config.output_cash_box_statement_ids),
+                'negativeAmount': config.output_negative_amount
+            },
+            'delete': {
+                'outputUserIds': map(lambda x: x.id, config.delete_output_user_ids),
+                'inputUserIds': map(lambda x: x.id, config.delete_input_user_ids),
+                'transferUserIds': map(lambda x: x.id, config.delete_transfer_user_ids)
+            },
+            'statementOpen': config.statement_open_config,
+            'statementConfirm' :{
+                'userIds': map(lambda x: x.id, config.statement_confirm_user),
+                'transferUserIds': map(lambda x: x.id , config.statement_confirm_transfer_user),
+                'balanceUserIds': map(lambda x: x.id, config.statement_confirm_balance),
+                'negativeAmountUserIds': map(lambda x: x.id, config.statement_confirm_negative_amount)
+            },
+            'statementCancelUserIds': map(lambda x: x.id, config.statement_cancel_user),
+            'statementUnlinkUserIds': map(lambda x: x.id, config.statement_unlink_user)
+        } for config in request.env['account.bank.statement.config'].search([('active', '=', True)],order='id')]
+
+    ''' Make JSON response to send '''
+    def make_response(self, data=None, status=200):
+        return Response(json.dumps(data), status=status, content_type='application/json')
+
+    ''' Make GZIP to JSON response '''
+    def make_gzip_response(self, data=None, status=200):
+        gzip_buffer = IO()
+        with GzipFile(mode='wb', compresslevel=GZIP_COMPRESSION_LEVEL, fileobj=gzip_buffer) as gzip_file:
+            gzip_file.write(json.dumps(data))
+
+        contents = gzip_buffer.getvalue()
+        gzip_buffer.close()
+
+        headers = Headers()
+        headers.add('Content-Encoding', 'gzip')
+        headers.add('Vary', 'Accept-Encoding')
+        headers.add('Content-Length', len(contents))
+
+        return Response(contents, status=status, headers=headers, content_type='application/json')
+
+    ''' Logger info '''
+    def make_info_log(self, log):
+        LOGGER.info('\033[1;34m[INFO] --> \033[m{}'.format(log))
+
+    ''' Bank statement Init '''
+    @http.route('/eiru_bank_statement/init', auth='user', methods=['GET'], cors='*')
+    def init_bank_statement(self,**kw):
+
+        self.make_info_log('Preparing data to {}'.format(kw.get('mode')))
+        journals = self.get_journals([kw.get('mode')]),
+
+        return self.make_gzip_response({
+            'date': self.get_server_date(),
+            'user': self.get_user(),
+            'currencies': self.get_currencies_from_journal(),
+            'journals': journals,
+            'statement': self.get_account_bank_statement(map(lambda x: x['id'], journals[0])),
+            'statementConfig' : self.get_account_bank_statement_config(),
+        #     'suppliers': self.get_suppliers(),
+        #     'products': self.get_products(kw.get('mode')),
+        #     'pickingTypes': self.get_picking_types(),
+        #     'paymentTerms': self.get_payment_terms(),
+        #     'banks': self.get_banks(),
+        #     'bankPaymentTypes': self.get_bank_payment_types(),
+        #     'chequeTypes': self.get_cheque_types()
+        })

+ 31 - 32
package.json

@@ -6,40 +6,39 @@
   "private": true,
   "scripts": {
     "start": "export NODE_ENV=development && ./node_modules/.bin/webpack --watch --config webpack.config.js",
-    "build": "export NODE_ENV=production && ./node_modules/.bin/webpack --progress --config webpack.config.js"
+    "build": "export NODE_ENV=production &&./node_modules/.bin/webpack --progress --config webpack.config.js"
   },
   "devDependencies": {
-    "babel-core": "^6.26.3",
-    "babel-loader": "^8.0.4",
-    "babel-preset-env": "^1.7.0",
-    "babel-preset-stage-2": "^6.24.1",
-    "copy-webpack-plugin": "^4.6.0",
-    "css-loader": "0.28.9",
-    "extract-text-webpack-plugin": "^3.0.2",
-    "file-loader": "1.1.6",
-    "hard-source-webpack-plugin": "^0.12.0",
-    "node-sass": "^4.9.4",
-    "offline-plugin": "^5.0.5",
-    "pug": "^2.0.3",
-    "sass-loader": "^7.1.0",
-    "style-loader": "^0.23.1",
-    "url-loader": "^1.1.2",
-    "vue-loader": "^15.4.2",
-    "vue-template-compiler": "^2.5.17",
-    "webpack": "^4.23.1",
-    "webpack-cli": "^3.1.2",
-    "webpack-livereload-plugin": "^2.1.1"
-  },
+ 		"babel-core": "^6.26.0",
+ 		"babel-loader": "^7.1.2",
+ 		"babel-preset-env": "^1.6.1",
+ 		"babel-preset-stage-2": "^6.24.1",
+ 		"copy-webpack-plugin": "^4.5.2",
+ 		"css-loader": "^0.28.9",
+ 		"extract-text-webpack-plugin": "^3.0.2",
+ 		"file-loader": "^1.1.6",
+ 		"hard-source-webpack-plugin": "^0.5.16",
+ 		"node-sass": "^4.7.2",
+ 		"offline-plugin": "^5.0.5",
+ 		"pug": "^2.0.0-rc.4",
+ 		"sass-loader": "^6.0.6",
+ 		"style-loader": "^0.20.1",
+ 		"url-loader": "^0.6.2",
+ 		"vue-loader": "^14.0.3",
+ 		"vue-template-compiler": "^2.5.13",
+ 		"webpack": "^3.10.0",
+ 		"webpack-livereload-plugin": "^1.0.0"
+ 	},
   "dependencies": {
-    "axios": "^0.18.0",
-    "fuse.js": "^3.3.0",
-    "install": "^0.12.2",
-    "velocity-animate": "^1.5.2",
-    "vue": "^2.5.17",
-    "vue-form-wizard": "^0.8.4",
-    "vue-js-modal": "^1.3.26",
-    "vue2-datepicker": "^2.6.2",
-    "vuex": "^3.0.1",
-    "vuex-persistedstate": "^2.5.4"
+      "axios": "^0.17.1",
+      "fuse.js": "^3.2.0",
+      "install": "^0.12.1",
+      "velocity-animate": "^2.0.5",
+      "vue": "^2.5.13",
+      "vue-form-wizard": "^0.8.2",
+      "vue-js-modal": "^1.3.9",
+      "vue2-datepicker": "^2.0.3",
+      "vuex": "^3.0.1",
+      "vuex-persistedstate": "^2.5.4"
   }
 }

+ 226 - 0
src/App.vue

@@ -0,0 +1,226 @@
+<template lang="pug">
+    .statement
+        form-wizard(title='' subtitle='' finishButtonText='Finalizar' color='#7c7bad' nextButtonText='Continuar' backButtonText='Volver' @on-complete='createPurchase' ref='wizard')
+            tab-content(title='Seleccione la caja')
+                statement-step
+</template>
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+
+    import { FormWizard, TabContent } from 'vue-form-wizard'
+    import 'vue-form-wizard/dist/vue-form-wizard.min.css'
+
+    /* steps */
+    import StatementStep from '@/components/steps/StatementStep'
+    // import SupplierStep from '@/components/steps/SupplierStep'/
+    // import ProductStep from '@/components/steps/ProductStep'
+    // import PaymentStep from '@/components/steps/PaymentStep'
+    // import { LoadingOverlay } from '@/components/common'
+
+    export default {
+        components: {
+            FormWizard,
+            TabContent,
+            StatementStep
+            // SupplierStep,
+            // ProductStep,
+            // PaymentStep,
+            // LoadingOverlay
+        },
+        // computed: mapGetters([
+        //     'isLoading',
+        //     'isCompleted',
+        //     'isProcessing',
+        //     'state',
+        //     'mode'
+        // ]),
+        // methods: {
+        //     ...mapActions([
+        //         'initPurchase',
+        //         'checkSupplier',
+        //         'checkCart',
+        //         'checkAmountReceived',
+        //         'createPurchase',
+        //         'resetPurchase'
+        //     ])
+        // },
+        // watch: {
+        //     isCompleted(value) {
+        //         if (!value) {
+        //             return
+        //         }
+        //
+        //         this.$refs.wizard.changeTab(2, 0, false)
+        //         this.resetPurchase()
+        //     }
+        // },
+        // mounted() {
+        //     this.initPurchase(this.$root.mode)
+        // }
+    }
+</script>
+
+<style lang="sass">
+    @import './assets/variables'
+
+    .statement
+        width: 100%
+        height: 100%
+        position: absolute
+        .vue-form-wizard
+            width: 100%
+            height: 100%
+            padding-bottom: 0
+            .wizard-header
+                display: none
+            .wizard-navigation
+                width: 100%
+                height: 100%
+                .wizard-progress-with-circle
+                    top: 35px
+                .wizard-icon-circle
+                    width: 60px
+                    height: 60px
+                .wizard-tab-content
+                    width: 100%
+                    height: calc(100% - 82px)
+                    padding: 0
+                    overflow: hidden
+                    .wizard-tab-container
+                        width: calc(100% - 20px)
+                        height: calc(100% - 20px)
+                        margin: 10px
+                        .purchase-step
+                            width: 100%
+                            height: 100%
+                            background: $app-bg-color
+            .wizard-card-footer
+                width: 100%
+                height: 50px
+                position: absolute
+                bottom: 10px
+                .wizard-btn
+                    width: 160px
+                    height: 40px
+                    border-radius: 0
+                    box-shadow: none
+                    border: none
+                    &:hover, &:focus
+                        background: $app-main-color
+</style>
+
+<!--
+<template lang="pug">
+    .purchases
+        form-wizard(title='' subtitle='' finishButtonText='Finalizar' :hideButtons='isProcessing' color='#7c7bad' nextButtonText='Continuar' backButtonText='Volver' @on-complete='createPurchase' ref='wizard')
+            tab-content(title='Cuál es su proveedor?' :before-change='checkSupplier')
+                supplier-step
+            tab-content(:title="mode === 'purchase' ? 'Qué productos comprarás?' : 'En que gastarás?'" :before-change='checkCart')
+                product-step
+            tab-content(title='Cómo quieres pagar?' :before-change='checkAmountReceived')
+                payment-step
+        loading-overlay(:show='isLoading')
+</template>
+
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+
+    import { FormWizard, TabContent } from 'vue-form-wizard'
+    import 'vue-form-wizard/dist/vue-form-wizard.min.css'
+
+    import SupplierStep from '@/components/steps/SupplierStep'
+    import ProductStep from '@/components/steps/ProductStep'
+    import PaymentStep from '@/components/steps/PaymentStep'
+
+    import { LoadingOverlay } from '@/components/common'
+
+    export default {
+        components: {
+            FormWizard,
+            TabContent,
+            SupplierStep,
+            ProductStep,
+            PaymentStep,
+            LoadingOverlay
+        },
+        computed: mapGetters([
+            'isLoading',
+            'isCompleted',
+            'isProcessing',
+            'state',
+            'mode'
+        ]),
+        methods: {
+            ...mapActions([
+                'initPurchase',
+                'checkSupplier',
+                'checkCart',
+                'checkAmountReceived',
+                'createPurchase',
+                'resetPurchase'
+            ])
+        },
+        watch: {
+            isCompleted(value) {
+                if (!value) {
+                    return
+                }
+
+                this.$refs.wizard.changeTab(2, 0, false)
+                this.resetPurchase()
+            }
+        },
+        mounted() {
+            this.initPurchase(this.$root.mode)
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import './assets/variables'
+
+    .purchases
+        width: 100%
+        height: 100%
+        position: absolute
+        .vue-form-wizard
+            width: 100%
+            height: 100%
+            padding-bottom: 0
+            .wizard-header
+                display: none
+            .wizard-navigation
+                width: 100%
+                height: 100%
+                .wizard-progress-with-circle
+                    top: 35px
+                .wizard-icon-circle
+                    width: 60px
+                    height: 60px
+                .wizard-tab-content
+                    width: 100%
+                    height: calc(100% - 82px)
+                    padding: 0
+                    overflow: hidden
+                    .wizard-tab-container
+                        width: calc(100% - 20px)
+                        height: calc(100% - 20px)
+                        margin: 10px
+                        .purchase-step
+                            width: 100%
+                            height: 100%
+                            background: $app-bg-color
+            .wizard-card-footer
+                width: 100%
+                height: 50px
+                position: absolute
+                bottom: 10px
+                .wizard-btn
+                    width: 160px
+                    height: 40px
+                    border-radius: 0
+                    box-shadow: none
+                    border: none
+                    &:hover, &:focus
+                        background: $app-main-color
+</style> -->

+ 8 - 0
src/assets/_variables.sass

@@ -0,0 +1,8 @@
+$app-main-color: #7c7bad
+$app-dark-color: #666
+$app-light-color: #f5f5f5
+$app-bg-color: #fff
+$app-border-color: #d3d3d3
+$app-title-color: #d3d3d3
+$app-separator-color: #9e9e9e
+$app-error-color: #ef9a9a

+ 38 - 0
src/components/common/AddCard.vue

@@ -0,0 +1,38 @@
+<template lang="pug">
+    .add-card(@click='onClick')
+        i.fa.fa-plus(aria-hidden='true')
+</template>
+
+<script>
+    export default {
+        methods: {
+            onClick(e) {
+                this.$emit('onClickAdd')
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+
+    .add-card
+        width: 130px
+        height: 160px
+        margin: 5px
+        border: 1px solid $app-border-color
+        display: inline-block
+        position: relative
+        &:hover
+            cursor: pointer
+        i
+            font-size: 36pt
+            margin: 0
+            border: none
+            position: absolute;
+            top: 50%
+            left: 50%
+            margin-right: -50%
+            transform: translate(-50%, -50%)
+            color: $app-main-color
+</style>

+ 101 - 0
src/components/common/Card.vue

@@ -0,0 +1,101 @@
+<template lang="pug">
+    .card(@click='onClick' :class="{ 'selected-card': isSelected }")
+        h2.card-title {{ title }}
+        img.card-image(:src="'data:image/png;base64,' + (image || 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAACXZwQWcAAABAAAAAQADq8/hgAAAEWklEQVRYw9WX6XKjRhCAef8HiySQvGt5vfZuEselOUAcEpe4GdI9MAgQOjb5k3SVyzY1801PX9OtNf9StP80QJR5miRpXtb/AFCnvmMySgmhlJn2Mal+BSBSj1NCGeNSGAMOd0/iQYCI95TAXnm+FCr/I2ZYPwJILEJhPaGm7flBFIW+Z5sUvwEivguovG7pMR0cV2e+BbYArF3cBqQclKfEvryvSB2KaHa6BYhgDSP7ZN7gmUNQCf86wCdgcBaKq04/cTzAuwbA/czKb8VdZYMSI8IAEOJ+XjTiFkF4SDjOARIIHLiBK+4E/xHOIdEloMSAAwZx7hEOBKIquwA4lFPbR/3uEhzCqSUmgBiwrGgeIlQm5b0zO0CN3yKw34QgQC4JKZqrGAFC0MpWvuwJ3V6hWD3BI5wchoDaBAumzYQgmsrd7ewZx5bosHIAAAtQp4+nXUuA+2yXy9Xyi4OsIorjauBLZQWtd0Gqrt3EvCXQlb4BMZYfsPP7cr0gvS4FaNw6Qus0ovtez8DZcYyHt8Wmk9XWdF+Mjf570Ke4q46UgAgUCtX55mKl/wSbsD83hrEE0VGJ1RrEWHz2aaXuIAEe7b3SNG/601oSzL/W20/T2r2uDNACARvjWelZQTTaCiCg2vSR1bzrsFgSQMk8SbPi8FWX+0GFbX2OXMarDoAmOGfo+wpXt7cwj4Hv+1n+rSMYW3HOfS4TAgHZIDIVYG38wNzchyB+kj4ZUwB4npw6ABokmgA2qz9kfbIkoWDLzQSQ0tbw2gA20kA/nmyqCHG8nmqQd2prbSKQZAIwnk5B5PSE/EWfACCUZGFSgHQKeE6DsCcExfc5wKEDRLMaJHBwTwA/zFzhOLBBPGODoCfEyYUb0XVBB1AGHXvho/SVDsSjF15QrtMG1xlpsDbCrCewj7UxAWAJSjsAlJOuHI0AX9Mi8IMgsJnMC2MMOJA2f7RhXI8AG/2LVxZZVlQWmKElnAFiT5nMH62L67Mb3lTmbIzVK3Uc9r6GvJAEyMa6d0KXP1oXliqbRPPzN0NvBcrBAmSpr37wlrB8GeRS6zkJECZVNRKeuLfty1C+wc/zp7TD9jVQN7DUDq2vkUEzfAymIl9uZ5iL1B0U1Rw7surmc4SE/sUBE3KaDB8Wd1QS7hJQga4Kayow2aAsXiV0L458HE/jx9UbPi33CIf+ITwDSnxM/IcIcAGIrHzaH+BX8Ky4awdq41nBZYsjG4/kEQLjg9Q5A9A1jJ7u3CJEa1OzmuvSKgubwPA24IT7WT7fJ5YmEtwbASWO2AkP94871WpPOCc8vmYHaORhv5lf75VrV3bD+9nZIrUJamhXN9v9kMlu3wonYVlGe9msU1/cGTgKpx0YmO2fsrKq66rMk8Bh7dd99sDIk+xxxsE5icqhqfsLflkz1pkbukSCBzI5bqG0EGrPGvfK2FeGDseRi1I5eVFuB8WvDp51FvsH13Fcz4+y6n86Oz8kfwPMD02INEiadQAAAABJRU5ErkJggg==')")
+        .card-details(v-if='details.length > 0')
+            span(v-for='detail in details') {{ computeDetail(detail) }}
+</template>
+
+<script>
+    export default {
+        props: {
+            title: {
+                type: String,
+                default: 'Sin título'
+            },
+            image: {
+                type: String,
+                default: 'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAACXZwQWcAAABAAAAAQADq8/hgAAAEWklEQVRYw9WX6XKjRhCAef8HiySQvGt5vfZuEselOUAcEpe4GdI9MAgQOjb5k3SVyzY1801PX9OtNf9StP80QJR5miRpXtb/AFCnvmMySgmhlJn2Mal+BSBSj1NCGeNSGAMOd0/iQYCI95TAXnm+FCr/I2ZYPwJILEJhPaGm7flBFIW+Z5sUvwEivguovG7pMR0cV2e+BbYArF3cBqQclKfEvryvSB2KaHa6BYhgDSP7ZN7gmUNQCf86wCdgcBaKq04/cTzAuwbA/czKb8VdZYMSI8IAEOJ+XjTiFkF4SDjOARIIHLiBK+4E/xHOIdEloMSAAwZx7hEOBKIquwA4lFPbR/3uEhzCqSUmgBiwrGgeIlQm5b0zO0CN3yKw34QgQC4JKZqrGAFC0MpWvuwJ3V6hWD3BI5wchoDaBAumzYQgmsrd7ewZx5bosHIAAAtQp4+nXUuA+2yXy9Xyi4OsIorjauBLZQWtd0Gqrt3EvCXQlb4BMZYfsPP7cr0gvS4FaNw6Qus0ovtez8DZcYyHt8Wmk9XWdF+Mjf570Ke4q46UgAgUCtX55mKl/wSbsD83hrEE0VGJ1RrEWHz2aaXuIAEe7b3SNG/601oSzL/W20/T2r2uDNACARvjWelZQTTaCiCg2vSR1bzrsFgSQMk8SbPi8FWX+0GFbX2OXMarDoAmOGfo+wpXt7cwj4Hv+1n+rSMYW3HOfS4TAgHZIDIVYG38wNzchyB+kj4ZUwB4npw6ABokmgA2qz9kfbIkoWDLzQSQ0tbw2gA20kA/nmyqCHG8nmqQd2prbSKQZAIwnk5B5PSE/EWfACCUZGFSgHQKeE6DsCcExfc5wKEDRLMaJHBwTwA/zFzhOLBBPGODoCfEyYUb0XVBB1AGHXvho/SVDsSjF15QrtMG1xlpsDbCrCewj7UxAWAJSjsAlJOuHI0AX9Mi8IMgsJnMC2MMOJA2f7RhXI8AG/2LVxZZVlQWmKElnAFiT5nMH62L67Mb3lTmbIzVK3Uc9r6GvJAEyMa6d0KXP1oXliqbRPPzN0NvBcrBAmSpr37wlrB8GeRS6zkJECZVNRKeuLfty1C+wc/zp7TD9jVQN7DUDq2vkUEzfAymIl9uZ5iL1B0U1Rw7surmc4SE/sUBE3KaDB8Wd1QS7hJQga4Kayow2aAsXiV0L458HE/jx9UbPi33CIf+ITwDSnxM/IcIcAGIrHzaH+BX8Ky4awdq41nBZYsjG4/kEQLjg9Q5A9A1jJ7u3CJEa1OzmuvSKgubwPA24IT7WT7fJ5YmEtwbASWO2AkP94871WpPOCc8vmYHaORhv5lf75VrV3bD+9nZIrUJamhXN9v9kMlu3wonYVlGe9msU1/cGTgKpx0YmO2fsrKq66rMk8Bh7dd99sDIk+xxxsE5icqhqfsLflkz1pkbukSCBzI5bqG0EGrPGvfK2FeGDseRi1I5eVFuB8WvDp51FvsH13Fcz4+y6n86Oz8kfwPMD02INEiadQAAAABJRU5ErkJggg=='
+            },
+            details: {
+                type: Array,
+                default: []
+            },
+            isSelected: {
+                type: Boolean,
+                default: true
+            },
+            options: {
+                type: Object,
+                default: {}
+            }
+        },
+        methods: {
+            computeDetail(detail) {
+                if (detail.format === 'currency') {
+                    return this.$options.filters.currency(detail.value, {...this.options})
+                }
+
+                return detail.value
+            },
+            onClick() {
+                this.$emit('onClick')
+            }
+        } 
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+
+    .card
+        width: 130px
+        height: 160px
+        margin: 5px
+        border: 1px solid $app-border-color
+        display: inline-block
+        position: relative
+        &.selected-card
+            transition-duration: 300ms
+            // border-bottom: 3px solid $app-main-color
+            border: 2px solid $app-main-color
+        &:hover
+            cursor: pointer
+        .card-title
+            width: 100%
+            height: 30px
+            font-size: 9pt
+            text-align: center
+            margin-top: 10px
+            position: absolute
+            top: 0
+        .card-image
+            width: 80px
+            height: 80px;
+            margin: 0
+            border: none
+            position: absolute
+            top: 50%
+            left: 50%
+            margin-right: -50%
+            transform: translate(-50%, -50%)
+        .card-details
+            width: 100%
+            height: 30px
+            padding-top: 5px
+            text-align: center
+            font-size: 10pt
+            font-weight: bold
+            background: $app-main-color
+            color: $app-bg-color
+            position: absolute
+            bottom: 0
+
+        @keyframes card-bubble
+            30%
+                transform: scaleX(0.75) scaleY(1.25)
+            40%
+                transform: scaleX(1.25) scaleY(0.75)
+            60%
+                transform: scaleX(0.85) scaleY(1.15)
+</style>

+ 151 - 0
src/components/common/CardGrid.vue

@@ -0,0 +1,151 @@
+<template lang="pug">
+    .card-grid-wrapper
+        .card-grid
+            add-card(v-if='canAdd' @onClickAdd='onClickAdd')
+            card(v-for='item in items' :key='item.id' :title='item.name' :image='item.imageMedium' :isSelected='item.id === selectedId' :details='computeDetails(item)' :options='defaultOptions.currency' @onClick='onClickCard(item)')
+        .no-items(v-show='!items || items.length == 0')
+            p No hay items
+</template>
+
+<script>
+    import AddCard from '@/components/common/AddCard'
+    import Card from '@/components/common/Card'
+
+    export default {
+        props: {
+            items: {
+                type: Array,
+                default: []
+            },
+            canAdd: {
+                type: Boolean,
+                default: false
+            },
+            details: {
+                type: Array,
+                default: []
+            },
+            loading: {
+                type: Boolean,
+                default: false
+            },
+            options: {
+                type: Object,
+                default: {}
+            }
+        },
+        components: {
+            AddCard,
+            Card
+        },
+        watch: {
+            options(value) {
+                this.computeOptions(value)
+            }
+        },
+        methods: {
+            computeDetails(item) {
+                if (!this.details) {
+                    return []
+                }
+
+                if (this.details.length === 0) {
+                    return []
+                }
+
+                let results = []
+                let computableDetails = this.details.map(item => item.split(/:/))
+
+                for (let detail of computableDetails) {
+                    for (let field in item) {
+                        if (field === detail[0]) {
+                            results.push({
+                                value: item[field],
+                                format: (() => {
+                                    if (!detail[1] || detail[1] === 's') {
+                                        return 'string'
+                                    }
+                                    
+                                    if (detail[1] === 'c') {
+                                        return 'currency'
+                                    }
+
+                                    if (detail[1] === 'd') {
+                                        return 'date'
+                                    }
+
+                                    return 'string'
+                                })()
+                            })
+
+                            break
+                        }
+                    }
+                }
+
+                return results
+            },
+            computeOptions(value) {
+                if (!value) {
+                    return
+                }
+
+                for(let key in value) {
+                    if(!this.defaultOptions.currency[key]) {
+                        continue
+                    }
+
+                    this.defaultOptions.currency[key] = value[key]
+                }
+            },
+            onClickAdd() {
+                this.$emit('onAdd')
+            },
+            onClickCard(item) {
+                this.selectedId = item.id
+                this.$emit('onSelect', item)
+            }
+        },
+        data() {
+            return {
+                selectedId: -1,
+                defaultOptions: {
+                    currency: {
+                        symbol: '$',
+                        position: 'before',
+                        thousandsSeparator: '.',
+                        decimalPlaces: 2,
+                        decimalSeparator: ',' 
+                    },
+                } 
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    .card-grid-wrapper
+        width: 100%
+        height: calc(100% - 50px)
+        margin-top: 10px
+        overflow-y: auto
+        &::-webkit-scrollbar
+            width: 2px
+            background: #f5f5f5
+        &::-webkit-scrollbar-thumb
+            background: #7c7bad
+        &::-webkit-scrollbar-track
+            -webkit-box-shadow: inset 0 0 6px #d3d3d3
+            background: #f5f5f5
+        .card-grid
+            width: 100%
+        .no-items
+            width: 100%
+            height: 100%
+            display: flex
+            align-items: center
+            justify-content: center
+            p
+                color: #9e9e9e
+                font-size: 11pt
+</style>

+ 138 - 0
src/components/common/Cart.vue

@@ -0,0 +1,138 @@
+<template lang="pug">
+    .cart(:style='{ width: defaultOptions.layout.width, height: defaultOptions.layout.height }')
+        .cart-total
+            h2.currency-cart-total {{ total | currency(...defaultOptions.currency) }}
+        .cart-items-wrapper
+            transition-group(name='list' tag='ul' class='cart-items')
+                cart-item(v-for='(item, index) in items' :key='index' :index='index' :item='item' @onClickQuantity='onClickQuantity' @onChange='onItemChanged' @onClickIncrement='onIncrementQty' @onClickDecrement='onDecrementQty' @onClickMoney='onChangePrice' @onClickUndo='onUndoPrice' @onClickDelete='onDeleteItem' :options='defaultOptions.currency')
+</template>
+
+<script>
+    import CartItem from './CartItem'
+
+    export default {
+        components: {
+            CartItem
+        },
+        props: {
+            items: {
+                type: Array,
+                default: [],
+                required: true
+            },
+            options: {
+                type: Object || String,
+                default: null
+            }
+        },
+        methods: {
+            computeOptions(value) {
+                if (!value) {
+                    return
+                }
+
+                for(let key in value) {
+                    if(!this.defaultOptions.currency[key]) {
+                        continue
+                    }
+
+                    this.defaultOptions.currency[key] = value[key]
+                }
+            },
+            computeTotal() {
+                let sum = 0
+
+                for (let item of this.items) {
+                    sum = sum + ((item.price || 0) * (item.quantity || 0))
+                }
+
+                this.total = sum
+
+                this.$emit('onTotalComputed', this.total)
+            },
+            onItemChanged(item) {
+                this.computeTotal();
+            },
+            onIncrementQty(item) {
+                this.$emit('onIncrementQty', item)
+            },
+            onDecrementQty(item) {
+                this.$emit('onDecrementQty', item)
+            },
+            onChangePrice(item) {
+                this.$emit('onChangePrice', item)
+            },
+            onUndoPrice(item) {
+                this.$emit('onUndoPrice', item)
+            },
+            onDeleteItem(item) {
+                this.$emit('onDeleteItem', item)
+            }
+        },
+        watch: {
+            items() {
+                this.computeTotal()
+            },
+            options(value) {
+                this.computeOptions(value)
+            }
+        },
+        data() {
+            return {
+                total: 0,
+                defaultOptions: {
+                    currency: {
+                        symbol: '$',
+                        position: 'before',
+                        thousandsSeparator: '.',
+                        decimalPlaces: 2,
+                        decimalSeparator: ',' 
+                    },
+                    layout: {
+                        width: '300px',
+                        height: '100%'
+                    }
+                }
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .cart
+        border-left: 1px solid $app-border-color
+        padding-left: 10px
+        display: inline-block
+        vertical-align: top
+        .cart-total
+            width: 100%
+            height: 50px
+            .currency-cart-total
+                width: 100%
+                height: 50px
+                margin: 0
+                font-size: 30pt
+        .cart-items-wrapper
+            width: 100%
+            height: calc(100% - 100px)
+            overflow-y: auto
+            overflow-x: hidden
+            &::-webkit-scrollbar
+                width: 2px
+                background: #f5f5f5
+            &::-webkit-scrollbar-thumb
+                background: #7c7bad
+            &::-webkit-scrollbar-track
+                -webkit-box-shadow: inset 0 0 6px #d3d3d3
+                background: #f5f5f5
+            .cart-items
+                width: 100%
+                padding: 0
+                margin: 0
+            .list-enter-active, .list-leave-active
+                transition: all 0.3s
+            .list-enter, .list-leave-to
+                opacity: 0
+                transform: translateX(300px)
+</style>

+ 236 - 0
src/components/common/CartItem.vue

@@ -0,0 +1,236 @@
+<template lang="pug">
+    li.cart-item(:class="{'cart-item-invalid': !isValid()}")
+        h3.item-name {{ item.displayName }}
+        input.item-quantity(type='number' v-model.number='quantity' @focus='onFocus' @blur='onBlur')
+        span.item-x x
+        span.item-price {{ item.price | currency(...options) }}
+        span.item-equals =
+        span.item-subtotal {{ (item.price * (item.quantity || 1)) | currency(...options) }}
+        .cart-item-options-wrapper
+            .cart-item-options
+                .cart-item-option(class='fa fa-plus' @click='onClickIncrement')
+                .cart-item-option(class='fa fa-minus' @click='onClickDecrement')
+                .cart-item-option(class='fa fa-money' @click='onClickMoney')
+                .cart-item-option(class='fa fa-undo' @click='onClickUndo')
+                .cart-item-option(class='fa fa-trash' @click='onClickDelete')
+</template>
+
+<script>
+    export default {
+        props: {
+            index: {
+                type: Number,
+                default: -1,
+                required: true
+            },
+            item: {
+                type: Object,
+                default: null
+            },
+            options: {
+                type: Object,
+                default: {
+                    symbol: '$',
+                    position: 'before',
+                    thousandsSeparator: '.',
+                    decimalPlaces: 2,
+                    decimalSeparator: ',' 
+                }
+            }
+        },
+        computed: {
+            quantity: {
+                get() {
+                    return this.item.quantity
+                },
+                set(value) {
+                    this.input = !value ? 1 : value
+
+                    if (this.editing) {
+                        this.handleEditing(value)
+
+                        return
+                    }
+
+                    this.computeQuantity()
+                }
+            }
+        },
+        watch: {
+            item: {
+                handler(value) {
+                    this.onChange(value)
+                },
+                deep: true,
+                immediate: true
+            }
+        },
+        methods: {
+            handleEditing(value) {
+                if (value === '') {
+                    clearTimeout(this.inputDaemon)
+                    return
+                }
+
+                if (this.inputDaemon !== null) {
+                    clearTimeout(this.inputDaemon)
+                }
+
+                this.inputDaemon = setTimeout(() => {
+                    this.computeQuantity()
+                }, 300)
+            },
+            computeQuantity() {
+                if (this.input > this.item.quantity) {
+                    this.onClickIncrement()
+                } else {
+                    this.onClickDecrement()
+                }
+            },
+            onFocus() {
+                this.editing = true
+                this.input = 0
+            },
+            onBlur() {
+                this.editing = false
+                this.input = 0
+            },
+            onInputChange(e) {
+                console.log(e)
+            },
+            onChange(item) {
+                this.$emit('onChange', item)
+            },
+            onClickIncrement() {
+                this.$emit('onClickIncrement', {
+                    id: this.item.id,
+                    quantity: this.editing ? this.input : 1
+                })
+            },
+            onClickDecrement() {
+                this.$emit('onClickDecrement', {
+                    id: this.item.id,
+                    quantity: this.editing ? this.input : -1
+                })
+            },
+            onClickMoney() {
+                this.$emit('onClickMoney', this.item)
+            },
+            onClickUndo() {
+                this.$emit('onClickUndo', this.item)
+            },
+            onClickDelete() {
+                this.$emit('onClickDelete', {
+                    id: this.item.id
+                })
+            },
+            isValid() {
+                return this.item.price > 0
+            }
+        },
+        data() {
+            return {
+                editing: false,
+                input: 0,
+                inputDaemon: null
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .cart-item
+        width: 100%
+        height: 90px
+        list-style: none outside none
+        border-bottom: 1px solid $app-border-color
+        box-sizing: border-box
+        position: relative
+        &.cart-item-invalid
+            border-bottom: 2px solid $app-error-color
+        &:nth-child(1)
+            border-top: 1px solid $app-border-color
+        &:hover
+            transition-duration: 1000ms
+            border-bottom: 2px solid $app-main-color
+        .item-name
+            width: 100%
+            height: 20px
+            margin: 10px 0 5px 0
+            float: left
+            font-size: 8pt
+            display: inline-block
+        .item-quantity
+            width: 60px
+            height: 28px
+            margin-top: 6px
+            text-align: right
+            float: left
+            display: inline-block
+            user-select: none
+            cursor: pointer
+            border-radius: 0
+        .item-x
+            width: 20px
+            height: 20px
+            margin-top: 12px
+            text-align: right
+            float: left
+            display: inline-block
+        .item-price
+            width: 80px
+            height: 20px
+            margin-top: 12px
+            text-align: right
+            float: left
+            display: inline-block
+        .item-equals
+            width: 20px
+            height: 20px
+            margin-top: 12px
+            text-align: center
+            float: left
+            display: inline-block
+        .item-subtotal
+            width: 100px
+            height: 20px
+            margin-top: 12px
+            text-align: right
+            font-weight: bold
+            display: inline-block
+        .cart-item-options-wrapper
+            width: 100%
+            height: 20px
+            position: absolute
+            bottom: 0
+            display: flex
+            justify-content: center
+            .cart-item-options
+                width: 120px
+                height: 20px
+                border: 1px solid #d3d3d3
+                border-bottom: none
+                display: flex
+                justify-content: center
+                .cart-item-option
+                    width: 18px
+                    height: 18px
+                    margin: 0 5px
+                    color: #666
+                    &:hover
+                        cursor: pointer
+                    &.fa
+                        padding-left: 2px
+                        line-height: 20px
+                        &.fa-plus:hover
+                            color: #2196f3
+                        &.fa-minus:hover
+                            color: #ffc107
+                        &.fa-money:hover
+                            color: #4caf50
+                        &.fa-undo:hover
+                            color: #3f51b5
+                        &.fa-trash:hover
+                            color: #f44336
+</style>

+ 210 - 0
src/components/common/DropdownSearcher.vue

@@ -0,0 +1,210 @@
+<template lang="pug">
+    .searcher
+        span.input-icon.fa.fa-search(
+            ref='searchSpan'
+            @click='onClickOptions'
+        )
+        input.search-input(
+            autofocus
+            v-model='search'
+            :placeholder='placeholder'
+        )
+        .dropdown-options(
+            ref='dropdownOptions'
+            :class="{'input-show': showOptions }"
+        )
+            ul.input-options
+                li.input-option(
+                    v-for='option in getOptions()'
+                    :key='option.id'
+                    @click='onSelectOption(option)'
+                ) 
+                    h2 {{ option.name }}
+</template>
+
+<script>
+    export default {
+        props: {
+            placeholder: {
+                type: String,
+                default: ''
+            },
+            autofocus: {
+                type: Boolean,
+                default: false
+            },
+            items: {
+                type: Array,
+                default: [],
+                required: true
+            },
+            keys: {
+                type: Array,
+                default: [],
+                required: true
+            }
+        },
+        watch: {
+            search(value, lastValue) {
+                value = value.trim()
+
+                if (!value && value.length != lastValue.length) {
+                    this.selectedOption = null
+                }
+
+                this.showOptions = !!value && !this.selectedOption
+
+                this.performSearch(value)
+            }
+        },
+        methods: {
+            getOptions() {
+                return this.results.length == 0 ? this.items : this.results
+            },
+            performSearch(value) {
+                this.results = []
+
+                if (this.selectedOption) {
+                    return
+                }
+                
+                for (let item of this.items) {
+                    for (let field in item) {
+                        if (typeof item[field] !== 'string') {
+                            continue
+                        }
+
+                        if (this.keys.length !== 0 && this.keys.indexOf(field) === -1) {
+                            continue
+                        }
+
+                        if (item[field].toLowerCase().indexOf(value.toLowerCase()) !== -1) {
+                            this.results.push(item)
+                            break
+                        }
+                    }
+                }
+            },
+            hideOptions() {
+                this.showOptions = false
+            },
+            onClickOptions() {
+                this.showOptions = !this.showOptions || !!this.search
+            },
+            onSelectOption(item) {
+                this.selectedOption = item
+
+                this.search = item.name
+                this.results = []
+                
+                this.$emit('onSelect', item)
+            },
+            onClickOutside(e) {
+                let searchSpan = this.$refs.searchSpan
+
+                if (!searchSpan) {
+                    return
+                }
+
+                let target = e.target
+
+                if (target === searchSpan) {
+                    return
+                }
+
+                let el = this.$refs.dropdownOptions
+
+                if (el !== target && !el.contains(target))  {
+                    this.hideOptions()
+                }
+            },
+            onKeyUp(e) {
+                if (e.code === 'ArrowUp') {
+                    // TODO: move selection up
+                }
+
+                if (e.code === 'ArrowDown') {
+                    // TODO: move selection down
+                }
+            },
+
+        },
+        created() {
+            document.addEventListener('click', this.onClickOutside)
+            document.addEventListener('keyup', this.onKeyUp)
+        },
+        destroyed() {
+            document.removeEventListener('click', this.onClickOutside)
+            document.removeEventListener('keyup', this.onKeyUp)
+        },
+        data() {
+            return {
+                search: '',
+                results: [],
+                selectedOption: null,
+                showOptions: false
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .searcher
+        width: 100%
+        height: 35px
+        position: relative
+        .input-icon
+            position: absolute
+            top: 10px
+            right: 10px
+            font-size: 12pt
+            color: $app-dark-color
+            &:hover
+                cursor: pointer
+        .search-input
+            width: 100%
+            height: 100%
+            border-radius: 0
+            font-size: 12pt
+            font-weight: normal
+            padding-right: 35px
+        .dropdown-options
+            width: 100%
+            height: 150px
+            display: none
+            position: absolute
+            background: $app-light-color
+            border: 1px solid $app-separator-color
+            box-shadow: 0 3px 5px $app-separator-color
+            z-index: 10
+            top: 35px
+            left: 0
+            right: 0
+            bottom: -325px
+            padding: 0
+            animation-duration: 500ms
+            overflow-y: auto
+            &.input-show
+                display: block
+            .input-options
+                padding-left: 0
+                .input-option
+                    height: 35px
+                    display: block
+                    text-align: left
+                    margin-top: 5px
+                    padding-top: 5px
+                    font-size: 10pt
+                    font-weight: normal
+                    border-bottom: 1px solid $app-border-color
+                    &:last-child
+                        margin-bottom: 10px
+                    &:hover
+                        border-bottom: 2px solid $app-main-color
+                        cursor: pointer
+                    h2
+                        font-size: 10pt
+                        font-weight: normal
+                        margin: 0 5px
+</style>

+ 279 - 0
src/components/common/InputDropdown.vue

@@ -0,0 +1,279 @@
+<template lang="pug">
+    div
+        .input-group
+            .input-group-button(v-if='hasPrefix()')
+                button(type='button') {{ prefix }}
+            input.input-formatted(
+                v-model='formattedValue'
+                type='text'
+                :disabled='!editable'
+                :autofocus='focus'
+            )
+            .input-group-button(v-if='hasSuffix()')
+                button(
+                    type='button'
+                    @click='onShowOptions()'
+                    ref='suffixButton'
+                ) {{ suffix }}
+                    span.caret(:style="{'display': hasOptions() ? 'inline-block' : 'none'}")
+                ul.dropdown(
+                    :style="{'display': isShowOptions ? 'block' : 'none'}"
+                    ref='suffixDropdown'
+                )
+                    li(
+                        v-for='(option, index) in options'
+                        :key='index'
+                    )
+                        a(@click='onClickOption(option)') {{ option }}
+</template>
+
+<script>
+    export default {
+        props: {
+            value: {
+                type: Number,
+                default: 0
+            },
+            format: {
+                type: String,
+                default: 'text'
+            },
+            editable: {
+                type: Boolean,
+                default: true
+            },
+            focus: {
+                type: Boolean,
+                default: false
+            },
+            prefix: {
+                type: String,
+                default: ''
+            },
+            suffix: {
+                type: String,
+                default: ''
+            },
+            options: {
+                type: Array,
+                default: []
+            }
+        },
+        computed: {
+            formattedValue: {
+                get() {
+                    let value = this.value
+
+                    if (this.format === 'number') {
+                        value = this.formatValue(value)
+                        value = !!this.value ? value : value.replace(/\d/, '')
+                        value = value.length === 0 ? 0 : value
+                    }
+
+                    return value
+                },
+                set(value) {
+                    if (this.format === 'number') {
+                        value = this.unformatValue(value)
+                    }
+
+                    this.value = value
+
+                    this.onChangeValue(value)
+                }
+            }
+        },
+        methods: {
+            formatValue(value, options) {
+                value = value.toString()
+
+                if (!(options instanceof Object)) {
+                    options = {}
+                }
+
+                options.thousandsSeparator = options.thousandsSeparator || '.'
+                options.decimalPlaces = options.decimalPlaces >= 0 || options.decimalPlaces <= 2 ? options.decimalPlaces : 2
+                options.decimalSeparator = options.decimalSeparator || ','
+                options.decimalZeros = !!options.decimalZeros
+
+                if (!!(`${options.thousandsSeparator}${options.decimalSeparator}`).replace(/\.,|,\./g, '')) {
+                    throw new Error('Same thousands and decimal separator is not allowed')
+                }
+
+                value = value.split('.')
+
+                value[0] = value[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, `$1${options.thousandsSeparator}`)
+
+                if (!!value[1]) {
+                    value[1] = Number.parseFloat(`1.${value[1]}e${options.decimalPlaces}`)
+                    value[1] = Math.round(value[1]).toString().replace(/^1/, '')
+                }
+
+                value = value.join(options.decimalSeparator)
+
+                if (!options.decimalZeros) {
+                    value = value.replace(/([\.|,]\d)0$/, '$1')
+                }
+
+                return value
+            },
+            unformatValue(value) {
+                value = value.replace(/[\.|,](\d{0,2}$)/, '?$1').split(/\?/)
+                value[0] = value[0].replace(/[^0-9]/g, '')
+                value = Number.parseFloat(value.join('.')) || 0
+
+                return value
+            },
+            hideOptions() {
+                this.isShowOptions = false
+            },
+            onChangeValue(value) {
+                this.$emit('onChangeValue', value)
+            },
+            hasPrefix() {
+                return !!this.prefix && this.prefix.length !== 0
+            },
+            hasSuffix() {
+                return !!this.suffix && this.suffix.length !== 0
+            },
+            hasOptions() {
+                return this.options.length !== 0
+            },
+            onShowOptions() {
+                if (!this.hasOptions()) {
+                    return
+                }
+                
+                this.isShowOptions = !this.isShowOptions
+            },
+            onClickOutside(e) {
+                let suffixButton = this.$refs.suffixButton
+
+                if (!suffixButton) {
+                    return
+                }
+
+                let target = e.target
+
+                if (target === suffixButton) {
+                    return
+                }
+
+                let el = this.$refs.suffixDropdown
+                
+                if (el !== target && !el.contains(target)) {
+                    this.hideOptions()
+                }
+            },
+            onClickOption(selectedOption) {
+                this.hideOptions()
+                this.$emit('onClickOption', selectedOption)
+            }
+        },
+        created() {
+            document.addEventListener('click', this.onClickOutside)
+        },
+        destroyed() {
+            document.removeEventListener('click', this.onClickOutside)
+        },
+        data() {
+            return {
+                value: '',
+                isShowOptions: false
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+
+    .input-group
+        position: relative
+        width: 100%
+        display: table
+        border-collapse: separate
+        .input-formatted
+            position: relative
+            width: 100%
+            height: 35px
+            float: left
+            z-index: 2
+            display: table-cell
+            margin-bottom: 0
+            border-radius: 0
+            font-size: 12pt
+            padding: 6px 12px
+            &:focus
+                z-index: 3
+        .input-group-button
+            position: relative
+            font-size: 0
+            width: 1%
+            white-space: nowrap
+            vertical-align: middle
+            display: table-cell
+            &:first-child
+                & > button
+                    margin-right: -1px
+            &:last-child
+                & > button
+                    margin-left: -1px
+            button
+                height: 35px
+                display: inline-block
+                margin-bottom: 0
+                text-align: center
+                vertical-align: middle
+                white-space: nowrap
+                border: 1px solid #ccc
+                border-radius: 0
+                box-shadow: none
+                background: $app-main-color
+                color: #fff
+                z-index: 2
+                position: relative
+                user-select: none
+                &:focus
+                    outline: 0 !important
+                    border: 1px solid #ccc
+                    box-shadow: none
+                &:caret
+                    display: inline-block
+                    width: 0
+                    height: 0
+                    margin-left: 0
+                    vertical-align: middle
+                    border-top: 4px dashed
+                    border-right: 4px solid transparent
+                    border-left: 4px solid transparent
+            .dropdown
+                position: absolute
+                top: 100%
+                float: left
+                left: auto
+                right: 0
+                list-style: none
+                text-align: left
+                border: 1px solid #ccc
+                background-color: #fff
+                background-clip: padding-box
+                min-width: 160px
+                padding: 5px 0
+                margin: 2px 0 0
+                z-index: 1000
+                font-size: 10pt
+                & > li > a
+                    height: 35px
+                    display: block
+                    padding: 3px 20px
+                    clear: both
+                    font-weight: normal
+                    line-height: 2.1
+                    white-space: nowrap
+                    color: #333
+                    border-bottom: 1px solid #e5e5e5
+                    &:hover, &:focus
+                        text-decoration: none
+                        border-bottom: 2px solid #7c7bad
+</style>

+ 34 - 0
src/components/common/InputSelect.vue

@@ -0,0 +1,34 @@
+<template lang="pug">
+    div
+        select.input-select(@change='onSelect')
+            option(
+                v-for='o in options'
+                :value='o.id'
+            ) {{ o.name }}
+</template>
+
+<script>
+    export default {
+        props: {
+            selected: {
+                type: Object,
+                default: ''
+            },
+            options: {
+                type: Array,
+                default: []
+            }
+        },
+        methods: {
+            onSelect(e) {
+                this.$emit('onSelect', e.target.value)
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    .input-select
+        width: 100%
+        height: 35px
+</style>

+ 48 - 0
src/components/common/LoadingOverlay.vue

@@ -0,0 +1,48 @@
+<template lang="pug">
+    transition(name='fade')
+        .loading-overlay(v-show='show')
+            spinner(type='wave')
+            h2.loading-text Por favor, espere...
+</template>
+
+<script>
+    import Spinner from './Spinner'
+
+    export default {
+        props: {
+            show: {
+                type: Boolean,
+                default: false
+            }
+        },
+        components: {
+            Spinner
+        }
+    }
+</script>
+
+<style lang="sass">
+    .loading-overlay
+        display: flex
+        justify-content: center
+        align-items: center
+        flex-direction: column
+        width: 100%
+        height: 100%
+        background: rgba(0, 0, 0, 0.8)
+        position: fixed
+        top: 0
+        left: 0
+        z-index: 9999
+        .spinner-wave
+            margin: auto !important
+            .spinner-rect
+                background: #fff !important
+        .loading-text
+            color: #fff
+            font-size: 8pt
+    .fade-enter-active, .fade-leave-active
+        transition: opacity .8s
+    .fade-enter, .fade-leave-to
+        opacity: 0
+</style>

+ 143 - 0
src/components/common/Searcher.vue

@@ -0,0 +1,143 @@
+<template lang="pug">
+    input.searcher(type='search' :placeholder='placeholder' :style='{ width, height }' v-model='search')
+</template>
+
+<script>
+    import Fuse from 'fuse.js'
+
+    export default {
+        props: {
+            items: {
+                type: Array,
+                default: [],
+                required: true
+            },
+            placeholder: {
+                type: String,
+                default: 'Buscar...'
+            },
+            width: {
+                type: String,
+                default: '100%'
+            },
+            height: {
+                type: String,
+                default: '35px'
+            },
+            shouldSort: {
+                type: Boolean,
+                default: true
+            },
+            threshold: {
+                type: Number,
+                default: 0.4,
+            },
+            location: {
+                type: Number,
+                default: 0
+            },
+            distance: {
+                type: Number,
+                default: 100
+            },
+            maxPatternLength: {
+                type: Number,
+                default: 32
+            },
+            minMatchCharLength: {
+                type: Number,
+                default: 1
+            },
+            keys: {
+                type: Array,
+                default: [],
+                required: true
+            },
+            mode: {
+                type: String,
+                default: 'fuzzy'
+            }
+        },
+        watch: {
+            items(values) {
+                if (this.mode !== 'fuzzy') {
+                    return
+                }
+
+                this.fuse.setCollection(values)
+            },
+            search(value) {
+                this.performSearch(value.trim())
+            },
+            results(values) {
+                this.$emit('onSearch', values)
+            }
+        },
+        methods: {
+            initFuse() {
+                this.fuse = new Fuse(this.items, {
+                    shouldSort: this.shouldSort,
+                    threshold: this.threshold,
+                    location: this.location,
+                    distance: this.distance,
+                    maxPatternLength: this.maxPatternLength,
+                    minMatchCharLength: this.minMatchCharLength,
+                    keys: this.keys
+                })
+            },
+            performSearch(value) {
+                if(!value) {
+                    this.results = []
+                    return
+                }
+
+                if (this.mode === 'fuzzy') {
+                    this.results = this.fuse.search(value)
+                } else {
+                    this.results = []
+
+                    for (let item of this.items) {
+                        for (let field in item) {
+                            if (typeof item[field] !== 'string') {
+                                continue
+                            }
+
+                            if (this.keys.length !== 0 && this.keys.indexOf(field) === -1) {
+                                continue
+                            }
+
+                            if (item[field].toLowerCase().indexOf(value.toLowerCase()) !== -1) {
+                                this.results.push(item)
+                                break
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        data() {
+            return {
+                fuse: null,
+                search: '',
+                results: []
+            }
+        },
+        mounted() {
+            if (this.mode !== 'fuzzy') {
+                return
+            }
+            
+            this.initFuse()
+        }
+    }
+</script>
+
+<style lang="sass">
+    .searcher
+        text-align: center
+        border-radius: 0 !important
+        font:
+            size: 11pt
+            style: normal
+            weight: bold
+</style>

+ 102 - 0
src/components/common/Spinner.vue

@@ -0,0 +1,102 @@
+<template lang="pug">
+    .spinner
+        .spinner-wave(v-if="type === 'wave'")
+            .spinner-rect.spinner-rect-1
+            .spinner-rect.spinner-rect-2
+            .spinner-rect.spinner-rect-3
+            .spinner-rect.spinner-rect-4
+            .spinner-rect.spinner-rect-5
+        .spinner-circle(v-if="type === 'circle'")
+            .spinner-circle-1.spinner-child
+            .spinner-circle-2.spinner-child
+            .spinner-circle-3.spinner-child
+            .spinner-circle-4.spinner-child
+            .spinner-circle-5.spinner-child
+            .spinner-circle-6.spinner-child
+            .spinner-circle-7.spinner-child
+            .spinner-circle-8.spinner-child
+            .spinner-circle-9.spinner-child
+            .spinner-circle-10.spinner-child
+            .spinner-circle-11.spinner-child
+            .spinner-circle-12.spinner-child
+</template>
+
+<script>
+    export default {
+        props: {
+            type: String,
+            default: 'wave'
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .spinner
+        .spinner-wave
+            $rect-count: 5
+            $animation-duration: 1000ms
+            $delay-range: 400ms
+
+            width: 50px
+            height: 40px
+            text-align: center
+            font-size: 10px
+            margin: 40px auto
+
+            .spinner-rect
+                width: 5px
+                height: 100%
+                background: $app-main-color
+                margin: 0 3px 0 0
+                display: inline-block
+                animation: spinner-rect-wave $animation-duration infinite ease-in-out
+
+            @for $i from 1 through $rect-count
+                .spinner-rect-#{$i}
+                    animation-delay: - $animation-duration + $delay-range / ($rect-count - 1) * ($i - 1)
+
+        .spinner-circle
+            $circle-count: 12
+            $animation-duration: 1200ms
+
+            margin: 40px auto
+            width: 40px
+            height: 40px
+            position: relative
+
+            .spinner-child
+                width: 100%
+                height: 100%
+                position: absolute
+                left: 0
+                top: 0
+            
+            .spinner-child:before
+                content: ''
+                display: block
+                margin: 0 auto
+                width: 15%
+                height: 15%
+                background: $app-main-color
+                border-radius: 100%
+                animation: spinner-circle-bounce $animation-duration infinite ease-in-out both
+            
+            @for $i from 2 through $circle-count
+                .spinner-circle#{$i}
+                    transform: rotate(360deg / $circle-count * ($i - 1))
+                .spinner-circle#{$i}:before
+                    animation-delay: - $animation-duration + $animation-duration / $circle-count * ($i - 1)
+    
+    @keyframes spinner-rect-wave
+        0%, 40%, 100%
+            transform: scaleY(0.4)
+        20%
+            transform: scaleY(1.0)
+
+    @keyframes spinner-circle-bounce
+        0%, 80%, 100%
+            transform: scale(0)
+        40%
+            transform: scale(1.0)
+</style>

+ 71 - 0
src/components/common/SwitchButtonInput.vue

@@ -0,0 +1,71 @@
+<template lang="pug">
+    .switch-button-input
+        button.switch-option(
+            id='primary'
+            :value='primaryValue'
+            :class="{'selected-option': selectedOption}"
+            @click.prevent='onChange'
+        ) {{ primaryValue }}
+        button.switch-option(
+            id='secondary'
+            :value='secondaryValue'
+            :class="{'selected-option': !selectedOption}"
+            @click.prevent='onChange'
+        ) {{ secondaryValue }}
+</template>
+
+<script>
+    export default {
+        props: {
+            primaryValue: {
+                type: String,
+                required: true
+            },
+            secondaryValue: {
+                type: String,
+                required: true
+            },
+            selectedValue: {
+                type: String,
+                default: null
+            }
+        },
+        methods: {
+            onChange(e) {
+                this.selectedOption = e.target.id === 'primary'
+                this.selectedValue = e.target.value
+
+                this.$emit('onChange', this.selectedOption)
+            }
+        },
+        data() {
+            return {
+                selectedOption: true
+            }
+        },
+        mounted() {
+            this.selectedOption = this.selectedValue === this.primaryValue
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+
+    .switch-button-input
+        .switch-option
+            width: 50%
+            height: 35px
+            border: 1px solid transparent
+            border-color: #adadad
+            box-shadow: none
+            border-radius: 0
+            background: $app-bg-color
+            &:hover
+                background: $app-light-color
+            &.selected-option
+                background: $app-main-color
+                box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125)
+                border-color: $app-main-color
+                color: $app-bg-color
+</style>

+ 162 - 0
src/components/common/Ticket.vue

@@ -0,0 +1,162 @@
+<template lang="pug">
+    .ticket
+        .ticket-summary
+            .ticket-summary-header
+                h3 {{ companyName }}
+                table
+                    tbody
+                        tr
+                            td Producto
+                            td Precio
+                            td Cant
+                            td Subtotal
+            .ticket-items-wrapper
+                table
+                    tbody
+                        tr(v-for='item in items' :key='item.id')
+                            td {{ item.name }}
+                            td {{ item.price }}
+                            td {{ item.quantity }}
+                            td {{ (item.price || 0) * (item.quantity || 0) }}
+            .ticket-summary-footer
+                table
+                    tbody
+                        tr
+                            td Total:
+                            td {{ total | currency(...defaultCurrency) }}
+                        tr
+                            td Cliente:
+                            td {{ customerName }}
+</template>
+
+<script>
+    export default {
+        props: {
+            companyName: {
+                type: String,
+                default: ''
+            },
+            customerName: {
+                type: String,
+                default: ''
+            },
+            defaultCurrency: {
+                type: Object,
+                default: {
+                    symbol: '$',
+                    position: 'before',
+                    thousandsSeparator: '.',
+                    decimalPlaces: 2,
+                    decimalSeparator: ',' 
+                }
+            },
+            total: {
+                type: Number,
+                default: 0
+            },
+            items: {
+                type: [],
+                default: []
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .ticket
+        width: 500px
+        height: 100%
+        .ticket-summary
+            width: 350px
+            height: 450px
+            border: 1px solid $app-border-color
+            margin: auto
+            box-shadow: -2px 2px 5pc $app-border-color, 2px 2px 5px $app-border-color
+            position: relative
+            .ticket-summary-header, .ticket-summary-footer
+                width: 100%
+                position: absolute
+            .ticket-summary-header
+                height: 65px
+                top: 0
+                h3
+                    text-align: center
+                    font-size: 14pt
+                    margin: 0 15px
+                    padding: 30px 0 15px 0
+                    color: $app-dark-color
+                table
+                    width: 308px
+                    height: 30px
+                    margin: 0 20px
+                    font-size: 7.5pt
+                    font-weight: bold
+                    tbody
+                        tr
+                            line-height: 30px
+                            border-top: 1px solid $app-border-color
+                            border-bottom: 1px solid $app-border-color
+                            td
+                                &:nth-child(1)
+                                    width: 180px
+                                    text-align: left
+                    
+                                &:nth-child(2)
+                                    width: 50px
+                                    text-align: right
+                    
+                                &:nth-child(3)
+                                    width: 30px
+                                    text-align: right
+
+                                &:nth-child(4)
+                                    width: 50px
+                                    text-align: right
+            .ticket-items-wrapper
+                width: 310px
+                height: 280px
+                margin: 95px 20px 75px 20px
+                padding-top: 5px
+                overflow-y: auto
+                table
+                    width: 100%
+                    font-size: 7.5pt
+                    tbody
+                        tr
+                            height: 28px
+                            line-height: 30px
+
+                            td
+                                &:nth-child(1)
+                                    width: 180px
+                    
+                                &:nth-child(2)
+                                    width: 50px
+                                    text-align: right
+                    
+                                &:nth-child(3)
+                                    width: 30px
+                                    text-align: right
+
+                                &:nth-child(4)
+                                    width: 50px
+                                    text-align: right
+            .ticket-summary-footer
+                width: 348px
+                height: 75px
+                bottom: 0
+                padding: 15px 25px
+                background: $app-bg-color
+                table
+                    width: 100%
+                    tbody
+                        tr
+                            height: 25px
+                            line-height: 20px
+                            td
+                                &:nth-child(1)
+                                    font-weight: bold
+                                &:nth-child(2)
+                                    text-align: right
+</style>

+ 23 - 0
src/components/common/index.js

@@ -0,0 +1,23 @@
+import CardGrid from './CardGrid'
+import Cart from './Cart'
+import DropdownSearcher from './DropdownSearcher'
+import InputDropdown from './InputDropdown'
+import InputSelect from './InputSelect'
+import LoadingOverlay from './LoadingOverlay'
+import Searcher from './Searcher'
+import Spinner from './Spinner'
+import SwitchButtonInput from './SwitchButtonInput'
+import Ticket from './Ticket'
+
+export {
+    CardGrid,
+    Cart,
+    DropdownSearcher,
+    InputDropdown,
+    InputSelect,
+    LoadingOverlay,
+    Searcher,
+    Spinner,
+    SwitchButtonInput,
+    Ticket
+}

+ 65 - 0
src/components/forms/StatementActionForm.vue

@@ -0,0 +1,65 @@
+<template lang="pug">
+    .statement-action
+        .form-header
+            h2 Acciones disponibles
+            hr
+            from.from-display
+                .form-separator
+                    h3 Seleccione una opción
+                .form-item
+                    button.form-action Modificar
+                .form-item
+                    button.form-action Crear transferencia
+                .form-item
+                    button.form-action Poner dinero
+                .form-item
+                    button.form-action Sacar dinero
+                .form-item
+                    button.form-action  Procesar Cierre
+                .form-item
+                    button.form-action  Eliminar caja
+                .form-item
+                    button.form-action  Reabrir Caja
+</template>
+
+<script>
+    export default {
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+
+    .statement-action
+        width: 250px
+        height: 100%
+        .form-header
+            h2
+                font-size: 10pt
+                color: $app-border-color
+                margin-left: 15px
+            hr
+                margin: 0 15px
+            .from-display
+                padding: 15px
+                .form-separator
+                    h3
+                        color: $app-separator-color
+                        font-size: 8pt
+                    hr
+                        margin-top: 5px
+                .form-item
+                    width: 100%
+                    margin-bottom: 15px
+                    .form-action
+                        width: 100%
+                        height: 40px
+                        border: none
+                        box-shadow: none
+                        border-radius: 0
+                        margin-right: 5px
+                        background: $app-main-color
+                        color: $app-bg-color
+                        font-size: 10pt
+
+</style>

+ 39 - 0
src/components/steps/StatementStep.vue

@@ -0,0 +1,39 @@
+<template lang="pug">
+    .statement-step
+        .statement-selection-setp
+            .statement-selector
+                searcher
+                card-grid
+            transition(name='slide-fade')
+                statement-action-form
+</template>
+
+<script>
+
+    import { Searcher, CardGrid } from '../common'
+    import StatementActionForm from '../forms/StatementActionForm'
+
+    export default {
+        components: {
+            Searcher,
+            CardGrid,
+            StatementActionForm
+        }
+    }
+</script>
+
+<style lang="sass">
+    .statement-step
+        .statement-selection-setp
+            width: 100%
+            height: 100%
+            display: flex
+            .statement-selector
+                width: 100%
+                height: 100%
+            .slide-fade-enter-active
+                transition: all 300ms ease
+            .slide-fade-enter
+                transition: translatex(300px)
+                opacity: 0
+</style>

+ 11 - 0
src/constants/app.js

@@ -0,0 +1,11 @@
+export const Modes = Object.freeze({
+    PURCHASE: 'purchase',
+    EXPENSE: 'expense'
+})
+
+export const States = Object.freeze({
+    NONE: 'none',
+    PROCESSING: 'processing',
+    DONE: 'done',
+    ERROR: 'error'
+})

+ 7 - 0
src/constants/resourcePaths.js

@@ -0,0 +1,7 @@
+const BASE_URL = '/eiru_purchases'
+
+export const INIT_PURCHASE_URL = `${BASE_URL}/init`
+
+export const CREATE_OBJECT_URL = `${BASE_URL}/create_object`
+
+export const PROCESS_PURCHASE_URL = `${BASE_URL}/process`

+ 61 - 0
src/index.js

@@ -0,0 +1,61 @@
+import Vue from 'vue'
+import App from '@/App'
+import VueModal from 'vue-js-modal'
+// import store from '@/store'
+
+// import currency from '@/components/filters/currency'
+// import absolute from '@/components/filters/absolute'
+
+// Vue.filter('currency', currency)
+// Vue.filter('absolute', absolute)
+
+Vue.use(VueModal)
+
+Vue.config.productionTip = false
+Vue.config.silent = true
+Vue.config.devTools = true
+
+openerp.eiru_bank_statement = (instance, local) => {
+
+    local.BankStatementWidget = instance.Widget.extend({
+        init(parent, action) {
+            this._super(parent)
+            this.mode = action.params.mode
+        },
+        start() {
+            console.log(this);
+            this.sidebarFold()
+
+            this.vm = new Vue({
+                // store,
+                el: this.el,
+                template: '<App />',
+                components: {
+                    App
+                },
+                data: {
+                    mode: this.mode
+                }
+            })
+        },
+        destroy() {
+            this.vm.$destroy()
+            $(this.vm.$el).remove()
+            this.vm = null
+
+            this.sidebarUnfold()
+
+            this._super.apply(this, arguments)
+        },
+        sidebarFold() {
+            if (!instance.eiru_sidebar_toggler) return
+            instance.eiru_sidebar_toggler.sidebar_fold()
+        },
+        sidebarUnfold() {
+            if (!instance.eiru_sidebar_toggler) return
+            instance.eiru_sidebar_toggler.sidebar_unfold()
+        }
+    })
+
+    instance.web.client_actions.add('eiru_bank_statement.action_launch', 'instance.eiru_bank_statement.BankStatementWidget')
+}

+ 25 - 0
src/templates.xml

@@ -0,0 +1,25 @@
+<openerp>
+    <data>
+        <template id="eiru_purchases.assets" name="Eiru Purchases" inherit_id="eiru_assets.assets">
+            <xpath expr="." position="inside">
+                <link rel="stylesheet" href="/eiru_purchases/static/src/main.css" />
+                <script type="text/javascript" src="/eiru_purchases/static/src/main.js" />
+            </xpath>
+        </template>
+
+        <record id="eiru_purchases.purchases_action" model="ir.actions.client">
+            <field name="name">Eiru Purchases</field>
+            <field name="tag">eiru_purchases.action_launch</field>
+            <field name="params">{'mode': 'purchase'}</field>
+        </record>
+
+        <record id="eiru_purchases.expenses_action" model="ir.actions.client">
+            <field name="name">Eiru Expenses</field>
+            <field name="tag">eiru_purchases.action_launch</field>
+            <field name="params">{'mode': 'expense'}</field>
+        </record>
+
+        <!-- <menuitem id="eiru_purchases.new_purchase" name="Nueva compra" parent="eiru_dashboard.eiru_dashboard_main" action="eiru_purchases.purchases_action" sequence="4" />
+        <menuitem id="eiru_purchases.new_expense" name="Nuevo gasto" parent="eiru_dashboard.eiru_dashboard_main" action="eiru_purchases.expenses_action" sequence="5" /> -->
+    </data>
+</openerp>

+ 30 - 0
views/templates.xml

@@ -0,0 +1,30 @@
+<openerp>
+    <data>
+        <template id="eiru_bank_statement.new_assets" name="Eiru Cash/Bank " inherit_id="eiru_assets.assets">
+            <xpath expr="." position="inside">
+                <link rel="stylesheet" href="/eiru_bank_statement/static/src/main.css" />
+                <script type="text/javascript" src="/eiru_bank_statement/static/src/main.js" />
+            </xpath>
+        </template>
+
+        <record id="eiru_bank_statement.cash" model="ir.actions.client">
+            <field name="name">Eiru Caja </field>
+            <field name="tag">eiru_bank_statement.action_launch</field>
+            <field name="params">{'mode': 'cash'}</field>
+        </record>
+
+        <!-- <record id="eiru_purchases.expenses_action" model="ir.actions.client">
+            <field name="name">Eiru Expenses</field>
+            <field name="tag">eiru_purchases.action_launch</field>
+            <field name="params">{'mode': 'expense'}</field>
+        </record> -->
+
+        <menuitem
+            id="eiru_bank_statement.bank"
+            name="Caja"
+            parent="account.menu_finance_bank_and_cash"
+            action="eiru_bank_statement.cash"
+            sequence="0" />
+        <!-- <menuitem id="eiru_purchases.new_expense" name="Nuevo gasto" parent="eiru_dashboard.eiru_dashboard_main" action="eiru_purchases.expenses_action" sequence="5" /> -->
+    </data>
+</openerp>

+ 7 - 6
webpack.config.js

@@ -1,6 +1,7 @@
 var Path = require('path');
-var LiveReloadPlugion = require('webpack-livereload-plugin')
+var LiveReloadPlugin = require('webpack-livereload-plugin')
 var ExtractTextPlugin = require('extract-text-webpack-plugin')
+
 var prod = process.env.NODE_ENV === 'production'
 
 module.exports = {
@@ -16,12 +17,12 @@ module.exports = {
             '@': Path.resolve('src')
         }
     },
-    plugin: [
-        new LiveReloadPlugion(),
+    plugins: [
+        new LiveReloadPlugin(),
         new ExtractTextPlugin('main.css')
     ],
     module: {
-        relues: [
+        rules: [
             {
                 test: /\.vue$/,
                 loader: 'vue-loader',
@@ -40,11 +41,11 @@ module.exports = {
                 loader: ['style-loader', 'css-loader']
             },
             {
-                test: /\.(png|jpe?g|git|svg)(\?.*)?$/,
+                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                 loader: 'url-loader',
                 options: {
                     limit: 10000,
-                    name: Path.posix.join('static','img/[name].[hash:7].[ext]')
+                    name: Path.posix.join('static', 'img/[name].[hash:7].[ext]')
                 }
             }
         ]