Преглед на файлове

[FIX] migration from pos

Gogs преди 7 години
родител
ревизия
3a8318068e
променени са 41 файла, в които са добавени 1557 реда и са изтрити 1107 реда
  1. 107 63
      controllers/main.py
  2. 27 19
      src/App.vue
  3. 4 1
      src/assets/_variables.sass
  4. 2 2
      src/components/common/AddCard.vue
  5. 9 1
      src/components/common/Card.vue
  6. 3 3
      src/components/common/CardGrid.vue
  7. 124 0
      src/components/common/Cart.vue
  8. 52 32
      src/components/common/CartItem.vue
  9. 1 1
      src/components/common/Searcher.vue
  10. 91 35
      src/components/common/Spinner.vue
  11. 162 0
      src/components/common/Ticket.vue
  12. 13 0
      src/components/common/index.js
  13. 42 0
      src/components/filters/currency.js
  14. 11 0
      src/components/forms/SupplierForm.vue
  15. 0 77
      src/components/journal/JournalStep.vue
  16. 0 80
      src/components/payment/PaymentStep.vue
  17. 0 51
      src/components/product/Cart.vue
  18. 0 101
      src/components/product/ProductForm.vue
  19. 170 0
      src/components/steps/PaymentAmount.vue
  20. 131 0
      src/components/steps/PaymentMethod.vue
  21. 13 20
      src/components/steps/Product.vue
  22. 50 0
      src/components/steps/Supplier.vue
  23. 0 55
      src/components/supplier/SupplierDetails.vue
  24. 0 104
      src/components/supplier/SupplierForm.vue
  25. 0 61
      src/components/supplier/SupplierStep.vue
  26. 11 0
      src/constants/app.js
  27. 7 0
      src/constants/resourcePaths.js
  28. 10 0
      src/index.js
  29. 0 134
      src/store/actions.js
  30. 161 0
      src/store/app.js
  31. 4 4
      src/store/index.js
  32. 45 48
      src/store/modules/cart.js
  33. 24 14
      src/store/modules/currency.js
  34. 15 12
      src/store/modules/date.js
  35. 23 19
      src/store/modules/journal.js
  36. 155 36
      src/store/modules/payment.js
  37. 16 12
      src/store/modules/picking.js
  38. 24 47
      src/store/modules/product.js
  39. 26 61
      src/store/modules/supplier.js
  40. 14 12
      src/store/modules/user.js
  41. 10 2
      templates.xml

+ 107 - 63
controllers/main.py

@@ -2,11 +2,19 @@
 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 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'
+GZIP_COMPRESSION_LEVEL = 9
 
 class Purchases(http.Controller):
 
@@ -14,7 +22,7 @@ class Purchases(http.Controller):
         Get server date to send
     '''
     def get_server_date(self):
-        return datetime.now().strftime('%Y-%m-%d')
+        return datetime.now().strftime(DATE_FORMAT)
 
     '''
         Get current user information
@@ -31,9 +39,17 @@ class Purchases(http.Controller):
                 '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
+    '''
     def get_currencies(self):
         return [{
             'id': currency.id,
@@ -41,10 +57,13 @@ class Purchases(http.Controller):
             'displayName': currency.display_name,
             'base': currency.base,
             'accuracy': currency.accuracy,
-            'position': currency.position,
-            'rate': currency.rate,
+            'rateSilent': currency.rate_silent,
             'rounding': currency.rounding,
-            'symbol': currency.symbol
+            'symbol': currency.symbol,
+            'position': currency.position,
+            'decimalSeparator': currency.decimal_separator,
+            'decimalPlaces': currency.decimal_places,
+            'thousandsSeparator': currency.thousands_separator
         } for currency in request.env['res.currency'].search([('active', '=', True)])]
 
     '''
@@ -63,28 +82,6 @@ class Purchases(http.Controller):
                 'name': journal.currency.name,
                 'displayName': journal.currency.display_name
             },
-            'defaultCreditAccount': {
-                'id': journal.default_credit_account_id.id,
-                'name': journal.default_credit_account_id.name,
-                'displayName': journal.default_credit_account_id.display_name,
-                'code': journal.default_credit_account_id.code,
-                'exchangeRate': 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
-                },
-            },
             'defaultDebitAccount': {
                 'id': journal.default_debit_account_id.id,
                 'name': journal.default_debit_account_id.name,
@@ -136,16 +133,20 @@ class Purchases(http.Controller):
             'imageMedium': product.image_medium,
             'standardPrice': product.standard_price,
             'variantCount': product.product_variant_count,
+            'quantity': 1,
+            'price': product.standard_price,
             'variants': [{
                 'id': variant.id,
                 'name': variant.name,
                 'displayName': variant.display_name,
                 'ean13': variant.ean13,
                 'imageMedium': variant.image_medium,
-                'standardPrice': variant.standard_price
+                'standardPrice': variant.standard_price,
+                'quantity': 1,
+                'price': variant.standard_price
             } for variant in product.product_variant_ids if variant.active]
         } for product in request.env['product.template'].search([('purchase_ok', '=', True), ('standard_price', '>', 0), ('active', '=', True)])]
-
+        
     '''
         Get all incoming and active picking types
     '''
@@ -169,7 +170,7 @@ class Purchases(http.Controller):
                 'days': line.days,
                 'days2': line.days2,
                 'value': line.value,
-                'value_amount': line.value_amount
+                'valueAmount': line.value_amount
             } for line in payment_term.line_ids]
         } for payment_term in request.env['account.payment.term'].search([('active', '=', True)])]
 
@@ -179,16 +180,34 @@ class Purchases(http.Controller):
     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')
+
     '''
     '''
     def make_info_log(self, log):
-        LOGGER.info(log)
+        LOGGER.info('[INFO]' + log)
 
     '''
         New purchase resource route
     '''
-    @http.route('/eiru_purchases/new', auth='user', methods=['GET'], cors='*')
-    def new_purchase(self, **kw):
+    @http.route('/eiru_purchases/init', auth='user', methods=['GET'], cors='*')
+    def init_purchase(self, **kw):
         self.make_info_log('Sending JSON response')
 
         return self.make_response({
@@ -254,29 +273,10 @@ class Purchases(http.Controller):
                 'displayName': variant.display_name,
                 'ean13': variant.ean13,
                 'imageMedium': variant.image_medium,
-                'listPrice': variant.list_price
+                'standardPrice': variant.standard_price
             } for variant in product.product_variant_ids if variant.active]
         }
 
-    '''
-        Purchase processing resource route
-    ''' 
-    @http.route('/eiru_purchases/create', type='json', auth='user', methods=['POST'], cors='*')
-    def process_purchase(self, **kw):
-        self.make_info_log('Creating purchase')
-
-        # Step 1: Select currency
-        currency_id = self.get_currency_id(int(kw.get('journal')))
-
-        # Step 2: Save purchase
-
-        print(currency_id)
-
-        return {
-            'status': 'ok'
-        }
-
-
     '''
         Get currency id based on journal
     '''
@@ -284,19 +284,63 @@ class Purchases(http.Controller):
         journal = request.env['account.journal'].browse(journal_id)
         return journal.default_debit_account_id.currency_id.id or journal.default_debit_account_id.company_currency_id.id
 
+
     '''
-        Save purchase and return it
+        Check currency in pricelist and return it
     '''
-    def create_purchase(self, partner_id, order_lines, currency_id=None, payment_term_id=None):
-        purchase_order = request.env['purchase.order'].create({
-            'partner_id': partner_id,
+    def get_pricelist_id(self, currency_id):
+        pricelist = request.env['product.pricelist'].search([('active', '=', True), ('type', '=', 'sale')])
+
+        if not True in pricelist.mapped(lambda p: p.currency_id.id == currency_id):
+            pricelist = pricelist[0].copy()
+            pricelist.write({
+                'currency_id': currency_id
+            })
+        else:
+            pricelist = pricelist.filtered(lambda p: p.currency_id.id == currency_id)
+        
+        return pricelist.id
+
+    '''
+        Create purchase order from cart and return id
+    '''
+    def create_purchase_order(self, supplier_id, cart_items, date_aprove, currency_id, pricelist_id, payment_term_id=None):
+        return request.env['purchase.order'].create({
+            'partner_id': supplier_id,
             'order_line': [[0, False, {
-                'product_id': line.id,
-                'product_qty': line.qty,
-                'price_unit': line.price
-            }] for line in order_lines],
-            'date_order': datetime.now().strftime('%Y-%m-%d'),
-            'currency_id': currency_id
+                'product_id': int(line.get('id')),
+                'product_uom_qty': float(line.get('quantity')),
+                'price_unit': float(line.get('price'))
+            }] for line in cart_items],
+            'date_aprove': date_aprove,
+            'currency_id': currency_id,
+            'pricelist_id': pricelist_id,
+            'payment_term_id': payment_term_id,
+            'state': 'draft'
         })
 
-        return purchase_order
+    '''
+        Purchase processing resource route
+    ''' 
+    @http.route('/eiru_purchases/process', type='json', auth='user', methods=['POST'], cors='*')
+    def process_purchase(self, **kw):
+        self.make_info_log('Processing purchase...')
+
+        # Get date
+        date_now = datetime.now().strftime(DATE_FORMAT)
+        self.make_info_log('Getting date')
+
+        # Get currency
+        currency_id = self.get_currency_id(kw.get('journalId'))
+        self.make_info_log('Getting currency')
+
+        # Get pricelist
+        pricelist_id = self.get_pricelist_id(currency_id)
+        self.make_info_log('Getting product pricelist')
+
+        purchase_order = self.create_purchase_order(kw.get('supplierId'), kw.get('items'), date_now, currency_id, pricelist_id, kw.get('paymentTermId'))
+        self.make_info_log('Created purchase order')
+
+        return {
+            'status': 'ok'
+        }

+ 27 - 19
src/App.vue

@@ -1,46 +1,54 @@
 <template lang="pug">
     .purchases
