Browse Source

componente para pie de lista

robert2206 8 năm trước cách đây
mục cha
commit
c52372b96d

+ 59 - 59
config.xml

@@ -1,60 +1,60 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<?xml version='1.0' encoding='utf-8'?>
 <widget id="com.eiru.odoo" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
-  <name>Odoo by Eiru</name>
-  <description>Odoo Mobile</description>
-  <author email="robert.gauto@gmail.com" href="http://ionicframework.com/">Robert Alexis GAuto</author>
-  <content src="index.html"/>
-  <access origin="*"/>
-  <allow-intent href="http://*/*"/>
-  <allow-intent href="https://*/*"/>
-  <allow-intent href="tel:*"/>
-  <allow-intent href="sms:*"/>
-  <allow-intent href="mailto:*"/>
-  <allow-intent href="geo:*"/>
-  <platform name="android">
-    <allow-intent href="market:*"/>
-    <icon src="resources/android/icon/drawable-ldpi-icon.png" density="ldpi"/>
-    <icon src="resources/android/icon/drawable-mdpi-icon.png" density="mdpi"/>
-    <icon src="resources/android/icon/drawable-hdpi-icon.png" density="hdpi"/>
-    <icon src="resources/android/icon/drawable-xhdpi-icon.png" density="xhdpi"/>
-    <icon src="resources/android/icon/drawable-xxhdpi-icon.png" density="xxhdpi"/>
-    <icon src="resources/android/icon/drawable-xxxhdpi-icon.png" density="xxxhdpi"/>
-    <splash src="resources/android/splash/drawable-land-ldpi-screen.png" density="land-ldpi"/>
-    <splash src="resources/android/splash/drawable-land-mdpi-screen.png" density="land-mdpi"/>
-    <splash src="resources/android/splash/drawable-land-hdpi-screen.png" density="land-hdpi"/>
-    <splash src="resources/android/splash/drawable-land-xhdpi-screen.png" density="land-xhdpi"/>
-    <splash src="resources/android/splash/drawable-land-xxhdpi-screen.png" density="land-xxhdpi"/>
-    <splash src="resources/android/splash/drawable-land-xxxhdpi-screen.png" density="land-xxxhdpi"/>
-    <splash src="resources/android/splash/drawable-port-ldpi-screen.png" density="port-ldpi"/>
-    <splash src="resources/android/splash/drawable-port-mdpi-screen.png" density="port-mdpi"/>
-    <splash src="resources/android/splash/drawable-port-hdpi-screen.png" density="port-hdpi"/>
-    <splash src="resources/android/splash/drawable-port-xhdpi-screen.png" density="port-xhdpi"/>
-    <splash src="resources/android/splash/drawable-port-xxhdpi-screen.png" density="port-xxhdpi"/>
-    <splash src="resources/android/splash/drawable-port-xxxhdpi-screen.png" density="port-xxxhdpi"/>
-  </platform>
-  <platform name="ios">
-    <allow-intent href="itms:*"/>
-    <allow-intent href="itms-apps:*"/>
-  </platform>
-  <preference name="webviewbounce" value="false"/>
-  <preference name="UIWebViewBounce" value="false"/>
-  <preference name="DisallowOverscroll" value="true"/>
-  <preference name="android-minSdkVersion" value="19"/>
-  <preference name="BackupWebStorage" value="none"/>
-  <preference name="SplashScreenDelay" value="1000"/>
-  <preference name="FadeSplashScreen" value="true"/>
-  <preference name="FadeSplashScreenDuration" value="500"/>
-  <preference name="SplashScreenBackgroundColor" value="0xFFFFFFFF"/>
-  <preference name="SplashScreen" value="screen"/>
-  <feature name="StatusBar">
-    <param name="ios-package" onload="true" value="CDVStatusBar"/>
-  </feature>
-  <plugin name="cordova-plugin-device" spec="~1.1.3"/>
-  <plugin name="cordova-plugin-console" spec="~1.0.4"/>
-  <plugin name="cordova-plugin-whitelist" spec="~1.3.0"/>
-  <plugin name="cordova-plugin-splashscreen" spec="~4.0.0"/>
-  <plugin name="cordova-plugin-statusbar" spec="~2.2.0"/>
-  <plugin name="ionic-plugin-keyboard" spec="~2.2.1"/>
-  <plugin name="cordova-sqlite-storage" spec="~1.4.8"/>
-  <icon src="resources/android/icon/drawable-xhdpi-icon.png"/>
-</widget>
+    <name>Odoo by Eiru</name>
+    <description>Odoo Mobile</description>
+    <author email="robert.gauto@gmail.com" href="http://ionicframework.com/">Robert Alexis GAuto</author>
+    <content src="index.html" />
+    <access origin="*" />
+    <allow-intent href="http://*/*" />
+    <allow-intent href="https://*/*" />
+    <allow-intent href="tel:*" />
+    <allow-intent href="sms:*" />
+    <allow-intent href="mailto:*" />
+    <allow-intent href="geo:*" />
+    <platform name="android">
+        <allow-intent href="market:*" />
+        <icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
+        <icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
+        <icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" />
+        <icon density="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" />
+        <icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" />
+        <icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-icon.png" />
+        <splash density="land-ldpi" src="resources/android/splash/drawable-land-ldpi-screen.png" />
+        <splash density="land-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" />
+        <splash density="land-hdpi" src="resources/android/splash/drawable-land-hdpi-screen.png" />
+        <splash density="land-xhdpi" src="resources/android/splash/drawable-land-xhdpi-screen.png" />
+        <splash density="land-xxhdpi" src="resources/android/splash/drawable-land-xxhdpi-screen.png" />
+        <splash density="land-xxxhdpi" src="resources/android/splash/drawable-land-xxxhdpi-screen.png" />
+        <splash density="port-ldpi" src="resources/android/splash/drawable-port-ldpi-screen.png" />
+        <splash density="port-mdpi" src="resources/android/splash/drawable-port-mdpi-screen.png" />
+        <splash density="port-hdpi" src="resources/android/splash/drawable-port-hdpi-screen.png" />
+        <splash density="port-xhdpi" src="resources/android/splash/drawable-port-xhdpi-screen.png" />
+        <splash density="port-xxhdpi" src="resources/android/splash/drawable-port-xxhdpi-screen.png" />
+        <splash density="port-xxxhdpi" src="resources/android/splash/drawable-port-xxxhdpi-screen.png" />
+    </platform>
+    <platform name="ios">
+        <allow-intent href="itms:*" />
+        <allow-intent href="itms-apps:*" />
+    </platform>
+    <preference name="webviewbounce" value="false" />
+    <preference name="UIWebViewBounce" value="false" />
+    <preference name="DisallowOverscroll" value="true" />
+    <preference name="android-minSdkVersion" value="19" />
+    <preference name="BackupWebStorage" value="none" />
+    <preference name="SplashScreenDelay" value="1000" />
+    <preference name="FadeSplashScreen" value="true" />
+    <preference name="FadeSplashScreenDuration" value="500" />
+    <preference name="SplashScreenBackgroundColor" value="0xFFFFFFFF" />
+    <preference name="SplashScreen" value="screen" />
+    <feature name="StatusBar">
+        <param name="ios-package" onload="true" value="CDVStatusBar" />
+    </feature>
+    <plugin name="cordova-plugin-device" spec="~1.1.3" />
+    <icon src="resources/android/icon/drawable-xhdpi-icon.png" />
+    <plugin name="cordova-plugin-console" spec="~1.0.5" />
+    <plugin name="cordova-plugin-whitelist" spec="~1.3.1" />
+    <plugin name="cordova-plugin-splashscreen" spec="~4.0.1" />
+    <plugin name="cordova-plugin-statusbar" spec="~2.2.1" />
+    <plugin name="ionic-plugin-keyboard" spec="~2.2.0" />
+    <plugin name="cordova-sqlite-storage" spec="~2.0.1" />
+</widget>

