Merge pull request #1284 from sebalix/8.0-fix-xmlrpc-marshall-none
authorRaphael Collet <rco@openerp.com>
Wed, 30 Jul 2014 13:05:03 +0000 (15:05 +0200)
committerRaphael Collet <rco@openerp.com>
Wed, 30 Jul 2014 13:05:03 +0000 (15:05 +0200)
[FIX] read() method returns None values instead of False, incompatible with XML-RPC

85 files changed:
addons/account/account_bank_statement.py
addons/account/account_invoice.py
addons/account/account_move_line.py
addons/account/partner_view.xml
addons/account/product_view.xml
addons/account/static/src/js/account_widgets.js
addons/base_action_rule/test_models.py
addons/base_import/tests/test_cases.py
addons/calendar/calendar.py
addons/crm/crm.py
addons/crm/crm_lead_view.xml
addons/crm/crm_phonecall.py
addons/crm/crm_view.xml
addons/crm/sales_team.py
addons/crm_claim/report/crm_claim_report_view.xml
addons/crm_helpdesk/report/crm_helpdesk_report_view.xml
addons/delivery/delivery.py
addons/event/event.py
addons/google_calendar/res_config.py
addons/google_calendar/static/src/js/calendar_sync.js
addons/hr/res_users.py
addons/hr_attendance/hr_attendance.py
addons/hr_payroll/hr_payroll.py
addons/mail/mail_alias.py
addons/mail/mail_followers.py
addons/mail/mail_group.py
addons/mail/res_config_view.xml
addons/mail/res_users.py
addons/mail/tests/test_mail_features.py
addons/point_of_sale/point_of_sale.py
addons/procurement/wizard/schedulers_all.py
addons/product/product.py
addons/product/product_view.xml
addons/project/project_view.xml
addons/project_issue/project_issue.py
addons/purchase/purchase.py
addons/sale/report/sale_report_view.xml
addons/sale/sale.py
addons/sale/sales_team.py
addons/sale/wizard/sale_make_invoice_advance.py
addons/sale_service/views/sale_service_view.xml
addons/sale_stock/sale_stock_view.xml
addons/sales_team/sales_team.xml
addons/share/res_users.py
addons/stock/stock.py
addons/web/static/src/js/chrome.js
addons/web/static/src/js/search.js
addons/web_kanban_gauge/static/src/js/kanban_gauge.js
addons/website/models/test_models.py
addons/website/models/website.py
addons/website/static/src/css/snippets.css
addons/website/static/src/css/snippets.sass
addons/website/static/src/xml/website.editor.xml
addons/website/views/snippets.xml
addons/website/views/website_templates.xml
addons/website/views/website_views.xml
addons/website_blog/controllers/main.py
addons/website_blog/static/src/js/website_blog.inline.discussion.js
addons/website_crm/controllers/main.py
addons/website_forum/controllers/main.py
addons/website_forum/models/forum.py
addons/website_forum_doc/models/documentation.py
addons/website_mail/static/src/js/follow.js
addons/website_mail/views/snippets.xml
addons/website_mail/views/website_mail.xml
addons/website_mail_group/models/mail_group.py
addons/website_mail_group/views/website_mail_group.xml
addons/website_sale/controllers/main.py
addons/website_sale/views/templates.xml
addons/website_sale_delivery/controllers/main.py
addons/website_sale_delivery/views/website_sale_delivery.xml
openerp/addons/base/ir/ir_attachment.py
openerp/addons/base/ir/ir_logging.py
openerp/addons/base/ir/ir_mail_server.py
openerp/addons/base/ir/ir_model.py
openerp/addons/base/ir/ir_qweb.py
openerp/addons/base/ir/ir_values.py
openerp/addons/base/module/module.py
openerp/addons/base/res/res_bank.py
openerp/addons/base/res/res_users.py
openerp/addons/base/tests/test_mail_examples.py
openerp/fields.py
openerp/models.py
openerp/tools/safe_eval.py
openerp/tools/translate.py

index 65dae8a..4d9c3f3 100644 (file)
@@ -326,16 +326,26 @@ class account_bank_statement(osv.osv):
                     or (not st.journal_id.default_debit_account_id):
                 raise osv.except_osv(_('Configuration Error!'), _('Please verify that an account is defined in the journal.'))
             for line in st.move_line_ids:
-                if line.state <> 'valid':
+                if line.state != 'valid':
                     raise osv.except_osv(_('Error!'), _('The account entries lines are not in valid state.'))
             move_ids = []
             for st_line in st.line_ids:
                 if not st_line.amount:
                     continue
-                if not st_line.journal_entry_id.id:
+                if st_line.account_id and not st_line.journal_entry_id.id:
+                    #make an account move as before
+                    vals = {
+                        'debit': st_line.amount < 0 and -st_line.amount or 0.0,
+                        'credit': st_line.amount > 0 and st_line.amount or 0.0,
+                        'account_id': st_line.account_id.id,
+                        'name': st_line.name
+                    }
+                    self.pool.get('account.bank.statement.line').process_reconciliation(cr, uid, st_line.id, [vals], context=context)
+                elif not st_line.journal_entry_id.id:
                     raise osv.except_osv(_('Error!'), _('All the account entries lines must be processed in order to close the statement.'))
                 move_ids.append(st_line.journal_entry_id.id)
-            self.pool.get('account.move').post(cr, uid, move_ids, context=context)
+            if move_ids:
+                self.pool.get('account.move').post(cr, uid, move_ids, context=context)
             self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context)
         self.link_bank_to_partner(cr, uid, ids, context=context)
         return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
@@ -728,7 +738,12 @@ class account_bank_statement_line(osv.osv):
         move_id = am_obj.create(cr, uid, move_vals, context=context)
 
         # Create the move line for the statement line
-        amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=context)
+        if st_line.statement_id.currency.id != company_currency.id:
+            ctx = context.copy()
+            ctx['date'] = st_line.date
+            amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount_currency, context=ctx)
+        else:
+            amount = st_line.amount
         bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context)
         aml_obj.create(cr, uid, bank_st_move_vals, context=context)
         # Complete the dicts
@@ -805,18 +820,19 @@ class account_bank_statement_line(osv.osv):
     _description = "Bank Statement Line"
     _inherit = ['ir.needaction_mixin']
     _columns = {
-        'name': fields.char('Description', required=True, copy=False),
-        'date': fields.date('Date', required=True, copy=False),
+        'name': fields.char('Description', required=True),
+        'date': fields.date('Date', required=True),
         'amount': fields.float('Amount', digits_compute=dp.get_precision('Account')),
         'partner_id': fields.many2one('res.partner', 'Partner'),
         'bank_account_id': fields.many2one('res.partner.bank','Bank Account'),
+        'account_id': fields.many2one('account.account', 'Account', help="This technical field can be used at the statement line creation/import time in order to avoid the reconciliation process on it later on. The statement line will simply create a counterpart on this account"),
         'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'),
         'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
         'ref': fields.char('Structured Communication'),
         'note': fields.text('Notes'),
         'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."),
         'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
-        'journal_entry_id': fields.many2one('account.move', 'Journal Entry'),
+        'journal_entry_id': fields.many2one('account.move', 'Journal Entry', copy=False),
         'amount_currency': fields.float('Amount Currency', help="The amount expressed in an optional other currency if it is a multi-currency entry.", digits_compute=dp.get_precision('Account')),
         'currency_id': fields.many2one('res.currency', 'Currency', help="The optional other currency if it is a multi-currency entry."),
     }
index 455feb1..6221fb5 100644 (file)
@@ -145,7 +145,7 @@ class account_invoice(models.Model):
             else:
                 self.residual = new_value
         # prevent the residual amount on the invoice to be less than 0
