[IMP] point_of_sale: partner edition ! -> create partners, edit partners, assign...
authorFrederic van der Essen <fva@openerp.com / fvdessen+o@gmail.com>
Wed, 17 Sep 2014 14:01:53 +0000 (16:01 +0200)
committerFrederic van der Essen <fva@openerp.com / fvdessen+o@gmail.com>
Wed, 17 Sep 2014 14:57:34 +0000 (16:57 +0200)
addons/point_of_sale/point_of_sale.py
addons/point_of_sale/static/src/css/pos.css
addons/point_of_sale/static/src/js/db.js
addons/point_of_sale/static/src/js/models.js
addons/point_of_sale/static/src/js/screens.js
addons/point_of_sale/static/src/xml/pos.xml

index aeb1ca8..28b3795 100644 (file)
@@ -1403,4 +1403,27 @@ class product_template(osv.osv):
         'available_in_pos': True,
     }
 
+class res_partner(osv.osv):
+    _inherit = 'res.partner'
+
+    def create_from_ui(self, cr, uid, partner, context=None):
+        """ create or modify a partner from the point of sale ui.
+            partner contains the partner's fields. """
+
+        #image is a dataurl, get the data after the comma
+        if partner.get('image',False):
+            img =  partner['image'].split(',')[1]
+            partner['image'] = img
+
+        if partner.get('id',False):  # Modifying existing partner
+            partner_id = partner['id']
+            del partner['id']
+            self.write(cr, uid, [partner_id], partner, context=context)
+        else:
+            partner_id = self.create(cr, uid, partner, context=context)
+        
+        return partner_id
+
+
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 8cbf870..d035c79 100644 (file)
@@ -6,14 +6,18 @@
 }
 
 /* --- Styling of OpenERP Elements --- */