-        form-wizard(title='' subtitle='' finishButtonText='Finalizar' color='#7c7bad' nextButtonText='Continuar' backButtonText='Volver' @on-complete='createPurchase')
+        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='Qué productos comprarás?' :before-change='checkCart')
-                products-step
+                product-step
             tab-content(title='Cómo quieres pagar?')
-                journal-step
+                payment-method-step
             tab-content(title='Qué monto vas a pagar?')
-                payment-step
+                payment-amount-step
 </template>
 
 <script>
-    import { FormWizard, TabContent } from 'vue-form-wizard'
-
-    import SupplierStep from '@/components/supplier/SupplierStep'
-    import ProductsStep from '@/components/product/ProductsStep'
-    import JournalStep from '@/components/journal/JournalStep'
-    import PaymentStep from '@/components/payment/PaymentStep'
+    import { mapGetters, mapActions } from 'vuex'
 
-    import Spinner from '@/components/common/Spinner'
+    import { FormWizard, TabContent } from 'vue-form-wizard'
+    import 'vue-form-wizard/dist/vue-form-wizard.min.css'
 
-    import { mapActions } from 'vuex'
+    import SupplierStep from '@/components/steps/Supplier'
+    import ProductStep from '@/components/steps/Product'
+    import PaymentMethodStep from '@/components/steps/PaymentMethod'
+    import PaymentAmountStep from '@/components/steps/PaymentAmount'
 
     export default {
         components: {
             FormWizard,
             TabContent,
             SupplierStep,
-            ProductsStep,
-            JournalStep,
-            PaymentStep,
-            Spinner
+            ProductStep,
+            PaymentMethodStep,
+            PaymentAmountStep
         },
+        computed: mapGetters([
+            'isProcessing',
+            'state'
+
+        ]),
         methods: mapActions([
             'initPurchase',
-            'checkSupplier',
-            'checkCart',
             'createPurchase'
         ]),
+        watch: {
+            state(value) {
+                if (value === 'done') {
+                    this.$refs.wizard.changeTab(3, 0, false)
+                }
+            }
+        },
         mounted() {
-            this.initPurchase()
+            this.initPurchase(this.$root.mode)
         }
     }
 </script>

+ 4 - 1
src/assets/_variables.sass

@@ -1,4 +1,7 @@
 $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-title-color: #d3d3d3
+$app-separator-color: #9e9e9e

+ 2 - 2
src/components/common/AddCard.vue

@@ -7,7 +7,7 @@
     export default {
         methods: {
             onClick(e) {
-                this.$emit('onClick')
+                this.$emit('onClickAdd')
             }
         }
     }
@@ -35,4 +35,4 @@
             margin-right: -50%
             transform: translate(-50%, -50%)
             color: $app-main-color
-</style>
+</style>

+ 9 - 1
src/components/common/Card.vue

@@ -36,6 +36,7 @@
 
 <style lang="sass">
     @import '../../assets/variables'
+
     .card
         width: 130px
         height: 160px
@@ -77,5 +78,12 @@
             color: $app-bg-color
             position: absolute
             bottom: 0
-</style>
 
+        @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>

+ 3 - 3
src/components/common/CardGrid.vue

@@ -1,9 +1,9 @@
 <template lang="pug">
     .card-grid-wrapper
         .card-grid-loading(v-if='loading')
-            spinner
+            spinner(type='wave')
         .card-grid(v-else)
-            add-card(v-if='canAdd' @onClick='onClickAdd')
+            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' :description='getDescription(item)' @onClick='onClickCard(item)')
 </template>
 
@@ -70,4 +70,4 @@
             justify-content: center
         .card-grid
             width: 100%
-</style>
+</style>

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

