[MERGE] forward port of branch 8.0 up to 92c7874
authorChristophe Simonis <chs@odoo.com>
Wed, 29 Oct 2014 18:45:09 +0000 (19:45 +0100)
committerChristophe Simonis <chs@odoo.com>
Wed, 29 Oct 2014 18:45:09 +0000 (19:45 +0100)
55 files changed:
addons/account/account_bank_statement.py
addons/account/account_invoice.py
addons/account/partner.py
addons/account/partner_view.xml
addons/account/report/account_partner_balance.py
addons/account_analytic_plans/account_analytic_plans.py
addons/board/static/src/css/dashboard.sass
addons/bus/static/src/js/bus.js
addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
addons/hr_timesheet_invoice/report/account_analytic_profit.py
addons/l10n_fr/views/report_l10nfrbilan.xml
addons/mrp/mrp.py
addons/product/pricelist.py
addons/product/product.py
addons/product/tests/__init__.py
addons/product/tests/test_pricelist.py [new file with mode: 0644]
addons/product_visible_discount/product_visible_discount.py
addons/purchase/views/report_purchaseorder.xml
addons/purchase/views/report_purchasequotation.xml
addons/sale/wizard/sale_line_invoice.py
addons/sale_margin/sale_margin.py
addons/stock/stock.py
addons/web/static/src/js/openerpframework.js
addons/web_calendar/static/src/js/web_calendar.js
addons/web_diagram/controllers/main.py
addons/web_graph/controllers/main.py
addons/website/models/website.py
addons/website/static/lib/jQuery.transfo.js
addons/website/static/src/js/website.snippets.editor.js
addons/website_blog/models/website_blog.py
addons/website_blog/views/website_blog_templates.xml
addons/website_sale/controllers/main.py
addons/website_sale/static/src/css/website_sale.css
addons/website_sale/static/src/css/website_sale.sass
addons/website_sale/static/src/js/website_sale.js
addons/website_sale/views/templates.xml
addons/website_sale_options/static/src/js/website_sale.js
addons/website_sale_options/views/templates.xml
doc/_themes/odoodoc/__init__.py
doc/_themes/odoodoc/layout.html
doc/_themes/odoodoc/static/app.js
doc/_themes/odoodoc/static/style.css
doc/_themes/odoodoc/static/style.less
doc/conf.py
doc/modules.rst
doc/modules/api_integration.rst [new file with mode: 0644]
openerp/addons/base/ir/ir_model.py
openerp/fields.py
openerp/models.py
openerp/modules/registry.py
openerp/osv/fields.py
openerp/service/common.py
openerp/sql_db.py
openerp/tools/func.py
openerp/tools/image.py

index 5426e8c..dab5cfd 100644 (file)
@@ -383,12 +383,17 @@ class account_bank_statement(osv.osv):
         return {'value': res}
 
     def unlink(self, cr, uid, ids, context=None):
+        statement_line_obj = self.pool['account.bank.statement.line']
         for item in self.browse(cr, uid, ids, context=context):
             if item.state != 'draft':
                 raise osv.except_osv(
                     _('Invalid Action!'), 
                     _('In order to delete a bank statement, you must first cancel it to delete related journal items.')
                 )
+            # Explicitly unlink bank statement lines
+            # so it will check that the related journal entries have
+            # been deleted first
+            statement_line_obj.unlink(cr, uid, [line.id for line in item.line_ids], context=context)
         return super(account_bank_statement, self).unlink(cr, uid, ids, context=context)
 
     def button_journal_entries(self, cr, uid, ids, context=None):
@@ -809,7 +814,7 @@ class account_bank_statement_line(osv.osv):
         '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'),
+        'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='restrict'),
         'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True),
         'partner_name': fields.char('Partner Name', help="This field is used to record the third party name when importing bank statement in electronic format, when the partner doesn't exist yet in the database (or cannot be found)."),
         'ref': fields.char('Reference'),
index 2069828..a51be33 100644 (file)
@@ -318,16 +318,26 @@ class account_invoice(models.Model):
     @api.model
     def fields_view_get(self, view_id=None, view_type=False, toolbar=False, submenu=False):
         context = self._context
+
+        def get_view_id(xid, name):
+            try:
+                return self.env['ir.model.data'].xmlid_to_res_id('account.' + xid, raise_if_not_found=True)
+            except ValueError:
+                try:
+                    return self.env['ir.ui.view'].search([('name', '=', name)], limit=1).id
+                except Exception:
+                    return False    # view not found
+
         if context.get('active_model') == 'res.partner' and context.get('active_ids'):
             partner = self.env['res.partner'].browse(context['active_ids'])[0]
             if not view_type:
-                view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.tree')]).id
+                view_id = get_view_id('invoice_tree', 'account.invoice.tree')
                 view_type = 'tree'
             elif view_type == 'form':
                 if partner.supplier and not partner.customer:
-                    view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.supplier.form')]).id
+                    view_id = get_view_id('invoice_supplier_form', 'account.invoice.supplier.form')
                 elif partner.customer and not partner.supplier:
-                    view_id = self.env['ir.ui.view'].search([('name', '=', 'account.invoice.form')]).id
+                    view_id = get_view_id('invoice_form', 'account.invoice.form')
 
         res = super(account_invoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
 
index e154c40..c0a1a69 100644 (file)
@@ -47,6 +47,16 @@ class account_fiscal_position(osv.osv):
         'active': True,
     }
 
+    def _check_country(self, cr, uid, ids, context=None):
+        obj = self.browse(cr, uid, ids[0], context=context)
+        if obj.country_id and obj.country_group_id:
+            return False
+        return True
+
+    _constraints = [
+        (_check_country, 'You can not select a country and a group of countries', ['country_id', 'country_group_id']),
+    ]
+
     @api.v7
     def map_tax(self, cr, uid, fposition_id, taxes, context=None):
         if not taxes:
@@ -69,12 +79,13 @@ class account_fiscal_position(osv.osv):
     def map_tax(self, taxes):
         result = self.env['account.tax'].browse()
         for tax in taxes:
+            tax_count = 0
             for t in self.tax_ids:
                 if t.tax_src_id == tax:
+                    tax_count += 1
                     if t.tax_dest_id:
                         result |= t.tax_dest_id
-                    break
-            else:
+            if not tax_count:
                 result |= tax
         return result
 
@@ -115,10 +126,17 @@ class account_fiscal_position(osv.osv):
         domain = [
             ('auto_apply', '=', True),
             '|', ('vat_required', '=', False), ('vat_required', '=', partner.vat_subjected),
-            '|', ('country_id', '=', None), ('country_id', '=', delivery.country_id.id),
-            '|', ('country_group_id', '=', None), ('country_group_id.country_ids', '=', delivery.country_id.id)
         ]
-        fiscal_position_ids = self.search(cr, uid, domain, context=context)
+
+        fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', delivery.country_id.id)], context=context, limit=1)
+        if fiscal_position_ids:
+            return fiscal_position_ids[0]
+
+        fiscal_position_ids = self.search(cr, uid, domain + [('country_group_id.country_ids', '=', delivery.country_id.id)], context=context, limit=1)
+        if fiscal_position_ids:
+            return fiscal_position_ids[0]
+
+        fiscal_position_ids = self.search(cr, uid, domain + [('country_id', '=', None), ('country_group_id', '=', None)], context=context, limit=1)
         if fiscal_position_ids:
             return fiscal_position_ids[0]
         return False
index 600a13e..11e09a6 100644 (file)
@@ -16,8 +16,8 @@
                         <field name="auto_apply"/>
                         <field name="sequence"/>
                         <field name="vat_required" attrs="{'readonly': [('auto_apply', '=', False)]}"/>
-                        <field name="country_id" attrs="{'readonly': ['|', ('country_group_id','!=',False), ('auto_apply', '=', False)]}" />
-                        <field name="country_group_id" attrs="{'readonly': ['|', ('country_id','!=',False), ('auto_apply', '=', False)]}"/>
+                        <field name="country_id"/>
+                        <field name="country_group_id"/>
                     </group>
                     <separator string="Taxes Mapping"/>
                     <field name="tax_ids" widget="one2many_list">
index ecd2875..51fff6c 100644 (file)
@@ -33,10 +33,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
         self.account_ids = []
         self.localcontext.update( {
             'time': time,
-            'lines': self.lines,
-            'sum_debit': self._sum_debit,
-            'sum_credit': self._sum_credit,
-            'sum_litige': self._sum_litige,
             'get_fiscalyear': self._get_fiscalyear,
             'get_journal': self._get_journal,
             'get_filter': self._get_filter,
@@ -70,7 +66,20 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
                     "WHERE a.type IN %s " \
                     "AND a.active", (self.ACCOUNT_TYPE,))
         self.account_ids = [a for (a,) in self.cr.fetchall()]
-        return super(partner_balance, self).set_context(objects, data, ids, report_type=report_type)
+        res = super(partner_balance, self).set_context(objects, data, ids, report_type=report_type)
+        lines = self.lines()
+        sum_debit = sum_credit = sum_litige = 0
+        for line in filter(lambda x: x['type'] == 3, lines):
+            sum_debit += line['debit'] or 0
+            sum_credit += line['credit'] or 0
+            sum_litige += line['enlitige'] or 0
+        self.localcontext.update({
+            'lines': lambda: lines,
+            'sum_debit': lambda: sum_debit,
+            'sum_credit': lambda: sum_credit,
+            'sum_litige': lambda: sum_litige,
+        })
+        return res
 
     def lines(self):
         move_state = ['draft','posted']
@@ -236,62 +245,6 @@ class partner_balance(report_sxw.rml_parse, common_report_header):
             i = i + 1
         return completearray
 
-    def _sum_debit(self):
-        move_state = ['draft','posted']
-        if self.target_move == 'posted':
-            move_state = ['posted']
-
-        if not self.ids:
-            return 0.0
-        self.cr.execute(
-                "SELECT sum(debit) " \
-                "FROM account_move_line AS l " \
-                "JOIN account_move am ON (am.id = l.move_id)" \
-                "WHERE l.account_id IN %s"  \
-                    "AND am.state IN %s" \
-                    "AND " + self.query + "",
-                    (tuple(self.account_ids), tuple(move_state)))
-        temp_res = float(self.cr.fetchone()[0] or 0.0)
-        return temp_res
-
-    def _sum_credit(self):
-        move_state = ['draft','posted']
-        if self.target_move == 'posted':
-            move_state = ['posted']
-
-        if not self.ids:
-            return 0.0
-        self.cr.execute(
-                "SELECT sum(credit) " \
-                "FROM account_move_line AS l " \
-                "JOIN account_move am ON (am.id = l.move_id)" \
-                "WHERE l.account_id IN %s" \
-                    "AND am.state IN %s" \
-                    "AND " + self.query + "",
-                    (tuple(self.account_ids), tuple(move_state)))
-        temp_res = float(self.cr.fetchone()[0] or 0.0)
-        return temp_res
-
-    def _sum_litige(self):
-        #gives the total of move lines with blocked boolean set to TRUE for the report selection
-        move_state = ['draft','posted']
-        if self.target_move == 'posted':
-            move_state = ['posted']
-
-        if not self.ids:
-            return 0.0
-        self.cr.execute(
-                "SELECT sum(debit-credit) " \
-                "FROM account_move_line AS l " \
-                "JOIN account_move am ON (am.id = l.move_id)" \
-                "WHERE l.account_id IN %s" \
-                    "AND am.state IN %s" \
-                    "AND " + self.query + " " \
-                    "AND l.blocked=TRUE ",
-                    (tuple(self.account_ids), tuple(move_state), ))
-        temp_res = float(self.cr.fetchone()[0] or 0.0)
-        return temp_res
-
     def _get_partners(self):
 
         if self.result_selection == 'customer':
index 02fc371..4bd3822 100644 (file)
@@ -392,7 +392,7 @@ class account_invoice(osv.osv):
                 if inv.type in ('in_invoice', 'in_refund'):
                     ref = inv.reference
                 else:
-                    ref = self._convert_ref(inv.number)
+                    ref = inv.number
                 obj_move_line = acct_ins_obj.browse(cr, uid, il['analytics_id'], context=context)
                 ctx = context.copy()
                 ctx.update({'date': inv.date_invoice})
index 1b3e023..e0abfd2 100644 (file)
@@ -1,3 +1,5 @@
+@charset "utf-8"
+
 @mixin radius($radius: 5px)
   -moz-border-radius: $radius
   -webkit-border-radius: $radius
index f85ccb8..393f101 100644 (file)
@@ -58,4 +58,4 @@
     // singleton
     bus.bus = new bus.Bus();
     return bus;
-})();
\ No newline at end of file
+})();
index 3c6df8e..3faf0a4 100644 (file)
@@ -7,6 +7,7 @@
             name="hr_timesheet_invoice.report_analyticprofit"
             file="hr_timesheet_invoice.report_analyticprofit"
             report_type="qweb-pdf"
+            menu="False"
             string="Timesheet Profit"
         />
     </data>