-.ui-dialog{
+.ui-dialog, .modal-dialog {
     background: white;
     padding: 10px;
     border-radius: 3px;
     font-family: sans-serif;
     box-shadow: 0px 10px 40px rgba(0,0,0,0.4);
+    position: absolute;
+    top: 30px;
+    height: 400px;
+    overflow: scroll;
 }
-.ui-dialog button{
+.ui-dialog button, .modal-dialog button {
     padding: 8px;
     min-width: 48px;
 }
@@ -1256,16 +1260,48 @@ td {
     float: left;
     margin-right: 16px;
     background: white;
+    position: relative;
 }
-.pos .clientlist-screen .client-picture > img{
-    vertical-align: middle;
+.pos .clientlist-screen .client-picture > img {
+    position: absolute;
+    top: -9999px;
+    bottom: -9999px;
+    right: -9999px;
+    left: -9999px;
     max-height: 64px;
+    margin: auto;
+}
+.pos .clientlist-screen .client-picture > .fa {
+    line-height: 64px;
+    font-size: 32px;
+}
+.pos .clientlist-screen .client-picture .image-uploader {
+    position: absolute;
+    z-index: 1000;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    opacity: 0;
+    cursor: pointer;
 }
-.pos .clientlist-screen .client-name{
+.pos .clientlist-screen .client-name {
     font-size: 32px;
     line-height: 64px;
     margin-bottom:16px;
 }
+.pos .clientlist-screen .edit-buttons {
+    position: absolute;
+    right: 16px;
+    top: 10px;
+}
+.pos .clientlist-screen .edit-buttons .button{
+    display: inline-block;
+    margin-left: 16px;
+    color: rgb(128,128,128);
+    cursor: pointer;
+    font-size: 36px;
+}
 .pos .clientlist-screen .client-details-box{
     position: relative;
     font-size: 16px;
@@ -1284,21 +1320,29 @@ td {
 .pos .clientlist-screen .client-detail > .label{
     font-weight: bold;
     display: inline-block;
-    width: 64px;
+    width: 75px;
     text-align: right;
     margin-right: 8px;
 }
-.pos .clientlist-screen .client-details input {
+.pos .clientlist-screen .client-details input,
+.pos .clientlist-screen .client-details select
+{
     padding: 4px;
     border-radius: 3px;
     border: solid 1px #cecbcb;
+    margin-bottom: 4px;
+    background: white;
+    font-family: "Lato","Lucida Grande", Helvetica, Verdana, Arial;
+    color: #555555;
     width: 340px;
+    font-size: 14px;
+    box-sizing: border-box;
 }
 .pos .clientlist-screen .client-details input.client-name {
     font-size: 24px;
     line-height: 24px;
     margin: 18px 6px;
-    width: 330px;
+    width: 340px;
 }
 .pos .clientlist-screen .client-detail > .empty{
     opacity: 0.3;
@@ -1312,6 +1356,10 @@ td {
 .pos .clientlist-screen .searchbox input{
     width: 120px;
 }
+.pos .clientlist-screen .button.new-customer {
+    left: 50%;
+    margin-left: 120px;
+}
 
 
 /*  ********* The OrderWidget  ********* */
index 7a91152..086bac9 100644 (file)
@@ -220,12 +220,12 @@ function openerp_pos_db(instance, module){
         },
         add_partners: function(partners){
             var updated_count = 0;
+            var new_write_date = '';
             for(var i = 0, len = partners.length; i < len; i++){
                 var partner = partners[i];
 
-                if (!this.partner_write_date) {
-                    this.partner_write_date = partner.write_date;
-                } else if ( this.partner_by_id[partner.id] &&
+                if (    this.partner_write_date && 
+                        this.partner_by_id[partner.id] &&
                         new Date(this.partner_write_date).getTime() + 1000 >=
                         new Date(partner.write_date).getTime() ) {
                     // FIXME: The write_date is stored with milisec precision in the database
@@ -233,8 +233,8 @@ function openerp_pos_db(instance, module){
                     // you read partners modified strictly after time X, you get back partners that were
                     // modified X - 1 sec ago. 
                     continue;
-                } else if ( this.partner_write_date < partner.write_date ) { 
-                    this.partner_write_date = partner.write_date;
+                } else if ( new_write_date < partner.write_date ) { 
+                    new_write_date  = partner.write_date;
                 }
                 if (!this.partner_by_id[partner.id]) {
                     this.partner_sorted.push(partner.id);
@@ -244,6 +244,8 @@ function openerp_pos_db(instance, module){
                 updated_count += 1;
             }
 
+            this.partner_write_date = new_write_date || this.partner_write_date;
+
             if (updated_count) {
                 // If there were updates, we need to completely 
                 // rebuild the search string and the ean13 indexing
index 199d781..b6ac5e8 100644 (file)
@@ -135,7 +135,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
             loaded: function(self,users){ self.user = users[0]; },
         },{ 
             model:  'res.company',
-            fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' ],
+            fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id'],
             domain: function(self){ return [['id','=',self.user.company_id[0]]]; },
             loaded: function(self,companies){ self.company = companies[0]; },
         },{
@@ -159,13 +159,25 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
             loaded: function(self,users){ self.users = users; },
         },{
             model:  'res.partner',
-            fields: ['name','street','city','country_id','phone','zip','mobile','email','ean13','write_date'],
+            fields: ['name','street','city','state_id','country_id','vat','phone','zip','mobile','email','ean13','write_date'],
             domain: null,
             loaded: function(self,partners){
                 self.partners = partners;
                 self.db.add_partners(partners);
             },
         },{
+            model:  'res.country',
+            fields: ['name'],
+            loaded: function(self,countries){
+                self.countries = countries;
+                self.company.country = null;
+                for (var i = 0; i < countries.length; i++) {
+                    if (countries[i].id === self.company.country_id[0]){
+                        self.company.country = countries[i];
+                    }
+                }
+            },
+        },{
             model:  'account.tax',
             fields: ['name','amount', 'price_include', 'type'],
             domain: null,
index 807fb40..3ca2a81 100644 (file)
@@ -573,13 +573,16 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
                 self.pos_widget.screen_selector.back();
             });
 
+            this.$('.new-customer').click(function(){
+                self.display_client_details('edit',{
+                    'country_id': self.pos.company.country_id,
+                });
+            });
+
             var partners = this.pos.db.get_partners_sorted(1000);
             this.render_list(partners);
             
-            this.pos.load_new_partners().then(function(){ 
-                // will only get called if new partners were reloaded.
-                self.render_list(self.pos.db.get_partners_sorted(1000));
-            });
+            this.reload_partners();
 
             if( this.old_client ){
                 this.display_client_details('show',this.old_client,0);
@@ -609,6 +612,13 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
                 self.clear_search();
             });
         },
+        barcode_client_action: function(code){
+            if (this.editing_client) {
+                this.$('.detail.barcode').val(code.code);
+            } else if (this.pos.db.get_partner_by_ean13(code.code)) {
+                this.display_client_details('show',this.pos.db.get_partner_by_ean13(code.code));
+            }
+        },
         perform_search: function(query, associate_result){
             if(query){
                 var customers = this.pos.db.search_partner(query);
@@ -663,7 +673,10 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
         },
         toggle_save_button: function(){
             var $button = this.$('.button.next');
-            if( this.new_client ){
+            if (this.editing_client) {
+                $button.addClass('oe_hidden');
+                return;
+            } else if( this.new_client ){
                 if( !this.old_client){
                     $button.text(_t('Set Customer'));
                 }else{
@@ -695,43 +708,207 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
         partner_icon_url: function(id){
             return '/web/binary/image?model=res.partner&id='+id+'&field=image_small';
         },
+
+        // ui handle for the 'edit selected customer' action
+        edit_client_details: function(partner) {
+            this.display_client_details('edit',partner);
+        },
+
+        // ui handle for the 'cancel customer edit changes' action
+        undo_client_details: function(partner) {
+            if (!partner.id) {
+                this.display_client_details('hide');
+            } else {
+                this.display_client_details('show',partner);
+            }
+        },
+
+        // what happens when we save the changes on the client edit form -> we fetch the fields, sanitize them,
+        // send them to the backend for update, and call saved_client_details() when the server tells us the
+        // save was successfull.
+        save_client_details: function(partner) {
+            var self = this;
+            
+            var fields = {}
+            this.$('.client-details-contents .detail').each(function(idx,el){
+                fields[el.name] = el.value;
+            });
+
+            if (!fields.name) {
+                this.pos_widget.screen_selector.show_popup('error',{
+                    message: _t('A Customer Name Is Required'),
+                });
+                return;
+            }
+            
+            if (this.uploaded_picture) {
+                fields.image = this.uploaded_picture;
+            }
+
+            fields.id           = partner.id || false;
+            fields.country_id   = fields.country_id || false;
+            fields.ean13        = fields.ean13 ? this.pos.barcode_reader.sanitize_ean(fields.ean13) : false; 
+
+            new instance.web.Model('res.partner').call('create_from_ui',[fields]).then(function(partner_id){
+                self.saved_client_details(partner_id);
+            });
+        },
+        
+        // what happens when we've just pushed modifications for a partner of id partner_id
+        saved_client_details: function(partner_id){
+            var self = this;
+            this.reload_partners().then(function(){
+                var partner = self.pos.db.get_partner_by_id(partner_id);
+                if (partner) {
+                    self.new_client = partner;
+                    self.toggle_save_button();
+                    self.display_client_details('show',partner);
+                } else {
+                    // should never happen, because create_from_ui must return the id of the partner it
+                    // has created, and reload_partner() must have loaded the newly created partner. 
+                    self.display_client_details('hide');
+                }
+            });
+        },
+
+        // resizes an image, keeping the aspect ratio intact,
+        // the resize is useful to avoid sending 12Mpixels jpegs
+        // over a wireless connection.
+        resize_image_to_dataurl: function(img, maxwidth, maxheight, callback){
+            img.onload = function(){
+                var png = new Image();
+                var canvas = document.createElement('canvas');
+                var ctx    = canvas.getContext('2d');
+                var ratio  = 1;
+
+                if (img.width > maxwidth) {
+                    ratio = maxwidth / img.width;
+                }
+                if (img.height * ratio > maxheight) {
+                    ratio = maxheight / img.height;
+                }
+                var width  = Math.floor(img.width * ratio);
+                var height = Math.floor(img.height * ratio);
+
+                canvas.width  = width;
+                canvas.height = height;
+                ctx.drawImage(img,0,0,width,height);
+
+                var dataurl = canvas.toDataURL();
+                callback(dataurl);
+            }
+        },
+
+        // Loads and resizes a File that contains an image.
+        // callback gets a dataurl in case of success.
+        load_image_file: function(file, callback){
+            var self = this;
+            if (!file.type.match(/image.*/)) {
+                this.pos_widget.screen_selector.show_popup('error',{
+                    message:_t('Unsupported File Format'),
+                    comment:_t('Only web-compatible Image formats such as .png or .jpeg are supported'),
+                });
+                return;
+            }
+            
+            var reader = new FileReader();
+            reader.onload = function(event){
+                var dataurl = event.target.result;
+                var img     = new Image();
+                img.src = dataurl;
+                self.resize_image_to_dataurl(img,800,600,callback);
+            }
+            reader.onerror = function(){
+                self.pos_widget.screen_selector.show_popup('error',{
+                    message:_t('Could Not Read Image'),
+                    comment:_t('The provided file could not be read due to an unknown error'),
+                });
+            };
+            reader.readAsDataURL(file);
+        },
+
+        // This fetches partner changes on the server, and in case of changes, 
+        // rerenders the affected views
+        reload_partners: function(){
+            var self = this;
+            return this.pos.load_new_partners().then(function(){
+                self.render_list(self.pos.db.get_partners_sorted(1000));
+                
+                // update the currently assigned client if it has been changed in db.
+                var curr_client = self.pos.get_order().get_client();
+                if (curr_client) {
+                    self.pos.get_order().set_client(self.pos.db.get_partner_by_id(curr_client.id));
+                }
+            });
+        },
+
+        // Shows,hides or edit the customer details box :
+        // visibility: 'show', 'hide' or 'edit'
+        // partner:    the partner object to show or edit
+        // clickpos:   the height of the click on the list (in pixel), used
+        //             to maintain consistent scroll.
         display_client_details: function(visibility,partner,clickpos){
+            var self = this;
+            var contents = this.$('.client-details-contents');
+            var parent   = this.$('.client-list').parent();
+            var scroll   = parent.scrollTop();
+            var height   = contents.height();
+
+            contents.off('click','.button.edit'); 
+            contents.off('click','.button.save'); 
+            contents.off('click','.button.undo'); 
+            contents.on('click','.button.edit',function(){ self.edit_client_details(partner); });
+            contents.on('click','.button.save',function(){ self.save_client_details(partner); });
+            contents.on('click','.button.undo',function(){ self.undo_client_details(partner); });
+            this.editing_client = false;
+            this.uploaded_picture = null;
+
             if(visibility === 'show'){
-                var contents = this.$('.client-details-contents');
-                var parent   = this.$('.client-list').parent();
-                var old_scroll   = parent.scrollTop();
-                var old_height   = contents.height();
                 contents.empty();
-                contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
+                contents.append($(QWeb.render('ClientDetails',{widget:this,partner:partner})));
+
                 var new_height   = contents.height();
 
                 if(!this.details_visible){
-                    if(clickpos < old_scroll + new_height + 20 ){
+                    if(clickpos < scroll + new_height + 20 ){
                         parent.scrollTop( clickpos - 20 );
                     }else{
                         parent.scrollTop(parent.scrollTop() + new_height);
                     }
                 }else{
-                    parent.scrollTop(parent.scrollTop() - old_height + new_height);
+                    parent.scrollTop(parent.scrollTop() - height + new_height);
                 }
 
                 this.details_visible = true;
-            }else if(visibility === 'hide'){
-                var contents = this.$('.client-details-contents');
-                var parent   = this.$('.client-list').parent();
-                var scroll   = parent.scrollTop();
-                var height   = contents.height();
+                this.toggle_save_button();
+            } else if (visibility === 'edit') {
+                this.editing_client = true;
+                contents.empty();
+                contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
+                this.toggle_save_button();
+
+                contents.find('.image-uploader').on('change',function(){
+                    self.load_image_file(event.target.files[0],function(res){
+                        if (res) {
+                            contents.find('.client-picture img, .client-picture .fa').remove();
+                            contents.find('.client-picture').append("<img src='"+res+"'>");
+                            contents.find('.detail.picture').remove();
+                            self.uploaded_picture = res;
+                        }
+                    });
+                });
+            } else if (visibility === 'hide') {
                 contents.empty();
                 if( height > scroll ){
                     contents.css({height:height+'px'});
                     contents.animate({height:0},400,function(){
                         contents.css({height:''});
                     });
-                    //parent.scrollTop(0);
                 }else{
                     parent.scrollTop( parent.scrollTop() - height);
                 }
                 this.details_visible = false;
+                this.toggle_save_button();
             }
         },
         close: function(){
index 58d20a8..38a0027 100644 (file)
     <t t-name="ClientDetailsEdit">
         <section class='client-details edit'>
             <div class='client-picture'>
-                <img t-att-src='widget.partner_icon_url(partner.id)' />
+                <t t-if='!partner.id'>
+                    <i class='fa fa-camera'></i>
+                </t>
+                <t t-if='partner.id'>
+                    <img t-att-src='widget.partner_icon_url(partner.id)' />
+                </t>
+                <input type='file' class='image-uploader'></input>   
+            </div>
+            <input class='detail client-name' name='name' t-att-value='partner.name' placeholder='Name'></input>
+            <div class='edit-buttons'>
+                <div class='button undo'><i class='fa fa-undo' /></div>
+                <div class='button save'><i class='fa fa-floppy-o' /></div>
             </div>
-            <input class='client-name' t-att-value='partner.name' placeholder='Name'></input>
             <div class='client-details-box clearfix'>
                 <div class='client-details-left'>
                     <div class='client-detail'>
                         <span class='label'>Street</span>
-                        <input class='detail client-address-street'  t-att-value='partner.street' placeholder='Street'></input>
+                        <input class='detail client-address-street' name='street'       t-att-value='partner.street' placeholder='Street'></input>
                     </div>
                     <div class='client-detail'>
                         <span class='label'>City</span>
-                        <input class='detail client-address-city'    t-att-value='partner.city' placeholder='City'></input>
+                        <input class='detail client-address-city'   name='city'         t-att-value='partner.city' placeholder='City'></input>
                     </div>
                     <div class='client-detail'>
-                        <span class='label'>ZIP</span>
-                        <input class='detail client-address-zip'     t-att-value='partner.zip' placeholder='ZIP'></input>
+                        <span class='label'>Postcode</span>
+                        <input class='detail client-address-zip'    name='zip'          t-att-value='partner.zip' placeholder='ZIP'></input>
                     </div>
                     <div class='client-detail'>
                         <span class='label'>Country</span>
-                        <input class='detail client-address-country' t-att-value='partner.country_id[1]' placeholder='Country'></input>
+                        <select class='detail client-address-country' name='country_id'>
+                            <option value=''>None</option>
+                            <t t-foreach='widget.pos.countries' t-as='country'>
+                                <option t-att-value='country.id' t-att-selected="partner_country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined"> 
+                                    <t t-esc='country.name'/>
+                                </option>
+                            </t>
+                        </select>
                     </div>
                 </div>
                 <div class='client-details-right'>
                     <div class='client-detail'>
-                        <span class='label'>email</span>
-                        <input class='detail client-email'  t-att-value='partner.email'></input>
+                        <span class='label'>Email</span>
+                        <input class='detail client-email'  name='email'    type='email'    t-att-value='partner.email || ""'></input>
+                    </div>
+                    <div class='client-detail'>
+                        <span class='label'>Phone</span>
+                        <input class='detail client-phone'  name='phone'    type='tel'      t-att-value='partner.phone || ""'></input>
                     </div>
                     <div class='client-detail'>
-                        <span class='label'>phone</span>
-                        <input class='detail client-phone'  t-att-value='partner.phone'></input>
+                        <span class='label'>Barcode</span>
+                        <input class='detail barcode'       name='ean13'    t-att-value='partner.ean13 || ""'></input>
                     </div>
                     <div class='client-detail'>
-                        <span class='label'>ID</span>
-                        <input class='detail client-id'     t-att-value='partner.ean13'></input>
+                        <span class='label'>Tax ID</span>
+                        <input class='detail vat'           name='vat'     t-att-value='partner.vat || ""'></input>
                     </div>
                 </div>
             </div>
                 <img t-att-src='widget.partner_icon_url(partner.id)' />
             </div>
             <div class='client-name'><t t-esc='partner.name' /></div>
+            <div class='edit-buttons'>
+                <div class='button edit'><i class='fa fa-pencil-square' /></div>
+            </div>
             <div class='client-details-box clearfix'>
                 <div class='client-details-left'>
                     <div class='client-detail'>
                         <span class='detail client-address'><t t-esc='partner.address' /></span>
                     </div>
                     <div class='client-detail'>
-                        <span class='label'>email</span>
+                        <span class='label'>Email</span>
                         <span class='detail client-email'><t t-esc='partner.email' /></span>
                     </div>
                     <div class='client-detail'>
-                        <span class='label'>phone</span>
+                        <span class='label'>Phone</span>
                         <t t-if='partner.phone'>
                             <span class='detail client-phone'><t t-esc='partner.phone' /></span>
                         </t>
                 </div>
                 <div class='client-details-right'>
                     <div class='client-detail'>
-                        <span class='label'>ID</span>
+                        <span class='label'>Barcode</span>
                         <t t-if='partner.ean13'>
                             <span class='detail client-id'><t t-esc='partner.ean13'/></span>
                         </t>
                             <span class='detail client-id empty'>N/A</span>
                         </t>
                     </div>
+                    <div class='client-detail'>
+                        <span class='label'>Tax ID</span>
+                        <t t-if='partner.vat'>
+                            <span class='detail vat'><t t-esc='partner.vat'/></span>
+                        </t>
+                        <t t-if='!partner.vat'>
+                            <span class='detail vat empty'>N/A</span>
+                        </t>
+                    </div>
                 </div>
             </div>
         </section>
                         <span class='search-clear'></span>
                     </span>
                     <span class='searchbox'></span>
+                    <span class='button new-customer'>
+                        <i class='fa fa-user'></i>
+                        <i class='fa fa-plus'></i>
+                    </span>
                     <span class='button next oe_hidden highlight'>
                         Select Customer
                         <i class='fa fa-angle-double-right'></i>