@@ -0,0 +1,124 @@
+<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' @onChange='onItemChanged' @onClickIncrement='onIncrementQty' @onClickDecrement='onDecrementQty' @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 || 1) * (item.quantity || 1))
+                }
+
+                this.total = sum
+
+                this.$emit('onTotalComputed', this.total)
+            },
+            onItemChanged(item) {
+                this.computeTotal();
+            },
+            onIncrementQty(item) {
+                this.$emit('onIncrementQty', item)
+            },
+            onDecrementQty(item) {
+                this.$emit('onDecrementQty', 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
+            .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>

+ 52 - 32
src/components/product/CartItem.vue → src/components/common/CartItem.vue

@@ -1,103 +1,123 @@
 <template lang="pug">
     li.cart-item
         h3.item-name {{ item.displayName }}
-        input.item-quantity(:value='quantity' v-model='quantity' number)
+        input.item-quantity(type='number' min='1' :value='item.quantity')
         span.item-x x
-        span.item-price {{ item.price }}
+        span.item-price {{ (item.price || 1) | currency(...options) }}
         span.item-equals =
-        span.item-subtotal {{ item.price * item.qty }}
+        span.item-subtotal {{ ((item.price || 1) * (item.quantity || 1)) | currency(...options) }}
         .cart-item-options-wrapper
             .cart-item-options
-                .cart-item-option(class='fa fa-plus' @click='addToCart(item)')
-                .cart-item-option(class='fa fa-minus' @click='decrementFromCart(item)')
-                .cart-item-option(class='fa fa-trash' @click='removeFromCart(item)')
+                .cart-item-option(class='fa fa-plus' @click='onClickIncrement')
+                .cart-item-option(class='fa fa-minus' @click='onClickDecrement')
+                .cart-item-option(class='fa fa-trash' @click='onClickDelete')
 </template>
 
 <script>
-    import { mapGetters, mapActions } from 'vuex'
-
     export default {
         props: {
+            index: {
+                type: Number,
+                default: -1,
+                required: true
+            },
             item: {
                 type: Object,
-                required: true,
-                default: {}
+                default: null
+            },
+            options: {
+                type: Object,
+                default: {
+                    symbol: '$',
+                    position: 'before',
+                    thousandsSeparator: '.',
+                    decimalPlaces: 2,
+                    decimalSeparator: ',' 
+                }
             }
         },
-        computed: {
-            quantity: {
-                get() {
-                    return this.item.qty || 1
+        watch: {
+            item: {
+                handler(value) {
+                    this.onChange(value)
                 },
-                set(value) {
-                    this.item.qty = value || 1
-                }
+                deep: true,
+                immediate: true
             }
         },
         methods: {
-            ...mapActions([
-                'addToCart',
-                'decrementFromCart',
-                'removeFromCart'
-            ])
-        },
-        mounted() {
-            this.$set(this.item, 'qty', 1)
-            this.$set(this.item, 'price', this.item.standardPrice)
+            onChange(item) {
+                this.$emit('onChange', item)
+            },
+            onClickIncrement() {
+                this.$emit('onClickIncrement', this.item)
+            },
+            onClickDecrement() {
+                this.$emit('onClickDecrement', this.item)
+            },
+            onClickDelete() {
+                this.$emit('onClickDelete', this.item)
+            }
         }
     }
 </script>
 
 <style lang="sass">
+    @import '../../assets/variables'
     .cart-item
         width: 100%
-        height: 85px
+        height: 90px
         list-style: none outside none
-        border-bottom: 1px solid #d3d3d3
+        border-bottom: 1px solid $app-border-color
         box-sizing: border-box
         position: relative
         &:nth-child(1)
-            border-top: 1px solid #d3d3d3
+            border-top: 1px solid $app-border-color
         &:hover
             transition-duration: 1000ms
-            border-bottom: 2px solid #7c7bad
+            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: 50px
             height: 28px
             margin-top: 6px
             text-align: right
             float: left
+            display: inline-block
         .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
-            padding-right: 15px
             text-align: right
             font-weight: bold
+            display: inline-block
         .cart-item-options-wrapper
             width: 100%
             height: 20px
@@ -128,4 +148,4 @@
                             color: #ffc107
                         &.fa-trash:hover
                             color: #f44336
-</style>
+</style>

+ 1 - 1
src/components/common/Searcher.vue

@@ -102,4 +102,4 @@
             size: 11pt
             style: normal
             weight: bold
-</style>
+</style>

+ 91 - 35
src/components/common/Spinner.vue

@@ -1,46 +1,102 @@
 <template lang="pug">
     .spinner
-        .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-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 
-        $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-bubbling $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)
-
-        @keyframes spinner-bubbling
-            0%, 40%, 100%
-                transform: scaleY(0.4)
-            20%
-                transform: scaleY(1.0)
-</style>
+    .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>

+ 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>

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

@@ -0,0 +1,13 @@
+import CardGrid from './CardGrid'
+import Cart from './Cart'
+import Searcher from './Searcher'
+import Ticket from './Ticket'
+import Spinner from './Spinner'
+
+export {
+    CardGrid,
+    Cart,
+    Searcher,
+    Ticket,
+    Spinner
+}

+ 42 - 0
src/components/filters/currency.js

@@ -0,0 +1,42 @@
+/**
+ * 
+ * @param {*} value 
+ * @param {*} options 
+ */
+const currency = (value = 0, options = {}) => {
+    value = value.toString()
+
+    if (!(options instanceof Object)) {
+        options = {}
+    }
+
+    options.symbol = options.symbol || '$'
+    options.position = options.position || 'before'
+    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 options.position === 'before' ? `${options.symbol} ${value}` : `${value} ${options.symbol}`
+}
+
+export default currency

+ 11 - 0
src/components/forms/SupplierForm.vue

@@ -0,0 +1,11 @@
+<template lang="pug">
+    
+</template>
+
+<script>
+    export default {
+    }
+</script>
+
+<style lang="sass">
+</style>

+ 0 - 77
src/components/journal/JournalStep.vue

@@ -1,77 +0,0 @@
-<template lang="pug">
-    .purchase-step
-        .journal-selector
-            form.journal-form
-                .form-separator
-                    h3 Detalles del Cliente
-                    hr
-                .form-item
-                    label.form-label Cliente
-                    input.form-input(:value="(supplierSelected && supplierSelected.name) || ''" readonly)
-                .form-separator
-                    h3 Detalles del Pago
-                    hr
-                .form-item
-                    label.form-label Método de Pago
-                    select.form-input(v-model='journal')
-                        option(v-for='journal in journals' :value='journal') {{ journal.displayName }}
-</template>
-
-<script>
-    import { mapGetters, mapActions } from 'vuex'
-
-    export default {
-        computed: {
-            journal: {
-                get() {
-                    return this.selectedJournal
-                },
-                set(value) {
-                    this.selectJournal(value)
-                }
-            },
-            ...mapGetters([
-                'supplierSelected',
-                'journals',
-                'selectedJournal'
-            ])
-        },
-        methods: mapActions([
-            'selectJournal'
-        ])
-    }
-</script>
-
-<style lang="sass">
-    .purchase-step
-        .journal-selector
-            width: 100%
-            height: 100%
-            display: flex
-            align-items: center
-            justify-content: center
-            .journal-form
-                width: 600px
-                height: 100%
-                background: #f5f5f5
-                padding: 25px
-                .form-separator
-                    h3
-                        color: #9e9e9e
-                        font-size: 8pt
-                    hr
-                        margin-top: 5px
-                .form-item
-                    width: 100%
-                    height: 45px
-                    margin-bottom: 15px
-                    .form-label
-                        width: 200px
-                        height: 45px
-                        font-size: 14pt
-                    .form-input
-                        width: 350px
-                        height: 45px
-                        font-size: 14pt
-                        border-radius: 0
-</style>

+ 0 - 80
src/components/payment/PaymentStep.vue

@@ -1,80 +0,0 @@
-<template lang="pug">
-    .purchase-step
-        .payment-wrapper
-            form.payment-form
-                .form-separator
-                    h3 Detalles del Pago
-                    hr
-                .form-item
-                    label.form-label Monto a pagar
-                    input.form-input(:value='cartTotal' readonly)
-                .form-item
-                    label.form-label Total entregado
-                    input.form-input(:value='amount' v-model='amount' autofocus)
-                hr
-                .form-item
-                    label.form-label Total a devolver
-                    input.form-input(:value='amountResidual' readonly)
-</template>
-
-
-<script>
-    import { mapGetters, mapActions } from 'vuex'
-
-    export default {
-        computed: {
-            amount: {
-                get() {
-                    return this.amountPaid
-                },
-                set(value) {
-                    this.changeAmountPaid(value)
-                }
-            },
-            ...mapGetters([
-                'selectedJournal',
-                'cartTotal',
-                'amountPaid',
-                'amountResidual'
-            ])
-        },
-        methods: mapActions([
-            'changeAmountPaid'
-        ])
-    }
-
-</script>
-
-<style lang="sass">
-    .purchase-step
-        .payment-wrapper
-            width: 100%
-            height: 100%
-            display: flex
-            align-items: center
-            justify-content: center
-            .payment-form
-                width: 600px
-                height: 100%
-                background: #f5f5f5
-                padding: 25px
-                .form-separator
-                    h3
-                        color: #9e9e9e
-                        font-size: 8pt
-                    hr
-                        margin-top: 5px
-                .form-item
-                    width: 100%
-                    height: 45px
-                    margin-bottom: 15px
-                    .form-label
-                        width: 200px
-                        height: 45px
-                        font-size: 14pt
-                    .form-input
-                        width: 350px
-                        height: 45px
-                        font-size: 14pt
-                        border-radius: 0
-</style>

+ 0 - 51
src/components/product/Cart.vue

@@ -1,51 +0,0 @@
-<template lang="pug">
-    .cart
-        .cart-total
-            h2.currency-cart-total {{ cartTotal }}
-        .cart-items-wrapper
-            transition-group(name='list' tag='ul' class='cart-items')
-                cart-item(v-for='item in cartItems' :key='item.id' :item='item')
-</template>
-
-<script>
-    import { mapGetters } from 'vuex'
-    import CartItem from '@/components/product/CartItem'
-
-    export default {
-        components: {
-            CartItem
-        },
-        computed: mapGetters([
-            'cartTotal',
-            'cartItems'
-        ])
-    }
-</script>
-
-<style lang="sass">
-    .cart
-        width: 300px
-        height: 100%
-        border-left: 1px solid #d3d3d3
-        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
-            .list-enter-active, .list-leave-active
-                transition: all 0.3s
-            .list-enter, .list-leave-to
-                opacity: 0
-                transform: translateX(300px)
-</style>

+ 0 - 101
src/components/product/ProductForm.vue

@@ -1,101 +0,0 @@
-<template lang="pug">
-    modal(name='product-modal' transition='nice-modal-fade' @before-close='beforeClose' :classes="['v--modal', 'product-modal']")
-        h2 Nuevo producto
-        hr
-        form.product-form
-            .form-item
-                label.form-label Nombre
-                input.form-input(v-model='product.name' autofocus)
-            .form-item
-                label.form-label Precio de Costo
-                input.form-input(v-model='product.price')
-            .form-item
-                label.form-label Código de Barras
-                input.form-input(v-model='product.ean13')
-            .form-actions
-                button.form-button(@click='submitProduct(product)') Aceptar
-                button.form-button(@click='submitProduct()') Cancelar
-</template>
-
-<script>
-    import { mapGetters, mapActions } from 'vuex'
-
-    export default {
-        computed: mapGetters([
-            'showProductForm'
-        ]),
-        watch: {
-            showProductForm(value) {
-                if(!value) {
-                    this.$modal.hide('product-modal')
-                    return
-                }
-
-                this.product.name = ''
-                this.product.price = ''
-                this.product.ean13 = ''
-
-                this.$modal.show('product-modal')
-            }
-        },
-        methods: {
-            beforeClose(e) {
-                if (this.showProductForm) {
-                    e.stop()
-                }
-            },
-            ...mapActions([
-                'submitProduct'
-            ])
-        },
-        data() {
-            return {
-                product: {
-                    name: '',
-                    price: '',
-                    ean13: ''
-                }
-            }
-        }
-    }
-</script>
-
-<style lang="sass">
-     @import '../../assets/variables'
-     .product-modal
-        h2
-            font-size: 10pt
-            color: $app-title-color
-            margin-left: 15px
-        hr
-            margin: 0 15px
-        .product-form
-            width: 100%
-            height: 250px
-            padding: 15px
-            .form-item
-                width: 100%
-                height: 40px
-                margin-bottom: 15px
-                .form-label
-                    width: 30%
-                    height: 40px
-                    font-size: 14pt
-                .form-input
-                    width: 70%
-                    height: 40px
-                    font-size: 18pt
-                    border-radius: 0
-            .form-actions
-                float: right
-                margin-top: 15px
-                .form-button
-                    width: 160px
-                    height: 40px
-                    border: none
-                    box-shadow: none
-                    border-radius: 0
-                    margin-right: 5px
-                    background: $app-main-color
-                    color: $app-bg-color
-</style>

+ 170 - 0
src/components/steps/PaymentAmount.vue

@@ -0,0 +1,170 @@
+<template lang="pug">
+    .purchase-step
+        ticket(:customerName='selectedSupplier && selectedSupplier.name' :companyName='user && user.company.name' :total='cartTotal' :items='cartItems' :defaultCurrency='selectedCurrency')
+        form.payment-amount
+            .form-loading(v-show='isProcessing')
+                .form-overlay
+                .form-spinner
+                    spinner(type='wave')
+            .form-separator
+                h3 Detalles del Pago
+                hr
+            .form-item(v-show="paymentType === 'cash'")
+                label.form-label Monto a Pagar
+                input.form-input(:value='cartTotal | currency(...selectedCurrency)' readonly)
+            .form-item
+                label.form-label Monto Recibido
+                input.form-input(v-model='amountReceived' autofocus)
+            .form-item(v-show="paymentType === 'cash'")
+                hr
+                label.form-label Monto a Devolver
+                input.form-input(:value='paymentResidual | currency(...selectedCurrency)' readonly)
+            .form-item-table(v-show="paymentType === 'credit'")
+                table
+                    thead
+                        tr
+                            th Monto a Pagar
+                            th Fecha de Pago
+                    tbody
+                        tr(v-for='line in paymentLines')
+                            td {{ line.total | currency(...selectedCurrency) }}
+                            td {{ line.date }}
+</template>
+
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+
+    import { Ticket, Spinner } from '../common'
+
+    export default {
+        components: {
+            Ticket,
+            Spinner
+        },
+        computed: {
+            amountReceived: {
+                get() {
+                    let formatted = this.$options.filters.currency(this.initialPayment, {...this.selectedCurrency})
+
+                    return !!this.initialPayment ? formatted : formatted.replace(/\d/, '')
+                },
+                set(value) {
+                    value = value.replace(/[\.|,](\d{0,2}$)/, '?$1').split(/\?/)
+                    value[0] = value[0].replace(/[^0-9]/g, '')
+                    value = Number.parseFloat(value.join('.')) || 0
+
+                    this.changeInitialPayment(value)
+                    this.computePaymentResidual(value)
+
+                    if (this.paymentType === 'credit') {
+                       this.computePaymentLines()		
+                    }
+                }
+            },
+            ...mapGetters([
+                'user',
+                'selectedSupplier',
+                'cartItems',
+                'cartTotal',
+                'paymentTerms',
+                'paymentType',
+                'paymentLines',
+                'initialPayment',
+                'selectedCurrency',
+                'isProcessing'
+            ])
+        },
+        methods: {
+            computePaymentResidual(value) {
+                this.paymentResidual = value < this.cartTotal ? 0 : value - this.cartTotal
+            },
+            ...mapActions([
+                'changeInitialPayment',
+                'computePaymentLines'
+            ])
+        },
+        data() {
+            return {
+                paymentResidual: 0
+            }
+        }
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .purchase-step
+        width: 100%
+        height: calc(100% - 50px)
+        padding-bottom: 50px
+        display: flex
+        .payment-amount
+            width: calc(100% - 450px)
+            height: 100%
+            margin-right: 50px
+            padding: 35px
+            background: $app-light-color
+            position: relative
+            .form-loading
+                width: 90%
+                height: 90%
+                position: absolute
+                .form-overlay
+                    width: 100%
+                    height: 100%
+                    background: $app-bg-color
+                    opacity: 0.5
+                    position: absolute
+                .form-spinner
+                    width: 100%
+                    height: 100%
+                    display: flex
+                    justify-content: center
+                    align-items: center
+            .form-separator
+                h3
+                    color: $app-separator-color
+                    font-size: 8pt
+                    margin: 0
+                hr
+                    margin-top: 15px
+            .form-item
+                width: 100%px
+                height: 45px
+                margin-bottom: 15px
+                .form-label
+                    width: 250px
+                    height: 45px
+                    font-size: 14pt
+                .form-input
+                    width: 350px
+                    height: 45px
+                    font-size: 14pt
+                    border-radius: 0
+                    &.input-only
+                        margin-left: 250px
+                        margin-bottom: 15px
+            .form-item-table
+                width: 100%
+                height: 250px
+                border: 1px solid $app-border-color
+                overflow-y: auto
+                table
+                    width: 100%
+                    thead
+                        th
+                            line-height: 35px
+                            padding-left: 10px
+                        th:nth-child(1)
+                            width: 200px
+                        th:nth-child(2)
+                            width: 200px
+                    tbody
+                        td
+                            height: 35px
+                            padding-left: 10px
+
+                        td:nth-child(1)
+                            width: 200px
+                        td:nth-child(2)
+</style>

+ 131 - 0
src/components/steps/PaymentMethod.vue

@@ -0,0 +1,131 @@
+<template lang="pug">
+    .purchase-step
+        ticket(:customerName='selectedSupplier && selectedSupplier.name' :companyName='user && user.company.name' :total='cartTotal' :items='cartItems' :defaultCurrency='selectedCurrency')
+        form.payment-method
+            .form-separator
+                h3 Detalles del Proveedor
+                hr
+            .form-item
+                label.form-label Proveedor
+                input.form-input(:value='selectedSupplier && selectedSupplier.name' readonly)
+            .form-separator
+                h3 Detalles del Pago
+                hr
+            .form-item
+                label.form-label Forma de Pago
+                .form-item-option
+                    input.form-input(type='radio' id='cash' value='cash' v-model='payment')
+                    label(for='cash') Contado
+                .form-item-option
+                    input.form-input(type='radio' id='credit' value='credit' v-model='payment')
+                    label(for='credit') Crédito
+            transition(name='fade')
+                .form-item(v-if="payment === 'credit'")
+                    select.form-input.input-only(v-model='paymentTermId')
+                        option(v-for='term in paymentTerms' :value='term.id' v-if="term.lines.length > 0 && (term.lines[0].days !== 0 || term.lines[0].value !==  'balance')") {{ term.displayName }}
+                .form-item(v-else)
+                    label.form-label Método de Pago
+                    select.form-input(v-model='journalId')
+                        option(v-for='journal in journals' :value='journal.id') {{ journal.displayName }}
+</template>
+
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+
+    import Ticket from '@/components/common/Ticket'
+
+    export default {
+        components: {
+            Ticket
+        },
+        computed: {
+            paymentTermId: {
+                get() {
+                    return (this.selectedPaymentTerm && this.selectedPaymentTerm.id) || -1
+                },
+                set(value) {
+                    this.selectPaymentTerm(value)
+                }
+            },
+            journalId: {
+                get() {
+                    return (this.selectedJournal && this.selectedJournal.id) || -1
+                },
+                set(value) {
+                   this.selectJournal(value)
+                }
+            },
+            payment: {
+                get() {
+                    return this.paymentType
+                },
+                set(value) {
+                    this.changePaymentType(value)
+                }
+            },
+            ...mapGetters([
+                'user',
+                'selectedSupplier',
+                'cartItems',
+                'cartTotal',
+                'paymentTerms',
+                'paymentType',
+                'selectedPaymentTerm',
+                'journals',
+                'selectedCurrency',
+                'selectedJournal'
+            ])
+        },
+        methods: mapActions([
+            'changePaymentType',
+            'selectJournal',
+            'selectPaymentTerm'
+        ])
+    }
+</script>
+
+<style lang="sass">
+    @import '../../assets/variables'
+    .purchase-step
+        width: 100%
+        height: calc(100% - 50px)
+        padding-bottom: 50px
+        display: flex
+        .payment-method
+            width: calc(100% - 450px)
+            height: 100%
+            margin-right: 50px
+            padding: 35px
+            background: $app-light-color
+            .form-separator
+                h3
+                    color: $app-separator-color
+                    font-size: 8pt
+                    margin: 0
+                hr
+                    margin-top: 15px
+            .form-item
+                width: 100%px
+                height: 45px
+                margin-bottom: 15px
+                .form-label
+                    width: 250px
+                    height: 45px
+                    font-size: 14pt
+                .form-input
+                    width: 350px
+                    height: 45px
+                    font-size: 14pt
+                    border-radius: 0
+                    &.input-only
+                        margin-left: 250px
+                        margin-bottom: 15px
+                .form-item-option
+                    display: inline-block
+                    input
+                        width: 20px
+                        height: 20px
+                    label
+                        font-size: 12pt
+                        margin: 0 45px 15px 5px
+</style>

+ 13 - 20
src/components/product/ProductsStep.vue → src/components/steps/Product.vue

@@ -2,35 +2,35 @@
     .purchase-step
         .products-selector
             searcher(:items='products' :keys="['name', 'displayName']" @onSearch='filterProducts')
-            card-grid(:items='visibleProducts' :loading='loadingProducts' canAdd description='standardPrice' @onAdd='showProductForm' @onSelect='selectProduct')
-            product-form
-        cart
+            card-grid(:items='visibleProducts' :loading='loadingProducts' @onSelect='selectProduct')
+        cart(:items='cartItems' @onIncrementQty='addToCart' @onDecrementQty='decreaseFromCart' @onDeleteItem='removeFromCart' @onTotalComputed='updateCartTotal' :options='selectedCurrency')
 </template>
 
 <script>
-    import Searcher from '@/components/common/Searcher'
-    import CardGrid from '@/components/common/CardGrid'
-    import ProductForm from '@/components/product/ProductForm'
-    import Cart from '@/components/product/Cart'
-
     import { mapGetters, mapActions } from 'vuex'
+    
+    import { Searcher, CardGrid, Cart } from '../common'
 
     export default {
         components: {
             Searcher,
             CardGrid,
-            ProductForm,
             Cart
         },
         computed: mapGetters([
             'products',
             'visibleProducts',
-            'loadingProducts'
+            'loadingProducts',
+            'cartItems',
+            'selectedCurrency'
         ]),
         methods: mapActions([
             'filterProducts',
-            'showProductForm',
-            'selectProduct'
+            'selectProduct',
+            'addToCart',
+            'decreaseFromCart',
+            'removeFromCart',
+            'updateCartTotal'
         ])
     }
 </script>
@@ -42,11 +42,4 @@
             height: 100%
             padding-right: 5px
             display: inline-block
-        .cart-viewer
-            width: 300px
-            height: 100%
-            border-left: 1px solid #d3d3d3
-            padding-left: 10px
-            display: inline-block
-            vertical-align: top
-</style>
+</style>

+ 50 - 0
src/components/steps/Supplier.vue

@@ -0,0 +1,50 @@
+<template lang="pug">
+    .purchase-step
+        .supplier-selection-step
+            .supplier-selector
+                searcher(:items='suppliers' :keys="['name', 'displayName']" @onSearch='filterSuplliers')
+                card-grid(:items='visibleSuppliers' :loading='loadingSuppliers' @onSelect='selectSupplier')
+            transition(name='slide-fade')
+                customer-form
+</template>
+
+<script>
+    import { mapGetters, mapActions } from 'vuex'
+
+    import { Searcher, CardGrid } from '../common'
+    import SupplierForm from '@/components/forms/SupplierForm'
+
+    export default {
+        components: {
+            Searcher,
+            CardGrid,
+            SupplierForm
+        },
+        computed: mapGetters([
+            'suppliers',
+            'visibleSuppliers',
+            'loadingSuppliers'
+        ]),
+        methods: mapActions([
+            'selectSupplier',
+            'filterSuppliers'
+        ])
+    }
+</script>
+
+<style lang="sass">
+    .purchase-step
+        .supplier-selection-step
+            width: 100%
+            height: 100%
+            display: flex
+            .supplier-selector
+                width: 100%
+                height: 100%
+            .slide-fade-enter-active
+                transition: all 300ms ease;
+            .slide-fade-enter
+                transform: translateX(300px)
+                opacity: 0
+</style>
+

+ 0 - 55
src/components/supplier/SupplierDetails.vue

@@ -1,55 +0,0 @@
-<template lang="pug">
-    .supplier-details
-        form.supplier-form
-            .form-separator
-                h3 Detalles Generales
-                hr
-            .form-item
-                label.form-label Nombre
-                input.form-input(:value="supplierSelected.name || ''" readonly)
-            .form-item
-                label.form-label Celular
-                input.form-input(:value="supplierSelected.mobile || ''" readonly)
-            .form-item
-                label.form-label Teléfono
-                input.form-input(:value="supplierSelected.phone || ''" readonly)
-            .form-item
-                label.form-label Email
-                input.form-input(:value="supplierSelected.email || ''" readonly)
-</template>
-
-<script>
-    import { mapGetters } from 'vuex'
-
-    export default {
-        computed: mapGetters([
-            'supplierSelected'
-        ])
-    }
-</script>
-
-<style lang="sass">
-    .supplier-details
-        width: 100%
-        height: 100%
-        .supplier-form
-            width: 100%
-            height: 100%
-            .form-separator
-                h3
-                    color: #9e9e9e
-                    font-size: 8pt
-                hr
-                    margin-top: 5px
-            .form-item
-                width: 100%
-                height: 35px
-                margin-bottom: 5px
-                .form-label
-                    width: 65px
-                    height: 35px
-                .form-input
-                    width: 210px
-                    height: 35px
-                    border-radius: 0
-</style>

+ 0 - 104
src/components/supplier/SupplierForm.vue

@@ -1,104 +0,0 @@
-<template lang="pug">
-    modal(name='supplier-modal' transition='nice-modal-fade' @before-close='beforeClose' :classes="['v--modal', 'supplier-modal']")
-        h2 Nuevo proveedor
-        hr
-        form.supplier-form
-            .form-item
-                label.form-label Nombre
-                input.form-input(v-model='supplier.name' autofocus)
-            .form-item
-                label.form-label R.U.C/C.I.N
-                input.form-input(v-model='supplier.ruc')
-            .form-item
-                label.form-label Teléfono
-                input.form-input(v-model='supplier.phone')
-            .form-actions
-                button.form-button(@click='submitSupplier(supplier)') Aceptar
-                button.form-button(@click='submitSupplier()') Cancelar
-</template>
-
-<script>
-    import { mapGetters, mapActions } from 'vuex'
-
-    export default {
-        computed: mapGetters([
-            'showSupplierForm'
-        ]),
-        watch: {
-            showSupplierForm(value) {
-                if (!value) {
-                    this.$modal.hide('supplier-modal')
-                    return
-                }
-
-                this.supplier.name = ''
-                this.supplier.ruc = ''
-                this.supplier.phone = ''
-
-                this.$modal.show('supplier-modal')
-            }
-        },
-        methods: {
-            beforeClose(e) {
-                if(this.showSupplierForm) {
-                    e.stop()
-                }
-            },
-            ...mapActions([
-                'submitSupplier'
-            ])
-        },
-        /**
-         * 
-         */
-        data() {
-            return {
-                supplier: {
-                    name: '',
-                    ruc: '',
-                    phone: ''
-                }
-            }
-        },
-    }
-</script>
-
-<style lang="sass">
-    @import '../../assets/variables'
-    .supplier-modal
-        h2
-            font-size: 10pt
-            color: $app-title-color
-            margin-left: 15px
-        hr
-            margin: 0 15px
-        .supplier-form
-            width: 100%
-            height: 250px
-            padding: 15px
-            .form-item
-                width: 100%
-                height: 40px
-                margin-bottom: 15px
-                .form-label
-                    width: 30%
-                    height: 45px
-                    font-size: 14pt
-                .form-input
-                    width: 70%
-                    height: 45px
-                    font-size: 18pt
-                    border-radius: 0
-            .form-actions
-                float: right
-                margin-top: 15px
-                .form-button
-                    width: 160px
-                    height: 40px
-                    border: none
-                    box-shadow: none
-                    border-radius: 0
-                    margin-right: 5px
-                    background: $app-main-color
-                    color: $app-bg-color
-</style>

+ 0 - 61
src/components/supplier/SupplierStep.vue

@@ -1,61 +0,0 @@
-<template lang="pug">
-    .purchase-step
-        .supplier-selection-step
-            .supplier-selector
-                searcher(:items='suppliers' :keys="['name', 'displayName']" @onSearch='filterSuppliers')
-                card-grid(:items='visibleSuppliers' :loading='loadingSuppliers' canAdd @onAdd='showSupplierForm' @onSelect='selectSupplier')
-                supplier-form
-            transition(name='slide-fade')
-                supplier-details(v-if='!!supplierSelected')
-</template>
-
-<script>
-    import { mapGetters, mapActions } from 'vuex'
-
-    import Searcher from '@/components/common/Searcher'
-    import CardGrid from '@/components/common/CardGrid'
-    import SupplierForm from '@/components/supplier/SupplierForm'
-    import SupplierDetails from '@/components/supplier/SupplierDetails'
-
-    export default {
-        components: {
-            Searcher,
-            CardGrid,
-            SupplierForm,
-            SupplierDetails,
-        },
-        computed: mapGetters([
-            'suppliers',
-            'visibleSuppliers',
-            'loadingSuppliers',
-            'supplierSelected'
-        ]),
-        methods: mapActions([
-            'filterSuppliers',
-            'showSupplierForm',
-            'selectSupplier'
-        ])
-    }
-</script>
-
-<style lang="sass">
-    .purchase-step 
-        .supplier-selection-step
-            width: 100%
-            height: 100%
-            display: flex
-            .supplier-selector, .supplier-details
-                height: 100%
-            .supplier-selector
-                width: 100%
-            .supplier-details
-                width: 360px
-                margin: 0 15px
-            .slide-fade-enter-active
-                transition: all 300ms ease
-            .slide-fade-leave-active
-                transition: all 800ms cubic-bezier(1.0, 0.5, 0.8, 1.0)
-            .slide-fade-enter, .slide-fade-leave-to
-                transform: translateX(10px)
-                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`

+ 10 - 0
src/index.js

@@ -3,6 +3,10 @@ import App from '@/App'
 import VueModal from 'vue-js-modal'
 import store from '@/store'
 
+import currency from '@/components/filters/currency'
+
+Vue.filter('currency', currency)
+
 Vue.use(VueModal)
 
 Vue.config.productionTip = false
@@ -12,6 +16,9 @@ Vue.config.devTools = false
 openerp.eiru_purchases = (instance, local) => {
 
     local.PurchasesWidget = instance.Widget.extend({
+        init(parent, action) {
+            this.mode = action.params.mode
+        },
         start() {
             this.sidebarFold()
 
@@ -21,6 +28,9 @@ openerp.eiru_purchases = (instance, local) => {
                 template: '<App />',
                 components: {
                     App
+                },
+                data: {
+                    mode: this.mode
                 }
             })
         },

+ 0 - 134
src/store/actions.js

@@ -1,134 +0,0 @@
-import axios from 'axios'
-
-const newUrl = '/eiru_purchases/new'
-
-const createSupplierUrl = '/eiru_purchases/create_supplier'
-const createProductUrl = '/eiru_purchases/create_product'
-const createUrl = '/eiru_purchases/create'
-
-const actions = {
-    /**
-     * Display notification with odoo style
-     * @param {*} param0 
-     * @param {*} payload 
-     */
-    notify({ commit }, payload) {
-        openerp.web.notification.do_warn('Atención', payload)
-        return false
-    },
-    /**
-     * Get all resources to init purchase app
-     * @param {*} param0 
-     */
-    initPurchase({ dispatch }) {
-        return axios.get(newUrl).then(response => {
-            dispatch('explodeData', response.data)
-        }).catch(error => {
-            console.log(error)
-        })
-    },
-    /**
-     * 
-     * @param {*} param0 
-     * @param {*} payload 
-     */
-    explodeData({ dispatch }, payload) {
-        Object.keys(payload).forEach(key => {
-            dispatch(`init${key[0].toLocaleUpperCase()}${key.slice(1)}`, payload[key])
-        })
-    },
-    /**
-     * Send data to create a supplier
-     * @param {*} param0 
-     * @param {*} payload 
-     */
-    createSupplier({ commit, dispatch }, payload) {
-        const data = {
-            jsonrpc: '2.0',
-            method: 'call',
-            params: {
-                ...payload
-            }
-        }
-
-        return axios.post(createSupplierUrl, data).then(response => {
-            dispatch('addSupplier', response.data.result)
-        }).catch(error => {
-            console.log(error)
-        })
-    },
-    /**
-     * Check if a supplier is selected or notify
-     * @param {*} param0 
-     */
-    checkSupplier({ getters, dispatch }) {
-        return !!getters.supplierSelected || dispatch('notify', 'Necesitas seleccionar un proveedor para continuar')
-    },
-    /**
-     * Send data to create a product
-     * @param {*} param0 
-     * @param {*} payload 
-     */
-    createProduct({ commit, dispatch }, payload) {
-        const data = {
-            jsonrpc: '2.0',
-            method: 'call',
-            params: {
-                ...payload
-            }
-        }
-
-        return axios.post(createProductUrl, data).then(response => {
-            dispatch('addProduct', response.data.result)
-        }).catch(error => {
-            console.log(error)
-        })
-    },
-    /**
-     * Check if exist product in cart or notify
-     * @param {*} param0 
-     */
-    checkCart({ getters, dispatch }) {
-        return !!getters.cartItems.length || dispatch('notify', 'Necesitas agregar productos al carrito para continuar')
-    },
-    /**
-     * Send data to create concrete purchase
-     * @param {*} param0 
-     */
-    createPurchase({ getters, dispatch }) {
-        const data = {
-            jsonrpc: '2.0',
-            method: 'call',
-            params: {
-                supplier: getters.supplierSelected.id,
-                products: getters.cartItems.map(item => ({
-                    id: item.id,
-                    qty: item.qty,
-                    price: item.standardPrice
-                })),
-                total: getters.cartTotal,
-                journal: getters.selectedJournal.id
-            }
-        }
-
-        return axios.post(createUrl, data).then(response => {
-            console.log(response)
-            // dispatch('resetPurchase')
-        }).catch(error => {
-            console.log(error)
-        })
-    },
-    /**
-     * Reset all default app state 
-     * @param {*} param0 
-     */
-    resetPurchase({ state, commit }) {
-        Object.keys(state).forEach(value => {
-            Object.keys(state[value]).forEach(key => {
-                commit(`set${key[0].toLocaleUpperCase()}${key.slice(1)}`, state[value][key].default)
-            })
-        })
-    }
-}
-
-export default actions

+ 161 - 0
src/store/app.js

@@ -0,0 +1,161 @@
+import axios from 'axios'
+import { Modes, States } from '../constants/app'
+import * as Urls from '../constants/resourcePaths'
+
+const state = {
+    currentMode: Modes.PURCHASE,
+    currentState: States.NONE
+}
+
+const getters = {
+    /**
+     * 
+     */
+    mode() {
+        return state.currentMode
+    },
+    /**
+     * 
+     */
+    isProcessing() {
+        return state.currentState === States.PROCESSING
+    },
+    /**
+     * 
+     */
+    isDone() {
+        return state.currentState === States.DONE
+    },
+    /**
+     * 
+     */
+    hasError() {
+        return state.currentState === States.ERROR
+    },
+    /**
+     * 
+     */
+    state() {
+        return state.currentState
+    }
+}
+
+const mutations = {
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    setMode(state, payload) {
+        state.currentMode = payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    setState(state, payload) {
+        state.currentState = payload
+    }
+}
+
+const actions = {
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    initPurchase({ getters, commit, dispatch }, payload) {
+        commit('setMode', payload || getters.mode)
+
+        return axios.get(Urls.INIT_PURCHASE_URL).then(response => {
+            commit('setState', States.NONE)
+            dispatch('explodeData', response.data)
+        }).catch(error => console.log(error))
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    explodeData({ dispatch }, payload) {
+        for (let key in payload) {
+            dispatch(`init${key[0].toUpperCase()}${key.slice(1)}`, payload[key])
+        }
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    createObject({ dispatch }, payload) {
+        const data = {
+            jsonrpc: '2.0',
+            method: 'call',
+            params: {
+                type: payload.type,
+                data: payload.data
+            }
+        }
+
+        return axios.post(Urls.CREATE_OBJECT_URL, data).then(response => {
+            console.log(response)
+        }).catch(error => console.log(error))
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    createPurchase({ getters, commit, dispatch }) {
+        commit('setState', States.PROCESSING)
+
+        const data = {
+            jsonrpc: '2.0',
+            method: 'call',
+            params: {
+                mode: getters.mode,
+                items: getters.cartItems.map(item => {
+                    return {
+                        id: item.id,
+                        quantity: item.quantity,
+                        price: item.price
+                    }
+                }),
+                supplierId: getters.selectedSupplierid,
+                paymentTermId: getters.selectedPaymentTerm.id,
+                journalId: getters.selectedJournal.id,
+                payment: getters.initialPayment > getters.cartTotal ? getters.cartTotal : getters.initialPayment
+            }
+        }
+
+        return axios.post(Urls.PROCESS_PURCHASE_URL, data).then(response => {
+            commit('setState', States.DONE)
+
+            dispatch('resetPurchase')
+        }).catch(error => {
+            commit('setState', States.ERROR)
+        })
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetPurchase({ rootState, dispatch }) {
+        for (let key in rootState) {
+            if (!(rootState[key] instanceof Object)) {
+                continue
+            }
+
+            dispatch(`reset${key[0].toUpperCase()}${key.slice(1)}`)
+        }
+
+        dispatch('initPurchase')
+    }
+}
+
+export default {
+    state,
+    getters,
+    mutations,
+    actions
+}

+ 4 - 4
src/store/index.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import Vuex from 'vuex'
 
-import actions from '@/store/actions'
+import app from './app'
 
 import date from '@/store/modules/date'
 import user from '@/store/modules/user'
@@ -16,7 +16,8 @@ import payment from '@/store/modules/payment'
 Vue.use(Vuex)
 
 const store = new Vuex.Store({
-    actions,
+    ...app,
+    strict: false,
     modules: {
         date,
         user,
@@ -27,8 +28,7 @@ const store = new Vuex.Store({
         cart,
         picking,
         payment
-    },
-    strict: false
+    }
 })
 
 export default store

+ 45 - 48
src/store/modules/cart.js

@@ -1,12 +1,6 @@
 const state = {
-    cartItems: {
-        default: [],
-        values: []
-    },
-    cartTotal: {
-        default: 0,
-        value: 0
-    }
+    cartItems: [],
+    cartTotal: 0
 }
 
 const getters = {
@@ -15,14 +9,14 @@ const getters = {
      * @param {*} state 
      */
     cartItems(state) {
-        return state.cartItems.values
+        return state.cartItems
     },
     /**
      * 
      * @param {*} state 
      */
     cartTotal(state) {
-        return state.cartTotal.value
+        return state.cartTotal
     }
 }
 
@@ -33,55 +27,44 @@ const mutations = {
      * @param {*} payload 
      */
     setCartItems(state, payload) {
-        state.cartItems.values = [...payload]
-    },
-    /**
-     * 
-     * @param {*} state 
-     * @param {*} payload 
-     */
-    setTotal(state, payload) {
-        state.cartTotal.value = payload
+        state.cartItems= [...payload]
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    addToCart(state, payload) {
-        let productFound = state.cartItems.values.find(item => item.id === payload.id)
+    pushToCart(state, payload) {
+        let productFound = state.cartItems.find(item => item.id === payload.id)
 
         if(productFound) {
-            payload.qty = (payload.qty || 0) + 1
+            productFound.quantity = productFound.quantity + 1
             return
         }
 
-        state.cartItems.values = [payload, ...state.cartItems.values]
+        state.cartItems = [payload, ...state.cartItems]
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    removeFromCart(state, payload) {
-        let foundIndex = state.cartItems.values.findIndex(item => item.id === payload.id)
-        state.cartItems.values.splice(foundIndex, 1)
+    pullFromCart(state, payload) {
+        let foundIndex = state.cartItems.findIndex(item => item.id === payload.item.id)
+
+        if (payload.mode === 'partial') {
+            state.cartItems[foundIndex].quantity = state.cartItems[foundIndex].quantity - 1
+        } else {
+            state.cartItems.splice(foundIndex, 1)
+        }
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    decrementFromCart(state, payload) {
-        let productFound = state.cartItems.values.find(item => item.id === payload.id)
-        productFound.qty = productFound.qty - 1
-    },
-    /**
-     * 
-     * @param {*} state 
-     */
-    computeTotal(state) {
-        state.cartTotal.value = state.cartItems.values.reduce((sum, item) => sum + ((item.price || item.standardPrice) * (item.qty || 1)), 0)
+    setCartTotal(state, payload) {
+        state.cartTotal = payload
     }
 }
 
@@ -92,8 +75,7 @@ const actions = {
      * @param {*} payload 
      */
     addToCart({ commit }, payload) {
-        commit('addToCart', payload)
-        commit('computeTotal')
+        commit('pushToCart', payload)
     },
     /**
      * Remove item from cart
@@ -101,22 +83,37 @@ const actions = {
      * @param {*} payload 
      */
     removeFromCart({ commit }, payload) {
-        commit('removeFromCart', payload)
-        commit('computeTotal')
+        commit('pullFromCart', {
+            item: payload,
+            mode: 'full'
+        })
     },
     /**
      * Decrement item quantity from cart
      * @param {*} param0 
      * @param {*} payload 
      */
-    decrementFromCart({ commit }, payload) {
-        if (payload.qty > 1) {
-            commit('decrementFromCart', payload)
-        } else {
-            commit('removeFromCart', payload)
-        }
-
-        commit('computeTotal')
+    decreaseFromCart({ commit }, payload) {
+        commit('pullFromCart', {
+            item: payload,
+            mode: 'partial'
+        })
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    updateCartTotal({ commit }, payload) {
+        commit('setCartTotal', payload)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetCart({ commit }) {
+        commit('setCartItems', [])
+        commit('setCartTotal', 0)
     }
 }
 

+ 24 - 14
src/store/modules/currency.js

@@ -1,12 +1,7 @@
 const state = {
-    currencies: {
-        default: [],
-        values: this.default
-    },
-    loadingCurrencies: {
-        default: false,
-        value: true
-    }
+    currencies: [],
+    loadingCurrencies: true,
+    selectedCurrency: null
 }
 
 const getters = {
@@ -15,14 +10,20 @@ const getters = {
      * @param {*} state 
      */
     currencies(state) {
-        return state.currencies.values
+        return state.currencies
     },
     /**
      * 
      * @param {*} state 
      */
     loadingCurrencies(state) {
-        return state.loadingCurrencies.value
+        return state.loadingCurrencies
+    },
+    /**
+     * 
+     */
+    selectedCurrency(state) {
+        return state.selectedCurrency
     }
 }
 
@@ -33,7 +34,7 @@ const mutations = {
      * @param {*} payload 
      */
     setCurrencies(state, payload) {
-        state.currencies.values = [...payload]
+        state.currencies = [...payload]
     },
     /**
      * 
@@ -41,7 +42,14 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingCurrencies(state, payload) {
-        state.loadingCurrencies.value = !!payload
+        state.loadingCurrencies = !!payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     */
+    autoSelectCurrency(state) {
+        state.selectedCurrency = state.currencies.find(item => item.base == true)
     }
 }
 
@@ -53,14 +61,16 @@ const actions = {
      */
     initCurrencies({ commit }, payload) {
         commit('setCurrencies', payload)
+        commit('autoSelectCurrency')
         commit('setLoadingCurrencies', false)
     },
     /**
      * 
      * @param {*} param0 
      */
-    setCurrencies({ commit }, payload) {
-        commit('setCurrencies', payload)
+    resetCurrency({ commit }, payload) {
+        commit('setLoadingCurrencies', true)
+        commit('setCurrencies', [])
     }
 }
 

+ 15 - 12
src/store/modules/date.js

@@ -1,12 +1,6 @@
 const state = {
-    date: {
-        default: null,
-        value: this.default
-    },
-    loadingDate: {
-        default: false,
-        value: true
-    }
+    date: null,
+    loadingDate: true
 }
 
 const getters = {
@@ -15,14 +9,14 @@ const getters = {
      * @param {*} state 
      */
     date(state) {
-        return state.date.value
+        return state.date
     },
     /**
      * 
      * @param {*} state 
      */
     loadingDate(state) {
-        return state.loadingDate.value
+        return state.loadingDate
     }
 }
 
@@ -33,7 +27,7 @@ const mutations = {
      * @param {*} payload 
      */
     setDate(state, payload) {
-        state.date.value = payload
+        state.date = payload
     },
     /**
      * 
@@ -41,7 +35,7 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingDate(state, payload) {
-        state.loadingDate.value = !!payload
+        state.loadingDate = !!payload
     }
 }
 
@@ -54,6 +48,15 @@ const actions = {
     initDate({ commit }, payload) {
         commit('setDate', payload)
         commit('setLoadingDate', false)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    resetDate({ commit }, payload) {
+        commit('setLoadingDate', true)
+        commit('setDate', [])
     }
 }
 

+ 23 - 19
src/store/modules/journal.js

@@ -1,16 +1,7 @@
 const state = {
-    journals: {
-        default: [],
-        values: []
-    },
-    loadingJournals: {
-        default: false,
-        value: true
-    },
-    selectedJournal: {
-        default: null,
-        value: null
-    }
+    journals: [],
+    loadingJournals: true,
+    selectedJournal: null
 }
 
 const getters = {
@@ -19,14 +10,14 @@ const getters = {
      * @param {*} state 
      */
     journals(state) {
-        return state.journals.values
+        return state.journals
     },
     /**
      * 
      * @param {*} state 
      */
     selectedJournal(state) {
-        return state.selectedJournal.value
+        return state.selectedJournal
     },
     /**
      * 
@@ -34,7 +25,7 @@ const getters = {
      * @param {*} payload 
      */
     setLoadingJournals(state, payload) {
-        return state.loadingJournals.value
+        return state.loadingJournals
     }
 }
 
@@ -45,7 +36,7 @@ const mutations = {
      * @param {*} payload 
      */
     setJournals(state, payload) {
-        state.journals.values = [...payload]
+        state.journals = [...payload]
     },
     /**
      * 
@@ -53,7 +44,7 @@ const mutations = {
      * @param {*} payload 
      */
     autoSelectJournal(state) {
-        state.selectedJournal.value = state.journals.values.find(item => item.type === 'cash')
+        state.selectedJournal = state.journals.find(j => j.type === 'cash')
     },
     /**
      * 
@@ -61,7 +52,12 @@ const mutations = {
      * @param {*} payload 
      */
     setSelectedJournal(state, payload) {
-        state.selectedJournal.value = payload
+        if (!payload) {
+            state.selectedJournal = payload
+            return
+        }
+
+        state.selectedJournal = state.journals.find(item => item.id === payload)
     },
     /**
      * 
@@ -69,7 +65,7 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingJournals(state, payload) {
-        state.loadingJournals.value = !!payload
+        state.loadingJournals = !!payload
     }
 }
 
@@ -91,6 +87,14 @@ const actions = {
      */
     selectJournal({ commit }, payload) {
         commit('setSelectedJournal', payload)
+    },
+    /**
+     * 
+     */
+    resetJournal({ commit }, payload) {
+        commit('setLoadingJournals', false)
+        commit('setJournals', [])
+        commit('setSelectedJournal', null)
     }
 }
 

+ 155 - 36
src/store/modules/payment.js

@@ -1,20 +1,10 @@
 const state = {
-    paymentTerms: {
-        default: [],
-        values: []
-    },
-    loadingPaymentTerms: {
-        default: false,
-        value: true
-    },
-    amountPaid: {
-        default: 0,
-        value: 0
-    },
-    amountResidual: {
-        default: 0,
-        value: 0
-    }
+    paymentTerms: [],
+    loadingPaymentTerms: true,
+    selectedPaymentTerm: null,
+    paymentType: 'cash',
+    initialPayment: 0,
+    paymentLines: []
 }
 
 const getters = {
@@ -24,28 +14,42 @@ const getters = {
      * @param {*} payload 
      */
     paymentTerms(state) {
-        return state.paymentTerms.values
+        return state.paymentTerms
+    },
+    /**
+     * 
+     * @param {*} state 
+     */
+    selectedPaymentTerm(state) {
+        return state.selectedPaymentTerm
+    },
+    /**
+     * 
+     * @param {*} state 
+     */
+    paymentType(state) {
+        return state.paymentType
     },
     /**
      * 
      * @param {*} state 
      */
-    amountPaid(state) {
-        return state.amountPaid.value
+    initialPayment(state) {
+        return state.initialPayment
     },
     /**
      * 
      * @param {*} state 
      */
-    amountResidual(state) {
-        return state.amountResidual.value
+    paymentLines(state) {
+        return state.paymentLines
     },
     /**
      * 
      * @param {*} state 
      */
     loadingPaymentTerms(state) {
-        return state.loadingPaymentTerms.value
+        return state.loadingPaymentTerms
     }
 }
 
@@ -56,39 +60,112 @@ const mutations = {
      * @param {*} payload 
      */
     setPaymentTerms(state, payload) {
-        state.paymentTerms.values = [...payload]
+        state.paymentTerms = [...payload]
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    setAmountPaid(state, payload) {
-        state.amountPaid.value = payload
+    setLoadingPaymentTerms(state, payload) {
+        state.loadingPaymentTerms = !!payload
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    computeAmountResidual(state, payload) {
-        state.amountResidual.value = payload.paymentAmount >= payload.totalAmount ? payload.paymentAmount - payload.totalAmount : 0
+    autoSelectPaymentTerm(state, payload) {
+        if (!payload || payload === 'cash') {
+            state.selectedPaymentTerm = state.paymentTerms.find(t => t.lines.length === 1 && t.lines[0].days === 0) || state.paymentTerms.find(t => t.lines.length === 1 && t.lines[0].days >= 0)
+        } else {
+            state.selectedPaymentTerm = state.paymentTerms.find(t => t.lines[0].days > 0)
+        }
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    setAmountResidual(state, payload) {
-        state.amountResidual.value = payload
+    setSelectedPaymentTerm(state, payload) {
+        if (!payload) {
+            state.selectedPaymentTerm = payload
+            return
+        }
+
+        state.selectedPaymentTerm = state.paymentTerms.find(item => item.id === payload)
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    setLoadingPaymentTerms(state, payload) {
-        state.paymentTerms.value = !!payload
+    setPaymentType(state, payload) {
+        state.paymentType = payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    setInitialPayment(state, payload) {
+        state.initialPayment = payload
+    },
+    /**
+     * 
+     * @param {*} state 
+     * @param {*} payload 
+     */
+    setPaymentLines(state, payload) {
+        state.paymentLines = []
+    
+        if (!payload) {
+            return
+        }
+    
+        let percentPaid = state.initialPayment / payload.total
+        let distributedPercentage = -(percentPaid / state.selectedPaymentTerm.lines.length)
+        let totals = []
+        let residual = payload.total
+        let dueDate = null
+    
+        for (let line of state.selectedPaymentTerm.lines) {
+            dueDate = moment(payload.date).add(line.days + line.days2, 'days').format('YYYY-MM-DD')
+    
+            if(percentPaid && percentPaid < 1) {
+                totals.push([payload.date, percentPaid])
+                percentPaid = 0
+    
+                if (dueDate === payload.date) {
+                    distributedPercentage = ((totals[0][1] - line.valueAmount) / (state.selectedPaymentTerm.lines.length - 1))
+                    continue
+                }
+            }
+    
+            if (line.value !== 'balance') {
+                totals.push([dueDate, line.valueAmount + distributedPercentage])
+                continue
+            }
+    
+            totals.push([dueDate, line.valueAmount])
+    
+            for (let line of totals) {
+                let currentPrice = (payload.total * line[1]).toFixed(2)
+    
+                if (currentPrice < 0) {
+                    continue
+                }
+    
+                residual = residual - currentPrice
+    
+                state.paymentLines.push({
+                    date: line[0],
+                    total: currentPrice !== parseFloat(0).toFixed(2) ? currentPrice : residual.toFixed(2)
+                })
+            }
+    
+            totals = []
+        }    
     }
 }
 
@@ -100,6 +177,7 @@ const actions = {
      */
     initPaymentTerms({ commit }, payload) {
         commit('setPaymentTerms', payload)
+        commit('autoSelectPaymentTerm')
         commit('setLoadingPaymentTerms', false)
     },
     /**
@@ -107,12 +185,53 @@ const actions = {
      * @param {*} param0 
      * @param {*} payload 
      */
-    changeAmountPaid({ commit, getters }, payload) {
-        commit('setAmountPaid', payload)
-        commit('computeAmountResidual', {
-            totalAmount: getters.cartTotal,
-            paymentAmount: payload
+    selectPaymentTerm({ commit }, payload) {
+        if (!payload) {
+            return
+        }
+
+        commit('setSelectedPaymentTerm', payload)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    changePaymentType({ commit }, payload) {
+        commit('setPaymentType', payload),
+        commit('autoSelectPaymentTerm', payload)
+        commit('setInitialPayment', 0)
+        commit('setPaymentLines')
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    changeInitialPayment({ commit }, payload) {
+        commit('setInitialPayment', payload)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     * @param {*} payload 
+     */
+    computePaymentLines({ commit, getters }) {
+        commit('setPaymentLines', {
+            date: getters.date,
+            total: getters.cartTotal
         })
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetPayment({ commit }) {
+        commit('setLoadingPaymentTerms', true)
+        commit('setPaymentTerms', [])
+        commit('setSelectedPaymentTerm', null)
+        commit('setPaymentType', 'cash')
+        commit('setInitialPayment', 0)
     }
 }
 

+ 16 - 12
src/store/modules/picking.js

@@ -1,12 +1,6 @@
 const state = {
-    pickingTypes: {
-        default: [],
-        values: this.default
-    },
-    loadingPickingTypes: {
-        default: false,
-        value: true
-    }
+    pickingTypes: [],
+    loadingPickingTypes: true
 }
 
 const getters = {
@@ -15,14 +9,14 @@ const getters = {
      * @param {*} state 
      */
     pickingTypes(state) {
-        return state.pickingTypes.values
+        return state.pickingTypes
     },
     /**
      * 
      * @param {*} state 
      */
     loadingPickingTypes(state) {
-        return state.loadingPickingTypes.value
+        return state.loadingPickingTypes
     }
 }
 
@@ -33,7 +27,7 @@ const mutations = {
      * @param {*} payload 
      */
     setPickingTypes(state, payload) {
-        state.pickingTypes.values = [...payload]
+        state.pickingTypes = [...payload]
     },
     /**
      * 
@@ -41,7 +35,7 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingPickingTypes(state, payload) {
-        state.loadingPickingTypes.value = !!payload
+        state.loadingPickingTypes = !!payload
     }
 }
 
@@ -52,6 +46,16 @@ const actions = {
      */
     initPickingTypes({ commit }, payload) {
         commit('setPickingTypes', payload)
+        commit('setLoadingPickingTypes', false)
+    },
+   /**
+    * 
+    * @param {*} param0 
+    * @param {*} payload 
+    */
+    resetPicking({ commit }, payload) {
+        commit('setLoadingPickingTypes', true)
+        commit('setPickingTypes', [])
     }
 }
 

+ 24 - 47
src/store/modules/product.js

@@ -1,24 +1,8 @@
 const state = {
-    products: {
-        default: [],
-        values: []
-    },
-    loadingProducts: {
-        default: false,
-        value: true
-    },
-    filteredProducts: {
-        default: [],
-        values: []
-    },
-    productWithVariant: {
-        default: null,
-        value: null
-    },
-    showProductForm: {
-        default: false,
-        value: false
-    }
+    products: [],
+    loadingProducts: true,
+    filteredProducts: [],
+    productWithVariant: null
 }
 
 const getters = {
@@ -27,35 +11,35 @@ const getters = {
      * @param {*} state 
      */
     products(state) {
-        return state.products.values
+        return state.products
     },
     /**
      * 
      * @param {*} state 
      */
     visibleProducts(state) {
-        return state.filteredProducts.values.length === 0 ? state.products.values : state.filteredProducts.values
+        return state.filteredProducts.length === 0 ? state.products : state.filteredProducts
     },
     /**
      * 
      * @param {*} state 
      */
     productWithVariant(state) {
-        return state.productWithVariant.value
+        return state.productWithVariant
     },
     /**
      * 
      * @param {*} state 
      */
     showProductForm(state) {
-        return state.showProductForm.value
+        return !!state.productWithVariant
     },
     /**
      * 
      * @param {*} state 
      */
     loadingProducts(state) {
-        return state.loadingProducts.value
+        return state.loadingProducts
     }
 }
 
@@ -66,7 +50,7 @@ const mutations = {
      * @param {*} payload 
      */
     setProducts(state, payload) {
-        state.products.values = [...payload]
+        state.products = [...payload]
     },
     /**
      * 
@@ -74,7 +58,7 @@ const mutations = {
      * @param {*} payload 
      */
     setProductWithVariant(state, payload) {
-        state.productWithVariant.value = value
+        state.productWithVariant = payload
     },
     /**
      * 
@@ -82,23 +66,7 @@ const mutations = {
      * @param {*} payload 
      */
     setFilteredProducts(state, payload) {
-        state.filteredProducts.values = [...payload]
-    },
-    /**
-     * 
-     * @param {*} state 
-     * @param {*} payload 
-     */
-    setShowProductForm(state, payload) {
-        state.showProductForm.value = !!payload
-    },
-    /**
-     * 
-     * @param {*} state 
-     * @param {*} payload 
-     */
-    addProduct(state, payload) {
-        state.products.values = [payload, ...state.products.values]
+        state.filteredProducts = [...payload]
     },
     /**
      * 
@@ -106,7 +74,7 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingProducts(state, payload) {
-        state.loadingProducts.value = !!payload
+        state.loadingProducts = !!payload
     }
 }
 
@@ -125,13 +93,13 @@ const actions = {
      * @param {*} param0 
      * @param {*} payload 
      */
-    selectProduct({ commit, dispatch }, payload) {
+    selectProduct({ commit }, payload) {
         if (payload.variantCount > 1) {
             commit('setProductWithVariant', payload)
             return
         }
 
-        dispatch('addToCart', payload.variants[0])
+        commit('pushToCart', payload.variants[0])
     },
     /**
      * 
@@ -195,6 +163,15 @@ const actions = {
     addProduct({ commit }, payload) {
         commit('addProduct', payload)
         commit('setLoadingProducts', false)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetProduct({ commit }) {
+        commit('setLoadingProducts', true)
+        commit('setProducts', [])
+        commit('setFilteredProducts', [])
     }
 }
 

+ 26 - 61
src/store/modules/supplier.js

@@ -1,24 +1,8 @@
 const state = {
-    suppliers: {
-        default: [],
-        values: []
-    },
-    loadingSuppliers: {
-        default: false,
-        value: true
-    },
-    filteredSuppliers: {
-        default: [],
-        values: []
-    },
-    supplierSelected: {
-        default: null,
-        value: null
-    },
-    showSupplierForm: {
-        default: false,
-        value: false
-    }
+    suppliers: [],
+    loadingSuppliers: true,
+    filteredSuppliers: [],
+    selectedSupplier: null
 }
 
 const getters = {
@@ -27,35 +11,28 @@ const getters = {
      * @param {*} state 
      */
     suppliers(state) {
-        return state.suppliers.values
+        return state.suppliers
     },
     /**
      * 
      * @param {*} state 
      */
     loadingSuppliers(state) {
-        return state.loadingSuppliers.value
+        return state.loadingSuppliers
     },
     /**
      * 
      * @param {*} state 
      */
     visibleSuppliers(state) {
-        return state.filteredSuppliers.values.length === 0 ? state.suppliers.values : state.filteredSuppliers.values
-    },
-    /**
-     * 
-     * @param {*} state 
-     */
-    supplierSelected(state) {
-        return state.supplierSelected.value
+        return state.filteredSuppliers.length === 0 ? state.suppliers : state.filteredSuppliers
     },
     /**
      * 
      * @param {*} state 
      */
-    showSupplierForm(state) {
-        return state.showSupplierForm.value
+    selectedSupplier(state) {
+        return state.selectedSupplier
     }
 }
 
@@ -66,7 +43,7 @@ const mutations = {
      * @param {*} payload 
      */
     setSuppliers(state, payload) {
-        state.suppliers.values = [...payload]
+        state.suppliers = [...payload]
     },
     /**
      * 
@@ -74,15 +51,15 @@ const mutations = {
      * @param {*} payload 
      */ 
     setLoadingSuppliers(state, payload) {
-        state.loadingSuppliers.value = !!payload
+        state.loadingSuppliers = !!payload
     },
     /**
      * 
      * @param {*} state 
      * @param {*} payload 
      */
-    setSupplierSelected(state, payload) {
-        state.supplierSelected.value = payload
+    setSelectedSupplier(state, payload) {
+        state.selectedSupplier = payload
     },
     /**
      * 
@@ -90,15 +67,7 @@ const mutations = {
      * @param {*} payload 
      */
     setFilteredSuppliers(state, payload) {
-        state.filteredSuppliers.values = [...payload]
-    },
-    /**
-     * 
-     * @param {*} state 
-     * @param {*} payload 
-     */
-    setShowSupplierForm(state, payload) {
-        state.showSupplierForm.value = !!payload
+        state.filteredSuppliers = [...payload]
     },
     /**
      * 
@@ -106,7 +75,7 @@ const mutations = {
      * @param {*} payload 
      */
     addSupplier(state, payload) {
-        state.suppliers.values = [payload, ...state.suppliers.values]
+        state.suppliers = [payload, ...state.suppliers]
     }
 }
 
@@ -126,7 +95,7 @@ const actions = {
      * @param {*} payload 
      */
     selectSupplier({ commit }, payload) {
-        commit('setSupplierSelected', payload)
+        commit('setSelectedSupplier', payload)
     },
     /**
      * 
@@ -136,20 +105,6 @@ const actions = {
     filterSuppliers({ commit }, payload) {
         commit('setFilteredSuppliers', payload)
     },
-    /**
-     * 
-     * @param {*} param0 
-     */
-    showSupplierForm({ commit }) {
-        commit('setShowSupplierForm', true)
-    },
-    /**
-     * 
-     * @param {*} param0 
-     */
-    hideSupplierForm({ commit }) {
-        commit('setShowSupplierForm', false)
-    },
     /**
      * 
      * @param {*} param0 
@@ -175,6 +130,16 @@ const actions = {
     addSupplier({ commit }, payload) {
         commit('addSupplier', payload)
         commit('setLoadingSuppliers', false)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetSupplier({ commit }) {
+        commit('setLoadingSuppliers', true)
+        commit('setSuppliers', [])
+        commit('setFilteredSuppliers', [])
+        commit('setSelectedSupplier', null)
     }
 }
 

+ 14 - 12
src/store/modules/user.js

@@ -1,12 +1,6 @@
 const state = {
-    user: {
-        default: null,
-        value: null
-    },
-    loadingUser: {
-        default: false,
-        value: true
-    }
+    user: null,
+    loadingUser: true
 }
 
 const getters = {
@@ -15,14 +9,14 @@ const getters = {
      * @param {*} state 
      */
     user(state) {
-        return state.user.value
+        return state.user
     },
     /**
      * 
      * @param {*} state 
      */
     loadingUser(state) {
-        return state.loadingUser.value
+        return state.loadingUser
     }
 }
 
@@ -33,7 +27,7 @@ const mutations = {
      * @param {*} payload 
      */
     setUser(state, payload) {
-        state.user.value = payload
+        state.user = payload
     },
     /**
      * 
@@ -41,7 +35,7 @@ const mutations = {
      * @param {*} payload 
      */
     setLoadingUser(state, payload) {
-        state.loadingUser.value = !!payload
+        state.loadingUser = !!payload
     }
 }
 
@@ -54,6 +48,14 @@ const actions = {
     initUser({ commit }, payload) {
         commit('setUser', payload)
         commit('setLoadingUser', false)
+    },
+    /**
+     * 
+     * @param {*} param0 
+     */
+    resetUser({ commit }) {
+        commit('setLoadingUser', true)
+        commit('setUser', null)
     }
 }
 

+ 10 - 2
templates.xml

@@ -7,11 +7,19 @@
             </xpath>
         </template>
 
-        <record id="eiru_purchases.client_action_launch" model="ir.actions.client">
+        <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>
 
-        <menuitem id="eiru_purchases.launch" name="Nueva compra" parent="eiru_dashboard.eiru_dashboard_main" action="eiru_purchases.client_action_launch" sequence="3" />
+        <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>