-        self.residual = max(self.residual, 0.0)            
+        self.residual = max(self.residual, 0.0)
 
     @api.one
     @api.depends(
@@ -1005,15 +1005,18 @@ class account_invoice(models.Model):
         #TODO: implement messages system
         return True
 
-    @api.one
-    def _compute_display_name(self):
+    @api.multi
+    def name_get(self):
         TYPES = {
             'out_invoice': _('Invoice'),
             'in_invoice': _('Supplier Invoice'),
             'out_refund': _('Refund'),
             'in_refund': _('Supplier Refund'),
         }
-        self.display_name = "%s %s" % (self.number or TYPES[self.type], self.name or '')
+        result = []
+        for inv in self:
+            result.append((inv.id, "%s %s" % (inv.number or TYPES[inv.type], inv.name or '')))
+        return result
 
     @api.model
     def name_search(self, name, args=None, operator='ilike', limit=100):
@@ -1619,8 +1622,8 @@ class mail_compose_message(models.Model):
                 context.get('default_res_id') and context.get('mark_invoice_as_sent'):
             invoice = self.env['account.invoice'].browse(context['default_res_id'])
             invoice = invoice.with_context(mail_post_autofollow=True)
-            self.write({'sent': True})
-            self.message_post(body=_("Invoice sent"))
+            invoice.write({'sent': True})
+            invoice.message_post(body=_("Invoice sent"))
         return super(mail_compose_message, self).send_mail()
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 7761116..2a9dd55 100644 (file)
@@ -1038,6 +1038,8 @@ class account_move_line(osv.osv):
         all_moves = list(set(all_moves) - set(move_ids))
         if unlink_ids:
             if opening_reconciliation:
+                raise osv.except_osv(_('Warning!'),
+                    _('Opening Entries have already been generated.  Please run "Cancel Closing Entries" wizard to cancel those entries and then run this wizard.'))
                 obj_move_rec.write(cr, uid, unlink_ids, {'opening_reconciliation': False})
             obj_move_rec.unlink(cr, uid, unlink_ids)
             if len(all_moves) >= 2:
index 309296c..36aa1ed 100644 (file)
@@ -74,6 +74,7 @@
                 <xpath expr="//div[@name='buttons']" position="inside">
                     <button type="action" 
                         class="oe_stat_button"
+                        id="invoice_button"
                         icon="fa-pencil-square-o"
                         name="%(account.action_invoice_tree)d" 
                         attrs="{'invisible': [('customer', '=', False)]}" 
index 9c1b0ee..44b9099 100644 (file)
@@ -18,7 +18,7 @@
                                 <field name="property_account_income" domain="[('type','=','other')]" groups="account.group_account_user"
                                     attrs="{'readonly': [('is_product_variant', '=', True)]}"/>
                                 <field name="taxes_id" colspan="2" widget="many2many_tags"
-                                    attrs="{'readonly':[ '|', ('sale_ok','=',0), ('is_product_variant', '=', True)]}"/>
+                                    attrs="{'readonly':[('sale_ok','=',0)]}"/>
                             </group>
                             <group>
                                 <field name="property_account_expense" domain="[('type','=','other')]" groups="account.group_account_user"
index 3b04058..c9bdaac 100644 (file)
@@ -139,7 +139,7 @@ openerp.account = function (instance) {
             }
     
             // Retreive statement infos and reconciliation data from the model
-            var lines_filter = [['journal_entry_id', '=', false]];
+            var lines_filter = [['journal_entry_id', '=', false], ['account_id', '=', false]];
             var deferred_promises = [];
     
             if (self.statement_id) {
index 2a88def..b931b0f 100644 (file)
@@ -1,4 +1,5 @@
 from openerp.osv import fields, osv
+from openerp import api
 
 AVAILABLE_STATES = [
     ('draft', 'New'),
@@ -25,6 +26,7 @@ class lead_test(osv.Model):
         'active' : True,
     }
 
+    @api.cr_uid_ids_context
     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
         pass
 
index ac5a444..bad79bc 100644 (file)
@@ -11,18 +11,10 @@ ID_FIELD = {
     'required': False,
     'fields': [],
 }
-DISPLAY_NAME_FIELD = {
-    'id': 'display_name',
-    'name': 'display_name',
-    'string': "Name",
-    'required': False,
-    'fields': [],
-}
 
 def make_field(name='value', string='unknown', required=False, fields=[]):
     return [
         ID_FIELD,
-        DISPLAY_NAME_FIELD,
         {'id': name, 'name': name, 'string': string, 'required': required, 'fields': fields},
     ]
 
@@ -50,7 +42,7 @@ class test_basic_fields(BaseImportCase):
 
     def test_readonly(self):
         """ Readonly fields should be filtered out"""
-        self.assertEqualFields(self.get_fields('char.readonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+        self.assertEqualFields(self.get_fields('char.readonly'), [ID_FIELD])
 
     def test_readonly_states(self):
         """ Readonly fields with states should not be filtered out"""
@@ -59,12 +51,12 @@ class test_basic_fields(BaseImportCase):
     def test_readonly_states_noreadonly(self):
         """ Readonly fields with states having nothing to do with
         readonly should still be filtered out"""
-        self.assertEqualFields(self.get_fields('char.noreadonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+        self.assertEqualFields(self.get_fields('char.noreadonly'), [ID_FIELD])
 
     def test_readonly_states_stillreadonly(self):
         """ Readonly fields with readonly states leaving them readonly
         always... filtered out"""
-        self.assertEqualFields(self.get_fields('char.stillreadonly'), [ID_FIELD, DISPLAY_NAME_FIELD])
+        self.assertEqualFields(self.get_fields('char.stillreadonly'), [ID_FIELD])
 
     def test_m2o(self):
         """ M2O fields should allow import of themselves (name_get),
@@ -92,7 +84,6 @@ class test_o2m(BaseImportCase):
     def test_shallow(self):
         self.assertEqualFields(self.get_fields('o2m'), make_field(fields=[
             ID_FIELD,
-            DISPLAY_NAME_FIELD,
             # FIXME: should reverse field be ignored?
             {'id': 'parent_id', 'name': 'parent_id', 'string': 'unknown', 'required': False, 'fields': [
                 {'id': 'parent_id', 'name': 'id', 'string': 'External ID', 'required': False, 'fields': []},
@@ -250,7 +241,6 @@ class test_preview(TransactionCase):
         # Order depends on iteration order of fields_get
         self.assertItemsEqual(result['fields'], [
             ID_FIELD,
-            DISPLAY_NAME_FIELD,
             {'id': 'name', 'name': 'name', 'string': 'Name', 'required':False, 'fields': []},
             {'id': 'somevalue', 'name': 'somevalue', 'string': 'Some Value', 'required':True, 'fields': []},
             {'id': 'othervalue', 'name': 'othervalue', 'string': 'Other Variable', 'required':False, 'fields': []},
index 35881da..1115343 100644 (file)
@@ -11,6 +11,7 @@ from datetime import datetime, timedelta
 from dateutil import parser
 from dateutil import rrule
 from dateutil.relativedelta import relativedelta
+from openerp import api
 from openerp import tools, SUPERUSER_ID
 from openerp.osv import fields, osv
 from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
@@ -1323,6 +1324,7 @@ class calendar_event(osv.Model):
             ('user_id', '=', uid),
         ]
 
+    @api.cr_uid_ids_context
     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, **kwargs):
         if isinstance(thread_id, str):
             thread_id = get_real_ids(thread_id)
index 20d025f..bb5cd03 100644 (file)
@@ -71,9 +71,9 @@ class crm_tracking_mixin(osv.AbstractModel):
 
     _columns = {
         'campaign_id': fields.many2one('crm.tracking.campaign', 'Campaign',  # old domain ="['|',('section_id','=',section_id),('section_id','=',False)]"
-                                       help="This is a name that helps you keep track of your different campaign efforts Example: Fall_Drive, Christmas_Special"),
-        'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Example: Search Engine, another domain, or name of email list"),
-        'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. EX: Postcard, Email, or Banner Ad"),
+                                       help="This is a name that helps you keep track of your different campaign efforts Ex: Fall_Drive, Christmas_Special"),
+        'source_id': fields.many2one('crm.tracking.source', 'Source', help="This is the source of the link Ex: Search Engine, another domain, or name of email list"),
+        'medium_id': fields.many2one('crm.tracking.medium', 'Channel', help="This is the method of delivery. Ex: Postcard, Email, or Banner Ad"),
     }
 
     def tracking_fields(self):
@@ -82,15 +82,17 @@ class crm_tracking_mixin(osv.AbstractModel):
     def tracking_get_values(self, cr, uid, vals, context=None):
         for key, field in self.tracking_fields():
             column = self._all_columns[field].column
-            value = vals.get(field) or (request and request.session.get(key))  # params.get sould be always in session by the dispatch from ir_http
-            if column._type in ['many2one'] and isinstance(value, basestring):  # if we receive a string for a many2one, we search / create  the id
+            value = vals.get(field) or (request and request.session.get(key))  # params.get should be always in session by the dispatch from ir_http
+            if column._type in ['many2one'] and isinstance(value, basestring):  # if we receive a string for a many2one, we search / create the id
                 if value:
                     Model = self.pool[column._obj]
                     rel_id = Model.name_search(cr, uid, value, context=context)
-                    if not rel_id:
+                    if rel_id:
+                        rel_id = rel_id[0][0]
+                    else:
                         rel_id = Model.create(cr, uid, {'name': value}, context=context)
                 vals[field] = rel_id
-            # Here the code for other cases that many2one
+            # Here the code for others cases that many2one
             else:
                 vals[field] = value
         return vals
@@ -123,17 +125,15 @@ class crm_case_stage(osv.osv):
         'on_change': fields.boolean('Change Probability Automatically', help="Setting this stage will change the probability automatically on the opportunity."),
         'requirements': fields.text('Requirements'),
         'section_ids': fields.many2many('crm.case.section', 'section_stage_rel', 'stage_id', 'section_id', string='Sections',
-                        help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
+                                        help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."),
         'case_default': fields.boolean('Default to New Sales Team',
-                        help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
+                                       help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."),
         'fold': fields.boolean('Folded in Kanban View',
                                help='This stage is folded in the kanban view when'
                                'there are no records in that stage to display.'),
-        'type': fields.selection([('lead', 'Lead'),
-                                    ('opportunity', 'Opportunity'),
-                                    ('both', 'Both')],
-                                    string='Type', required=True,
-                                    help="This field is used to distinguish stages related to Leads from stages related to Opportunities, or to specify stages available for both types."),
+        'type': fields.selection([('lead', 'Lead'), ('opportunity', 'Opportunity'), ('both', 'Both')],
+                                 string='Type', required=True,
+                                 help="This field is used to distinguish stages related to Leads from stages related to Opportunities, or to specify stages available for both types."),
     }
 
     _defaults = {
@@ -145,6 +145,7 @@ class crm_case_stage(osv.osv):
         'case_default': True,
     }
 
+
 class crm_case_categ(osv.osv):
     """ Category of Case """
     _name = "crm.case.categ"
@@ -154,16 +155,18 @@ class crm_case_categ(osv.osv):
         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
         'object_id': fields.many2one('ir.model', 'Object Name'),
     }
+
     def _find_object_id(self, cr, uid, context=None):
         """Finds id for case object"""
         context = context or {}
         object_id = context.get('object_id', False)
-        ids = self.pool.get('ir.model').search(cr, uid, ['|',('id', '=', object_id),('model', '=', context.get('object_name', False))])
+        ids = self.pool.get('ir.model').search(cr, uid, ['|', ('id', '=', object_id), ('model', '=', context.get('object_name', False))])
         return ids and ids[0] or False
     _defaults = {
-        'object_id' : _find_object_id
+        'object_id': _find_object_id
     }
 
+
 class crm_payment_mode(osv.osv):
     """ Payment Mode for Fund """
     _name = "crm.payment.mode"
@@ -173,5 +176,4 @@ class crm_payment_mode(osv.osv):
         'section_id': fields.many2one('crm.case.section', 'Sales Team'),
     }
 
-
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index 3f10dcf..a567be1 100644 (file)
                         <filter string="Sales Team" domain="[]" context="{'group_by':'section_id'}" groups="base.group_multi_salesteams"/>
                         <filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
                         <filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
+                        <filter string="Campaign"  domain="[]" context="{'group_by':'campaign_id'}"/>
+                        <filter string="Source"  domain="[]" context="{'group_by':'source_id'}"/>
+                        <filter string="Channel"  domain="[]" context="{'group_by':'medium_id'}"/>
                         <filter string="Expected Closing" domain="[]" context="{'group_by':'date_deadline:week'}"/>
                         <filter string="Last Message" name="group_message_last_post" domain="[]" context="{'group_by':'message_last_post:week'}"/>
                     </group>
index 3821027..b983dbf 100644 (file)
@@ -141,6 +141,7 @@ class crm_phonecall(osv.osv):
                     'partner_phone' : call.partner_phone,
                     'partner_mobile' : call.partner_mobile,
                     'priority': call.priority,
+                    'opportunity_id': call.opportunity_id and call.opportunity_id.id or False,
             }
             new_id = self.create(cr, uid, vals, context=context)
             if action == 'log':
index b0e6404..3b76e45 100644 (file)
@@ -23,9 +23,6 @@
         <menuitem id="menu_crm_config_phonecall" name="Phone Calls"
             parent="base.menu_base_config" sequence="45" groups="base.group_sale_salesman"/>
 
-        <menuitem id="base.next_id_64" name="Sales"
-            parent="base.menu_reporting" sequence="1"/>
-
         <!-- crm.tracking.medium -->
         <record id="crm_tracking_medium_view_tree" model="ir.ui.view">
             <field name="name">crm.tracking.medium.tree</field>
index ce15e17..5a6b02a 100644 (file)
@@ -44,10 +44,10 @@ class crm_case_section(osv.Model):
             help="The first contact you get with a potential customer is a lead you qualify before converting it into a real business opportunity. Check this box to manage leads in this sales team."),
         'use_opportunities': fields.boolean('Opportunities', help="Check this box to manage opportunities in this sales team."),
         'monthly_open_leads': fields.function(_get_opportunities_data,
-            type="any", readonly=True, multi='_get_opportunities_data',
+            type="char", readonly=True, multi='_get_opportunities_data',
             string='Open Leads per Month'),
         'monthly_planned_revenue': fields.function(_get_opportunities_data,
-            type="any", readonly=True, multi='_get_opportunities_data',
+            type="char", readonly=True, multi='_get_opportunities_data',
             string='Planned Revenue per Month'),
         'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="restrict", required=True, help="The email address associated with this team. New emails received will automatically create new leads assigned to the team."),
     }
index bc9293e..06de0ce 100644 (file)
             <field name="act_window_id" ref="action_report_crm_claim"/>
         </record>
 
-        <menuitem id="base.menu_project_report" name="Project"
-            groups="base.group_no_one"
-            parent="base.menu_reporting" sequence="30"/>
-
         <menuitem name="Claims Analysis" id="menu_report_crm_claim_tree"
-            action="action_report_crm_claim" parent="base.menu_project_report" sequence="15"/>
-
+            action="action_report_crm_claim" parent="base.next_id_64" sequence="15"/>
 
     </data>
 </openerp>
index adc788a..a70c38f 100644 (file)
             <field name="act_window_id" ref="action_report_crm_helpdesk"/>
         </record>
 
-        <menuitem id="base.menu_project_report" name="Project"
-            groups="base.group_no_one"
-            parent="base.menu_reporting" sequence="30"/>
-
         <menuitem name="Helpdesk Analysis" action="action_report_crm_helpdesk"
-            id="menu_report_crm_helpdesks_tree" parent="base.menu_project_report" sequence="20"/>
+            id="menu_report_crm_helpdesks_tree" parent="base.next_id_64" sequence="20"/>
 
     </data>
 </openerp>
index 33db829..0281c27 100644 (file)
 #
 ##############################################################################
 
+import logging
 import time
 from openerp.osv import fields,osv
 from openerp.tools.translate import _
 import openerp.addons.decimal_precision as dp
 
+_logger = logging.getLogger(__name__)
+
 class delivery_carrier(osv.osv):
     _name = "delivery.carrier"
     _description = "Carrier"
@@ -51,14 +54,24 @@ class delivery_carrier(osv.osv):
         for carrier in self.browse(cr, uid, ids, context=context):
             order_id=context.get('order_id',False)
             price=False
+            available = False
             if order_id:
               order = sale_obj.browse(cr, uid, order_id, context=context)
               carrier_grid=self.grid_get(cr,uid,[carrier.id],order.partner_shipping_id.id,context)
               if carrier_grid:
-                  price=grid_obj.get_price(cr, uid, carrier_grid, order, time.strftime('%Y-%m-%d'), context)
+                  try:
+                    price=grid_obj.get_price(cr, uid, carrier_grid, order, time.strftime('%Y-%m-%d'), context)
+                    available = True
+                  except osv.except_osv, e:
+                    # no suitable delivery method found, probably configuration error
+                    _logger.error("Carrier %s: %s\n%s" % (carrier.name, e.name, e.value))
+                    price = 0.0
               else:
                   price = 0.0
-            res[carrier.id]=price
+            res[carrier.id] = {
+                'price': price,
+                'available': available
+            }
         return res
 
     _columns = {
@@ -66,7 +79,9 @@ class delivery_carrier(osv.osv):
         'partner_id': fields.many2one('res.partner', 'Transport Company', required=True, help="The partner that is doing the delivery service."),
         'product_id': fields.many2one('product.product', 'Delivery Product', required=True),
         'grids_id': fields.one2many('delivery.grid', 'carrier_id', 'Delivery Grids'),
-        'price' : fields.function(get_price, string='Price'),
+        'available' : fields.function(get_price, string='Available',type='boolean', multi='price',
+            help="Is the carrier method possible with the current order."),
+        'price' : fields.function(get_price, string='Price', multi='price'),
         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the delivery carrier without removing it."),
         'normal_price': fields.float('Normal Price', help="Keep empty if the pricing depends on the advanced pricing per destination"),
         'free_if_more_than': fields.boolean('Free If Order Total Amount Is More Than', help="If the order is more expensive than a certain amount, the customer can benefit from a free shipping"),
index e5a12fb..325682f 100644 (file)
@@ -185,12 +185,15 @@ class event_event(models.Model):
             for reg in self.registration_ids
         )
 
-    @api.one
+    @api.multi
     @api.depends('name', 'date_begin', 'date_end')
-    def _compute_display_name(self):
-        dates = [dt.split(' ')[0] for dt in [self.date_begin, self.date_end] if dt]
-        dates = sorted(set(dates))
-        self.display_name = '%s (%s)' % (self.name, ' - '.join(dates))
+    def name_get(self):
+        result = []
+        for event in self:
+            dates = [dt.split(' ')[0] for dt in [event.date_begin, event.date_end] if dt]
+            dates = sorted(set(dates))
+            result.append((event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
+        return result
 
     @api.one
     @api.constrains('seats_max', 'seats_available')
index ec0f2e4..180a53d 100644 (file)
@@ -22,7 +22,7 @@ class calendar_config_settings(osv.TransientModel):
         
         cal_client_id = params.get_param(cr, uid, 'google_calendar_client_id',default='',context=context)
         cal_client_secret = params.get_param(cr, uid, 'google_calendar_client_secret',default='',context=context)
-        server_uri= "%s/google_account/authentication" % params.get_param(cr, uid, 'web.base.url',default="http://yourcompany.my.openerp.com",context=context)          
+        server_uri= "%s/google_account/authentication" % params.get_param(cr, uid, 'web.base.url',default="http://yourcompany.odoo.com",context=context)
         return dict(cal_client_id=cal_client_id,cal_client_secret=cal_client_secret,server_uri=server_uri)
         
         
index 4dc924f..b827f25 100644 (file)
@@ -25,35 +25,37 @@ openerp.google_calendar = function(instance) {
                 local_context: context
             }).done(function(o) {
                 if (o.status === "need_auth") {
-                    alert(_t("You will be redirected on gmail to authorize your Odoo to access your calendar !"));
+                    alert(_t("You will be redirected to Google to authorize access to your calendar!"));
                     instance.web.redirect(o.url);
                 }
                 else if (o.status === "need_config_from_admin"){
                     if (!_.isUndefined(o.action) && parseInt(o.action)){
-                        if (confirm(_t("An admin need to configure Google Synchronization before to use it, do you want to configure it now ? !"))){
+                        if (confirm(_t("The Google Synchronization needs to be configured before you can use it, do you want to do it now?"))) {
                             self.do_action(o.action);
                         }
                     }
                     else{
-                        alert(_t("An admin need to configure Google Synchronization before to use it !"));
+                        alert(_t("An administrator needs to configure Google Synchronization before you can use it!"));
                     }
                 }
                 else if (o.status === "need_refresh"){
                     self.$calendar.fullCalendar('refetchEvents');
                 }
                 else if (o.status === "need_reset"){
-                    if (confirm(_t("The account that you are trying to synchronize (" + o.info.new_name + "), is not the same that the last one used \
-(" + o.info.old_name + "! )" + "\r\n\r\nDo you want remove all references from the old account ?"))){
-
+                    var confirm_text1 = _t("The account you are trying to synchronize (%s) is not the same as the last one used (%s)!");
+                    var confirm_text2 = _t("In order to do this, you first need to disconnect all existing events from the old account.");
+                    var confirm_text3 = _t("Do you want to do this now?");
+                    var text = _.str.sprintf(confirm_text1 + "\n" + confirm_text2 + "\n\n" + confirm_text3, o.info.new_name, o.info.old_name);
+                    if (confirm(text)) {
                         self.rpc('/google_calendar/remove_references', {
                             model:res.model,
                             local_context:context
                         }).done(function(o) {
                             if (o.status === "OK") {
-                                alert(_t("All old references have been deleted. You can now restart the synchronization"));
+                                alert(_t("All events have been disconnected from your previous account. You can now restart the synchronization"));
                             }
                             else if (o.status === "KO") {
-                                alert(_t("An error has occured when we was removing all old references. Please retry or contact your administrator."));
+                                alert(_t("An error occured while disconnecting events from your previous account. Please retry or contact your administrator."));
                             }
                             //else NOP
                         });
index 78c0b84..e6f2ca3 100644 (file)
@@ -1,5 +1,5 @@
+from openerp import api
 from openerp.osv import fields, osv
-from openerp.tools.translate import _
 
 
 class res_users(osv.Model):
@@ -52,6 +52,7 @@ class res_users(osv.Model):
             thread_id = thread_id[0]
         return self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', thread_id)], context=context)
 
+    @api.cr_uid_ids_context
     def message_post(self, cr, uid, thread_id, context=None, **kwargs):
         """ Redirect the posting of message on res.users to the related employee.
             This is done because when giving the context of Chatter on the
index a7950fe..4887d41 100644 (file)
@@ -61,13 +61,16 @@ class hr_attendance(osv.osv):
                     ('employee_id', '=', obj.employee_id.id),
                     ('name', '<', obj.name), ('action', '=', 'sign_in')
                 ], limit=1, order='name DESC')
-                last_signin = self.browse(cr, uid, last_signin_id, context=context)[0]
-
-                # Compute time elapsed between sign-in and sign-out
-                last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S')
-                signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S')
-                workedhours_datetime = (signout_datetime - last_signin_datetime)
-                res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60
+                if last_signin_id:
+                    last_signin = self.browse(cr, uid, last_signin_id, context=context)[0]
+
+                    # Compute time elapsed between sign-in and sign-out
+                    last_signin_datetime = datetime.strptime(last_signin.name, '%Y-%m-%d %H:%M:%S')
+                    signout_datetime = datetime.strptime(obj.name, '%Y-%m-%d %H:%M:%S')
+                    workedhours_datetime = (signout_datetime - last_signin_datetime)
+                    res[obj.id] = ((workedhours_datetime.seconds) / 60) / 60
+                else:
+                    res[obj.id] = False
         return res
 
     _columns = {
index 4dea994..397938f 100644 (file)
@@ -368,7 +368,7 @@ class hr_payslip(osv.osv):
         #OR if it starts between the given dates
         clause_2 = ['&',('date_start', '<=', date_to),('date_start','>=', date_from)]
         #OR if it starts before the date_from and finish after the date_end (or never finish)
-        clause_3 = [('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
+        clause_3 = ['&',('date_start','<=', date_from),'|',('date_end', '=', False),('date_end','>=', date_to)]
         clause_final =  [('employee_id', '=', employee.id),'|','|'] + clause_1 + clause_2 + clause_3
         contract_ids = contract_obj.search(cr, uid, clause_final, context=context)
         return contract_ids
index 2a8bf60..303d398 100644 (file)
@@ -66,7 +66,7 @@ class mail_alias(osv.Model):
 
     _columns = {
         'alias_name': fields.char('Alias Name',
-            help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.my.openerp.com>",),
+            help="The name of the email alias, e.g. 'jobs' if you want to catch emails for <jobs@example.odoo.com>",),
         'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade",
                                           help="The model (Odoo Document Kind) to which this alias "
                                                "corresponds. Any incoming email that does not reply to an "
@@ -132,7 +132,7 @@ class mail_alias(osv.Model):
     def name_get(self, cr, uid, ids, context=None):
         """Return the mail alias display alias_name, including the implicit
            mail catchall domain if exists from config otherwise "New Alias".
-           e.g. `jobs@openerp.my.openerp.com` or `jobs` or 'New Alias'
+           e.g. `jobs@mail.odoo.com` or `jobs` or 'New Alias'
         """
         res = []
         for record in self.browse(cr, uid, ids, context=context):
index a005eb7..aeb0a2b 100644 (file)
@@ -149,10 +149,11 @@ class mail_notification(osv.Model):
             company = "<a style='color:inherit' href='%s'>%s</a>" % (website_url, user.company_id.name)
         else:
             company = user.company_id.name
-        sent_by = _('Sent from %(company)s using %(openerp)s')
+        sent_by = _('Sent by %(company)s using %(odoo)s.')
+
         signature_company = '<small>%s</small>' % (sent_by % {
             'company': company,
-            'openerp': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
+            'odoo': "<a style='color:inherit' href='https://www.odoo.com/'>Odoo</a>"
         })
         footer = tools.append_content_to_html(footer, signature_company, plaintext=False, container_tag='div')
 
@@ -185,8 +186,9 @@ class mail_notification(osv.Model):
 
         # compute email body (signature, company data)
         body_html = message.body
-        user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
-        if user_signature:
+        # add user signature except for mail groups, where users are usually adding their own signatures already
+        if user_signature and message.model != 'mail.group':
+            user_id = message.author_id and message.author_id.user_ids and message.author_id.user_ids[0] and message.author_id.user_ids[0].id or None
             signature_company = self.get_signature_footer(cr, uid, user_id, res_model=message.model, res_id=message.res_id, context=context)
             body_html = tools.append_content_to_html(body_html, signature_company, plaintext=False, container_tag='div')
 
index 721593d..a654a7b 100644 (file)
@@ -226,8 +226,15 @@ class mail_group(osv.Model):
         except Exception:
             headers = {}
         headers['Precedence'] = 'list'
+        # avoid out-of-office replies from MS Exchange
+        # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
+        headers['X-Auto-Response-Suppress'] = 'OOF'
         if group.alias_domain and group.alias_name:
             headers['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain)
             headers['List-Post'] = '<mailto:%s@%s>' % (group.alias_name, group.alias_domain)
+            # Avoid users thinking it was a personal message
+            # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
+            list_to = '"%s" <%s@%s>' % (group.name, group.alias_name, group.alias_domain)
+            headers['X-Forge-To'] = list_to
         res['headers'] = '%s' % headers
         return res
index d3797ba..6f75667 100644 (file)
@@ -9,7 +9,7 @@
                 <xpath expr="//div[@name='email']" position='inside'>
                     <div>
                         <label for="alias_domain" class="oe_inline"/>
-                        <field name="alias_domain" placeholder="mycompany.my.openerp.com" class="oe_inline"/>
+                        <field name="alias_domain" placeholder="mycompany.odoo.com" class="oe_inline"/>
                     </div>
                 </xpath>
             </field>
index a4b4e68..097893a 100644 (file)
@@ -20,6 +20,7 @@
 ##############################################################################
 
 from openerp.osv import fields, osv
+from openerp import api
 from openerp import SUPERUSER_ID
 from openerp.tools.translate import _
 import openerp
@@ -115,6 +116,7 @@ class res_users(osv.Model):
             thread_id = thread_id[0]
         return self.browse(cr, SUPERUSER_ID, thread_id).partner_id.id
 
+    @api.cr_uid_ids_context
     def message_post(self, cr, uid, thread_id, context=None, **kwargs):
         """ Redirect the posting of message on res.users to the related partner.
             This is done because when giving the context of Chatter on the
index fdd87bb..84e0d42 100644 (file)
@@ -467,14 +467,10 @@ class test_mail(TestMail):
                             'message_post: notification email subject incorrect')
             self.assertIn(_body1, sent_email['body'],
                             'message_post: notification email body incorrect')
-            self.assertIn(user_raoul.signature, sent_email['body'],
-                            'message_post: notification email body should contain the sender signature')
             self.assertIn('Pigs rules', sent_email['body_alternative'],
                             'message_post: notification email body alternative should contain the body')
             self.assertNotIn('<p>', sent_email['body_alternative'],
                             'message_post: notification email body alternative still contains html')
-            self.assertIn(html2plaintext(user_raoul.signature), sent_email['body_alternative'],
-                            'message_post: notification email body alternative should contain the sender signature')
             self.assertFalse(sent_email['references'],
                             'message_post: references should be False when sending a message that is not a reply')
 
@@ -538,14 +534,10 @@ class test_mail(TestMail):
                             'message_post: notification email subject incorrect')
             self.assertIn(html_sanitize(_body2), sent_email['body'],
                             'message_post: notification email does not contain the body')
-            self.assertIn(user_raoul.signature, sent_email['body'],
-                            'message_post: notification email body should contain the sender signature')
             self.assertIn('Pigs rocks', sent_email['body_alternative'],
                             'message_post: notification email body alternative should contain the body')
             self.assertNotIn('<p>', sent_email['body_alternative'],
                             'message_post: notification email body alternative still contains html')
-            self.assertIn(html2plaintext(user_raoul.signature), sent_email['body_alternative'],
-                            'message_post: notification email body alternative should contain the sender signature')
             self.assertIn(msg_message_id, sent_email['references'],
                             'message_post: notification email references lacks parent message message_id')
         # Test: attachments + download
index 09ebde7..03e5680 100644 (file)
@@ -492,6 +492,8 @@ class pos_session(osv.osv):
             pos_order_obj._create_account_move_line(cr, uid, order_ids, session, move_id, context=local_context)
 
             for order in session.order_ids:
+                if order.state == 'done':
+                    continue
                 if order.state not in ('paid', 'invoiced'):
                     raise osv.except_osv(
                         _('Error!'),
@@ -811,6 +813,16 @@ class pos_order(osv.osv):
             'name': order.name + ': ' + (data.get('payment_name', '') or ''),
             'partner_id': order.partner_id and order.partner_id.id or None,
         }
+        account_def = property_obj.get(cr, uid, 'property_account_receivable', 'res.partner', context=context)
+        args['account_id'] = (order.partner_id and order.partner_id.property_account_receivable \
+                             and order.partner_id.property_account_receivable.id) or (account_def and account_def.id) or False
+
+        if not args['account_id']:
+            if not args['partner_id']:
+                msg = _('There is no receivable account defined to make payment.')
+            else:
+                msg = _('There is no receivable account defined to make payment for the partner: "%s" (id:%d).') % (order.partner_id.name, order.partner_id.id,)
+            raise osv.except_osv(_('Configuration Error!'), msg)
 
         context.pop('pos_session_id', False)
 
@@ -830,10 +842,10 @@ class pos_order(osv.osv):
             raise osv.except_osv(_('Error!'), _('You have to open at least one cashbox.'))
 
         args.update({
-            'statement_id' : statement_id,
-            'pos_statement_id' : order_id,
-            'journal_id' : journal_id,
-            'ref' : order.session_id.name,
+            'statement_id': statement_id,
+            'pos_statement_id': order_id,
+            'journal_id': journal_id,
+            'ref': order.session_id.name,
         })
 
         statement_line_obj.create(cr, uid, args, context=context)
index 47f4dac..5d05291 100644 (file)
 #
 ##############################################################################
 
+import logging
 import threading
+from openerp import tools
 
 from openerp.osv import osv
 from openerp.api import Environment
 
+_logger = logging.getLogger(__name__)
+
 class procurement_compute_all(osv.osv_memory):
     _name = 'procurement.order.compute.all'
     _description = 'Compute all schedulers'
@@ -39,8 +43,17 @@ class procurement_compute_all(osv.osv_memory):
         with Environment.manage():
             proc_obj = self.pool.get('procurement.order')
             #As this function is in a new thread, i need to open a new cursor, because the old one may be closed
-            
+
             new_cr = self.pool.cursor()
+            # Avoid to run the scheduler multiple times in the same time
+            try:
+                with tools.mute_logger('openerp.sql_db'):
+                    new_cr.execute("SELECT id FROM ir_cron WHERE id = %s FOR UPDATE NOWAIT", (scheduler_cron_id,))
+            except Exception:
+                _logger.info('Attempt to run procurement scheduler aborted, as already running')
+                new_cr.rollback()
+                new_cr.close()
+                return {}
             user = self.pool.get('res.users').browse(new_cr, uid, uid, context=context)
             comps = [x.id for x in user.company_ids]
             for comp in comps:
index 32b3a0b..96ce458 100644 (file)
@@ -395,6 +395,7 @@ class product_attribute_price(osv.osv):
 
 class product_attribute_line(osv.osv):
     _name = "product.attribute.line"
+    _rec_name = 'attribute_id'
     _columns = {
         'product_tmpl_id': fields.many2one('product.template', 'Product Template', required=True, ondelete='cascade'),
         'attribute_id': fields.many2one('product.attribute', 'Attribute', required=True, ondelete='restrict'),
index 007f2c0..5b8b2c1 100644 (file)
                         <div class="oe_right">
                             <button class="oe_inline oe_stat_button" string="Variant Prices"  name="%(variants_template_action)d" type="action" icon="fa-strikethrough"/>
                             <button class="oe_inline oe_stat_button" name="%(product.product_variant_action)d" type="action" icon="fa-sitemap">
-                                <field string="Variants" name="product_variant_count" widget="statinfo" />
+                                <field string="List of Variants" name="product_variant_count" widget="statinfo" />
                             </button>
                         </div>
                         <field name="attribute_line_ids" widget="one2many_list">
                                              <field name="name"/> 
                                         </a>
                                     </h4>
-                                    <a name="%(product.product_variant_action)d" type="action" t-if="record.product_variant_count.raw_value&gt;1" >
+                                    <a name="%(product.product_variant_action)d" type="action">
                                         <t t-esc="record.product_variant_count.value"/> Variants
                                     </a>
                                     <div name="tags"/>
index e48c2e6..5823f2c 100644 (file)
                     </h1>
                     <group>
                         <group>
-                            <field name="project_id" domain="[('state', '!=', 'close')]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
+                            <field name="project_id" domain="[('state','not in', ('close', 'cancelled'))]" on_change="onchange_project(project_id)" context="{'default_use_tasks':1}"/>
                             <field name="user_id"
                                 options='{"no_open": True}'
                                 context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'project.group_project_user']}"/>
index 0c3ba56..016313b 100644 (file)
@@ -21,6 +21,7 @@
 
 from datetime import datetime
 
+from openerp import api
 from openerp import SUPERUSER_ID
 from openerp import tools
 from openerp.osv import fields, osv, orm
@@ -449,6 +450,7 @@ class project_issue(osv.Model):
         res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=defaults, context=context)
         return res_id
 
+    @api.cr_uid_ids_context
     def message_post(self, cr, uid, thread_id, body='', subject=None, type='notification', subtype=None, parent_id=False, attachments=None, context=None, content_subtype='html', **kwargs):
         """ Overrides mail_thread message_post so that we can set the date of last action field when
             a new message is posted on the issue.
index c7ce205..99efd15 100644 (file)
@@ -818,7 +818,12 @@ class purchase_order(osv.osv):
 
     def action_picking_create(self, cr, uid, ids, context=None):
         for order in self.browse(cr, uid, ids):
-            picking_id = self.pool.get('stock.picking').create(cr, uid, {'picking_type_id': order.picking_type_id.id, 'partner_id': order.dest_address_id.id or order.partner_id.id}, context=context)
+            picking_vals = {
+                'picking_type_id': order.picking_type_id.id,
+                'partner_id': order.dest_address_id.id or order.partner_id.id,
+                'date': max([l.date_planned for l in order.order_line])
+            }
+            picking_id = self.pool.get('stock.picking').create(cr, uid, picking_vals, context=context)
             self._create_stock_moves(cr, uid, order, order.order_line, picking_id, context=context)
 
     def picking_done(self, cr, uid, ids, context=None):
index acba427..bc7c6ee 100644 (file)
@@ -86,7 +86,6 @@
         <field name="help">This report performs analysis on your quotations and sales orders. Analysis check your sales revenues and sort it by different group criteria (salesman, partner, product, etc.) Use this report to perform analysis on sales not having invoiced yet. If you want to analyse your turnover, you should use the Invoice Analysis report in the Accounting application.</field>
     </record>
 
-    <menuitem id="base.next_id_64" name="Sales" parent="base.menu_reporting" sequence="1" groups="base.group_sale_manager"/>
     <menuitem action="action_order_report_all" id="menu_report_product_all" parent="base.next_id_64" sequence="10"/>
 
 </data>
index b85716b..44f87b7 100644 (file)
@@ -632,7 +632,7 @@ class sale_order(osv.osv):
             compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
         except ValueError:
             compose_form_id = False 
-        ctx = dict(context)
+        ctx = dict()
         ctx.update({
             'default_model': 'sale.order',
             'default_res_id': ids[0],
index 43eea5d..b601383 100644 (file)
@@ -50,13 +50,13 @@ class crm_case_section(osv.osv):
             help="Target of invoice revenue for the current month. This is the amount the sales \n"
                     "team estimates to be able to invoice this month."),
         'monthly_quoted': fields.function(_get_sale_orders_data,
-            type='any', readonly=True, multi='_get_sale_orders_data',
+            type='char', readonly=True, multi='_get_sale_orders_data',
             string='Rate of created quotation per duration'),
         'monthly_confirmed': fields.function(_get_sale_orders_data,
-            type='any', readonly=True, multi='_get_sale_orders_data',
+            type='char', readonly=True, multi='_get_sale_orders_data',
             string='Rate of validate sales orders per duration'),
         'monthly_invoiced': fields.function(_get_invoices_data,
-            type='any', readonly=True,
+            type='char', readonly=True,
             string='Rate of sent invoices per duration'),
     }
 
index c3f1d6c..3d8cc3e 100644 (file)
@@ -37,6 +37,7 @@ class sale_advance_payment_inv(osv.osv_memory):
                 Use Some Order Lines to invoice a selection of the sales order lines."""),
         'qtty': fields.float('Quantity', digits=(16, 2), required=True),
         'product_id': fields.many2one('product.product', 'Advance Product',
+            domain=[('type', '=', 'service')],
             help="""Select a product of type service which is called 'Advance Product'.
                 You may have to create it and set it as a default value on this field."""),
         'amount': fields.float('Advance Amount', digits_compute= dp.get_precision('Account'),
index 0865d95..59d018a 100644 (file)
@@ -16,7 +16,7 @@
             <field name="model">product.template</field>
             <field name="inherit_id" ref="product.product_template_form_view"/>
             <field name="arch" type="xml">
-                <group name="procurement_uom" position="after">
+                <group name="procurement" position="after">
                     <group string="Project Management Information" attrs="{'invisible': [('type', '!=', 'service')]}">
                         <field name="auto_create_task"/>
                         <field name="project_id" attrs="{'invisible':['|', ('type','!=','service'), ('auto_create_task', '=', False)]}"/>
index 130bcef..e6d7060 100644 (file)
@@ -15,7 +15,7 @@
                    <xpath expr="//button[@name='action_view_invoice']" position="after">
                        <field name="picking_ids" invisible="1"/>
                        <button name="action_view_delivery" string="View Delivery Order" type="object" class="oe_highlight"
-                           attrs="{'invisible': ['|','|','|',('picking_ids','=',False),('picking_ids','=',[]), ('state', 'not in', ('progress','manual', 'done')),('shipped','=',True)]}" groups="base.group_user"/>
+                           attrs="{'invisible': ['|',('picking_ids','=',False),('picking_ids','=',[])]}" groups="base.group_user"/>
                    </xpath>
                     <xpath expr="//button[@name='action_cancel']" position="after">
                         <button name="ship_cancel" states="shipping_except" string="Cancel Order"/>
index cd2442b..2882f1c 100644 (file)
             </xpath>
         </template>
 
+        <menuitem id="base.next_id_64" name="Sales" parent="base.menu_reporting" sequence="1" groups="base.group_sale_manager"/>
+
     </data>
 </openerp>
index 9c9ae00..56a3887 100644 (file)
@@ -33,7 +33,10 @@ class res_users(osv.osv):
 
     def _get_users_from_group(self, cr, uid, ids, context=None):
         result = set()
-        for group in self.pool['res.groups'].browse(cr, uid, ids, context=context):
+        groups = self.pool['res.groups'].browse(cr, uid, ids, context=context)
+        # Clear cache to avoid perf degradation on databases with thousands of users 
+        groups.invalidate_cache()
+        for group in groups:
             result.update(user.id for user in group.users)
         return list(result)
 
index c8661a4..68db426 100644 (file)
@@ -1531,9 +1531,9 @@ class stock_move(osv.osv):
         res = dict.fromkeys(ids, False)
         for move in self.browse(cr, uid, ids, context=context):
             if move.state == 'done':
-                res[move.id] = [q.id for q in move.quant_ids]
+                res[move.id] = [q.lot_id.id for q in move.quant_ids if q.lot_id]
             else:
-                res[move.id] = [q.id for q in move.reserved_quant_ids]
+                res[move.id] = [q.lot_id.id for q in move.reserved_quant_ids if q.lot_id]
         return res
 
     def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
@@ -1688,7 +1688,7 @@ class stock_move(osv.osv):
         'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
         'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
-        'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
+        'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.production.lot', string='Lots'),
         'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move', copy=False),
         'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
         'reserved_availability': fields.function(_get_reserved_availability, type='float', string='Quantity Reserved', readonly=True, help='Quantity that has already been reserved for this move'),
@@ -4149,7 +4149,7 @@ class stock_picking_type(osv.osv):
 
         # Statistics for the kanban view
         'last_done_picking': fields.function(_get_tristate_values,
-            type='any',
+            type='char',
             string='Last 10 Done Pickings'),
 
         'count_picking_draft': fields.function(_get_picking_count,
index b035b22..ef3aedb 100644 (file)
@@ -910,7 +910,7 @@ instance.web.Menu =  instance.web.Widget.extend({
         }
         // add a tooltip to cropped menu items
         this.$secondary_menus.find('.oe_secondary_submenu li a span').each(function() {
-            $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'auto right'} :'destroy');
+            $(this).tooltip(this.scrollWidth > this.clientWidth ? {title: $(this).text().trim(), placement: 'right'} :'destroy');
        });
     },
     /**
index 1bfdc86..566179c 100644 (file)
@@ -782,6 +782,7 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({
 
     start: function() {
         var self = this;
+        if (this.searchview.headless) return $.when(this._super(), this.searchview.ready);
         var filters_ready = this.searchview.fields_view_get
                                 .then(this.proxy('prepare_filters'));
         return $.when(this._super(), filters_ready).then(function () {
index 89ac27e..27c83f2 100644 (file)
@@ -37,7 +37,7 @@ instance.web_kanban.GaugeWidget = instance.web_kanban.AbstractField.extend({
         }
         var title = this.$node.html() || this.field.string;
         // current gauge value
-        var val = this.field.value;
+        var val = this.field.raw_value;
         if (_.isArray(JSON.parse(val))) {
             val = JSON.parse(val);
         }
index 107b8ae..52e614a 100644 (file)
@@ -4,6 +4,9 @@ from openerp.osv import orm, fields
 class test_converter(orm.Model):
     _name = 'website.converter.test'
 
+    # disable translation export for those brilliant field labels and values
+    _translate = False
+
     _columns = {
         'char': fields.char(),
         'integer': fields.integer(),
index 9c9215f..681d656 100644 (file)
@@ -17,9 +17,6 @@ from PIL import Image
 from sys import maxint
 
 import werkzeug
-import werkzeug.exceptions
-import werkzeug.utils
-import werkzeug.wrappers
 # optional python-slugify import (https://github.com/un33k/python-slugify)
 try:
     import slugify as slugify_lib
@@ -571,7 +568,7 @@ class website_menu(osv.osv):
     _description = "Website Menu"
     _columns = {
         'name': fields.char('Menu', required=True, translate=True),
-        'url': fields.char('Url', translate=True),
+        'url': fields.char('Url'),
         'new_window': fields.boolean('New Window'),
         'sequence': fields.integer('Sequence'),
         # TODO: support multiwebsite once done for ir.ui.views
index 778c67e..1eb7436 100644 (file)
 }
 .oe_overlay .oe_overlay_options > .btn-group {
   left: -50%;
+  white-space: nowrap;
+}
+.oe_overlay .oe_overlay_options > .btn-group > a {
+  cursor: pointer;
+  display: inline-block;
+  float: none;
+  margin: 0 -3px;
 }
 .oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
   cursor: pointer;
index 868292b..503a49a 100644 (file)
         z-index: 1002
         > .btn-group
             left: -50%
+            white-space: nowrap
+            > a
+                cursor: pointer
+                display: inline-block
+                float: none
+                margin: 0 -3px
         .btn, a
             cursor: pointer
         .dropdown
index 0e3873b..24d54ec 100644 (file)
                         class="form-control url pull-left"
                         style="width: 400px;"
                         id="urlvideo"
-                        placeholder="//www.youtube.com/embed/yws1tbgNV7k"/>
+                        placeholder="//www.youtube.com/embed/yws1tbgNV7k"
+                        t-translation="off"/>
                     <button class="btn btn-default">Preview</button>
                 </div>
                 <div class="form-group btn-group">
                         class="form-control url pull-left"
                         style="width: 400px;"
                         id="embedvideo"
-                        placeholder='&lt;iframe src="//www.youtube.com/embed/yws1tbgNV7k"&gt;&lt;/iframe&gt;'/>
+                        placeholder='&lt;iframe src="//www.youtube.com/embed/yws1tbgNV7k"&gt;&lt;/iframe&gt;'
+                        t-translation="off"/>
                     <button class="btn btn-default">Preview</button>
                 </div>
             </div>
index 494673c..b283c11 100644 (file)
                             A great way to catch your reader's attention is to tell a story.
                             Everything you consider writing can be told as a story.
                         </p><p>
-                            <b>Great stories have personality</b>. Consider telling
+                            <b>Great stories have personality.</b> Consider telling
                             a great story that provides personality. Writing a story
                             with personality for potential clients will asist with
                             making a relationship connection. This shows up in small
                             of view, not from someone else's experience.
                         </p><p>
                             <b>Great stories are for everyone even when only written for
-                            just one person</b>. If you try to write with a wide general
+                            just one person.</b> If you try to write with a wide general
                             audience in mind, your story will ring false and be bland.
                             No one will be interested. Write for one person. If it’s genuine for the one, it’s genuine for the rest.
                         </p>
index 36104f4..fd9cf31 100644 (file)
 
 <template id="footer_custom" inherit_id="website.layout" name="Footer">
     <xpath expr="//div[@id='footer_container']" position="replace">
-        <div class="oe_structure">
+        <div class="oe_structure" id="footer">
             <section data-snippet-id='three-columns' class="mt16 mb16">
                 <div class="container">
                     <div class="row">
 </template>
 
 <template id="footer_default" inherit_id="website.footer_custom" optional="enabled" name="Automatic Footer">
-    <xpath expr="//div[@class='oe_structure']" position="replace">
+    <xpath expr="//div[@id='footer']" position="replace">
         <div class="container hidden-print">
             <div class="row">
                 <div class="col-md-3">
index 0d266b7..06b982e 100644 (file)
                     <div name="social_media">
                         <separator string="Social Media"/>
                         <group name="social_media">
-                            <field name="social_twitter" placeholder="https://twitter.com/openerp"/>
-                            <field name="social_facebook" placeholder="https://facebook.com/openerp"/>
-                            <field name="social_googleplus" placeholder="https://plus.google.com/+openerp"/>
-                            <field name="social_linkedin" placeholder="http://www.linkedin.com/company/openerp"/>
-                            <field name="social_youtube" placeholder="http://www.youtube.com/channel/HCU842OHPPNrQ"/>
+                            <field name="social_twitter" placeholder="https://twitter.com/odooapps"/>
+                            <field name="social_facebook" placeholder="https://facebook.com/odoo"/>
+                            <field name="social_googleplus" placeholder="https://plus.google.com/+Odooapps"/>
+                            <field name="social_linkedin" placeholder="http://www.linkedin.com/company/odoo"/>
+                            <field name="social_youtube" placeholder="https://www.youtube.com/channel/UCkQPikELWZFLgQNHd73jkdg"/>
                             <field name="social_github" placeholder="https://youraccount.github.io"/>
                         </group>
                     </div>
index a9331c5..12fd63f 100644 (file)
@@ -244,7 +244,6 @@ class WebsiteBlog(http.Controller):
         cr, uid, context = request.cr, request.uid, request.context
         blog_post = request.registry['blog.post']
         partner_obj = request.registry['res.partner']
-        thread_obj = request.registry['mail.thread']
 
         if uid != request.website.user_id.id:
             partner_ids = [user.partner_id.id]
@@ -343,6 +342,14 @@ class WebsiteBlog(http.Controller):
             return ids
         return self._get_discussion_detail(ids, publish, **post)
 
+    @http.route('/blogpost/get_discussions/', type='json', auth="public", website=True)
+    def discussions(self, post_id=0, paths=None, count=False, **post):
+        ret = []
+        for path in paths:
+            result = self.discussion(post_id=post_id, path=path, count=count, **post)
+            ret.append({"path": path, "val": result})
+        return ret
+
     @http.route('/blogpost/change_background', type='json', auth="public", website=True)
     def change_bg(self, post_id=0, image=None, **post):
         if not post_id:
index 56851c3..426016f 100644 (file)
@@ -25,9 +25,8 @@
                 $('<div id="discussions_wrapper"></div>').insertAfter($('#blog_content'));
             }
             // Attach a discussion to each paragraph.
-            $(self.settings.content).each(function(i) {
-                self.discussion_handler(i, $(this));
-            });
+            self.discussions_handler(self.settings.content);
+
             // Hide the discussion.
             $('html').click(function(event) {
                 if($(event.target).parents('#discussions_wrapper, .main-discussion-link-wrp').length === 0) {
                 'count': comment_count, //if true only get length of total comment, display on discussion thread.
             })
         },
-        discussion_handler : function(i, node) {
+        prepare_multi_data : function(identifiers, comment_count) {
             var self = this;
-            var identifier = node.attr('data-chatter-id');
-            if (identifier) {
-                self.prepare_data(identifier, true).then( function (data) {
-                    self.prepare_discuss_link(data, identifier, node);
+            return openerp.jsonRpc("/blogpost/get_discussions/", 'call', {
+                'post_id': self.settings.post_id,
+                'paths': identifiers,
+                'count': comment_count, //if true only get length of total comment, display on discussion thread.
+            })
+        },
+        discussions_handler: function() {
+            var self = this;
+            var node_by_id = {};
+            $(self.settings.content).each(function(i) {
+                var node = $(this);
+                var identifier = node.attr('data-chatter-id');
+                if (identifier) {
+                    node_by_id[identifier] = node;
+                }
+            });
+            self.prepare_multi_data(_.keys(node_by_id), true).then( function (multi_data) {
+                _.forEach(multi_data, function(data) {
+                    self.prepare_discuss_link(data.val, data.path, node_by_id[data.path]);
                 });
-            }
+            });
         },
         prepare_discuss_link :  function(data, identifier, node) {
             var self = this;
index b209c70..5e2d8e7 100644 (file)
@@ -25,6 +25,10 @@ class contactus(http.Controller):
         values.update(kwargs=kwargs.items())
         return request.website.render("website.contactus", values)
 
+    def create_lead(self, request, values):
+        """ Allow to be overrided """
+        return request.registry['crm.lead'].create(request.cr, SUPERUSER_ID, values, request.context)
+
     @http.route(['/crm/contactus'], type='http', auth="public", website=True)
     def contactus(self, **kwargs):
         def dict_to_str(title, dictvar):
@@ -41,12 +45,10 @@ class contactus(http.Controller):
         post_description = []  # Info to add after the message
         values = {}
 
-        lead_model = request.registry['crm.lead']
-
         for field_name, field_value in kwargs.items():
             if hasattr(field_value, 'filename'):
                 post_file.append(field_value)
-            elif field_name in lead_model._all_columns and field_name not in _BLACKLIST:
+            elif field_name in request.registry['crm.lead']._all_columns and field_name not in _BLACKLIST:
                 values[field_name] = field_value
             elif field_name not in _TECHNICAL:  # allow to add some free fields or blacklisted field like ID
                 post_description.append("%s: %s" % (field_name, field_value))
@@ -80,7 +82,7 @@ class contactus(http.Controller):
             post_description.append("%s: %s" % ("REFERER", environ.get("HTTP_REFERER")))
             values['description'] += dict_to_str(_("Environ Fields: "), post_description)
 
-        lead_id = lead_model.create(request.cr, SUPERUSER_ID, dict(values, user_id=False), request.context)
+        lead_id = self.create_lead(request, dict(values, user_id=False))
         if lead_id:
             for field_value in post_file:
                 attachment_value = {
index 2d2e5f2..78d69de 100644 (file)
@@ -62,7 +62,7 @@ class WebsiteForum(http.Controller):
         forum_id = request.registry['forum.forum'].create(request.cr, request.uid, {
             'name': forum_name,
         }, context=request.context)
-        return request.redirect("/forum/%s" % slug(forum_id))
+        return request.redirect("/forum/%s" % forum_id)
 
     @http.route('/forum/notification_read', type='json', auth="user", methods=['POST'], website=True)
     def notification_read(self, **kwargs):
@@ -529,7 +529,7 @@ class WebsiteForum(http.Controller):
             'website': kwargs.get('website'),
             'email': kwargs.get('email'),
             'city': kwargs.get('city'),
-            'country_id': int(kwargs.get('country')),
+            'country_id': int(kwargs.get('country')) if kwargs.get('country') else False,
             'website_description': kwargs.get('description'),
         }, context=request.context)
         return werkzeug.utils.redirect("/forum/%s/user/%d" % (slug(forum), user.id))
index 891ce3b..2b28eb4 100644 (file)
@@ -400,8 +400,9 @@ class Post(osv.Model):
             raise KarmaError('Not enough karma to downvote.')
 
         Vote = self.pool['forum.post.vote']
-        vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], limit=1, context=context)
-        new_vote = 0
+        vote_ids = Vote.search(cr, uid, [('post_id', 'in', ids), ('user_id', '=', uid)], context=context)
+        new_vote = '1' if upvote else '-1'
+        voted_forum_ids = set()
         if vote_ids:
             for vote in Vote.browse(cr, uid, vote_ids, context=context):
                 if upvote:
@@ -409,9 +410,9 @@ class Post(osv.Model):
                 else:
                     new_vote = '0' if vote.vote == '1' else '-1'
                 Vote.write(cr, uid, vote_ids, {'vote': new_vote}, context=context)
-        else:
+                voted_forum_ids.add(vote.post_id.id)
+        for post_id in set(ids) - voted_forum_ids:
             for post_id in ids:
-                new_vote = '1' if upvote else '-1'
                 Vote.create(cr, uid, {'post_id': post_id, 'vote': new_vote}, context=context)
         return {'vote_count': self._get_vote_count(cr, uid, ids, None, None, context=context)[ids[0]], 'user_vote': new_vote}
 
index c393f6e..655040f 100644 (file)
@@ -24,13 +24,13 @@ class Documentation(osv.Model):
             res.append((record['id'], name))
         return res
 
+    # TODO master remove me
     def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None):
         res = self.name_get(cr, uid, ids, context=context)
         return dict(res)
 
     _columns = {
         'sequence': fields.integer('Sequence'),
-        'display_name': fields.function(_name_get_fnc, type="char", string='Full Name'),
         'name': fields.char('Name', required=True, translate=True),
         'introduction': fields.html('Introduction', translate=True),
         'parent_id': fields.many2one('forum.documentation.toc', 'Parent Table Of Content', ondelete='cascade'),
index 18c3637..a8b351e 100644 (file)
             }
             this.$target.removeClass('has-error');
 
-            openerp.jsonRpc('/website_mail/follow', 'call', {
-                'id': +this.$target.data('id'),
-                'object': this.$target.data('object'),
-                'message_is_follower': this.$target.attr("data-follow") || "off",
-                'email': $email.length ? $email.val() : false,
-            }).then(function (follow) {
-                self.toggle_subscription(follow, self.email);
-            });
+            var email = $email.length ? $email.val() : false;
+            if (email) {
+                openerp.jsonRpc('/website_mail/follow', 'call', {
+                    'id': +this.$target.data('id'),
+                    'object': this.$target.data('object'),
+                    'message_is_follower': this.$target.attr("data-follow") || "off",
+                    'email': email,
+                }).then(function (follow) {
+                    self.toggle_subscription(follow, email);
+                });
+            }
         },
         toggle_subscription: function(follow, email) {
             console.log(follow, email);
+            follow = follow || (!email && this.$target.attr('data-unsubscribe'));
             if (follow) {
                 this.$target.find(".js_follow_btn").addClass("hidden");
                 this.$target.find(".js_unfollow_btn").removeClass("hidden");
@@ -60,8 +64,8 @@
                 this.$target.find(".js_unfollow_btn").addClass("hidden");
             }
             this.$target.find('input.js_follow_email')
-                .val(email ? email : "")
-                .attr("disabled", follow || (email.length && this.is_user) ? "disabled" : false);
+                .val(email || "")
+                .attr("disabled", email && (follow || this.is_user) ? "disabled" : false);
             this.$target.attr("data-follow", follow ? 'on' : 'off');
         },
     });
index ebbcf6d..5e62191 100644 (file)
@@ -39,7 +39,7 @@
                                     Everything you consider writing can be told as a story.
                                 </p>
                                 <p style="overflow:hidden">
-                                    <strong>Great stories have personality</strong>. Consider telling 
+                                    <strong>Great stories have personality.</strong> Consider telling
                                     a great story that provides personality. Writing a story 
                                     with personality for potential clients will asist with 
                                     making a relationship connection. This shows up in small
@@ -48,7 +48,7 @@
                                 </p>
                                 <p style="overflow:hidden">
                                     <strong>Great stories are for everyone even when only written for
-                                    just one person</strong>. If you try to write with a wide general
+                                    just one person.</strong> If you try to write with a wide general
                                     audience in mind, your story will ring false and be bland. 
                                     No one will be interested. Write for one person. If it’s genuine
                                     for the one, it’s genuine for the rest.
index 0ccae00..2b81009 100644 (file)
@@ -5,7 +5,8 @@
 <template id="follow">
     <div class="input-group js_follow" t-att-data-id="object.id"
               t-att-data-object="object._name"
-              t-att-data-follow="object.id and object.message_is_follower and 'on' or 'off'">
+              t-att-data-follow="object.id and object.message_is_follower and 'on' or 'off'"
+              t-att-data-unsubscribe="'unsubscribe' if 'unsubscribe' in request.params else None">
         <input
               type="email" name="email"
               class="js_follow_email form-control"
index 8e790b2..00bdcdf 100644 (file)
@@ -1,8 +1,10 @@
 # -*- coding: utf-8 -*-
 
 from openerp.osv import osv
+from openerp import tools
+from openerp.tools.translate import _
 from openerp.tools.safe_eval import safe_eval as eval
-
+from openerp.addons.website.models.website import slug
 
 class MailGroup(osv.Model):
     _inherit = 'mail.group'
@@ -16,9 +18,41 @@ class MailGroup(osv.Model):
         except Exception:
             headers = {}
         headers.update({
-            'List-Archive': '<%s/groups/%s>' % (base_url, group.id),
+            'List-Archive': '<%s/groups/%s>' % (base_url, slug(group)),
             'List-Subscribe': '<%s/groups>' % (base_url),
-            'List-Unsubscribe': '<%s/groups>' % (base_url),
+            'List-Unsubscribe': '<%s/groups?unsubscribe>' % (base_url,),
         })
         res['headers'] = '%s' % headers
         return res
+
+
+class MailMail(osv.Model):
+    _inherit = 'mail.mail'
+
+    def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
+        """ Short-circuit parent method for mail groups, replace the default
+            footer with one appropriate for mailing-lists."""
+
+        if mail.model == 'mail.group' and mail.res_id:
+            # no super() call on purpose, no private links that could be quoted!
+            group = self.pool['mail.group'].browse(cr, uid, mail.res_id, context=context)
+            base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
+            vals = {
+                'maillist': _('Mailing-List'),
+                'post_to': _('Post to'),
+                'unsub': _('Unsubscribe'),
+                'mailto': 'mailto:%s@%s' % (group.alias_name, group.alias_domain),
+                'group_url': '%s/groups/%s' % (base_url, slug(group)),
+                'unsub_url': '%s/groups?unsubscribe' % (base_url,),
+            }
+            footer = """_______________________________________________
+                        %(maillist)s: %(group_url)s
+                        %(post_to)s: %(mailto)s
+                        %(unsub)s: %(unsub_url)s
+                    """ % vals
+            body = tools.append_content_to_html(mail.body, footer, container_tag='div')
+            return body
+        else:
+            return super(MailMail, self).send_get_mail_body(cr, uid, mail,
+                                                            partner=partner,
+                                                            context=context)
index a106502..a5170e2 100644 (file)
@@ -31,6 +31,9 @@
             </section>
         </div>
         <div class="container mt32">
+            <div t-if="'unsubscribe' in request.params" class="col-md-offset-9 col-md-3 alert alert-info">
+               <h3>Need to unsubscribe? It's right here! <span class="fa fa-2x fa-arrow-down pull-right"></span></h3>
+            </div>
             <div class="row mt8" t-foreach="groups" t-as="group">
                 <div class="col-md-3">
                     <img t-att-src="'/website/image?model=mail.group&amp;field=image_small&amp;id='+str(group['id'])" class="pull-left"/>
index 5c8bf28..91eada1 100644 (file)
@@ -660,7 +660,7 @@ class website_sale(http.Controller):
         if not order:
             return {
                 'state': 'error',
-                'message': '<p>There seems to be an error with your request.</p>',
+                'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
             }
 
         tx_ids = request.registry['payment.transaction'].search(
@@ -672,7 +672,7 @@ class website_sale(http.Controller):
             if order.amount_total:
                 return {
                     'state': 'error',
-                    'message': '<p>There seems to be an error with your request.</p>',
+                    'message': '<p>%s</p>' % _('There seems to be an error with your request.'),
                 }
             else:
                 state = 'done'
@@ -682,15 +682,15 @@ class website_sale(http.Controller):
             tx = request.registry['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
             state = tx.state
             if state == 'done':
-                message = '<p>Your payment has been received.</p>'
+                message = '<p>%s</p>' % _('Your payment has been received.')
             elif state == 'cancel':
-                message = '<p>The payment seems to have been canceled.</p>'
+                message = '<p>%s</p>' % _('The payment seems to have been canceled.')
             elif state == 'pending' and tx.acquirer_id.validation == 'manual':
-                message = '<p>Your transaction is waiting confirmation.</p>'
+                message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
                 if tx.acquirer_id.post_msg:
                     message += tx.acquirer_id.post_msg
             else:
-                message = '<p>Your transaction is waiting confirmation.</p>'
+                message = '<p>%s</p>' % _('Your transaction is waiting confirmation.')
             validation = tx.acquirer_id.validation
 
         return {
@@ -820,9 +820,22 @@ class website_sale(http.Controller):
         product = product_obj.browse(request.cr, request.uid, id, context=request.context)
         return product.write({'website_size_x': x, 'website_size_y': y})
 
+    def order_lines_2_google_api(self, order_lines):
+        """ Transforms a list of order lines into a dict for google analytics """
+        ret = []
+        for line in order_lines:
+            ret.append({
+                'id': line.order_id and line.order_id.id,
+                'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
+                'sku': line.product_id.id,
+                'quantity': line.product_uom_qty,
+                'price': line.price_unit,
+            })
+        return ret
+
     @http.route(['/shop/tracking_last_order'], type='json', auth="public")
     def tracking_cart(self, **post):
-        """ return JS code for google analytics"""
+        """ return data about order in JSON needed for google analytics"""
         cr, uid, context = request.cr, request.uid, request.context
         ret = {}
         sale_order_id = request.session.get('sale_last_order_id')
@@ -834,16 +847,7 @@ class website_sale(http.Controller):
                 'revenue': order.amount_total,
                 'currency': order.currency_id.name
             }
-            ret['lines'] = []
-            for line in order.order_line:
-                if not line.is_delivery:
-                    ret['lines'].append({
-                        'id': line.order_id and line.order_id.id,
-                        'name': line.product_id.categ_id and line.product_id.categ_id.name or '-',
-                        'sku': line.product_id.id,
-                        'quantity': line.product_uom_qty,
-                        'price': line.price_unit,
-                    })
+            ret['lines'] = self.order_lines_2_google_api(order.order_line)
         return ret
 
 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4:
index 1e65c91..67bb544 100644 (file)
                         <select class="form-control" name="attrib">
                           <option value=""/>
                           <t t-foreach="a.value_ids" t-as="v">
-                            <option t-att-value="'%s-%s' % (a.id,v.id)" t-field="v.name" t-att-selected="'selected' if v.id in attrib_set else ''"/>
+                            <option t-att-value="'%s-%s' % (a.id,v.id)" t-esc="v.name" t-att-selected="'selected' if v.id in attrib_set else ''"/>
                           </t>
                         </select>
                       </t>
 
                   <div class="clearfix"/>
 
-                  <div class="form-group col-lg-6" groups="sale.group_delivery_invoice_address">
+                  <div class="form-group col-lg-6">
                       <label>
                           <input type="checkbox" name="shipping_different" t-att-checked="checkout.get('shipping_different')"/>
                           <span>Ship to a different address</span>
                       </label>
                   </div>
               </div>
-              <div class="js_shipping row mb16" t-att-style="not checkout.get('shipping_different') and 'display:none' or ''" groups="sale.group_delivery_invoice_address">
+              <div class="js_shipping row mb16" t-att-style="not checkout.get('shipping_different') and 'display:none' or ''">
                   <h3 class="oe_shipping col-lg-12 mt16">Shipping Information</h3>
 
                   <div t-attf-class="form-group #{error.get('shipping_name') and 'has-error' or ''} col-lg-6">
                 <div>
                   <a href="/shop/checkout"><span class="fa fa-arrow-right"/> Change Address</a>
                 </div>
-                <t groups="sale.group_delivery_invoice_address">
+                <t>
                     <h4 class="mt32">Ship To:</h4>
                     <t t-if="website_sale_order.partner_shipping_id and website_sale_order.partner_shipping_id.id != website_sale_order.partner_invoice_id.id">
                       <div t-field="order.partner_shipping_id" t-field-options='{
                     "widget": "contact",
                     "fields": ["address", "name", "phone", "email"]
                     }'/>
-                <t groups="sale.group_delivery_invoice_address">
+                <t>
                     <h4 class="mt32">Ship To:</h4>
                     <t t-if="order.partner_shipping_id and order.partner_shipping_id.id != order.partner_invoice_id.id">
                       <div t-field="order.partner_shipping_id" t-field-options='{
index 93c6aa2..15bc881 100644 (file)
@@ -1,10 +1,10 @@
 # -*- coding: utf-8 -*-
 import openerp
 from openerp import http
-from openerp import SUPERUSER_ID
 from openerp.http import request
 import openerp.addons.website_sale.controllers.main
 
+
 class website_sale(openerp.addons.website_sale.controllers.main.website_sale):
 
     @http.route(['/shop/payment'], type='http', auth="public", website=True)
@@ -21,3 +21,8 @@ class website_sale(openerp.addons.website_sale.controllers.main.website_sale):
 
         res = super(website_sale, self).payment(**post)
         return res
+
+    def order_lines_2_google_api(self, order_lines):
+        """ Transforms a list of order lines into a dict for google analytics """
+        order_lines_not_delivery = [line for line in order_lines if not line.is_delivery]
+        return super(website_sale, self).order_lines_2_google_api(order_lines_not_delivery)
index 7a80a0d..1f2ef69 100644 (file)
                         <li t-foreach="deliveries" t-as="delivery">
                             <label>
                                 <input t-att-value="delivery.id" type="radio" name="delivery_type"
-                                    t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False"/>
+                                    t-att-checked="order.carrier_id and order.carrier_id.id == delivery.id and 'checked' or False" 
+                                    t-att-disabled="delivery.available and '0' or '1'"/>
                                 <span t-field="delivery.name"/>
-                                <span class="badge" t-field="delivery.price"
-                                    t-field-options='{
-                                        "widget": "monetary",
-                                        "display_currency": "website.pricelist_id.currency_id"
-                                    }'/>
+                                <t t-if="delivery.available">
+                                    <span class="badge" t-field="delivery.price"
+                                        t-field-options='{
+                                            "widget": "monetary",
+                                            "display_currency": "website.pricelist_id.currency_id"
+                                        }'/>
+                                </t>
                             </label>
                         </li>
                     </ul>
index d868207..de768d2 100644 (file)
@@ -69,7 +69,7 @@ class ir_attachment(osv.osv):
     def _storage(self, cr, uid, context=None):
         return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file')
 
-    @tools.ormcache_context()
+    @tools.ormcache(skiparg=3)
     def _filestore(self, cr, uid, context=None):
         return tools.config.filestore(cr.dbname)
 
index dd06a19..712af17 100644 (file)
@@ -36,8 +36,8 @@ class ir_logging(osv.Model):
         'create_uid': fields.integer('Uid', readonly=True),  # Integer not m2o is intentionnal
         'name': fields.char('Name', required=True),
         'type': fields.selection(EXCEPTIONS_TYPE, string='Type', required=True, select=True),
-        'dbname': fields.char('Database Name'),
-        'level': fields.char('Level'),
+        'dbname': fields.char('Database Name', select=True),
+        'level': fields.char('Level', select=True),
         'message': fields.text('Message', required=True),
         'path': fields.char('Path', required=True),
         'func': fields.char('Function', required=True),
index aeded8a..cb5bda8 100644 (file)
@@ -415,6 +415,13 @@ class ir_mail_server(osv.osv):
         smtp_to_list = filter(None, tools.flatten(map(extract_rfc2822_addresses,[email_to, email_cc, email_bcc])))
         assert smtp_to_list, "At least one valid recipient address should be specified for outgoing emails (To/Cc/Bcc)"
 
+        x_forge_to = message['X-Forge-To']
+        if x_forge_to:
+            # `To:` header forged, e.g. for posting on mail.groups, to avoid confusion
+            del message['X-Forge-To']
+            del message['To'] # avoid multiple To: headers!
+            message['To'] = x_forge_to
+
         # Do not actually send emails in testing mode!
         if getattr(threading.currentThread(), 'testing', False):
             _test_logger.info("skip sending email in test mode")
index cbbfabe..b0c32cd 100644 (file)
@@ -132,10 +132,15 @@ class ir_model(osv.osv):
         ('obj_name_uniq', 'unique (model)', 'Each model must be unique!'),
     ]
 
-    def _search_display_name(self, operator, value):
-        # overridden to allow searching both on model name (model field) and
-        # model description (name field)
-        return ['|', ('model', operator, value), ('name', operator, value)]
+    # overridden to allow searching both on model name (model field)
+    # and model description (name field)
+    def _name_search(self, cr, uid, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
+        if args is None:
+            args = []
+        domain = args + ['|', ('model', operator, name), ('name', operator, name)]
+        return self.name_get(cr, name_get_uid or uid,
+                             super(ir_model, self).search(cr, uid, domain, limit=limit, context=context),
+                             context=context)
 
     def _drop_table(self, cr, uid, ids, context=None):
         for model in self.browse(cr, uid, ids, context):
@@ -806,20 +811,19 @@ class ir_model_data(osv.osv):
     """
     _name = 'ir.model.data'
     _order = 'module,model,name'
-    def _display_name_get(self, cr, uid, ids, prop, unknow_none, context=None):
+    def name_get(self, cr, uid, ids, context=None):
         result = {}
-        result2 = {}
+        result2 = []
         for res in self.browse(cr, uid, ids, context=context):
             if res.id:
                 result.setdefault(res.model, {})
                 result[res.model][res.res_id] = res.id
-            result2[res.id] = False
 
         for model in result:
             try:
                 r = dict(self.pool[model].name_get(cr, uid, result[model].keys(), context=context))
                 for key,val in result[model].items():
-                    result2[val] = r.get(key, False)
+                    result2.append((val, r.get(key, False)))
             except:
                 # some object have no valid name_get implemented, we accept this
                 pass
@@ -836,7 +840,6 @@ class ir_model_data(osv.osv):
                             help="External Key/Identifier that can be used for "
                                  "data integration with third-party systems"),
         'complete_name': fields.function(_complete_name_get, type='char', string='Complete ID'),
-        'display_name': fields.function(_display_name_get, type='char', string='Record Name'),
         'model': fields.char('Model Name', required=True, select=1),
         'module': fields.char('Module', required=True, select=1),
         'res_id': fields.integer('Record ID', select=1,
index 741f8d4..48d9c24 100644 (file)
@@ -289,7 +289,7 @@ class QWeb(orm.AbstractModel):
             result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
 
         if element.tail:
-            result += element.tail
+            result += element.tail.encode('utf-8')
 
         if isinstance(result, unicode):
             return result.encode('utf-8')
index 4720f60..f34f02a 100644 (file)
@@ -319,10 +319,10 @@ class ir_values(osv.osv):
                              (SELECT company_id from res_users where id = %%s)
                           )
                       %s
-                   ORDER BY v.user_id, u.company_id"""
+                   ORDER BY v.user_id, u.company_id, v.key2"""
         params = ('default', model, uid, uid)
         if condition:
-            query %= 'AND v.key2 = %s'
+            query %= 'AND (v.key2 = %s OR v.key2 IS NULL)'
             params += (condition[:200],)
         else:
             query %= 'AND v.key2 is NULL'
index be4642b..1ff3b30 100644 (file)
@@ -592,6 +592,19 @@ class module(osv.osv):
             'summary': terp.get('summary', ''),
         }
 
+
+    def create(self, cr, uid, vals, context=None):
+        new_id = super(module, self).create(cr, uid, vals, context=context)
+        module_metadata = {
+            'name': 'module_%s' % vals['name'],
+            'model': 'ir.module.module',
+            'module': 'base',
+            'res_id': new_id,
+            'noupdate': True,
+        }
+        self.pool['ir.model.data'].create(cr, uid, module_metadata)
+        return new_id
+
     # update the list of available packages
     def update_list(self, cr, uid, context=None):
         res = [0, 0]    # [update, add]
index 7b3f8ec..84529f0 100644 (file)
@@ -192,7 +192,7 @@ class res_partner_bank(osv.osv):
     def name_get(self, cr, uid, ids, context=None):
         if not len(ids):
             return []
-        bank_dicts = self.read(cr, uid, ids, context=context)
+        bank_dicts = self.read(cr, uid, ids, self.fields_get_keys(cr, uid, context=context), context=context)
         return self._prepare_name_get(cr, uid, bank_dicts, context=context)
 
     def onchange_company_id(self, cr, uid, ids, company_id, context=None):
index a864725..f1ddf4f 100644 (file)
@@ -226,11 +226,14 @@ class res_users(osv.osv):
     def _get_company(self,cr, uid, context=None, uid2=False):
         if not uid2:
             uid2 = uid
-        # use read method to compute default values to don't create browse record and fetch all fields
-        # browse crash for install or update module
-        user = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context)
-        company_id = user['company_id'] and user['company_id'][0] or False
-        return company_id
+        # Use read() to compute default company, and pass load=_classic_write to
+        # avoid useless name_get() calls. This will avoid prefetching fields
+        # while computing default values for new db columns, as the
+        # db backend may not be fully initialized yet.
+        user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'],
+                                                context=context, load='_classic_write')
+        comp_id = user_data['company_id']
+        return comp_id or False
 
     def _get_companies(self, cr, uid, context=None):
         c = self._get_company(cr, uid, context)
index 6886ce8..0760bd3 100644 (file)
@@ -897,7 +897,7 @@ BUG2 = """
               <th nowrap="" valign="BASELINE" align="RIGHT">Répondre
 
                 Ã &nbsp;: </th>
-              <td><a class="moz-txt-link-abbreviated" href="mailto:catchall@openerp.my.openerp.com">catchall@openerp.my.openerp.com</a></td>
+              <td><a class="moz-txt-link-abbreviated" href="mailto:catchall@mail.odoo.com">catchall@mail.odoo.com</a></td>
             </tr>
             <tr>
               <th nowrap="" valign="BASELINE" align="RIGHT">Pour&nbsp;:
index bb60911..08270ae 100644 (file)
@@ -604,16 +604,23 @@ class Field(object):
         """ return the null value for this field in the given environment """
         return False
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         """ convert `value` to the cache level in `env`; `value` may come from
             an assignment, or have the format of methods :meth:`BaseModel.read`
             or :meth:`BaseModel.write`
+
+            :param bool validate: when True, field-specific validation of
+                `value` will be performed
         """
         return value
 
     def convert_to_read(self, value, use_name_get=True):
         """ convert `value` from the cache to a value as returned by method
             :meth:`BaseModel.read`
+
+            :param bool use_name_get: when True, value's diplay name will
+                be computed using :meth:`BaseModel.name_get`, if relevant
+                for the field
         """
         return False if value is None else value
 
@@ -753,7 +760,7 @@ class Field(object):
                             try:
                                 values = target._convert_to_cache({
                                     f.name: source[f.name] for f in self.computed_fields
-                                })
+                                }, validate=False)
                             except MissingError as e:
                                 values = FailedValue(e)
                             target._cache.update(values)
@@ -845,17 +852,11 @@ class Field(object):
         return spec
 
 
-class Any(Field):
-    """ Field for arbitrary Python values. """
-    # Warning: no storage is defined for this type of field!
-    type = 'any'
-
-
 class Boolean(Field):
     """ Boolean field. """
     type = 'boolean'
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         return bool(value)
 
     def convert_to_export(self, value, env):
@@ -868,7 +869,7 @@ class Integer(Field):
     """ Integer field. """
     type = 'integer'
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         return int(value or 0)
 
     def convert_to_read(self, value, use_name_get=True):
@@ -908,7 +909,7 @@ class Float(Field):
     _column_digits = property(lambda self: not callable(self._digits) and self._digits)
     _column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         # apply rounding here, otherwise value in cache may be wrong!
         if self.digits:
             return float_round(float(value or 0.0), precision_digits=self.digits[1])
@@ -942,7 +943,7 @@ class Char(_String):
     _related_size = property(attrgetter('size'))
     _description_size = property(attrgetter('size'))
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         return bool(value) and ustr(value)[:self.size]
 
 
@@ -956,7 +957,7 @@ class Text(_String):
     """
     type = 'text'
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         return bool(value) and ustr(value)
 
 
@@ -964,7 +965,7 @@ class Html(_String):
     """ Html field. """
     type = 'html'
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         return bool(value) and html_sanitize(value)
 
 
@@ -1013,7 +1014,7 @@ class Date(Field):
         """ Convert a :class:`date` value into the format expected by the ORM. """
         return value.strftime(DATE_FORMAT)
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         if not value:
             return False
         if isinstance(value, basestring):
@@ -1078,7 +1079,7 @@ class Datetime(Field):
         """ Convert a :class:`datetime` value into the format expected by the ORM. """
         return value.strftime(DATETIME_FORMAT)
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         if not value:
             return False
         if isinstance(value, basestring):
@@ -1158,7 +1159,9 @@ class Selection(Field):
             selection = selection(env[self.model_name])
         return [value for value, _ in selection]
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
+        if not validate:
+            return value or False
         if value in self.get_values(env):
             return value
         elif not value:
@@ -1196,9 +1199,10 @@ class Reference(Selection):
 
     _column_size = property(attrgetter('size'))
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         if isinstance(value, BaseModel):
-            if value._name in self.get_values(env) and len(value) <= 1:
+            if ((not validate or value._name in self.get_values(env))
+                    and len(value) <= 1):
                 return value.with_env(env) or False
         elif isinstance(value, basestring):
             res_model, res_id = value.split(',')
@@ -1293,7 +1297,7 @@ class Many2one(_Relational):
         """ Update the cached value of `self` for `records` with `value`. """
         records._cache[self] = value
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         if isinstance(value, (NoneType, int)):
             return env[self.comodel_name].browse(value)
         if isinstance(value, BaseModel):
@@ -1346,7 +1350,7 @@ class _RelationalMulti(_Relational):
         for record in records:
             record._cache[self] = record[self.name] | value
 
-    def convert_to_cache(self, value, env):
+    def convert_to_cache(self, value, env, validate=True):
         if isinstance(value, BaseModel):
             if value._name == self.comodel_name:
                 return value.with_env(env)
index ecc8c72..8c86f2e 100644 (file)
@@ -295,6 +295,7 @@ class BaseModel(object):
     _sequence = None
     _description = None
     _needaction = False
+    _translate = True # set to False to disable translations export for this model
 
     # dict of {field:method}, with method returning the (name_get of records, {id: fold})
     # to include in the _read_group, if grouped on this field
@@ -500,9 +501,8 @@ class BaseModel(object):
         # this field 'id' must override any other column or field
         cls._add_field('id', fields.Id(automatic=True))
 
-        add('display_name', fields.Char(string='Name',
-            compute='_compute_display_name', inverse='_inverse_display_name',
-            search='_search_display_name', automatic=True))
+        add('display_name', fields.Char(string='Display Name', automatic=True,
+            compute='_compute_display_name'))
 
         if cls._log_access:
             add('create_uid', fields.Many2one('res.users', string='Created by', automatic=True))
@@ -591,7 +591,7 @@ class BaseModel(object):
 
             depends = dict(parent_class._depends)
             for m, fs in cls._depends.iteritems():
-                depends.setdefault(m, []).extend(fs)
+                depends[m] = depends.get(m, []) + fs
 
             old_constraints = parent_class._constraints
             new_constraints = cls._constraints
@@ -1648,30 +1648,8 @@ class BaseModel(object):
 
     @api.depends(lambda self: (self._rec_name,) if self._rec_name else ())
     def _compute_display_name(self):
-        name = self._rec_name
-        if name in self._fields:
-            convert = self._fields[name].convert_to_display_name
-            for record in self:
-                record.display_name = convert(record[name])
-        else:
-            for record in self:
-                record.display_name = "%s,%s" % (record._name, record.id)
-
-    def _inverse_display_name(self):
-        name = self._rec_name
-        if name in self._fields and not self._fields[name].relational:
-            for record in self:
-                record[name] = record.display_name
-        else:
-            _logger.warning("Cannot inverse field display_name on %s", self._name)
-
-    def _search_display_name(self, operator, value):
-        name = self._rec_name
-        if name in self._fields:
-            return [(name, operator, value)]
-        else:
-            _logger.warning("Cannot search field display_name on %s", self._name)
-            return [(0, '=', 1)]
+        for i, got_name in enumerate(self.name_get()):
+            self[i].display_name = got_name[1]
 
     @api.multi
     def name_get(self):
@@ -1682,11 +1660,15 @@ class BaseModel(object):
             :return: list of pairs ``(id, text_repr)`` for all records
         """
         result = []
-        for record in self:
-            try:
-                result.append((record.id, record.display_name))
-            except MissingError:
-                pass
+        name = self._rec_name
+        if name in self._fields:
+            convert = self._fields[name].convert_to_display_name
+            for record in self:
+                result.append((record.id, convert(record[name])))
+        else:
+            for record in self:
+                result.append((record.id, "%s,%s" % (record._name, record.id)))
+
         return result
 
     @api.model
@@ -1702,13 +1684,12 @@ class BaseModel(object):
             :rtype: tuple
             :return: the :meth:`~.name_get` pair value of the created record
         """
-        # Shortcut the inverse function of 'display_name' with self._rec_name.
-        # This is useful when self._rec_name is a required field: in that case,
-        # create() creates a record without the field, and inverse display_name
-        # afterwards.
-        field_name = self._rec_name if self._rec_name else 'display_name'
-        record = self.create({field_name: name})
-        return (record.id, record.display_name)
+        if self._rec_name:
+            record = self.create({self._rec_name: name})
+            return record.name_get()[0]
+        else:
+            _logger.warning("Cannot execute name_create, no _rec_name defined on %s", self._name)
+            return False
 
     @api.model
     def name_search(self, name='', args=None, operator='ilike', limit=100):
@@ -1734,8 +1715,10 @@ class BaseModel(object):
             :return: list of pairs ``(id, text_repr)`` for all matching records.
         """
         args = list(args or [])
-        if not (name == '' and operator == 'ilike'):
-            args += [('display_name', operator, name)]
+        if not self._rec_name:
+            _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name)
+        elif not (name == '' and operator == 'ilike'):
+            args += [(self._rec_name, operator, name)]
         return self.search(args, limit=limit).name_get()
 
     def _name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100, name_get_uid=None):
@@ -1743,8 +1726,10 @@ class BaseModel(object):
         # for the name_get part to solve some access rights issues
         args = list(args or [])
         # optimize out the default criterion of ``ilike ''`` that matches everything
-        if not (name == '' and operator == 'ilike'):
-            args += [('display_name', operator, name)]
+        if not self._rec_name:
+            _logger.warning("Cannot execute name_search, no _rec_name defined on %s", self._name)
+        elif not (name == '' and operator == 'ilike'):
+            args += [(self._rec_name, operator, name)]
         access_rights_uid = name_get_uid or user
         ids = self._search(cr, user, args, limit=limit, context=context, access_rights_uid=access_rights_uid)
         res = self.name_get(cr, access_rights_uid, ids, context)
@@ -2415,13 +2400,14 @@ class BaseModel(object):
             if column_name in defaults:
                 default = field.convert_to_write(defaults[column_name])
 
-        if default is not None:
-            _logger.debug("Table '%s': setting default value of new column %s",
-                          self._table, column_name)
-            ss = self._columns[column_name]._symbol_set
+        ss = self._columns[column_name]._symbol_set
+        store_default = ss[1](default)
+        if store_default is not None:
+            _logger.debug("Table '%s': setting default value of new column %s to %r",
+                          self._table, column_name, default)
             query = 'UPDATE "%s" SET "%s"=%s WHERE "%s" is NULL' % (
                 self._table, column_name, ss[0], column_name)
-            cr.execute(query, (ss[1](default),))
+            cr.execute(query, (store_default,))
             # this is a disgrace
             cr.commit()
 
@@ -3136,7 +3122,7 @@ class BaseModel(object):
         if field not in self._cache:
             for values in result:
                 record = self.browse(values.pop('id'))
-                record._cache.update(record._convert_to_cache(values))
+                record._cache.update(record._convert_to_cache(values, validate=False))
             if field not in self._cache:
                 e = AccessError("No value found for %s.%s" % (self, field.name))
                 self._cache[field] = FailedValue(e)
@@ -3215,7 +3201,7 @@ class BaseModel(object):
             # store result in cache for POST fields
             for vals in result:
                 record = self.browse(vals['id'])
-                record._cache.update(record._convert_to_cache(vals))
+                record._cache.update(record._convert_to_cache(vals, validate=False))
 
             # determine the fields that must be processed now
             fields_post = [f for f in field_names if not self._columns[f]._classic_write]
@@ -3256,7 +3242,7 @@ class BaseModel(object):
         # store result in cache
         for vals in result:
             record = self.browse(vals.pop('id'))
-            record._cache.update(record._convert_to_cache(vals))
+            record._cache.update(record._convert_to_cache(vals, validate=False))
 
         # store failed values in cache for the records that could not be read
         fetched = self.browse(ids)
@@ -3596,7 +3582,6 @@ class BaseModel(object):
         if not self:
             return True
 
-        cr, uid, context = self.env.args
         self._check_concurrency(self._ids)
         self.check_access_rights('write')
 
@@ -5098,11 +5083,11 @@ class BaseModel(object):
         context = dict(args[0] if args else self._context, **kwargs)
         return self.with_env(self.env(context=context))
 
-    def _convert_to_cache(self, values):
+    def _convert_to_cache(self, values, validate=True):
         """ Convert the `values` dictionary into cached values. """
         fields = self._fields
         return {
-            name: fields[name].convert_to_cache(value, self.env)
+            name: fields[name].convert_to_cache(value, self.env, validate=validate)
             for name, value in values.iteritems()
             if name in fields
         }
@@ -5555,7 +5540,7 @@ class BaseModel(object):
                 return
             if 'value' in method_res:
                 method_res['value'].pop('id', None)
-                self.update(self._convert_to_cache(method_res['value']))
+                self.update(self._convert_to_cache(method_res['value'], validate=False))
             if 'domain' in method_res:
                 result.setdefault('domain', {}).update(method_res['domain'])
             if 'warning' in method_res:
index 471827b..94d417c 100644 (file)
@@ -68,7 +68,7 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
     'STORE_NAME', 'GET_ITER', 'FOR_ITER', 'LIST_APPEND', 'DELETE_NAME',
     'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
     'MAKE_FUNCTION', 'SLICE+0', 'SLICE+1', 'SLICE+2', 'SLICE+3', 'BREAK_LOOP',
-    'CONTINUE_LOOP', 'RAISE_VARARGS',
+    'CONTINUE_LOOP', 'RAISE_VARARGS', 'YIELD_VALUE',
     # New in Python 2.7 - http://bugs.python.org/issue4715 :
     'JUMP_IF_FALSE_OR_POP', 'JUMP_IF_TRUE_OR_POP', 'POP_JUMP_IF_FALSE',
     'POP_JUMP_IF_TRUE', 'SETUP_EXCEPT', 'END_FINALLY',
index 2e5d540..25ee421 100644 (file)
@@ -39,7 +39,6 @@ from lxml import etree
 
 import config
 import misc
-from misc import UpdateableStr
 from misc import SKIPPED_ELEMENT_TYPES
 import osutil
 import openerp
@@ -669,6 +668,10 @@ def trans_generate(lang, modules, cr):
             _logger.error("Unable to find object %r", model)
             continue
 
+        if not registry[model]._translate:
+            # explicitly disabled
+            continue
+
         exists = registry[model].exists(cr, uid, res_id)
         if not exists:
             _logger.warning("Unable to find object %r with id %d", model, res_id)
@@ -689,7 +692,8 @@ def trans_generate(lang, modules, cr):
                 _logger.error("name error in %s: %s", xml_name, str(exc))
                 continue
             objmodel = registry.get(obj.model)
-            if objmodel is None or field_name not in objmodel._columns:
+            if (objmodel is None or field_name not in objmodel._columns
+                    or not objmodel._translate):
                 continue
             field_def = objmodel._columns[field_name]