index 3f7108f..15116ee 100644 (file)
@@ -39,13 +39,15 @@ class account_analytic_profit(report_sxw.rml_parse):
         return user_obj.browse(self.cr, self.uid, ids)
 
     def _journal_ids(self, form, user_id):
+        if isinstance(user_id, (int, long)):
+            user_id = [user_id]
         line_obj = self.pool['account.analytic.line']
         journal_obj = self.pool['account.analytic.journal']
         line_ids=line_obj.search(self.cr, self.uid, [
             ('date', '>=', form['date_from']),
             ('date', '<=', form['date_to']),
             ('journal_id', 'in', form['journal_ids'][0][2]),
-            ('user_id', '=', user_id),
+            ('user_id', 'in', user_id),
             ])
         ids=list(set([b.journal_id.id for b in line_obj.browse(self.cr, self.uid, line_ids)]))
         return journal_obj.browse(self.cr, self.uid, ids)
index 5b7335e..f5c697c 100644 (file)
@@ -20,6 +20,7 @@
                         </p>
                     </div>
                 </div>
+                <h3>Actif</h3>
                 <table class="table table-condensed">
                     <thead>
                         <tr>
                         </td>
                     </tr>
                 </table>
+
+                <h3>Passif</h3>
+                <table class="table table-condensed">
+                    <tbody>
+                        <tr>
+                            <td><strong>CAPITAUX PROPRES</strong></td>
+                            <td></td>
+                        </tr>
+                        <tr>
+                            <td>Capital [dont vers&#xE9;...]</td>
+                            <td><span t-esc="bpvar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Primes d'&#xE9;mission, de fusion, d'apport</td>
+                            <td><span t-esc="bpvar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>&#xC9;carts de r&#xE9;&#xE9;valuation</td>
+                            <td><span t-esc="bpvar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>&#xC9;cart d'&#xE9;quivalence</td>
+                            <td><span t-esc="bpvar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td><strong>R&#xC9;SERVES</strong></td>
+                            <td></td>
+                        </tr>
+                        <tr>
+                            <td>R&#xE9;serve l&#xE9;gale</td>
+                            <td><span t-esc="bpvar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>R&#xE9;serves statutaires ou contractuelles</td>
+                            <td><span t-esc="bpvar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>R&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr>
+                        <tr>
+                            <td>Autres r&#xE9;serves</td>
+                            <td><span t-esc="bpvar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Report &#xE0; nouveau</td>
+                            <td><span t-esc="bpvar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td><strong>R&#xC9;SULTAT DE L'EXERCICE [b&#xE9;n&#xE9;fice ou perte]</strong></td>
+                            <td><span t-esc="bpvar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Subventions d'investissement</td>
+                            <td><span t-esc="bpvar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Provisions r&#xE9;glement&#xE9;es</td>
+                            <td><span t-esc="bpvar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td class="text-right"><strong>TOTAL I</strong></td>
+                            <td><span t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td><strong>PROVISIONS</strong></td>
+                            <td></td>
+                        </tr>
+                        <tr>
+                            <td>Provisions pour risques</td>
+                            <td><span t-esc="bpvar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Provisions pour charges</td>
+                            <td><span t-esc="bpvar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td class="text-right"><strong>TOTAL II</strong></td>
+                            <td><span t-esc="pt2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td><strong>DETTES</strong></td>
+                            <td></td>
+                        </tr>
+                        <tr>
+                            <td>Emprunts obligataires convertibles</td>
+                            <td><span t-esc="bpvar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Autres emprunts obligataires</td>
+                            <td><span t-esc="bpvar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Emprunts et dettes aupr&#xE8;s des &#xE9;tablissements de cr&#xE9;dit</td>
+                            <td><span t-esc="bpvar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Emprunts et dettes financi&#xE8;res diverses</td>
+                            <td><span t-esc="bpvar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Avances et acomptes re&#xE7;us sur commandes en cours</td>
+                            <td><span t-esc="bpvar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Dettes fournisseurs et comptes rattach&#xE9;s </td>
+                            <td><span t-esc="bpvar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Dettes fiscales et sociales</td>
+                            <td><span t-esc="bpvar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Dettes sur immobilisations et comptes rattach&#xE9;s</td>
+                            <td><span t-esc="bpvar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Autres dettes</td>
+                            <td><span t-esc="bpvar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Instruments de tr&#xE9;sorerie</td>
+                            <td><span t-esc="bpvar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>Produits constat&#xE9;s d'avance</td>
+                            <td><span t-esc="bpvar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td class="text-right"><strong>TOTAL III</strong></td>
+                            <td><span t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>&#xC9;carts de conversion passif <font face="Times-Roman">( IV )</font></td>
+                            <td><span t-esc="bpvar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td><strong>TOTAL G&#xC9;N&#xC9;RAL (I + II + III + IV)</strong></td>
+                            <td><span t-esc="passif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                        <tr>
+                            <td>&amp;nbsp;</td>
+                            <td>&amp;nbsp;</td>
+                        </tr>
+                        <tr>
+                            <td><strong>ACTIF - PASSIF</strong></td>
+                            <td><span t-esc="round(actif-passif,2)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+                        </tr>
+                    </tbody>
+                </table>
             </div>
         </t>
     </t>
index 856b56d..36b8a34 100644 (file)
@@ -931,9 +931,19 @@ class mrp_production(osv.osv):
                 lot_id = False
                 if wiz:
                     lot_id = wiz.lot_id.id
-                new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], (subproduct_factor * production_qty_uom),
+                qty = min(subproduct_factor * production_qty_uom, produce_product.product_qty) #Needed when producing more than maximum quantity
+                new_moves = stock_mov_obj.action_consume(cr, uid, [produce_product.id], qty,
                                                          location_id=produce_product.location_id.id, restrict_lot_id=lot_id, context=context)
                 stock_mov_obj.write(cr, uid, new_moves, {'production_id': production_id}, context=context)
+                remaining_qty = subproduct_factor * production_qty_uom - qty
+                if remaining_qty: # In case you need to make more than planned
+                    #consumed more in wizard than previously planned
+                    extra_move_id = stock_mov_obj.copy(cr, uid, produce_product.id, default={'state': 'confirmed',
+                                                                                             'product_uom_qty': remaining_qty,
+                                                                                             'production_id': production_id}, context=context)
+                    if extra_move_id:
+                        stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
+
                 if produce_product.product_id.id == production.product_id.id:
                     main_production_move = produce_product.id
 
@@ -952,13 +962,16 @@ class mrp_production(osv.osv):
                     if consume['product_id'] != raw_material_line.product_id.id:
                         continue
                     consumed_qty = min(remaining_qty, raw_material_line.product_qty)
-                    stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id, restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
+                    stock_mov_obj.action_consume(cr, uid, [raw_material_line.id], consumed_qty, raw_material_line.location_id.id,
+                                                 restrict_lot_id=consume['lot_id'], consumed_for=main_production_move, context=context)
                     remaining_qty -= consumed_qty
                 if remaining_qty:
                     #consumed more in wizard than previously planned
                     product = self.pool.get('product.product').browse(cr, uid, consume['product_id'], context=context)
                     extra_move_id = self._make_consume_line_from_data(cr, uid, production, product, product.uom_id.id, remaining_qty, False, 0, context=context)
                     if extra_move_id:
+                        if consume['lot_id']:
+                            stock_mov_obj.write(cr, uid, [extra_move_id], {'restrict_lot_id': consume['lot_id']}, context=context)
                         stock_mov_obj.action_done(cr, uid, [extra_move_id], context=context)
 
         self.message_post(cr, uid, production_id, body=_("%s produced") % self._description, context=context)
index 07efc90..29a0fa0 100644 (file)
@@ -315,7 +315,12 @@ class product_pricelist(osv.osv):
                     price = price * (1.0+(rule.price_discount or 0.0))
                     if rule.price_round:
                         price = tools.float_round(price, precision_rounding=rule.price_round)
-                    price += (rule.price_surcharge or 0.0)
+                    if context.get('uom'):
+                        # compute price_surcharge based on reference uom
+                        factor = product_uom_obj.browse(cr, uid, context.get('uom'), context=context).factor
+                    else:
+                        factor = 1.0
+                    price += (rule.price_surcharge or 0.0) / factor
                     if rule.price_min_margin:
                         price = max(price, price_limit+rule.price_min_margin)
                     if rule.price_max_margin:
index 10bc3da..43a152d 100644 (file)
@@ -557,7 +557,7 @@ class product_template(osv.osv):
 
         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the product without removing it."),
         'color': fields.integer('Color Index'),
-        'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Only one product variant'),
+        'is_product_variant': fields.function( _is_product_variant, type='boolean', string='Is product variant'),
 
         'attribute_line_ids': fields.one2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes'),
         'product_variant_ids': fields.one2many('product.product', 'product_tmpl_id', 'Products', required=True),
@@ -918,6 +918,7 @@ class product_product(osv.osv):
             'product.product': (lambda self, cr, uid, ids, c=None: ids, [], 10),
         }, select=True),
         'attribute_value_ids': fields.many2many('product.attribute.value', id1='prod_id', id2='att_id', string='Attributes', readonly=True, ondelete='restrict'),
+        'is_product_variant': fields.function( _is_product_variant_impl, type='boolean', string='Is product variant'),
 
         # image: all image fields are base64 encoded and PIL-supported
         'image_variant': fields.binary("Variant Image",
@@ -1110,6 +1111,34 @@ class product_product(osv.osv):
     def need_procurement(self, cr, uid, ids, context=None):
         return False
 
+    def _compute_uos_qty(self, cr, uid, ids, uom, qty, uos, context=None):
+        '''
+        Computes product's invoicing quantity in UoS from quantity in UoM.
+        Takes into account the
+        :param uom: Source unit
+        :param qty: Source quantity
+        :param uos: Target UoS unit.
+        '''
+        if not uom or not qty or not uos:
+            return qty
+        uom_obj = self.pool['product.uom']
+        product_id = ids[0] if isinstance(ids, (list, tuple)) else ids
+        product = self.browse(cr, uid, product_id, context=context)
+        if isinstance(uos, (int, long)):
+            uos = uom_obj.browse(cr, uid, uos, context=context)
+        if isinstance(uom, (int, long)):
+            uom = uom_obj.browse(cr, uid, uom, context=context)
+        if product.uos_id:  # Product has UoS defined
+            # We cannot convert directly between units even if the units are of the same category
+            # as we need to apply the conversion coefficient which is valid only between quantities
+            # in product's default UoM/UoS
+            qty_default_uom = uom_obj._compute_qty_obj(cr, uid, uom, qty, product.uom_id)  # qty in product's default UoM
+            qty_default_uos = qty_default_uom * product.uos_coeff
+            return uom_obj._compute_qty_obj(cr, uid, product.uos_id, qty_default_uos, uos)
+        else:
+            return uom_obj._compute_qty_obj(cr, uid, uom, qty, uos)
+
+
 
 class product_packaging(osv.osv):
     _name = "product.packaging"
index 850189a..4a1ce43 100644 (file)
@@ -1,5 +1,6 @@
-from . import test_uom
+from . import test_uom, test_pricelist
 
 fast_suite = [
        test_uom,
+       test_pricelist
 ]
diff --git a/addons/product/tests/test_pricelist.py b/addons/product/tests/test_pricelist.py
new file mode 100644 (file)
index 0000000..4e61cb1
--- /dev/null
@@ -0,0 +1,70 @@
+from openerp.tests.common import TransactionCase
+
+class TestPricelist(TransactionCase):
+    """Tests for unit of measure conversion"""
+
+    def setUp(self):
+        super(TestPricelist, self).setUp()
+        cr, uid, context = self.cr, self.uid, {}
+        self.ir_model_data = self.registry('ir.model.data')
+        self.product_product = self.registry('product.product')
+        self.product_pricelist = self.registry('product.pricelist')
+        self.uom = self.registry('product.uom')
+
+        self.usb_adapter_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_48')[1]
+        self.datacard_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_product_46')[1]
+        self.unit_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_uom_unit')[1]
+        self.dozen_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'product_uom_dozen')[1]
+
+        self.public_pricelist_id = self.ir_model_data.get_object_reference(cr, uid, 'product', 'list0')[1]
+        self.sale_pricelist_id = self.product_pricelist.create(cr, uid, {
+            'name': 'Sale pricelist',
+            'type': 'sale',
+            'version_id': [(0, 0, {
+                'name': 'v1.0',
+                'items_id': [(0, 0, {
+                    'name': 'Discount 10%',
+                    'base': 1, # based on public price
+                    'price_discount': -0.1,
+                    'product_id': self.usb_adapter_id
+                }), (0, 0, {
+                    'name': 'Discount -0.5',
+                    'base': 1, # based on public price
+                    'price_surcharge': -0.5,
+                    'product_id': self.datacard_id
+                })]
+            })]
+        }, context=context)
+
+    def test_10_discount(self):
+        # Make sure the price using a pricelist is the same than without after
+        # applying the computation manually
+        cr, uid, context = self.cr, self.uid, {}
+
+        public_context = dict(context, pricelist=self.public_pricelist_id)
+        pricelist_context = dict(context, pricelist=self.sale_pricelist_id)
+
+        usb_adapter_without_pricelist = self.product_product.browse(cr, uid, self.usb_adapter_id, context=public_context)
+        usb_adapter_with_pricelist = self.product_product.browse(cr, uid, self.usb_adapter_id, context=pricelist_context)
+        self.assertEqual(usb_adapter_with_pricelist.price, usb_adapter_without_pricelist.price*0.9)
+
+        datacard_without_pricelist = self.product_product.browse(cr, uid, self.datacard_id, context=public_context)
+        datacard_with_pricelist = self.product_product.browse(cr, uid, self.datacard_id, context=pricelist_context)
+        self.assertEqual(datacard_with_pricelist.price, datacard_without_pricelist.price-0.5)
+
+        # Make sure that changing the unit of measure does not break the unit
+        # price (after converting)
+        unit_context = dict(context,
+            pricelist=self.sale_pricelist_id,
+            uom=self.unit_id)
+        dozen_context = dict(context,
+            pricelist=self.sale_pricelist_id,
+            uom=self.dozen_id)
+
+        usb_adapter_unit = self.product_product.browse(cr, uid, self.usb_adapter_id, context=unit_context)
+        usb_adapter_dozen = self.product_product.browse(cr, uid, self.usb_adapter_id, context=dozen_context)
+        self.assertAlmostEqual(usb_adapter_unit.price*12, usb_adapter_dozen.price)
+
+        datacard_unit = self.product_product.browse(cr, uid, self.datacard_id, context=unit_context)
+        datacard_dozen = self.product_product.browse(cr, uid, self.datacard_id, context=dozen_context)
+        self.assertAlmostEqual(datacard_unit.price*12, datacard_dozen.price)
index c27bb86..a654cd1 100644 (file)
@@ -43,6 +43,7 @@ class sale_order_line(osv.osv):
             fiscal_position=False, flag=False, context=None):
 
         def get_real_price(res_dict, product_id, qty, uom, pricelist):