+ 4 - 1
src/app/app.component.ts

@@ -9,6 +9,7 @@ import { HomePage } from '../pages/home/home';
 
 import { CustomerListPage } from '../pages/customer-list/customer-list';
 import { BudgetListPage } from '../pages/budget-list/budget-list';
+import { SaleOrderListPage } from "../pages/sale-order-list/sale-order-list";
 import { ProductListPage } from '../pages/product-list/product-list';
 import { LeadListPage } from '../pages/lead-list/lead-list';
 import { OpportunityListPage } from '../pages/opportunity-list/opportunity-list';
@@ -24,6 +25,8 @@ import { AboutPage } from '../pages/about/about';
 import { Slots } from "../utils/slots";
 import { Toaster } from "../utils/toaster";
 
+// import "reflect-metadata";
+
 @Component({
     templateUrl: 'app.html'
 })
@@ -57,7 +60,7 @@ export class OdooMobileApp {
                         visible: true,
                         title: "Presupuestos",
                         icon: "basket",
-                        component: BudgetListPage 
+                        component: SaleOrderListPage 
                     },
                     {
                         visible: true,

+ 9 - 1
src/app/app.module.ts

@@ -9,11 +9,14 @@ import { HomePage } from '../pages/home/home';
 import { OLoader } from "../components/oloader/oloader";
 import { OListHeader } from "../components/olist-header/olist-header";
 import { OListFooter } from "../components/olist-footer/olist-footer";
+import { ODetailsHeader } from "../components/odetails-header/odetails-header";
 
 import { CustomerListPage } from '../pages/customer-list/customer-list';
 import { CustomerOptions } from "../pages/customer-list/customer-options";
 import { CustomerDetailsPage } from "../pages/customer-details/customer-details";
 import { BudgetListPage } from '../pages/budget-list/budget-list';
+import { SaleOrderListPage } from "../pages/sale-order-list/sale-order-list";
+import { SaleOrderDetailsPage } from "../pages/sale-order-details/sale-order-details";
 import { BudgetDetailsPage } from "../pages/budget-details/budget-details";
 import { ProductListPage } from '../pages/product-list/product-list';
 import { ProductOptions } from '../pages/product-list/product-options';
@@ -61,6 +64,8 @@ import { DoubleTapDirective } from "../directives/double-tap-directive";
         CustomerDetailsPage,
         BudgetListPage,
         BudgetDetailsPage,
+        SaleOrderListPage,
+        SaleOrderDetailsPage,
         ProductListPage,
         ProductOptions,
         ProductDetailsPage,
@@ -81,7 +86,8 @@ import { DoubleTapDirective } from "../directives/double-tap-directive";
         DoubleTapDirective,
         // Components
         OListHeader,
-        OListFooter
+        OListFooter,
+        ODetailsHeader
     ],
     imports: [
         IonicModule.forRoot(OdooMobileApp),
@@ -99,6 +105,8 @@ import { DoubleTapDirective } from "../directives/double-tap-directive";
         CustomerOptions,
         CustomerDetailsPage,
         BudgetListPage,
+        SaleOrderListPage,
+        SaleOrderDetailsPage,
         BudgetDetailsPage,
         ProductListPage,
         ProductOptions,

+ 11 - 0
src/components/odetails-header/odetails-header.html

@@ -0,0 +1,11 @@
+<ion-header>
+    <ion-navbar color="primary">
+        <ion-title>{{ title }}</ion-title>
+
+        <ion-buttons *ngIf="showButtons" end>
+            <button ion-button (click)="saveClick()">
+                <ion-icon color="ligth" name="create"></ion-icon>
+            </button>
+        </ion-buttons>
+    </ion-navbar>
+</ion-header>

+ 0 - 0
src/components/odetails-header/odetails-header.scss


+ 30 - 0
src/components/odetails-header/odetails-header.ts

@@ -0,0 +1,30 @@
+import { Component, Input, Output, EventEmitter } from "@angular/core";
+
+@Component({
+    selector: "odetails-header",
+    templateUrl: "odetails-header.html",
+})
+export class ODetailsHeader {
+
+
+    @Input()
+    title: string;
+
+    @Input()
+    showButtons: boolean;
+
+    @Output()
+    save: EventEmitter<any>;
+
+    constructor() {
+        this.showButtons = true;
+        this.save = new EventEmitter();
+    }
+
+    /**
+     *
+     */
+    saveClick(event) {
+        this.save.emit();
+    }
+}

+ 1 - 0
src/components/olist-footer/olist-footer.ts

@@ -1,5 +1,6 @@
 import { Component, Input, Output, EventEmitter, ContentChild } from "@angular/core";
 import { Content } from "ionic-angular";
+import { Slots } from "../../utils/slots";
 
 @Component({
     selector: "olist-footer",

+ 47 - 0
src/defaults/default-details-view.ts

@@ -0,0 +1,47 @@
+import { ReflectiveInjector, OnInit, OnDestroy } from "@angular/core";
+import { Events, Platform } from "ionic-angular";
+import { DataProvider } from "../providers/data-provider";
+import { Slots } from "../utils/slots";
+
+export class DetailsView<E> {
+
+    private _type: any;
+    private _data: E;
+    private _dataProvider: DataProvider
+    private _events: Events;
+
+    constructor(_type: any) {
+        this._type = _type;
+
+        let injector = ReflectiveInjector.resolveAndCreate([Events, Platform, DataProvider]);
+        this._events = injector.get(Events);
+        this._dataProvider = injector.get(DataProvider);
+
+        this.initialize();
+    }
+
+    /**
+     *
+     */
+    private initialize(): void {
+    
+    }
+
+    /**
+     *
+     */
+    get dataProvider() {
+        return this._dataProvider;
+    }
+
+    get events() {
+        return this._events;
+    }
+
+    /**
+     *
+     */
+    save(): void {
+
+    }
+}

+ 52 - 9
src/defaults/default-list-view.ts

@@ -1,5 +1,6 @@
 import { ReflectiveInjector, OnInit, OnDestroy } from "@angular/core";
-import { Events, App, IonicApp, Config, Platform } from "ionic-angular";
+import { Nav, Events, Platform } from "ionic-angular";
+import { DataProvider } from "../providers/data-provider";
 import { Slots } from "../utils/slots";
 
 /**
@@ -7,24 +8,31 @@ import { Slots } from "../utils/slots";
  */
 export abstract class ListView<E> implements OnInit, OnDestroy {
 
-    private _eventsSlots: Events;
+    private _type: E;
+    private _document: any;
     private _isSearch: boolean;
-    private _itemsFound: Array<E>;
+    private _itemsAux: Array<E>;
     private _items: Array<E>;
+    private _eventsSlots: Events;
+    private _database: DataProvider;
 
-    constructor() {
-        let injector = ReflectiveInjector.resolveAndCreate([App, IonicApp, Config, Platform, Events]);
+    constructor(_type: any) {
+        this._type = _type;
+        this._document = Reflect.getMetadata("document", _type);
+        this._isSearch = false;
+        this._itemsAux = [];
+        this._items = [];
 
+        let injector = ReflectiveInjector.resolveAndCreate([Events, DataProvider, Platform]);
         this._eventsSlots = injector.get(Events);
+        this._database = injector.get(DataProvider);
     }
 
     /**
      *
      */
     ngOnInit() {
-        this._eventsSlots.subscribe(Slots.ITEM_SAVED, data => {
-            this.add(data);
-        });
+        this.initialize();
     }
 
     /**
@@ -34,6 +42,25 @@ export abstract class ListView<E> implements OnInit, OnDestroy {
         this._eventsSlots.unsubscribe(Slots.ITEM_SAVED);
     }
 
+    /**
+     *
+     */
+    private initialize() {
+        this._eventsSlots.publish(Slots.APP_LOADING);
+
+        this._database.getAll(this._document).then(items => { 
+            this.items.concat(items);
+            this._eventsSlots.publish(Slots.APP_LOADED);
+        }).catch(e => { 
+            this._eventsSlots.publish(Slots.APP_LOADED);
+            this._eventsSlots.publish(Slots.APP_ERROR, "No se ha podido cargar los datos");
+        });
+        
+        this._eventsSlots.subscribe(Slots.ITEM_SAVED, data => {
+            this.add(data);
+        });
+    }
+
     /**
      *
      */
@@ -84,6 +111,13 @@ export abstract class ListView<E> implements OnInit, OnDestroy {
         return this._isSearch;
     }
 
+    /**
+     *
+     */
+    count(): number {
+        return this.items.length;
+    }
+
     /**
      *
      */
@@ -91,7 +125,16 @@ export abstract class ListView<E> implements OnInit, OnDestroy {
         this._isSearch = !this._isSearch;
 
         if (this.isSearch()) {
-
+            this._itemsAux = this._items;
+        } else {
+            this._items = this._itemsAux;
         }
     }
+
+    /**
+     *
+     */
+    search(text: string): void {
+        
+    }
 }

+ 5 - 1
src/interfaces/navigable-interface.ts

@@ -1,3 +1,7 @@
 export interface INavigable {
-    goToPage(page: any): void;
+
+    /**
+     *
+     */
+    goToPage(data: any): void;
 }

+ 2 - 0
src/models/sale.order.ts

@@ -1,5 +1,7 @@
 import { BaseModel } from "./base.model";
 
+@Reflect.metadata("document", "sale.order")
+@Reflect.metadata("endpoint", "sale_order")    
 export class SaleOrder extends BaseModel {
 
     public amount_tax: number;

+ 18 - 18
src/pages/budget-list/budget-list.ts

@@ -1,10 +1,9 @@
 import { Component, OnInit, OnDestroy } from '@angular/core';
-import { NavController, ToastController, ActionSheetController, Events } from 'ionic-angular';
+import { NavController, ToastController, ActionSheetController, LoadingController, Events } from 'ionic-angular';
 import { Slots } from "../../utils/slots";
 import { INavigable } from "../../interfaces/navigable-interface";
 import { DataProvider } from "../../providers/data-provider";
 import { DefaultListable } from "../../defaults/default-listable";
-import { DefaultListView } from "../../defaults/default-list-view";
 import { SaleOrder } from "../../models/sale.order";
 import { BudgetDetailsPage } from "../budget-details/budget-details";
 
@@ -12,16 +11,17 @@ import { BudgetDetailsPage } from "../budget-details/budget-details";
     selector: 'page-budget-list',
     templateUrl: 'budget-list.html'
 })
-export class BudgetListPage extends DefaultListView implements INavigable {
+export class BudgetListPage extends DefaultListable<SaleOrder> implements INavigable {
 
     constructor(
         public navCtrl: NavController,
         public toastCtrl: ToastController,
         public actionSheetCtrl: ActionSheetController,
+        public loadingCtrl: LoadingController,
         public events: Events,
         public db: DataProvider
     ) { 
-        super();
+        super(loadingCtrl);
     }
     
     /**
@@ -31,21 +31,21 @@ export class BudgetListPage extends DefaultListView implements INavigable {
         this.initialize();
     }
 
-    // /**
-    //  *
-    //  */
-    // ngOnInit() {
-    //     this.events.subscribe(Slots.ITEM_SAVED, data => {
-    //         this.add(data[0]);
-    //     });
-    // }
+    /**
+     *
+     */
+    ngOnInit() {
+        this.events.subscribe(Slots.ITEM_SAVED, data => {
+            this.add(data[0]);
+        });
+    }
 
-    // /**
-    //  *
-    //  */
-    // ngOnDestroy() {
-    //     this.events.unsubscribe(Slots.ITEM_SAVED);
-    // }
+    /**
+     *
+     */
+    ngOnDestroy() {
+        this.events.unsubscribe(Slots.ITEM_SAVED);
+    }
 
     /**
      *

+ 1 - 1
src/pages/customer-list/customer-list.html

@@ -26,7 +26,7 @@
 </ion-toolbar>
 
 <ion-content>
-    <ion-card *ngFor="let c of elements; trackBy:trackByElements">
+    <ion-card *ngFor="let c of visibleElements; trackBy:trackByElements">
         <ion-item>
             <ion-thumbnail item-left>
                 <img src="./assets/images/customer.png" *ngIf="!c.image_medium"/>

+ 168 - 0
src/pages/sale-order-details/sale-order-details.html

@@ -0,0 +1,168 @@
+<!--Encabezado-->
+<odetails-header title="Presupuesto" [showButtons]=false></odetails-header>
+
+<!--Contenido del formulario-->
+<ion-content class="has-header">
+
+    <ion-slides #saleSlides [options]="{onlyExternal: !customer.id}" (ionWillChange)="onSlideChanged($event)">
+        <!--Cliente-->
+        <ion-slide>
+            <h1>Seleccione un cliente:</h1>
+            <ion-searchbar #customerSearchInput placeholder="Buscar cliente" (ionInput)="searchCustomer($event)"></ion-searchbar>
+
+            <ion-list inset *ngIf="search" [virtualScroll]="customers" approxItemHeight="30px">
+                <ion-item *virtualItem="let item" (doubleTap)="selectCustomer(item)">
+                    {{ item.name }}
+                </ion-item>
+            </ion-list>
+
+            <div *ngIf="!search">
+                <h1>Datos del cliente:</h1>
+
+                <ion-list inset>
+                    <ion-item>
+                        <ion-label>Nombre:</ion-label>
+                        <ion-input [ngModel]="customer.name" readonly></ion-input>
+                    </ion-item>
+
+                    <ion-item>
+                        <ion-label>Teléfono:</ion-label>
+                        <ion-input [ngModel]="customer.phone" readonly></ion-input>
+                    </ion-item>
+
+                    <ion-item>
+                        <ion-label>Celular:</ion-label>
+                        <ion-input [ngModel]="customer.mobile" readonly></ion-input>
+                    </ion-item>
+
+                    <ion-item>
+                        <ion-label>Dirección:</ion-label>
+                        <ion-input [ngModel]="customer.street" readonly></ion-input>
+                    </ion-item>
+
+                    <ion-item>
+                        <ion-label>Ciudad:</ion-label>
+                        <ion-input [ngModel]="customer.city" readonly></ion-input>
+                    </ion-item>
+                </ion-list>
+            </div>
+        </ion-slide>
+
+        <!--Líneas de presupuesto-->
+        <ion-slide>
+            <h1>Líneas del presupuesto</h1>
+            <ion-searchbar #productSearchInput placeholder="Buscar producto" (ionInput)="searchProduct($event)" (doubleTap)="readBarcode()"></ion-searchbar>
+
+            <!--<div class="no-element" *ngIf="lines.length == 0 && !search">
+                <p>Sin elementos</p>
+            </div>-->
+
+           <div class="sale-content-wrapper" *ngIf="search">
+
+               <ion-scroll scrollY="true" inset [virtualScroll]="products" approxItemHeight="30px">
+                    <ion-item *virtualItem="let p" (doubleTap)="selectProduct(p)">
+                        <ion-thumbnail item-left>
+                            <ion-img [src]="p.image_medium" *ngIf="!p.image_medium"></ion-img>
+                            <img src="./assets/images/product.png" *ngIf="!p.image_medium"/>
+                        </ion-thumbnail>
+
+                        <h2>{{ p.name }}</h2>
+
+                        <p>
+                            <strong>Precio:</strong>
+                            {{ p.list_price }}
+                        </p>
+                    </ion-item>
+               </ion-scroll>
+
+           </div>
+
+           <div class="sale-content-wrapper" *ngIf="!search">
+
+                <ion-scroll scrollY="true" [virtualScroll]="lines" approxItemHeight="30px">
+                    <ion-item *virtualItem="let l">
+                        <ion-thumbnail item-left>
+                            <ion-img src="l.image_medium" *ngIf="l.image_medium"></ion-img>
+                            <img src="./assets/images/product.png" *ngIf="!l.image_medium"/>
+                        </ion-thumbnail>
+
+                        <h2>{{ l.product.name }}</h2>
+
+                        <p>
+                            <strong>Precio:</strong>
+                            {{ l.price }}
+                        </p>
+
+                        <p>
+                            <strong>Cantidad:</strong>
+                            {{ l.quantity }}
+                        </p>
+
+                        <p>
+                            <strong>Subtotal:</strong>
+                            {{ l.subtotal }}
+                        </p>
+
+                        <button ion-button primary clear item-right (click)="askIfRemoveItem(l)">
+                            <ion-icon name="backspace"></ion-icon>
+                        </button>
+
+                        <ion-row>
+                             <ion-col width-33>
+                                <button ion-button primary clear small icon-only (click)="removeQuantity(l)">
+                                    <ion-icon name="remove-circle"></ion-icon>
+                                </button>
+                            </ion-col>
+
+                            <ion-col width-33>
+                                <button ion-button primary clear small icon-only (click)="addQuantity(l)">
+                                    <ion-icon name="add-circle"></ion-icon>
+                                </button>
+                            </ion-col>
+
+                            <ion-col width-33>
+                                <button ion-button primary clear small icon-only (click)="askIfChangePrice(l)">
+                                    <ion-icon name="cash"></ion-icon>
+                                </button>
+                            </ion-col>
+                        </ion-row>
+                    </ion-item>
+                </ion-scroll>
+
+                <div class="sale-total">
+                    <ion-list>
+                        <ion-item>
+                            <ion-label>Total:</ion-label>
+                            <ion-input readonly text-right [ngModel]="total"></ion-input>
+                        </ion-item>
+                    </ion-list>
+                </div>
+            </div>
+        </ion-slide>
+
+        <!--Finalizar presupuesto-->
+        <ion-slide>
+            <h1>Finalizar</h1>
+
+        </ion-slide>
+    </ion-slides>
+</ion-content>
+
+<!-- Pie del formulario -->
+<ion-footer *ngIf="footer.show">
+    <ion-toolbar>
+        <ion-buttons start>
+            <button ion-button color="primary" clear icon-left *ngIf="footer.back.show" (click)="goToBack()">
+                <ion-icon name="arrow-back"></ion-icon>
+                {{ footer.back.text }}
+            </button>
+        </ion-buttons>
+
+        <ion-buttons end>
+            <button ion-button color="primary" clear icon-right *ngIf="footer.forward.show" (click)="goToNext()">
+                {{ footer.forward.text }}
+                <ion-icon name="arrow-forward"></ion-icon>
+            </button>
+        </ion-buttons>
+    </ion-toolbar>
+</ion-footer>

+ 22 - 0
src/pages/sale-order-details/sale-order-details.scss

@@ -0,0 +1,22 @@
+page-sale-order-details {
+
+    ion-scroll, .sale-content-wrapper, .slide-zoom {
+        width: 100% !important;
+        height: 100% !important;
+    }
+
+    .sale-content-wrapper, .slide-zoom {
+        display: flex;
+        flex-direction: column;
+    }
+
+    .sale-content-wrapper {
+        padding: 0 10px;
+        margin-bottom: 35px;
+    }
+
+    h1 {
+        font-size: 12pt;
+        color: #909090;
+    }
+}

+ 362 - 0
src/pages/sale-order-details/sale-order-details.ts

@@ -0,0 +1,362 @@
+import { Component, ViewChild } from "@angular/core";
+import { NavController, Slides, Searchbar, AlertController } from "ionic-angular";
+import { Keyboard } from "ionic-native";
+import { DetailsView } from "../../defaults/default-details-view";
+import { SaleOrder } from "../../models/sale.order";
+import { Partner } from "../../models/partner";
+import { Product } from "../../models/product";
+import { DataProvider } from "../../providers/data-provider";
+import { Slots } from "../../utils/slots";
+
+@Component({
+    selector: 'page-sale-order-details',
+    templateUrl: 'sale-order-details.html',
+    providers: [Partner]
+})
+export class SaleOrderDetailsPage extends DetailsView<SaleOrder> {
+
+    @ViewChild("saleSlides")
+    slider: Slides;
+    @ViewChild("customerSearchInput")
+    customerSearchBar: Searchbar;
+    @ViewChild("productSearchInput")
+    productSearchBar: Searchbar;
+    search: boolean = false;
+    footer: any = {
+        show: true,
+        back: {
+            text: "Atrás",
+            show: false
+        },
+        forward: {
+            text: "Siguiente",
+            show: false
+        }
+    }
+    customers: Array<Partner>;
+    products: Array<Product>;
+    lines: Array<{ product: Product, price: number, quantity: number, subtotal: number }>
+    total: number;
+    
+    constructor(
+        public navCtrl: NavController,
+        public alertCtrl: AlertController,
+        public db: DataProvider,
+        public customer: Partner,
+    ) {
+        super(SaleOrder);
+
+        this.lines = [];
+        this.total = 0;
+
+        this.initKeyBoardEvents();
+    }
+
+    /**
+     *
+     */
+    initKeyBoardEvents(): void {
+        Keyboard.onKeyboardShow().subscribe(() => { 
+            this.footer.show = false;
+        });
+        Keyboard.onKeyboardHide().subscribe(() => { 
+            this.footer.show = true;
+        });
+    }
+
+    /**
+     *
+     */
+    ionViewDidLoad() {
+        console.log('Hello SaleOrderDetailsPage Page');
+        this.loadCustomers();
+        this.loadProducts();
+    }
+
+    /**
+     *
+     */
+    loadCustomers() {
+        this.events.publish(Slots.APP_LOADING);
+
+        this.db.getAll(DataProvider.DOCS.PARTNER).then((partners: Array<Partner>) => {
+            this.customers = partners.filter(item => { 
+                return item.customer && item.doc_state != "deleted";
+            });
+
+            this.events.publish(Slots.APP_LOADED);
+        }).catch(e => {
+            this.events.publish(Slots.APP_LOADED);
+        });
+    }
+
+    /**
+     *
+     */
+    loadProducts() {
+        this.db.getAll(DataProvider.DOCS.PRODUCT_TEMPLATE).then((products: Array<Product>) => {
+            this.products = products.filter(item => { 
+                return item.doc_state != "deleted";
+            });
+        }).catch(e => {
+            console.log(e);
+            
+        });
+    }
+
+    /**
+     *
+     */
+    onSlideChanged(e: Slides): void {
+        this.search = false;
+        this.customerSearchBar.value = "";
+        this.productSearchBar.value = "";
+    }
+
+    /**
+     *
+     */
+    goToNext(): void {
+        this.slider.slideNext();
+        this.footer.back.show = true;
+
+        if (this.slider.getActiveIndex() == this.slider.length() - 1) {
+            this.footer.forward.show = false;
+        }
+    }
+
+    /**
+     *
+     */
+    goToBack(): void {
+        this.slider.slidePrev();
+        this.footer.forward.show = true;
+
+        if (this.slider.getActiveIndex() == 0) {
+            this.footer.back.show = false;
+        }
+    }
+
+    /**
+     *
+     */
+    showList(): void {
+        this.search = !this.search;
+    }
+
+    /**
+     *
+     */
+    searchCustomer(e: any): void {
+        let value = e.target.value;
+
+        if (value && value.trim() != "") {
+            this.search = true;
+            
+            return;
+        }
+
+        this.search = false;
+        this.footer.forward.show = false;
+    }
+
+    /**
+     *
+     */
+    selectCustomer(item: Partner): void {
+        this.customer = item;
+        this.search = false;
+        this.customerSearchBar.value = "";
+        this.footer.forward.show = true;
+    }
+
+    /**
+     *
+     */
+    searchProduct(e: any): void {
+        let value = e.target.value;
+
+        if (value && value.trim() != "") {
+            this.search = true;
+            
+            return;
+        }
+        
+        this.search = false;
+    }
+
+    /**
+     *
+     */
+    readBarcode(): void {
+        console.log("read");
+        
+    }
+
+    /**
+     *
+     */
+    selectProduct(item: Product): void {
+        this.addItem(item);
+        this.search = false;
+        this.productSearchBar.value = "";
+    }
+
+    /**
+     *
+     */
+    addItem(item: Product): void {
+        let index = this.indexOf(item);
+
+        if (index == -1) {
+            this.lines.push({ product: item, price: 100, quantity: 1, subtotal: 100 });
+        } else {
+            let line = this.lines[index];
+
+            line.quantity = line.quantity + 1;
+            line.subtotal = line.price * line.quantity;
+
+            this.lines[index] = line;
+        }
+        
+        this.sumTotal();
+    }
+
+    /**
+     *
+     */
+    indexOf(item: Product): number {
+        for (let i = 0; i < this.lines.length; i++) {
+            if (this.lines[i].product.id === item.id) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     *
+     */
+    sumTotal(): void {
+        let sum: number = 0;
+        for (let i = 0; i < this.lines.length; i++) {
+            sum = sum + this.lines[i].subtotal;
+        }
+        
+        this.total = sum;
+    }
+
+    /**
+     *
+     */
+    addQuantity(item: any): void {
+        this.addItem(item.product);
+    }
+
+    /**
+     *
+     */
+    removeQuantity(item: any): void {
+        let index = this.indexOf(item.product);
+        let line = this.lines[index];
+
+        line.quantity = line.quantity - 1;
+        line.subtotal = line.price * line.quantity;
+
+        if (line.quantity == 0) {
+            this.askIfRemoveItem(item);
+            return;
+        }
+
+        this.lines[index] = line;
+        this.sumTotal();
+    }
+
+    /**
+     *
+     */
+    askIfChangePrice(item: any): void {
+        this.alertCtrl.create({
+            title: "Ingresar",
+            message: "Ingrese el precio",
+            inputs: [
+                {
+                    name: "price",
+                    type: "number",
+                    value: item.price
+                }
+            ],
+            buttons: [
+                {
+                    text: "Cancelar"
+                },
+                {
+                    text: "Aceptar",
+                    handler: data => {
+                        this.changePrice(item, data.price);
+                    }
+                }
+            ]
+        }).present();
+    }
+
+    /**
+     *
+     */
+    isPriceOk(price: number): boolean {
+        if (price <= 0) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     *
+     */
+    changePrice(item: any, price: number): void {
+        if (!this.isPriceOk(price)) {
+            return;
+        }
+
+        let index = this.indexOf(item.product);
+        let line = this.lines[index];
+
+        line.price = price;
+        line.subtotal = line.price * line.quantity;
+
+        this.lines[index] = line;
+        this.sumTotal();
+    }
+
+    /**
+     *
+     */
+    askIfRemoveItem(item: Product): void {
+        this.alertCtrl.create({
+            title: "Confirmar",
+            subTitle: "Desea quitar este producto?",
+            buttons: [
+                {
+                    text: "Sí",
+                    handler: () => {
+                        this.removeItem(item);
+                    }
+                },
+                {
+                    text: "No"
+                }
+            ]
+        }).present();
+    }
+
+    /**
+     *
+     */
+    removeItem(line: any): void {
+        let index = this.lines.indexOf(line);
+        this.lines.splice(index, 1);
+        this.sumTotal();
+    }
+}

+ 7 - 0
src/pages/sale-order-list/sale-order-list.html

@@ -0,0 +1,7 @@
+<olist-header title="Presupuestos" (toggle)="toggleSearch()" (search)="search($event)"></olist-header>
+
+<ion-content class="has-header">
+
+</ion-content>
+
+<olist-footer [hasElements]="hasItems()" (create)="goToPage(null)"></olist-footer>

+ 3 - 0
src/pages/sale-order-list/sale-order-list.scss

@@ -0,0 +1,3 @@
+page-sale-order-list {
+
+}

+ 26 - 0
src/pages/sale-order-list/sale-order-list.ts

@@ -0,0 +1,26 @@
+import { Component } from "@angular/core";
+import { NavController } from "ionic-angular";
+import { ListView } from "../../defaults/default-list-view";
+import { INavigable } from "../../interfaces/navigable-interface";
+import { SaleOrderDetailsPage } from "../../pages/sale-order-details/sale-order-details";
+import { SaleOrder } from "../../models/sale.order";
+
+@Component({
+    selector: 'page-sale-order-list',
+    templateUrl: 'sale-order-list.html'
+})
+export class SaleOrderListPage extends ListView<SaleOrder> implements INavigable {
+
+    constructor(
+        public navCtrl: NavController
+    ) {
+        super(SaleOrder);
+    }
+
+    /**
+     *
+     */
+    goToPage(data: any): void {
+        this.navCtrl.push(SaleOrderDetailsPage, data);
+    }
+}

+ 6 - 0
src/utils/app-events.ts

@@ -0,0 +1,6 @@
+import { ReflectiveInjector } from "@angular/core";
+
+export abstract class AppEvents {
+    
+    constructor() { }
+}

+ 0 - 1
src/utils/slots.ts

@@ -10,5 +10,4 @@ export abstract class Slots {
     public static readonly ITEM_SAVED = "item:saved";
     public static readonly ITEM_MODIFIED = "item:modified";
     public static readonly ITEM_DELETED = "item:deleted";
-    
 }