+            """Retrieve the price before applying the pricelist"""
             item_obj = self.pool.get('product.pricelist.item')
             price_type_obj = self.pool.get('product.price.type')
             product_obj = self.pool.get('product.product')
@@ -58,9 +59,8 @@ class sale_order_line(osv.osv):
 
             factor = 1.0
             if uom and uom != product.uom_id.id:
-                product_uom_obj = self.pool.get('product.uom')
-                uom_data = product_uom_obj.browse(cr, uid,  product.uom_id.id)
-                factor = uom_data.factor
+                # the unit price is in a different uom
+                factor = self.pool['product.uom']._compute_qty(cr, uid, uom, 1.0, product.uom_id.id)
             return product_read[field_name] * factor
 
 
@@ -77,7 +77,7 @@ class sale_order_line(osv.osv):
                 price=result['price_unit']
             else:
                 return res
-
+            uom = result.get('product_uom', uom)
             product = product_obj.browse(cr, uid, product, context)
             pricelist_context = dict(context, uom=uom, date=date_order)
             list_price = pricelist_obj.price_rule_get(cr, uid, [pricelist],
index 91486c0..fd199a6 100644 (file)
 <?xml version="1.0" encoding="utf-8"?>
 <openerp>
 <data>
-<template id="report_purchaseorder">
-    <t t-call="report.html_container">
-        <t t-foreach="docs" t-as="o">
-            <t t-call="report.external_layout">
-                <div class="page">
-                    <div class="oe_structure"/>
-                    <div class="row">
-                        <div class="col-xs-6">
-                            Shipping address :<br/>
-                            <div t-if="o.dest_address_id">
-                                <div t-field="o.dest_address_id" 
-                                    t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                                <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
-                            </div>
+<template id="report_purchaseorder_document">
+    <t t-call="report.external_layout">
+        <div class="page">
+            <div class="oe_structure"/>
+            <div class="row">
+                <div class="col-xs-6">
+                    Shipping address :<br/>
+                    <div t-if="o.dest_address_id">
+                        <div t-field="o.dest_address_id"
+                            t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                        <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
+                    </div>
 
-                            <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
-                                <span t-field="o.picking_type_id.warehouse_id.name"/>
-                                <div t-field="o.picking_type_id.warehouse_id.partner_id" 
-                                    t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                                <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
-                            </div>
-                        </div>
-                        <div class="col-xs-5 col-xs-offset-1">
-                            <div t-field="o.partner_id" 
-                                t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                        </div>
+                    <div t-if="o.picking_type_id and o.picking_type_id.warehouse_id">
+                        <span t-field="o.picking_type_id.warehouse_id.name"/>
+                        <div t-field="o.picking_type_id.warehouse_id.partner_id"
+                            t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                        <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
                     </div>
+                </div>
+                <div class="col-xs-5 col-xs-offset-1">
+                    <div t-field="o.partner_id"
+                        t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                </div>
+            </div>
 
-                    <h2 t-if="o.state != 'draft'">Purchase Order Confirmation N°<span t-field="o.name"/></h2>
-                    <h2 t-if="o.state == 'draft'">Request for Quotation N°<span t-field="o.name"/></h2>
+            <h2 t-if="o.state != 'draft'">Purchase Order Confirmation N°<span t-field="o.name"/></h2>
+            <h2 t-if="o.state == 'draft'">Request for Quotation N°<span t-field="o.name"/></h2>
 
-                    <div class="row mt32 mb32">
-                        <div t-if="o.name" class="col-xs-3">
-                            <strong>Our Order Reference:</strong>
-                            <p t-field="o.name"/>
-                        </div>
-                        <div t-if="o.partner_ref" class="col-xs-3">
-                            <strong>Your Order Reference</strong>
-                            <p t-field="o.partner_ref"/>
-                        </div>
-                        <div t-if="o.date_order" class="col-xs-3">
-                            <strong>Order Date:</strong>
-                            <p t-field="o.date_order"/>
-                        </div>
-                        <div t-if="o.validator" class="col-xs-3">
-                            <strong>Validated By:</strong>
-                            <p t-field="o.validator"/>
-                        </div>
-                    </div>
+            <div class="row mt32 mb32">
+                <div t-if="o.name" class="col-xs-3">
+                    <strong>Our Order Reference:</strong>
+                    <p t-field="o.name"/>
+                </div>
+                <div t-if="o.partner_ref" class="col-xs-3">
+                    <strong>Your Order Reference</strong>
+                    <p t-field="o.partner_ref"/>
+                </div>
+                <div t-if="o.date_order" class="col-xs-3">
+                    <strong>Order Date:</strong>
+                    <p t-field="o.date_order"/>
+                </div>
+                <div t-if="o.validator" class="col-xs-3">
+                    <strong>Validated By:</strong>
+                    <p t-field="o.validator"/>
+                </div>
+            </div>
 
+            <table class="table table-condensed">
+                <thead>
+                    <tr>
+                        <th><strong>Description</strong></th>
+                        <th><strong>Taxes</strong></th>
+                        <th class="text-center"><strong>Date Req.</strong></th>
+                        <th class="text-right"><strong>Qty</strong></th>
+                        <th class="text-right"><strong>Unit Price</strong></th>
+                        <th class="text-right"><strong>Net Price</strong></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr t-foreach="o.order_line" t-as="line">
+                        <td>
+                            <span t-field="line.name"/>
+                        </td>
+                        <td>
+                            <span t-esc="', '.join(map(lambda x: x.name, line.taxes_id))"/>
+                        </td>
+                        <td class="text-center">
+                            <span t-field="line.date_planned"/>
+                        </td>
+                        <td class="text-right">
+                            <span t-field="line.product_qty"/>
+                            <span t-field="line.product_uom.name" groups="product.group_uom"/>
+                        </td>
+                        <td class="text-right">
+                            <span t-field="line.price_unit"/>
+                        </td>
+                        <td class="text-right">
+                            <span t-field="line.price_subtotal"
+                                t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+
+            <div class="row">
+                <div class="col-xs-4 pull-right">
                     <table class="table table-condensed">
-                        <thead>
-                            <tr>
-                                <th><strong>Description</strong></th>
-                                <th><strong>Taxes</strong></th>
-                                <th class="text-center"><strong>Date Req.</strong></th>
-                                <th class="text-right"><strong>Qty</strong></th>
-                                <th class="text-right"><strong>Unit Price</strong></th>
-                                <th class="text-right"><strong>Net Price</strong></th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr t-foreach="o.order_line" t-as="line">
-                                <td>
-                                    <span t-field="line.name"/>
-                                </td>
-                                <td>
-                                    <span t-esc="', '.join(map(lambda x: x.name, line.taxes_id))"/>
-                                </td>
-                                <td class="text-center">
-                                    <span t-field="line.date_planned"/>
-                                </td>
-                                <td class="text-right">
-                                    <span t-field="line.product_qty"/>
-                                    <span t-field="line.product_uom.name" groups="product.group_uom"/>
-                                </td>
-                                <td class="text-right">
-                                    <span t-field="line.price_unit"/>
-                                </td>
-                                <td class="text-right">
-                                    <span t-field="line.price_subtotal"
-                                        t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
-                                </td>
-                            </tr>
-                        </tbody>
+                        <tr class="border-black">
+                            <td><strong>Total Without Taxes</strong></td>
+                            <td class="text-right">
+                                <span t-field="o.amount_untaxed"
+                                    t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Taxes</td>
+                            <td class="text-right">
+                                <span t-field="o.amount_tax"
+                                    t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+                            </td>
+                        </tr>
+                        <tr class="border-black">
+                            <td><strong>Total</strong></td>
+                            <td class="text-right">
+                                <span t-field="o.amount_total"
+                                    t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
+                            </td>
+                        </tr>
                     </table>
-
-                    <div class="row">
-                        <div class="col-xs-4 pull-right">
-                            <table class="table table-condensed">
-                                <tr class="border-black">
-                                    <td><strong>Total Without Taxes</strong></td>
-                                    <td class="text-right">
-                                        <span t-field="o.amount_untaxed"
-                                            t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
-                                    </td>
-                                </tr>
-                                <tr>
-                                    <td>Taxes</td>
-                                    <td class="text-right">
-                                        <span t-field="o.amount_tax"
-                                            t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
-                                    </td>
-                                </tr>
-                                <tr class="border-black">
-                                    <td><strong>Total</strong></td>
-                                    <td class="text-right">
-                                        <span t-field="o.amount_total"
-                                            t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
-                                    </td>
-                                </tr>
-                            </table>
-                        </div>
-                    </div>
-                
-                    <p t-field="o.notes"/>
-                    <div class="oe_structure"/>
                 </div>
-            </t>
+            </div>
+
+            <p t-field="o.notes"/>
+            <div class="oe_structure"/>
+        </div>
+    </t>
+</template>
+
+<template id="report_purchaseorder">
+    <t t-call="report.html_container">
+        <t t-foreach="doc_ids" t-as="doc_id">
+            <t t-raw="translate_doc(doc_id, doc_model, 'partner_id.lang', 'purchase.report_purchaseorder_document')"/>
         </t>
     </t>
 </template>
index 6abd64b..60f1081 100644 (file)
@@ -1,67 +1,71 @@
 <?xml version="1.0" encoding="utf-8"?>
 <openerp>
 <data>
-<template id="report_purchasequotation">
-    <t t-call="report.html_container">
-        <t t-foreach="docs" t-as="o">
-            <t t-call="report.external_layout">
-                <div class="page">
-                    <div class="oe_structure"/>
+<template id="report_purchasequotation_document">
+    <t t-call="report.external_layout">
+        <div class="page">
+            <div class="oe_structure"/>
 
-                    <div class="row mt32 mb32">
-                        <div class="col-xs-6">
-                            Shipping address :<br/>
-                            <div t-if="o.dest_address_id">
-                                <div t-field="o.dest_address_id" 
-                                    t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                                <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
-                            </div>
-                            <div t-if="o.picking_type_id.warehouse_id">
-                                <span t-field="o.picking_type_id.warehouse_id.name"/>
-                                <div t-field="o.picking_type_id.warehouse_id.partner_id" 
-                                    t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                                <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
-                            </div>
-                        </div>
-                        <div class="col-xs-5 col-xs-offset-1">
-                            <div t-field="o.partner_id" 
-                                t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
-                        </div>
+            <div class="row mt32 mb32">
+                <div class="col-xs-6">
+                    Shipping address :<br/>
+                    <div t-if="o.dest_address_id">
+                        <div t-field="o.dest_address_id"
+                            t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                        <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
+                    </div>
+                    <div t-if="o.picking_type_id.warehouse_id">
+                        <span t-field="o.picking_type_id.warehouse_id.name"/>
+                        <div t-field="o.picking_type_id.warehouse_id.partner_id"
+                            t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                        <p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
                     </div>
+                </div>
+                <div class="col-xs-5 col-xs-offset-1">
+                    <div t-field="o.partner_id"
+                        t-field-options='{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}'/>
+                </div>
+            </div>
 
-                    <h2>Request for Quotation <span t-field="o.name"/></h2>
+            <h2>Request for Quotation <span t-field="o.name"/></h2>
 
-                    <table class="table table-condensed">
-                        <thead>
-                            <tr>
-                                <th><strong>Description</strong></th>
-                                <th class="text-center"><strong>Expected Date</strong></th>
-                                <th class="text-right"><strong>Qty</strong></th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <tr t-foreach="o.order_line" t-as="order_line">
-                                <td>
-                                    <span t-field="order_line.name"/>
-                                </td>
-                                <td class="text-center">
-                                    <span t-field="order_line.date_planned"/>
-                                </td>
-                                <td class="text-right">
-                                    <span t-field="order_line.product_qty"/>
-                                    <span t-field="order_line.product_uom" groups="product.group_uom"/>
-                                </td>
-                            </tr>
-                        </tbody>
-                    </table>
+            <table class="table table-condensed">
+                <thead>
+                    <tr>
+                        <th><strong>Description</strong></th>
+                        <th class="text-center"><strong>Expected Date</strong></th>
+                        <th class="text-right"><strong>Qty</strong></th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <tr t-foreach="o.order_line" t-as="order_line">
+                        <td>
+                            <span t-field="order_line.name"/>
+                        </td>
+                        <td class="text-center">
+                            <span t-field="order_line.date_planned"/>
+                        </td>
+                        <td class="text-right">
+                            <span t-field="order_line.product_qty"/>
+                            <span t-field="order_line.product_uom" groups="product.group_uom"/>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
 
-                    <p t-field="o.notes"/>
-                    <span>Regards,</span>
-                    <span t-field="user.signature"/>
+            <p t-field="o.notes"/>
+            <span>Regards,</span>
+            <span t-field="user.signature"/>
 
-                    <div class="oe_structure"/>
-                </div>
-            </t>
+            <div class="oe_structure"/>
+        </div>
+    </t>
+</template>
+
+<template id="report_purchasequotation">
+    <t t-call="report.html_container">
+        <t t-foreach="doc_ids" t-as="doc_id">
+            <t t-raw="translate_doc(doc_id, doc_model, 'partner_id.lang', 'purchase.report_purchasequotation_document')"/>
         </t>
     </t>
 </template>
index 6666f20..b353287 100644 (file)
@@ -101,7 +101,8 @@ class sale_order_line_make_invoice(osv.osv_memory):
                     flag = False
                     break
             if flag:
-                workflow.trg_validate(uid, 'sale.order', order.id, 'manual_invoice', cr)
+                line.order_id.write({'state': 'progress'})
+                workflow.trg_validate(uid, 'sale.order', order.id, 'all_lines', cr)
 
         if not invoices:
             raise osv.except_osv(_('Warning!'), _('Invoice cannot be created for this Sales Order Line due to one of the following reasons:\n1.The state of this sales order line is either "draft" or "cancel"!\n2.The Sales Order Line is Invoiced!'))
index 5cbb178..b5c1821 100644 (file)
@@ -36,7 +36,11 @@ class sale_order_line(osv.osv):
         frm_cur = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
         to_cur = self.pool.get('product.pricelist').browse(cr, uid, [pricelist])[0].currency_id.id
         if product:
-            purchase_price = self.pool.get('product.product').browse(cr, uid, product).standard_price
+            product = self.pool['product.product'].browse(cr, uid, product, context=context)
+            purchase_price = product.standard_price
+            to_uom = res.get('product_uom', uom)
+            if to_uom != product.uom_id.id:
+                purchase_price = self.pool['product.uom']._compute_price(cr, uid, product.uom_id.id, purchase_price, to_uom)
             ctx = context.copy()
             ctx['date'] = date_order
             price = self.pool.get('res.currency').compute(cr, uid, frm_cur, to_cur, purchase_price, round=False, context=ctx)
index ccbd78b..ecaed01 100644 (file)
@@ -1267,7 +1267,7 @@ class stock_picking(osv.osv):
             'product_id': product.id,
             'product_uom': product.uom_id.id,
             'product_uom_qty': remaining_qty,
-            'name': _('Extra Move: ') + op.product_id.name,
+            'name': _('Extra Move: ') + product.name,
             'state': 'draft',
             }
         return res
index b112783..fb5ae0d 100644 (file)
@@ -1068,11 +1068,11 @@ openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, {
     },
     check_session_id: function() {
         var self = this;
-        if (this.avoid_recursion || self.use_cors)
+        if (this.avoid_recursion)
             return $.when();
         if (this.session_id)
             return $.when(); // we already have the session id
-        if (this.override_session || ! this.origin_server) {
+        if (!this.use_cors && (this.override_session || ! this.origin_server)) {
             // If we don't use the origin server we consider we should always create a new session.
             // Even if some browsers could support cookies when using jsonp that behavior is
             // not consistent and the browser creators are tending to removing that feature.
index 84e43c2..e3faabd 100644 (file)
@@ -832,7 +832,7 @@ openerp.web_calendar = function(instance) {
             }
             else {
                 var pop = new instance.web.form.FormOpenPopup(this);
-                pop.show_element(this.dataset.model, id, this.dataset.get_context(), {
+                pop.show_element(this.dataset.model, parseInt(id), this.dataset.get_context(), {
                     title: _.str.sprintf(_t("View: %s"),title),
                     view_id: +this.open_popup_action,
                     res_id: id,
index d9a435f..0e89eb1 100644 (file)
@@ -48,7 +48,7 @@ class DiagramView(openerp.http.Controller):
         for tr in transitions:
             list_tr.append(tr)
             connectors.setdefault(tr, {
-                'id': tr,
+                'id': int(tr),
                 's_id': transitions[tr][0],
                 'd_id': transitions[tr][1]
             })
index 320758e..0899fc7 100644 (file)
@@ -21,7 +21,7 @@ class TableExporter(http.Controller):
         jdata = simplejson.loads(data)
         nbr_measures = jdata['nbr_measures']
         workbook = xlwt.Workbook()
-        worksheet = workbook.add_sheet(jdata['title'])
+        worksheet = workbook.add_sheet(jdata['title'][:30])
         header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;")
         header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;")
         bold = xlwt.easyxf("font: bold on;")
index c6c1c49..645326e 100644 (file)
@@ -616,7 +616,7 @@ class website(osv.osv):
             response.data = data
         else:
             size = (max_w, max_h)
-            img = image_resize_and_sharpen(image, size)
+            img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
             image_save_for_web(img, response.stream, format=image.format)
             # invalidate content-length computed by make_conditional as
             # writing to response.stream does not do it (as of werkzeug 0.9.3)
index 725e5a0..855d6f4 100644 (file)
@@ -131,12 +131,13 @@ OTHER DEALINGS IN THE SOFTWARE.
             // generate all the controls markup
             var css = "box-sizing: border-box; position: absolute; background-color: #fff; border: 1px solid #ccc; width: 8px; height: 8px; margin-left: -4px; margin-top: -4px;";
             transfo.$markup = $(''
-                + '<div class="transfo-controls">'
+                + '<div class="transfo-container">'
+                +  '<div class="transfo-controls">'
                 +   '<div style="cursor: crosshair; position: absolute; margin: -30px; top: 0; right: 0; padding: 1px 0 0 1px;" class="transfo-rotator">'
-                +     '<span class="fa-stack fa-lg">'
-                +     '<i class="fa fa-circle fa-stack-2x"></i>'
-                +     '<i class="fa fa-repeat fa-stack-1x fa-inverse"></i>'
-                +     '</span>'
+                +    '<span class="fa-stack fa-lg">'
+                +    '<i class="fa fa-circle fa-stack-2x"></i>'
+                +    '<i class="fa fa-repeat fa-stack-1x fa-inverse"></i>'
+                +    '</span>'
                 +   '</div>'
                 +   '<div style="' + css + 'top: 0%; left: 0%; cursor: nw-resize;" class="transfo-scaler-tl"></div>'
                 +   '<div style="' + css + 'top: 0%; left: 100%; cursor: ne-resize;" class="transfo-scaler-tr"></div>'
@@ -147,6 +148,7 @@ OTHER DEALINGS IN THE SOFTWARE.
                 +   '<div style="' + css + 'top: 50%; left: 0%; cursor: w-resize;" class="transfo-scaler-ml"></div>'
                 +   '<div style="' + css + 'top: 50%; left: 100%; cursor: e-resize;" class="transfo-scaler-mr"></div>'
                 +   '<div style="' + css + 'border: 0; width: 0px; height: 0px; top: 50%; left: 50%;" class="transfo-scaler-mc"></div>'
+                +  '</div>'
                 + '</div>');
             transfo.$center = transfo.$markup.find(".transfo-scaler-mc");
 
@@ -165,12 +167,20 @@ OTHER DEALINGS IN THE SOFTWARE.
             _bind($this, transfo);
             
             _targetCss($this, transfo);
+            _stop_animation($this[0]);
         }
 
         function _overwriteOptions ($this, transfo, settings) {
             transfo.settings = $.extend(transfo.settings, settings || {});
         }
 
+        function _stop_animation (target) {
+            target.style.webkitAnimationPlayState = "paused";
+            target.style.animationPlayState = "paused";
+            target.style.webkitTransition = "none";
+            target.style.transition = "none";
+        }
+
         function _setOptions ($this, transfo) {
             var style = $this.attr("style") || "";
             var transform = style.match(/transform\s*:([^;]+)/) ? style.match(/transform\s*:([^;]+)/)[1] : "";
@@ -182,7 +192,10 @@ OTHER DEALINGS IN THE SOFTWARE.
             transfo.settings.scaley=     transform.indexOf('scaleY') != -1 ? parseFloat(transform.match(/scaleY\(([^)]+)\)/)[1]) : 1;
 
             transfo.settings.style = style.replace(/[^;]*transform[^;]+/g, '').replace(/;+/g, ';');
+
             $this.attr("style", transfo.settings.style);
+            _stop_animation($this[0]);
+            transfo.settings.pos = $this.offset();
 
             transfo.settings.height = $this.innerHeight();
             transfo.settings.width = $this.innerWidth();
@@ -205,7 +218,6 @@ OTHER DEALINGS IN THE SOFTWARE.
             }
 
             transfo.settings.css = window.getComputedStyle($this[0], null);
-            transfo.settings.pos = $this.offset();
 
             transfo.settings.rotationStep = 5;
             transfo.settings.hide = false;
@@ -226,7 +238,7 @@ OTHER DEALINGS IN THE SOFTWARE.
             }
 
             transfo.$markup.off().on("mousedown", mousedown);
-            transfo.$markup.find(">:not(.transfo-scaler-mc)").off().on("mousedown", mousedown);
+            transfo.$markup.find(".transfo-controls >:not(.transfo-scaler-mc)").off().on("mousedown", mousedown);
         }
 
         function _mouseDown($this, div, transfo, event) {
@@ -339,6 +351,7 @@ OTHER DEALINGS IN THE SOFTWARE.
             settings.scaley = Math.round(settings.scaley*100)/100;
 
             _targetCss($this, transfo);
+            _stop_animation($this[0]);
             return false;
         }
 
@@ -394,15 +407,22 @@ OTHER DEALINGS IN THE SOFTWARE.
 
             _setCss($this, settings.style, settings);
 
-            _setCss(transfo.$markup,
-                "position: absolute;" +
-                "top:" + settings.pos.top + "px;" +
-                "left:" + settings.pos.left + "px;" +
+            transfo.$markup.css({
+                "position": "absolute",
+                "width": width + "px",
+                "height": height + "px",
+                "top": settings.pos.top + "px",
+                "left": settings.pos.left + "px"
+            });
+
+            var $controls = transfo.$markup.find('.transfo-controls');
+            _setCss($controls,
                 "width:" + width + "px;" +
                 "height:" + height + "px;" +
                 "cursor: move;",
                 settings);
-            transfo.$markup.find(">").css("transform", "scaleX("+(1/settings.scalex)+") scaleY("+(1/settings.scaley)+")");
+
+            $controls.children().css("transform", "scaleX("+(1/settings.scalex)+") scaleY("+(1/settings.scaley)+")");
 
             _showHide($this, transfo);
 
@@ -412,10 +432,10 @@ OTHER DEALINGS IN THE SOFTWARE.
         function _showHide ($this, transfo) {
             transfo.$markup.css("z-index", transfo.settings.hide ? -1 : 1000);
             if (transfo.settings.hide) {
-                transfo.$markup.find(">").hide();
+                transfo.$markup.find(".transfo-controls > *").hide();
                 transfo.$markup.find(".transfo-scaler-mc").show();
             } else {
-                transfo.$markup.find(">").show();
+                transfo.$markup.find(".transfo-controls > *").show();
             }
         }
 
index 6147ada..90fbf3a 100644 (file)
             this.$target.transfo({
                 hide: true,
                 callback: function () {
-                    var pos = $(this).data("transfo").$center.offset();
+                    var center = $(this).data("transfo").$markup.find('.transfo-scaler-mc').offset();
+                    var $option = self.$overlay.find('.btn-group:first');
                     self.$overlay.css({
-                        'top': pos.top,
-                        'left': pos.left,
+                        'top': center.top - $option.height()/2,
+                        'left': center.left,
                         'position': 'absolute',
                     });
                     self.$overlay.find(".oe_overlay_options").attr("style", "width:0; left:0!important; top:0;");
index dc0eaad..8a52caa 100644 (file)
@@ -88,6 +88,9 @@ class BlogPost(osv.Model):
             'res.users', 'Last Contributor',
             select=True, readonly=True,
         ),
+        'author_avatar': fields.related(
+            'author_id', 'image_small',
+            string="Avatar", type="binary"),
         'visits': fields.integer('No of Views'),
         'ranking': fields.function(_compute_ranking, string='Ranking', type='float'),
     }
index 726182d..f5a6430 100644 (file)
                 </div>
 
                 <div t-foreach="blog_posts" t-as="blog_post" class="mb32">
-
-                  <img class="img-circle pull-right mt16"
-                    t-att-src="website.image_url(blog_post.author_id, 'image_small')"
-                    style="width: 50px;"/>
-
+                  <span t-field="blog_post.author_avatar" t-field-options='{"widget": "image", "class": "img-circle pull-right mt16 media-object"}' />
                   <a t-attf-href="/blog/#{ slug(blog_post.blog_id) }/post/#{ slug(blog_post) }">
                       <h2 t-field="blog_post.name" class="mb4"/>
                   </a>
index 44800e8..f809314 100644 (file)
@@ -369,7 +369,6 @@ class website_sale(http.Controller):
         shipping_ids = []
         checkout = {}
         if not data:
-            print request.uid, request.website.user_id.id
             if request.uid != request.website.user_id.id:
                 checkout.update( self.checkout_parse("billing", partner) )
                 shipping_ids = orm_partner.search(cr, SUPERUSER_ID, [("parent_id", "=", partner.id), ('type', "=", 'delivery')], context=context)
@@ -434,10 +433,10 @@ class website_sale(http.Controller):
 
         return values
 
-    mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id", "zip"]
-    optional_billing_fields = ["street", "state_id", "vat", "vat_subjected"]
-    mandatory_shipping_fields = ["name", "phone", "street", "city", "country_id", "zip"]
-    optional_shipping_fields = ["state_id"]
+    mandatory_billing_fields = ["name", "phone", "email", "street2", "city", "country_id"]
+    optional_billing_fields = ["street", "state_id", "vat", "vat_subjected", "zip"]
+    mandatory_shipping_fields = ["name", "phone", "street", "city", "country_id"]
+    optional_shipping_fields = ["state_id", "zip"]
 
     def checkout_parse(self, address_type, data, remove_prefix=False):
         """ data is a dict OR a partner browse record
@@ -458,7 +457,7 @@ class website_sale(http.Controller):
         else:
             query = dict((prefix + field_name, getattr(data, field_name))
                 for field_name in all_fields if getattr(data, field_name))
-            if data.parent_id:
+            if address_type == 'billing' and data.parent_id:
                 query[prefix + 'street'] = data.parent_id.name
 
         if query.get(prefix + 'state_id'):
index ce978eb..f8b637e 100644 (file)
@@ -341,6 +341,11 @@ label.css_attribute_color.css_not_available {
   display: block;
 }
 
+.oe_website_sale input.js_quantity {
+  min-width: 48px;
+  text-align: center;
+}
+
 /* ---- Publish managment and options ---- */
 .oe_overlay_options .dropdown ul[name="size"] table {
   margin-left: 20px;
index d480459..9874876 100644 (file)
@@ -287,6 +287,10 @@ label.css_attribute_color.css_not_available
 .discount .oe_default_price
     display: block
 
+.oe_website_sale input.js_quantity
+    min-width: 48px
+    text-align: center
+
 /* ---- Publish managment and options ---- */
 
 .oe_overlay_options
index 34cf1fc..6fbd299 100644 (file)
@@ -7,10 +7,12 @@ $('.oe_website_sale').each(function () {
         var value = +$shippingDifferent.val();
         var data = $shippingDifferent.find("option:selected").data();
         var $snipping = $(".js_shipping", oe_website_sale);
-        var $inputs = $snipping.find("input,select");
+        var $inputs = $snipping.find("input");
+        var $selects = $snipping.find("select");
 
         $snipping.toggle(!!value);
         $inputs.attr("readonly", value <= 0 ? null : "readonly" ).prop("readonly", value <= 0 ? null : "readonly" );
+        $selects.attr("disabled", value <= 0 ? null : "disabled" ).prop("disabled", value <= 0 ? null : "disabled" );
 
         $inputs.each(function () {
             $(this).val( data[$(this).attr("name")] || "" );
@@ -95,7 +97,7 @@ $('.oe_website_sale').each(function () {
         $parent.find(".oe_default_price:first .oe_currency_value").html( price_to_str(+$(this).data('lst_price')) );
         $parent.find(".oe_price:first .oe_currency_value").html(price_to_str(+$(this).data('price')) );
 
-        var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
+        var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
         $img.attr("src", "/website/image/product.product/" + $(this).val() + "/image");
     });
 
@@ -129,7 +131,7 @@ $('.oe_website_sale').each(function () {
         }
 
         if (product_id) {
-            var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img, img.product_detail_img');
+            var $img = $(this).closest('tr.js_product, .oe_website_sale').find('span[data-oe-model^="product."][data-oe-type="image"] img:first, img.product_detail_img');
             $img.attr("src", "/website/image/product.product/" + product_id + "/image");
             $img.parent().attr('data-oe-model', 'product.product').attr('data-oe-id', product_id)
                 .data('oe-model', 'product.product').data('oe-id', product_id);
index ee56a6b..f56485b 100644 (file)
                       <input type="text" name="city" class="form-control" t-att-value="checkout.get('city')"/>
                   </div>
                   <div t-attf-class="form-group #{error.get('zip') and 'has-error' or ''} col-lg-6">
-                      <label class="control-label" for="zip">Zip / Postal Code</label>
+                      <label class="control-label" for="zip" style="font-weight: normal">Zip / Postal Code</label>
                       <input type="text" name="zip" class="form-control" t-att-value="checkout.get('zip')"/>
                   </div>
                   <div t-attf-class="form-group #{error.get('country_id') and 'has-error' or ''} col-lg-6">
                                 t-att-data-shipping_country_id="shipping.country_id and shipping.country_id.id"
                                 ><t t-esc="', '.join('\n'.join(shipping.name_get()[0][1].split(',')).split('\n')[1:])"/></option>
                           </t>
-                          <option value="-1">-- Create a new address --</option>
+                          <option value="-1" t-att-selected="error and len(error) > 0 and shipping_id == -1">-- Create a new address --</option>
                       </select>
                   </div>
               </div>
                       <input type="text" name="shipping_city" class="form-control" t-att-value="checkout.get('shipping_city', '')" t-att-readonly=" 'readonly' if shipping_id &gt;= 0 else ''"/>
                   </div>
                   <div t-attf-class="form-group #{error.get('shipping_zip') and 'has-error' or ''} col-lg-6">
-                      <label class="control-label" for="shipping_zip">Zip / Postal Code</label>
+                      <label class="control-label" for="shipping_zip" style="font-weight: normal">Zip / Postal Code</label>
                       <input type="text" name="shipping_zip" class="form-control" t-att-value="checkout.get('shipping_zip', '')" t-att-readonly=" 'readonly' if shipping_id &gt;= 0 else ''"/>
                   </div>
                   <div t-attf-class="form-group #{error.get('shipping_country_id') and 'has-error' or ''} col-lg-6">
                       <label class="control-label" for="shipping_country_id">Country</label>
-                      <select name="shipping_country_id" class="form-control" t-att-readonly="  'readonly' if shipping_id &gt;= 0 else ''">
+                      <select name="shipping_country_id" class="form-control" t-att-disabled="  'disabled' if shipping_id &gt;= 0 else ''">
                           <option value="">Country...</option>
                           <t t-foreach="countries or []" t-as="country">
                               <option t-att-value="country.id" t-att-selected="country.id == checkout.get('shipping_country_id')"><t t-esc="country.name"/></option>
index 72a4513..3d8ecee 100644 (file)
@@ -53,7 +53,7 @@ $(document).ready(function () {
                             $(".js_remove .js_items").addClass("hidden");
                             $(".js_remove .js_item").removeClass("hidden");
                         } else {
-                            $(".js_remove .js_items").removeClass("hidden").text($(".js_remove .js_items").text().replace(/[0-9.,]+/, qty));
+                            $(".js_remove .js_items").removeClass("hidden").text($(".js_remove .js_items:first").text().replace(/[0-9.,]+/, qty));
                             $(".js_remove .js_item").addClass("hidden");
                         }
                     });
@@ -65,5 +65,14 @@ $(document).ready(function () {
                 });
             return false;
         });
-
+    
+    
+    $('#cart_products input.js_quantity').change(function () {
+        var value = $(this).val();
+        var $next = $(this).closest('tr').next('.optional_product');
+        while($next.length) {
+            $next.find('.js_quantity').text(value);
+            $next = $next.next('.optional_product');
+        }
+    });
 });
index 1f0a430..2963666 100644 (file)
                     <a href="#" class="js_remove"><small>Remove from cart</small></a>
                   </span>
                 </td>
+                <t t-set="option_inc" t-value="option_inc+1"/>
               </tr>
 
             </tbody>
index f646839..38ee122 100644 (file)
@@ -25,3 +25,8 @@ class Exercise(admonitions.BaseAdmonition):
 
 from sphinx.locale import admonitionlabels, l_
 admonitionlabels['exercise'] = l_('Exercise')
+
+# monkeypatch PHP lexer to not require <?php
+from sphinx.highlighting import lexers
+from pygments.lexers.web import PhpLexer
+lexers['php'] = PhpLexer(startinline=True)
index 1fcb690..7881c70 100644 (file)
@@ -21,7 +21,7 @@
 {%- endblock -%}
 
 {%- block content -%}
-  <div class="document-super">
+  <div class="document-super {% if meta is defined %}{{ meta.classes }}{% endif %}">
     {{ super() }}
   </div>
 {%- endblock -%}
       </p>
       </div>
     </div>
+    {%- if google_analytics_key -%}
+    <script>
+    (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+    (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+    m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+    })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+    ga('create', '{{ google_analytics_key }}', 'auto');
+    ga('send','pageview');
+    </script>
+    {%- endif -%}
 {%- endblock %}
index 3619cd9..a03d3b2 100644 (file)
@@ -25,6 +25,76 @@ $(function () {
         });
     }, 100);
 
+    // lang switcher
+    function findSheet(pattern, fromSheet) {
+        if (fromSheet) {
+            for(var i=0; i<fromSheet.cssRules.length; ++i) {
+                var rule = fromSheet.cssRules[i];
+                if (rule.type !== CSSRule.IMPORT_RULE) { continue; }
+                if (pattern.test(rule.href)) {
+                    return rule.styleSheet;
+                }
+            }
+            return null;
+        }
+        var sheets = document.styleSheets;
+        for(var j=0; j<sheets.length; ++j) {
+            var sheet = sheets[j];
+            if (pattern.test(sheet.href)) {
+                return sheet;
+            }
+            var subSheet;
+            if (subSheet = findSheet(pattern, sheet)) {
+                return subSheet;
+            }
+        }
+        return null;
+    }
+    function buildSwitcher(languages) {
+        var root = document.createElement('ul');
+        root.className = "switcher";
+        for(var i=0; i<languages.length; ++i) {
+            var item = document.createElement('li');
+            item.textContent = languages[i];
+            if (i === 0) {
+                item.className = "active";
+            }
+            root.appendChild(item);
+        }
+        return root;
+    }
+    if ($('div.document-super').hasClass('stripe')) { (function () {
+        var sheet = findSheet(/style\.css$/);
+        if (!sheet) { return; }
+
+        // collect languages
+        var languages = {};
+        $('div.switchable').each(function () {
+            var classes = this.className.split(/\s+/);
+            for (var i = 0; i < classes.length; ++i) {
+                var cls = classes[i];
+                if (!/^highlight-/.test(cls)) { continue; }
+                languages[cls.slice(10)] = true;
+            }
+        });
+
+        $(buildSwitcher(Object.keys(languages)))
+            .prependTo('div.documentwrapper')
+            .on('click', 'li', function (e) {
+                $(e.target).addClass('active')
+                    .siblings().removeClass('active');
+                var id = e.target.textContent;
+                var lastIndex = sheet.cssRules.length - 1;
+                var content = sheet.cssRules[lastIndex].style.cssText;
+                var sel = [
+                    '.stripe .only-', id, ', ',
+                    '.stripe .highlight-', id, ' > .highlight'
+                ].join('');
+                sheet.deleteRule(lastIndex);
+                sheet.insertRule(sel + '{' + content + '}', lastIndex);
+        });
+    })(); }
+
     // Config ZeroClipboard
     ZeroClipboard.config({
         moviePath: '_static/ZeroClipboard.swf',
index 9b5468f..fba3309 100644 (file)
@@ -6174,7 +6174,11 @@ button.close {
     display: none !important;
   }
 }
+* {
+  box-sizing: border-box;
+}
 body {
+  overflow: auto;
   position: relative;
 }
 .document-super {
@@ -6643,20 +6647,11 @@ div.section > h2 {
  *
  * Generated via Pygments
  */
-.highlight {
-  padding: 9px 14px;
-  margin-bottom: 14px;
-  background-color: #f7f7f9 !important;
-  border: 1px solid #e1e1e8;
-  border-radius: 4px;
-}
 .highlight pre {
-  color: #333;
-  padding: 0 45px 0 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  background-color: transparent;
-  border: 0;
+  padding: 4px;
+  font-size: 75%;
+  word-break: normal;
+  word-wrap: normal;
 }
 /*
  * ZeroClipboard styles
@@ -6665,6 +6660,11 @@ div.section > h2 {
   position: relative;
   display: none;
 }
+@media (min-width: 768px) {
+  .zero-clipboard {
+    display: block;
+  }
+}
 .btn-clipboard {
   position: absolute;
   top: 0;
@@ -6684,11 +6684,6 @@ div.section > h2 {
   background-color: #a24689;
   border-color: #a24689;
 }
-@media (min-width: 768px) {
-  .zero-clipboard {
-    display: block;
-  }
-}
 img.align-center {
   display: block;
   margin: 0 auto;
@@ -6710,10 +6705,98 @@ td.field-body > ul {
   margin: 0;
   padding: 0;
 }
-pre {
-  word-break: normal;
-  word-wrap: normal;
-}
 .descclassname {
   opacity: 0.5;
 }
+.stripe .section {
+  margin-bottom: 2em;
+}
+@media (min-width: 992px) {
+  .stripe .section > *,
+  .stripe .section > .force-left {
+    width: 49%;
+    float: left;
+    clear: left;
+  }
+  .stripe .section > .force-right,
+  .stripe .section > [class*=highlight] {
+    float: none;
+    clear: none;
+    margin-left: 51%;
+  }
+  .stripe .section > h1,
+  .stripe .section > h2,
+  .stripe .section > h3,
+  .stripe .section > h4,
+  .stripe .section > h5,
+  .stripe .section > h6 {
+    background-color: rgba(255, 255, 255, 0.7);
+  }
+  .stripe .section > h1,
+  .stripe .section > h2,
+  .stripe .section > h3,
+  .stripe .section > h4,
+  .stripe .section > h5,
+  .stripe .section > h6,
+  .stripe .section > .section {
+    position: relative;
+    width: auto;
+    float: none;
+    clear: both;
+  }
+  .stripe .bodywrapper {
+    position: relative;
+  }
+  .stripe .bodywrapper:before {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 50%;
+    content: "";
+    width: 0;
+    border-left: 1px solid #777777;
+  }
+}
+.stripe .switcher {
+  color: white;
+  width: auto !important;
+  float: none !important;
+  position: fixed;
+  display: -webkit-flex;
+  display: flex;
+  -webkit-justify-content: flex-end;
+  justify-content: flex-end;
+  right: 0.5em;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  z-index: 5;
+}
+.stripe .switcher li {
+  background-color: #0f131a;
+  padding: 0.4em 1em;
+  border: 1px solid #333;
+  border-left-width: 0;
+  cursor: pointer;
+}
+.stripe .switcher li:first-child {
+  border-left-width: 1px;
+  border-radius: 5px 0 0 5px;
+}
+.stripe .switcher li:last-child {
+  border-radius: 0 5px 5px 0;
+}
+.stripe .switcher li:hover {
+  background-color: #222;
+}
+.stripe .switcher li.active {
+  background-color: #333;
+}
+.stripe [class*=only-],
+.stripe .switchable > .highlight {
+  display: none;
+}
+.stripe .only-python,
+.stripe .highlight-python > .highlight {
+  display: block;
+}
index ca8be90..7107647 100644 (file)
 // indent level for various items list e.g. dl, fields lists, ...
 @item-indent: 30px;
 
+* {
+  box-sizing: border-box;
+}
 body {
+  overflow: auto;
   position: relative;
 }
 
@@ -461,21 +465,13 @@ div.section > h2 {
  *
  * Generated via Pygments
  */
-
-.highlight {
-  padding: 9px 14px;
-  margin-bottom: 14px;
-  background-color: #f7f7f9 !important;
-  border: 1px solid #e1e1e8;
-  border-radius: 4px;
-}
 .highlight pre {
-  color: #333;
-  padding: 0 45px 0 0;
-  margin-top: 0;
-  margin-bottom: 0;
-  background-color: transparent;
-  border: 0;
+  padding: 4px;
+
+  font-size: 75%;
+  // code block lines should not wrap
+  word-break: normal;
+  word-wrap: normal;
 }
 
 /*
@@ -485,6 +481,9 @@ div.section > h2 {
 .zero-clipboard {
   position: relative;
   display: none;
+  @media (min-width: @screen-sm-min) {
+    display: block;
+  }
 }
 .btn-clipboard {
   position: absolute;
@@ -506,12 +505,6 @@ div.section > h2 {
   border-color: @brand-primary;
 }
 
-@media (min-width: 768px) {
-  .zero-clipboard {
-    display: block;
-  }
-}
-
 // rST styles
 img.align-center {
   display: block;
@@ -546,13 +539,107 @@ td.field-body {
     padding: 0;
 }
 
-// code block lines should not wrap
-pre {
-  word-break: normal;
-  word-wrap: normal;
-}
-
 // lighten js namespace/class name
 .descclassname {
   opacity: 0.5;
 }
+
+// STRIPE-STYLE PAGES
+.stripe {
+  .section {
+    margin-bottom: 2em;
+  }
+
+  // === columning only on medium+ ===
+  @media (min-width: @screen-md-min) {
+    // column 1
+    .section > *,
+    .section > .force-left {
+      width: 49%;
+      float: left;
+      clear: left;
+    }
+    // column 2
+    .section > .force-right,
+    .section > [class*=highlight] {
+      float: none;
+      clear: none;
+      margin-left: 51%;
+    }
+    // fullwidth elements
+    .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+    .section > h6 {
+      background-color: fadeout(@body-bg, 30%);
+    }
+    .section > h1, .section > h2, .section > h3, .section > h4, .section > h5,
+    .section > h6, .section > .section {
+      position: relative;
+      width: auto;
+      float: none;
+      clear: both;
+    }
+
+    .bodywrapper {
+      position: relative;
+      // middle separator
+      &:before {
+        position: absolute;
+        top: 0;
+        bottom: 0;
+        left: 50%;
+        content: "";
+        width: 0;
+        border-left: 1px solid @gray-light;
+      }
+    }
+  }
+
+  .switcher {
+    color: white;
+    width: auto !important;
+    float: none !important;
+
+    position: fixed;
+    display: -webkit-flex;
+    display: flex;
+    -webkit-justify-content: flex-end;
+    justify-content: flex-end;
+
+    right: 0.5em;
+    list-style: none;
+    padding: 0;
+    margin: 0;
+    z-index: 5;
+
+    li {
+      background-color: #0f131a;
+      padding: 0.4em 1em;
+      border: 1px solid #333;
+      border-left-width: 0;
+      cursor: pointer;
+      &:first-child {
+        border-left-width: 1px;
+        border-radius: 5px 0 0 5px;
+      }
+      &:last-child {
+        border-radius: 0 5px 5px 0;
+      }
+      &:hover {
+        background-color: #222;
+      }
+      &.active {
+        background-color: #333;
+      }
+    }
+  }
+
+  // === show/hide code snippets ===
+  [class*=only-],
+  .switchable > .highlight {
+    display: none;
+  }
+  // must be final rule of page
+  .only-python, .highlight-python > .highlight {
+    display: block;
+  }
+}
index 34f77c9..fa6ca77 100644 (file)
@@ -187,6 +187,9 @@ def setup(app):
     app.connect('html-page-context', versionize)
     app.add_config_value('versions', '', 'env')
 
+    app.connect('html-page-context', analytics)
+    app.add_config_value('google_analytics_key', False, 'env')
+
 def canonicalize(app, pagename, templatename, context, doctree):
     """ Adds a 'canonical' URL for the current document in the rendering
     context. Requires the ``canonical_root`` setting being set. The canonical
@@ -212,6 +215,12 @@ def versionize(app, pagename, templatename, context, doctree):
         if vs != app.config.version
     ]
 
+def analytics(app, pagename, templatename, context, doctree):
+    if not app.config.google_analytics_key:
+        return
+
+    context['google_analytics_key'] = app.config.google_analytics_key
+
 def _build_url(root, branch, pagename):
     return "{canonical_url}{canonical_branch}/{canonical_page}".format(
         canonical_url=root,
index 16133d1..cb5ed8d 100644 (file)
@@ -5,3 +5,4 @@ Module Objects
 .. toctree::
     :titlesonly:
 
+    modules/api_integration
diff --git a/doc/modules/api_integration.rst b/doc/modules/api_integration.rst
new file mode 100644 (file)
index 0000000..f6a51f0
--- /dev/null
@@ -0,0 +1,816 @@
+:classes: stripe
+
+===========
+Odoo as API
+===========
+
+Odoo is mostly extended internally via modules, but much of its features and
+all of its data is also available from the outside for external analysis or
+integration with various tools. Part of the :ref:`reference/orm/model` API is
+easily available over XML-RPC_ and accessible from a variety of languages.
+
+.. Odoo XML-RPC idiosyncracies:
+   * uses multiple endpoint and a nested call syntax instead of a
+     "hierarchical" server structure (e.g. ``openerp.res.partner.read()``)
+   * uses its own own manual auth system instead of basic auth or sessions
+     (basic is directly supported the Python and Ruby stdlibs as well as
+     ws-xmlrpc, not sure about ripcord)
+   * own auth is inconvenient as (uid, password) have to be explicitly passed
+     into every call. Session would allow db to be stored as well
+   These issues are especially visible in Java, somewhat less so in PHP
+
+Connection and authentication
+=============================
+
+Configuration
+-------------
+
+If you already have an Odoo server installed, you can just use its
+parameters
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        url = <insert server URL>
+        db = <insert database name>
+        username = 'admin'
+        password = <insert password for your admin user (default: admin)>
+
+    .. code-block:: ruby
+
+        url = <insert server URL>
+        db = <insert database name>
+        username = "admin"
+        password = <insert password for your admin user (default: admin)>
+
+    .. code-block:: php
+
+        $url = <insert server URL>;
+        $db = <insert database name>;
+        $username = "admin";
+        $password = <insert password for your admin user (default: admin)>;
+
+    .. code-block:: java
+
+        final String url = <insert server URL>,
+                      db = <insert database name>,
+                username = "admin",
+                password = <insert password for your admin user (default: admin)>;
+
+To make exploration simpler, you can also ask https://demo.odoo.com for a test
+database:
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        import xmlrpclib
+        info = xmlrpclib.ServerProxy('https://demo.odoo.com/start').start()
+        url, db, username, password = \
+            info['host'], info['database'], info['user'], info['password']
+
+    .. code-block:: ruby
+
+        require "xmlrpc/client"
+        info = XMLRPC::Client.new2('https://demo.odoo.com/start').call('start')
+        url, db, username, password = \
+            info['host'], info['database'], info['user'], info['password']
+
+    .. code-block:: php
+
+        require_once('ripcord.php');
+        $info = ripcord::client('https://demo.odoo.com/start')->start();
+        list($url, $db, $username, $password) =
+          array($info['host'], $info['database'], $info['user'], $info['password']);
+
+    .. code-block:: java
+
+        final XmlRpcClient client = new XmlRpcClient();
+
+        final XmlRpcClientConfigImpl start_config = new XmlRpcClientConfigImpl();
+        start_config.setServerURL(new URL("https://demo.odoo.com/start"));
+        final Map<String, String> info = (Map<String, String>)client.execute(
+            start_config, "start", Collections.emptyList());
+
+        final String url = info.get("host"),
+                      db = info.get("database"),
+                username = info.get("user"),
+                password = info.get("password");
+
+.. rst-class:: force-right
+
+    .. note::
+        :class: only-php
+
+        These examples use the `Ripcord <https://code.google.com/p/ripcord/>`_
+        library, which provides a simple XML-RPC API. Ripcord requires that
+        `XML-RPC support be enabled
+        <http://php.net/manual/en/xmlrpc.installation.php>`_ in your PHP
+        installation.
+
+        Since calls are performed over
+        `HTTPS <http://en.wikipedia.org/wiki/HTTP_Secure>`_, it also requires that
+        the `OpenSSL extension
+        <http://php.net/manual/en/openssl.installation.php>`_ be enabled.
+
+    .. note::
+        :class: only-java
+
+        These examples use the `Apache XML-RPC library
+        <https://ws.apache.org/xmlrpc/>`_
+
+Logging in
+----------
+
+Odoo requires users of the API to be authenticated before being able to query
+much data.
+
+The ``xmlrpc/2/common`` endpoint provides meta-calls which don't require
+authentication, such as the authentication itself or fetching version
+information. To verify if the connection information is correct before trying
+to authenticate, the simplest call is to ask for the server's version. The
+authentication itself is done through the ``authenticate`` function and
+returns a user identifier (``uid``) used in authenticated calls instead of
+the login.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        common = xmlrpclib.ServerProxy('{}/xmlrpc/2/common'.format(url))
+        common.version()
+
+    .. code-block:: ruby
+
+        common = XMLRPC::Client.new2("#{url}/xmlrpc/2/common")
+        common.call('version')
+
+    .. code-block:: php
+
+        $common = ripcord::client("$url/xmlrpc/2/common");
+        $common->version();
+
+    .. code-block:: java
+
+        final XmlRpcClientConfigImpl common_config = new XmlRpcClientConfigImpl();
+        common_config.setServerURL(new URL(String.format("%s/xmlrpc/2/common", url)));
+        client.execute(common_config, "version", Collections.emptyList());
+
+.. code-block:: json
+
+    {
+        "server_version": "8.0",
+        "server_version_info": [8, 0, 0, "final", 0],
+        "server_serie": "8.0",
+        "protocol_version": 1,
+    }
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        uid = common.authenticate(db, username, password, {})
+
+    .. code-block:: ruby
+
+        uid = common.call('authenticate', db, username, password, {})
+
+    .. code-block:: php
+
+        $uid = $common->authenticate($db, $username, $password, array());
+
+    .. code-block:: java
+
+        int uid = (int)client.execute(
+            common_config, "authenticate", Arrays.asList(
+                db, username, password, Collections.emptyMap()));
+
+Calling methods
+===============
+
+The second â€” and most generally useful â€” is ``xmlrpc/2/object`` which is used
+to call methods of odoo models via the ``execute_kw`` RPC function.
+
+Each call to ``execute_kw`` takes the following parameters:
+
+* the database to use, a string
+* the user id (retrieved through ``authenticate``), an integer
+* the user's password, a string
+* the model name, a string
+* the method name, a string
+* an array/list of parameters passed by position
+* a mapping/dict of parameters to pass by keyword (optional)
+
+.. rst-class:: force-right
+
+For instance to see if we can read the ``res.partner`` model we can call
+``check_access_rights`` with ``operation`` passed by position and
+``raise_exception`` passed by keyword (in order to get a true/false result
+rather than true/error):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models = xmlrpclib.ServerProxy('{}/xmlrpc/2/object'.format(url))
+        models.execute_kw(db, uid, password,
+            'res.partner', 'check_access_rights',
+            ['read'], {'raise_exception': False})
+
+    .. code-block:: ruby
+
+        models = XMLRPC::Client.new2("#{url}/xmlrpc/2/object").proxy
+        models.execute_kw(db, uid, password,
+            'res.partner', 'check_access_rights',
+            ['read'], {raise_exception: false})
+
+    .. code-block:: php
+
+        $models = ripcord::client("$url/xmlrpc/2/object");
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'check_access_rights',
+            array('read'), array('raise_exception' => false));
+
+    .. code-block:: java
+
+        final XmlRpcClient models = new XmlRpcClient() {{
+            setConfig(new XmlRpcClientConfigImpl() {{
+                setServerURL(new URL(String.format("%s/xmlrpc/2/object", url)));
+            }});
+        }};
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "check_access_rights",
+            Arrays.asList("read"),
+            new HashMap() {{ put("raise_exception", false); }}
+        ));
+
+.. code-block:: json
+
+    true
+
+.. todo:: this should be runnable and checked
+
+List records
+------------
+
+Records can be listed and filtered via :meth:`~openerp.models.Model.search`.
+
+:meth:`~openerp.models.Model.search` takes a mandatory
+:ref:`domain <reference/orm/domains>` filter (possibly empty), and returns the
+database identifiers of all records matching the filter. To list customer
+companies for instance:
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', True]]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', true]]])
+
+    .. code-block:: php
+
+        $domain = array(array('is_company', '=', true),
+                        array('customer', '=', true));
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search', array($domain));
+
+    .. code-block:: java
+
+        final List domain = Arrays.asList(
+            Arrays.asList("is_company", "=", true),
+            Arrays.asList("customer", "=", true));
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(domain)
+        )));
+
+.. code-block:: json
+
+    [7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74]
+
+Pagination
+''''''''''
+
+By default a research will return the ids of all records matching the
+condition, which may be a huge number. ``offset`` and ``limit`` parameters are
+available to only retrieve a subset of all matched records.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', True]]],
+            {'offset': 10, 'limit': 5})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', true]]],
+            {offset: 10, limit: 5})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search',
+            array($domain),
+            array('offset'=>10, 'limit'=>5));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(domain),
+            new HashMap() {{ put("offset", 10); put("limit", 5); }}
+        )));
+
+.. code-block:: json
+
+    [13, 20, 30, 22, 29]
+
+Count records
+-------------
+
+Rather than retrieve a possibly gigantic list of records and count them
+afterwards, :meth:`~openerp.models.Model.search_count` can be used to retrieve
+only the number of records matching the query. It takes the same
+:ref:`domain <reference/orm/domains>` filter as
+:meth:`~openerp.models.Model.search` and no other parameter.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_count',
+            [[['is_company', '=', True], ['customer', '=', True]]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_count',
+            [[['is_company', '=', true], ['customer', '=', true]]])
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search_count',
+            array($domain));
+
+    .. code-block:: java
+
+        (Integer)models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search_count",
+            Arrays.asList(domain)
+        ));
+
+.. code-block:: json
+
+    19
+
+.. warning::
+
+    calling ``search`` then ``search_count`` (or the other way around) may not
+    yield coherent results if other users are using the server: stored data
+    could have changed between the calls
+
+Read records
+------------
+
+Record data is accessible via the :meth:`~openerp.models.Model.read` method,
+which takes a list of ids (as returned by
+:meth:`~openerp.models.Model.search`) and optionally a list of fields to
+fetch. By default, it will fetch all the fields the current user can read,
+which tends to be a huge amount.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        ids = models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', True], ['customer', '=', True]]],
+            {'limit': 1})
+        [record] = models.execute_kw(db, uid, password,
+            'res.partner', 'read', [ids])
+        # count the number of fields fetched by default
+        len(record)
+
+    .. code-block:: ruby
+
+        ids = models.execute_kw(db, uid, password,
+            'res.partner', 'search',
+            [[['is_company', '=', true], ['customer', '=', true]]],
+            {limit: 1})
+        record = models.execute_kw(db, uid, password,
+            'res.partner', 'read', [ids]).first
+        # count the number of fields fetched by default
+        record.length
+
+    .. code-block:: php
+
+        $ids = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search',
+            array($domain),
+            array('limit'=>1));
+        $records = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'read', array($ids));
+        // count the number of fields fetched by default
+        count($records[0]);
+
+    .. code-block:: java
+
+        final List ids = Arrays.asList((Object[])models.execute(
+            "execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "search",
+                Arrays.asList(domain),
+                new HashMap() {{ put("limit", 1); }})));
+        final Map record = (Map)((Object[])models.execute(
+            "execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "read",
+                Arrays.asList(ids)
+            )
+        ))[0];
+        // count the number of fields fetched by default
+        record.size();
+
+.. code-block:: json
+
+    121
+
+Conversedly, picking only three fields deemed interesting.
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'read',
+            [ids], {'fields': ['name', 'country_id', 'comment']})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'read',
+            [ids], {fields: %w(name country_id comment)})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'read',
+            array($ids),
+            array('fields'=>array('name', 'country_id', 'comment')));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "read",
+            Arrays.asList(ids),
+            new HashMap() {{
+                put("fields", Arrays.asList("name", "country_id", "comment"));
+            }}
+        )));
+
+.. code-block:: json
+
+    [{"comment": false, "country_id": [21, "Belgium"], "id": 7, "name": "Agrolait"}]
+
+.. note:: even if the ``id`` field is not requested, it is always returned
+
+Listing record fields
+---------------------
+
+:meth:`~openerp.models.Model.fields_get` can be used to inspect
+a model's fields and check which ones seem to be of interest.
+
+Because
+it returns a great amount of meta-information (it is also used by client
+programs) it should be filtered before printing, the most interesting items
+for a human user are ``string`` (the field's label), ``help`` (a help text if
+available) and ``type`` (to know which values to expect, or to send when
+updating a record):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+        # filter keys of field attributes for display
+        {field: {
+                    k: v for k, v in attributes.iteritems()
+                    if k in ['string', 'help', 'type']
+                }
+         for field, attributes in fields.iteritems()}
+
+    .. code-block:: ruby
+
+        fields = models.execute_kw(db, uid, password, 'res.partner', 'fields_get', [])
+        # filter keys of field attributes for display
+        fields.each {|k, v|
+            fields[k] = v.keep_if {|kk, vv| %w(string help type).include? kk}
+        }
+
+    .. code-block:: php
+
+        $fields_full = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'fields_get', array());
+        // filter keys of field attributes for display
+        $allowed = array_flip(array('string', 'help', 'type'));
+        $fields = array();
+        foreach($fields_full as $field => $attributes) {
+          $fields[$field] = array_intersect_key($attributes, $allowed);
+        }
+
+    .. code-block:: java
+
+        final Map<String, Map<String, Object>> fields  =
+            (Map<String, Map<String, Object>>)models.execute("execute_kw", Arrays.asList(
+                db, uid, password,
+                "res.partner", "fields_get",
+                Collections.emptyList()));
+        // filter keys of field attributes for display
+        final List<String> allowed = Arrays.asList("string", "help", "type");
+        new HashMap<String, Map<String, Object>>() {{
+            for(Entry<String, Map<String, Object>> item: fields.entrySet()) {
+                put(item.getKey(), new HashMap<String, Object>() {{
+                    for(Entry<String, Object> it: item.getValue().entrySet()) {
+                        if (allowed.contains(it.getKey())) {
+                            put(it.getKey(), it.getValue());
+                        }
+                    }
+                }});
+            }
+        }};
+
+.. code-block:: json
+
+    {
+        "ean13": {
+            "type": "char",
+            "help": "BarCode",
+            "string": "EAN13"
+        },
+        "property_account_position": {
+            "type": "many2one",
+            "help": "The fiscal position will determine taxes and accounts used for the partner.",
+            "string": "Fiscal Position"
+        },
+        "signup_valid": {
+            "type": "boolean",
+            "help": "",
+            "string": "Signup Token is Valid"
+        },
+        "date_localization": {
+            "type": "date",
+            "help": "",
+            "string": "Geo Localization Date"
+        },
+        "ref_companies": {
+            "type": "one2many",
+            "help": "",
+            "string": "Companies that refers to partner"
+        },
+        "sale_order_count": {
+            "type": "integer",
+            "help": "",
+            "string": "# of Sales Order"
+        },
+        "purchase_order_count": {
+            "type": "integer",
+            "help": "",
+            "string": "# of Purchase Order"
+        },
+
+Search and read
+---------------
+
+Because that is a very common task, Odoo provides a
+:meth:`~openerp.models.Model.search_read` shortcut which as its name notes is
+equivalent to a :meth:`~openerp.models.Model.search` followed by a
+:meth:`~openerp.models.Model.read`, but avoids having to perform two requests
+and keep ids around. Its arguments are similar to
+:meth:`~openerp.models.Model.search`'s, but it can also take a list of
+``fields`` (like :meth:`~openerp.models.Model.read`, if that list is not
+provided it'll fetch all fields of matched records):
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_read',
+            [[['is_company', '=', True], ['customer', '=', True]]],
+            {'fields': ['name', 'country_id', 'comment'], 'limit': 5})
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search_read',
+            [[['is_company', '=', true], ['customer', '=', true]]],
+            {fields: %w(name country_id comment), limit: 5})
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search_read',
+            array($domain),
+            array('fields'=>array('name', 'country_id', 'comment'), 'limit'=>5));
+
+    .. code-block:: java
+
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search_read",
+            Arrays.asList(domain),
+            new HashMap() {{
+                put("fields", Arrays.asList("name", "country_id", "comment"));
+                put("limit", 5);
+            }}
+        )));
+
+.. code-block:: json
+
+    [
+        {
+            "comment": false,
+            "country_id": [ 21, "Belgium" ],
+            "id": 7,
+            "name": "Agrolait"
+        },
+        {
+            "comment": false,
+            "country_id": [ 76, "France" ],
+            "id": 18,
+            "name": "Axelor"
+        },
+        {
+            "comment": false,
+            "country_id": [ 233, "United Kingdom" ],
+            "id": 12,
+            "name": "Bank Wealthy and sons"
+        },
+        {
+            "comment": false,
+            "country_id": [ 105, "India" ],
+            "id": 14,
+            "name": "Best Designers"
+        },
+        {
+            "comment": false,
+            "country_id": [ 76, "France" ],
+            "id": 17,
+            "name": "Camptocamp"
+        }
+    ]
+
+
+Create records
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+            'name': "New Partner",
+        }])
+
+    .. code-block:: ruby
+
+        id = models.execute_kw(db, uid, password, 'res.partner', 'create', [{
+            name: "New Partner",
+        }])
+
+    .. code-block:: php
+
+        $id = $models->execute_kw($db, $uid, $password,
+            'res.partner', 'create',
+            array(array('name'=>"New Partner")));
+
+    .. code-block:: java
+
+        final Integer id = (Integer)models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "create",
+            Arrays.asList(new HashMap() {{ put("name", "New Partner"); }})
+        ));
+
+.. code-block:: json
+
+    78
+
+Update records
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
+            'name': "Newer partner"
+        }])
+        # get record name after having changed it
+        models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password, 'res.partner', 'write', [[id], {
+            name: "Newer partner"
+        }])
+        # get record name after having changed it
+        models.execute_kw(db, uid, password, 'res.partner', 'name_get', [[id]])
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password, 'res.partner', 'write',
+            array(array($id), array('name'=>"Newer partner")));
+        // get record name after having changed it
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'name_get', array(array($id)));
+
+    .. code-block:: java
+
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "write",
+            Arrays.asList(
+                Arrays.asList(id),
+                new HashMap() {{ put("name", "Newer Partner"); }}
+            )
+        ));
+        // get record name after having changed it
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "name_get",
+            Arrays.asList(Arrays.asList(id))
+        )));
+
+.. code-block:: json
+
+    [[78, "Newer partner"]]
+
+Delete records
+--------------
+
+.. rst-class:: switchable
+
+    .. code-block:: python
+
+        models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
+        # check if the deleted record is still in the database
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search', [[['id', '=', id]]])
+
+    .. code-block:: ruby
+
+        models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[id]])
+        # check if the deleted record is still in the database
+        models.execute_kw(db, uid, password,
+            'res.partner', 'search', [[['id', '=', id]]])
+
+    .. code-block:: php
+
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'unlink',
+            array(array($id)));
+        // check if the deleted record is still in the database
+        $models->execute_kw($db, $uid, $password,
+            'res.partner', 'search',
+            array(array(array('id', '=', $id))));
+
+    .. code-block:: java
+
+        models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "unlink",
+            Arrays.asList(Arrays.asList(id))));
+        // check if the deleted record is still in the database
+        Arrays.asList((Object[])models.execute("execute_kw", Arrays.asList(
+            db, uid, password,
+            "res.partner", "search",
+            Arrays.asList(Arrays.asList(Arrays.asList("id", "=", 78)))
+        )));
+
+.. code-block:: json
+
+    []
+
+.. _PostgreSQL: http://www.postgresql.org
+.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
index e3c72f1..aa14400 100644 (file)
@@ -782,7 +782,7 @@ class ir_model_access(osv.osv):
             _logger.warning('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, uid, model_name)
             msg = '%s %s' % (msg_heads[mode], msg_tail)
             raise openerp.exceptions.AccessError(msg % msg_params)
-        return r or False
+        return bool(r)
 
     __cache_clearing_methods = []
 
index ae8f437..52d594d 100644 (file)
@@ -319,7 +319,8 @@ class Field(object):
                 self._free_attrs.append(attr)
             setattr(self, attr, value)
 
-        if not self.string:
+        if not self.string and not self.related:
+            # related fields get their string from their parent field
             self.string = name.replace('_', ' ').capitalize()
 
         # determine self.default and cls._defaults in a consistent way
index 8f9e5ec..bfc768b 100644 (file)
@@ -1654,7 +1654,7 @@ class BaseModel(object):
 
     @api.returns('self')
     def search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False):
-        """ search(args[, offset=0][, limit=None][, order=None][, count=False])
+        """ search(args[, offset=0][, limit=None][, order=None])
 
         Searches for records based on the ``args``
         :ref:`search domain <reference/orm/domains>`.
@@ -1664,9 +1664,6 @@ class BaseModel(object):
         :param int offset: number of results to ignore (default: none)
         :param int limit: maximum number of records to return (default: all)
         :param str order: sort string
-        :param bool count: if ``True``, the call should return the number of
-                           records matching ``args`` rather than the records
-                           themselves.
         :returns: at most ``limit`` records matching the search criteria
 
         :raise AccessError: * if user tries to bypass access rules for read on the requested object.
@@ -2996,6 +2993,12 @@ class BaseModel(object):
                 if not partial:
                     raise
 
+        # update columns (fields may have changed), and column_infos
+        for name, field in self._fields.iteritems():
+            if field.store:
+                self._columns[name] = field.to_column()
+        self._inherits_reload()
+
         # group fields by compute to determine field.computed_fields
         fields_by_compute = defaultdict(list)
         for field in self._fields.itervalues():
index 679fae0..34672c4 100644 (file)
 
 """
 from collections import Mapping
-from contextlib import contextmanager
 import logging
+import os
 import threading
 
 import openerp
 from .. import SUPERUSER_ID
-from openerp.tools import assertion_report, lazy_property
+from openerp.tools import assertion_report, lazy_property, classproperty, config
+from openerp.tools.lru import LRU
 
 _logger = logging.getLogger(__name__)
 
@@ -259,12 +260,27 @@ class RegistryManager(object):
         registries (essentially database connection/model registry pairs).
 
     """
-    # Mapping between db name and model registry.
-    # Accessed through the methods below.
-    registries = {}
+    _registries = None
     _lock = threading.RLock()
     _saved_lock = None
 
+    @classproperty
+    def registries(cls):
+        if cls._registries is None:
+            size = config.get('registry_lru_size', None)
+            if not size:
+                # Size the LRU depending of the memory limits
+                if os.name != 'posix':
+                    # cannot specify the memory limit soft on windows...
+                    size = 42
+                else:
+                    # On average, a clean registry take 25MB of memory + cache
+                    avgsz = 30 * 1024 * 1024
+                    size = int(config['limit_memory_soft'] / avgsz)
+
+            cls._registries = LRU(size)
+        return cls._registries
+
     @classmethod
     def lock(cls):
         """ Return the current registry lock. """
index 30fe1f9..9506a5c 100644 (file)
@@ -83,7 +83,28 @@ class _column(object):
     _symbol_set = (_symbol_c, _symbol_f)
     _symbol_get = None
     _deprecated = False
-    copy = True # whether the field is copied by BaseModel.copy()
+
+    copy = True                 # whether value is copied by BaseModel.copy()
+    string = None
+    help = ""
+    required = False
+    readonly = False
+    _domain = []
+    _context = {}
+    states = None
+    priority = 0
+    change_default = False
+    size = None
+    ondelete = None
+    translate = False
+    select = False
+    manual = False
+    write = False
+    read = False
+    selectable = True
+    group_operator = False
+    groups = False              # CSV list of ext IDs of groups
+    deprecated = False          # Optional deprecation warning
 
     def __init__(self, string='unknown', required=False, readonly=False, domain=None, context=None, states=None, priority=0, change_default=False, size=None, ondelete=None, translate=False, select=False, manual=False, **args):
         """
@@ -92,33 +113,28 @@ class _column(object):
         It corresponds to the 'state' column in ir_model_fields.
 
         """
-        if domain is None:
-            domain = []
-        if context is None:
-            context = {}
-        self.states = states or {}
-        self.string = string
-        self.readonly = readonly
-        self.required = required
-        self.size = size
-        self.help = args.get('help', '')
-        self.priority = priority
-        self.change_default = change_default
-        self.ondelete = ondelete.lower() if ondelete else 'set null'
-        self.translate = translate
-        self._domain = domain
-        self._context = context
-        self.write = False
-        self.read = False
-        self.select = select
-        self.manual = manual
-        self.selectable = True
-        self.group_operator = args.get('group_operator', False)
-        self.groups = False  # CSV list of ext IDs of groups that can access this field
-        self.deprecated = False # Optional deprecation warning
+        args0 = {
+            'string': string,
+            'required': required,
+            'readonly': readonly,
+            '_domain': domain,
+            '_context': context,
+            'states': states,
+            'priority': priority,
+            'change_default': change_default,
+            'size': size,
+            'ondelete': ondelete.lower() if ondelete else None,
+            'translate': translate,
+            'select': select,
+            'manual': manual,
+        }
+        for key, val in args0.iteritems():
+            if val:
+                setattr(self, key, val)
+
         self._args = args
-        for a in args:
-            setattr(self, a, args[a])
+        for key, val in args.iteritems():
+            setattr(self, key, val)
 
         # prefetch only if self._classic_write, not self.groups, and not
         # self.deprecated
@@ -150,6 +166,8 @@ class _column(object):
         base_items = [
             ('column', self),                   # field interfaces self
             ('copy', self.copy),
+        ]
+        truthy_items = filter(itemgetter(1), [
             ('index', self.select),
             ('manual', self.manual),
             ('string', self.string),
@@ -160,8 +178,6 @@ class _column(object):
             ('groups', self.groups),
             ('change_default', self.change_default),
             ('deprecated', self.deprecated),
-        ]
-        truthy_items = filter(itemgetter(1), [
             ('size', self.size),
             ('ondelete', self.ondelete),
             ('translate', self.translate),
@@ -620,6 +636,8 @@ class many2one(_column):
     _symbol_f = lambda x: x or None
     _symbol_set = (_symbol_c, _symbol_f)
 
+    ondelete = 'set null'
+
     def __init__(self, obj, string='unknown', auto_join=False, **args):
         _column.__init__(self, string=string, **args)
         self._obj = obj
index 97f36b7..16cbb5d 100644 (file)
@@ -18,14 +18,8 @@ RPC_VERSION_1 = {
 }
 
 def dispatch(method, params):
-    if method in ['login', 'about', 'timezone_get',
-                  'version', 'authenticate']:
-        pass
-    elif method in ['set_loglevel']:
-        passwd = params[0]
-        params = params[1:]
-        security.check_super(passwd)
-    else:
+    if method not in ['login', 'about', 'timezone_get',
+                      'version', 'authenticate', 'set_loglevel']:
         raise Exception("Method not found: %s" % method)
 
     fn = globals()['exp_' + method]
index 739c32a..589da7f 100644 (file)
@@ -477,8 +477,6 @@ class ConnectionPool(object):
 
     @locked
     def borrow(self, dsn):
-        self._debug('Borrow connection to %r', dsn)
-
         # free dead and leaked connections
         for i, (cnx, _) in tools.reverse_enumerate(self._connections):
             if cnx.closed:
@@ -503,7 +501,7 @@ class ConnectionPool(object):
                     continue
                 self._connections.pop(i)
                 self._connections.append((cnx, True))
-                self._debug('Existing connection found at index %d', i)
+                self._debug('Borrow existing connection to %r at index %d', cnx.dsn, i)
 
                 return cnx
 
@@ -546,11 +544,15 @@ class ConnectionPool(object):
 
     @locked
     def close_all(self, dsn=None):
-        _logger.info('%r: Close all connections to %r', self, dsn)
+        count = 0
+        last = None
         for i, (cnx, used) in tools.reverse_enumerate(self._connections):
             if dsn is None or cnx._original_dsn == dsn:
                 cnx.close()
-                self._connections.pop(i)
+                last = self._connections.pop(i)[0]
+                count += 1
+        _logger.info('%r: Closed %d connections %s', self, count,
+                    (dsn and last and 'to %r' % last.dsn) or '')
 
 
 class Connection(object):
index 03b4f76..f04bacd 100644 (file)
@@ -20,7 +20,7 @@
 #
 ##############################################################################
 
-__all__ = ['synchronized', 'lazy_property', 'conditional']
+__all__ = ['synchronized', 'lazy_property', 'classproperty', 'conditional']
 
 from functools import wraps
 from inspect import getsourcefile
@@ -116,4 +116,12 @@ def compose(a, b):
         return a(b(*args, **kwargs))
     return wrapper
 
+
+class _ClassProperty(property):
+    def __get__(self, cls, owner):
+        return self.fget.__get__(None, owner)()
+
+def classproperty(func):
+    return _ClassProperty(classmethod(func))
+
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
index aa110d7..0b02d27 100644 (file)
@@ -96,19 +96,21 @@ def image_resize_image(base64_source, size=(1024, 1024), encoding='base64', file
     image.save(background_stream, filetype)
     return background_stream.getvalue().encode(encoding)
 
-def image_resize_and_sharpen(image, size, factor=2.0):
+def image_resize_and_sharpen(image, size, preserve_aspect_ratio=False, factor=2.0):
     """
         Create a thumbnail by resizing while keeping ratio.
         A sharpen filter is applied for a better looking result.
 
         :param image: PIL.Image.Image()
         :param size: 2-tuple(width, height)
+        :param preserve_aspect_ratio: boolean (default: False)
         :param factor: Sharpen factor (default: 2.0)
     """
     if image.mode != 'RGBA':
         image = image.convert('RGBA')
     image.thumbnail(size, Image.ANTIALIAS)
-    size = image.size
+    if preserve_aspect_ratio:
+        size = image.size
     sharpener = ImageEnhance.Sharpness(image)
     resized_image = sharpener.enhance(factor)
     # create a transparent image for background and paste the image on it