diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py
index 5426e8ce033..dab5cfd1406 100644
--- a/addons/account/account_bank_statement.py
+++ b/addons/account/account_bank_statement.py
@@ -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'),
diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py
index 2069828b369..a51be3347b0 100644
--- a/addons/account/account_invoice.py
+++ b/addons/account/account_invoice.py
@@ -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)
diff --git a/addons/account/partner.py b/addons/account/partner.py
index e154c407716..c0a1a695e0c 100644
--- a/addons/account/partner.py
+++ b/addons/account/partner.py
@@ -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
diff --git a/addons/account/partner_view.xml b/addons/account/partner_view.xml
index 600a13edfeb..11e09a635e9 100644
--- a/addons/account/partner_view.xml
+++ b/addons/account/partner_view.xml
@@ -16,8 +16,8 @@
-
-
+
+
diff --git a/addons/account/report/account_partner_balance.py b/addons/account/report/account_partner_balance.py
index ecd287551e5..51fff6c1413 100644
--- a/addons/account/report/account_partner_balance.py
+++ b/addons/account/report/account_partner_balance.py
@@ -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':
diff --git a/addons/account_analytic_plans/account_analytic_plans.py b/addons/account_analytic_plans/account_analytic_plans.py
index 02fc371f41e..4bd3822e8d8 100644
--- a/addons/account_analytic_plans/account_analytic_plans.py
+++ b/addons/account_analytic_plans/account_analytic_plans.py
@@ -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})
diff --git a/addons/board/static/src/css/dashboard.sass b/addons/board/static/src/css/dashboard.sass
index 1b3e023dcf1..e0abfd28b38 100644
--- a/addons/board/static/src/css/dashboard.sass
+++ b/addons/board/static/src/css/dashboard.sass
@@ -1,3 +1,5 @@
+@charset "utf-8"
+
@mixin radius($radius: 5px)
-moz-border-radius: $radius
-webkit-border-radius: $radius
diff --git a/addons/bus/static/src/js/bus.js b/addons/bus/static/src/js/bus.js
index f85ccb86e0a..393f101c77c 100644
--- a/addons/bus/static/src/js/bus.js
+++ b/addons/bus/static/src/js/bus.js
@@ -58,4 +58,4 @@
// singleton
bus.bus = new bus.Bus();
return bus;
-})();
\ No newline at end of file
+})();
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
index 3c6df8ef378..3faf0a47e6e 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_report.xml
@@ -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"
/>
diff --git a/addons/hr_timesheet_invoice/report/account_analytic_profit.py b/addons/hr_timesheet_invoice/report/account_analytic_profit.py
index 3f7108fb665..15116ee8431 100644
--- a/addons/hr_timesheet_invoice/report/account_analytic_profit.py
+++ b/addons/hr_timesheet_invoice/report/account_analytic_profit.py
@@ -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)
diff --git a/addons/l10n_fr/views/report_l10nfrbilan.xml b/addons/l10n_fr/views/report_l10nfrbilan.xml
index 5b7335e0db0..f5c697c3fad 100644
--- a/addons/l10n_fr/views/report_l10nfrbilan.xml
+++ b/addons/l10n_fr/views/report_l10nfrbilan.xml
@@ -20,6 +20,7 @@
+ Actif
+
+ Passif
+
+
+
+ | CAPITAUX PROPRES |
+ |
+
+
+ | Capital [dont versé...] |
+ |
+
+
+ | Primes d'émission, de fusion, d'apport |
+ |
+
+
+ | Écarts de réévaluation |
+ |
+
+
+ | Écart d'équivalence |
+ |
+
+
+ | RÉSERVES |
+ |
+
+
+ | Réserve légale |
+ |
+
+
+ | Réserves statutaires ou contractuelles |
+ |
+
+
+ | Réserves réglementées | |
+
+ | Autres réserves |
+ |
+
+
+ | Report à nouveau |
+ |
+
+
+ | RÉSULTAT DE L'EXERCICE [bénéfice ou perte] |
+ |
+
+
+ | Subventions d'investissement |
+ |
+
+
+ | Provisions réglementées |
+ |
+
+
+ | TOTAL I |
+ |
+
+
+ | PROVISIONS |
+ |
+
+
+ | Provisions pour risques |
+ |
+
+
+ | Provisions pour charges |
+ |
+
+
+ | TOTAL II |
+ |
+
+
+ | DETTES |
+ |
+
+
+ | Emprunts obligataires convertibles |
+ |
+
+
+ | Autres emprunts obligataires |
+ |
+
+
+ | Emprunts et dettes auprès des établissements de crédit |
+ |
+
+
+ | Emprunts et dettes financières diverses |
+ |
+
+
+ | Avances et acomptes reçus sur commandes en cours |
+ |
+
+
+ | Dettes fournisseurs et comptes rattachés |
+ |
+
+
+ | Dettes fiscales et sociales |
+ |
+
+
+ | Dettes sur immobilisations et comptes rattachés |
+ |
+
+
+ | Autres dettes |
+ |
+
+
+ | Instruments de trésorerie |
+ |
+
+
+ | Produits constatés d'avance |
+ |
+
+
+ | TOTAL III |
+ |
+
+
+ | Écarts de conversion passif ( IV ) |
+ |
+
+
+ | TOTAL GÉNÉRAL (I + II + III + IV) |
+ |
+
+
+ | |
+ |
+
+
+ | ACTIF - PASSIF |
+ |
+
+
+
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index 856b56dbc73..36b8a348afd 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -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)
diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py
index 07efc907bb2..29a0fa00d67 100644
--- a/addons/product/pricelist.py
+++ b/addons/product/pricelist.py
@@ -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:
diff --git a/addons/product/product.py b/addons/product/product.py
index 10bc3dad2a7..43a152d4639 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -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"
diff --git a/addons/product/tests/__init__.py b/addons/product/tests/__init__.py
index 850189ac0f4..4a1ce43351b 100644
--- a/addons/product/tests/__init__.py
+++ b/addons/product/tests/__init__.py
@@ -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
index 00000000000..4e61cb11b75
--- /dev/null
+++ b/addons/product/tests/test_pricelist.py
@@ -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)
diff --git a/addons/product_visible_discount/product_visible_discount.py b/addons/product_visible_discount/product_visible_discount.py
index c27bb86c901..a654cd15276 100644
--- a/addons/product_visible_discount/product_visible_discount.py
+++ b/addons/product_visible_discount/product_visible_discount.py
@@ -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],
diff --git a/addons/purchase/views/report_purchaseorder.xml b/addons/purchase/views/report_purchaseorder.xml
index 91486c035aa..fd199a6db65 100644
--- a/addons/purchase/views/report_purchaseorder.xml
+++ b/addons/purchase/views/report_purchaseorder.xml
@@ -1,125 +1,129 @@
+
+
+
+
+
+
+ Shipping address :
+
+
+
+
+
+
+
+
Purchase Order Confirmation N°
+
Request for Quotation N°
+
+
+
+
Our Order Reference:
+
+
+
+
Your Order Reference
+
+
+
+
+
+
+
+
+
+ | Description |
+ Taxes |
+ Date Req. |
+ Qty |
+ Unit Price |
+ Net Price |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+ | Total Without Taxes |
+
+
+ |
+
+
+ | Taxes |
+
+
+ |
+
+
+ | Total |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- Shipping address :
-
-
-
-
-
-
-
-
Purchase Order Confirmation N°
-
Request for Quotation N°
-
-
-
-
Our Order Reference:
-
-
-
-
Your Order Reference
-
-
-
-
-
-
-
-
-
- | Description |
- Taxes |
- Date Req. |
- Qty |
- Unit Price |
- Net Price |
-
-
-
-
- |
-
- |
-
-
- |
-
-
- |
-
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
-
-
-
-
- | Total Without Taxes |
-
-
- |
-
-
- | Taxes |
-
-
- |
-
-
- | Total |
-
-
- |
-
-
-
-
-
-
-
-
-
+
+
diff --git a/addons/purchase/views/report_purchasequotation.xml b/addons/purchase/views/report_purchasequotation.xml
index 6abd64b2f33..60f1081eb06 100644
--- a/addons/purchase/views/report_purchasequotation.xml
+++ b/addons/purchase/views/report_purchasequotation.xml
@@ -1,67 +1,71 @@
+
+
+
+
+
+
+
+ Shipping address :
+
+
+
+
+
+
+
Request for Quotation
+
+
+
+
+ | Description |
+ Expected Date |
+ Qty |
+
+
+
+
+ |
+
+ |
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+
Regards,
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- Shipping address :
-
-
-
-
-
-
-
Request for Quotation
-
-
-
-
- | Description |
- Expected Date |
- Qty |
-
-
-
-
- |
-
- |
-
-
- |
-
-
-
- |
-
-
-
-
-
-
Regards,
-
-
-
-
-
+
+
diff --git a/addons/sale/wizard/sale_line_invoice.py b/addons/sale/wizard/sale_line_invoice.py
index 6666f20c351..b35328724e3 100644
--- a/addons/sale/wizard/sale_line_invoice.py
+++ b/addons/sale/wizard/sale_line_invoice.py
@@ -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!'))
diff --git a/addons/sale_margin/sale_margin.py b/addons/sale_margin/sale_margin.py
index 5cbb17873c1..b5c1821ab98 100644
--- a/addons/sale_margin/sale_margin.py
+++ b/addons/sale_margin/sale_margin.py
@@ -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)
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index ccbd78ba18b..ecaed0133a0 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -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
diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js
index b112783793f..fb5ae0d200a 100644
--- a/addons/web/static/src/js/openerpframework.js
+++ b/addons/web/static/src/js/openerpframework.js
@@ -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.
diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js
index 84e43c234ee..e3faabd496e 100644
--- a/addons/web_calendar/static/src/js/web_calendar.js
+++ b/addons/web_calendar/static/src/js/web_calendar.js
@@ -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,
diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py
index d9a435f1924..0e89eb13db2 100644
--- a/addons/web_diagram/controllers/main.py
+++ b/addons/web_diagram/controllers/main.py
@@ -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]
})
diff --git a/addons/web_graph/controllers/main.py b/addons/web_graph/controllers/main.py
index 320758e0950..0899fc7271f 100644
--- a/addons/web_graph/controllers/main.py
+++ b/addons/web_graph/controllers/main.py
@@ -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;")
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index c6c1c495a3a..645326e1804 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -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)
diff --git a/addons/website/static/lib/jQuery.transfo.js b/addons/website/static/lib/jQuery.transfo.js
index 725e5a02216..855d6f48560 100644
--- a/addons/website/static/lib/jQuery.transfo.js
+++ b/addons/website/static/lib/jQuery.transfo.js
@@ -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 = $(''
- + ''
+ + '
'
+ + '
'
+ '
'
- + ''
- + ''
- + ''
- + ''
+ + ''
+ + ''
+ + ''
+ + ''
+ '
'
+ '
'
+ '
'
@@ -147,6 +148,7 @@ OTHER DEALINGS IN THE SOFTWARE.
+ '
'
+ '
'
+ '
'
+ + '
'
+ '
');
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();
}
}
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
index 6147ada2ea3..90fbf3a4ce3 100644
--- a/addons/website/static/src/js/website.snippets.editor.js
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -1472,10 +1472,11 @@
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;");
diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py
index dc0eaad8534..8a52caa5716 100644
--- a/addons/website_blog/models/website_blog.py
+++ b/addons/website_blog/models/website_blog.py
@@ -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'),
}
diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml
index 726182d3822..f5a6430726f 100644
--- a/addons/website_blog/views/website_blog_templates.xml
+++ b/addons/website_blog/views/website_blog_templates.xml
@@ -131,11 +131,7 @@
-
-
![]()
-
+
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 44800e81db0..f809314a2a3 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -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'):
diff --git a/addons/website_sale/static/src/css/website_sale.css b/addons/website_sale/static/src/css/website_sale.css
index ce978eb3984..f8b637ea222 100644
--- a/addons/website_sale/static/src/css/website_sale.css
+++ b/addons/website_sale/static/src/css/website_sale.css
@@ -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;
diff --git a/addons/website_sale/static/src/css/website_sale.sass b/addons/website_sale/static/src/css/website_sale.sass
index d4804597127..98748768bae 100644
--- a/addons/website_sale/static/src/css/website_sale.sass
+++ b/addons/website_sale/static/src/css/website_sale.sass
@@ -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
diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js
index 34cf1fce863..6fbd2995125 100644
--- a/addons/website_sale/static/src/js/website_sale.js
+++ b/addons/website_sale/static/src/js/website_sale.js
@@ -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);
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index ee56a6b48e8..f56485b4942 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -823,7 +823,7 @@
-
+
@@ -862,7 +862,7 @@
t-att-data-shipping_country_id="shipping.country_id and shipping.country_id.id"
>
-
+
@@ -887,12 +887,12 @@
-
+
-
+ {%- if google_analytics_key -%}
+
+ {%- endif -%}
{%- endblock %}
diff --git a/doc/_themes/odoodoc/static/app.js b/doc/_themes/odoodoc/static/app.js
index 3619cd9cff1..a03d3b2ad27 100644
--- a/doc/_themes/odoodoc/static/app.js
+++ b/doc/_themes/odoodoc/static/app.js
@@ -25,6 +25,76 @@ $(function () {
});
}, 100);
+ // lang switcher
+ function findSheet(pattern, fromSheet) {
+ if (fromSheet) {
+ for(var i=0; i .highlight'
+ ].join('');
+ sheet.deleteRule(lastIndex);
+ sheet.insertRule(sel + '{' + content + '}', lastIndex);
+ });
+ })(); }
+
// Config ZeroClipboard
ZeroClipboard.config({
moviePath: '_static/ZeroClipboard.swf',
diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css
index 9b5468f1bdd..fba330944dd 100644
--- a/doc/_themes/odoodoc/static/style.css
+++ b/doc/_themes/odoodoc/static/style.css
@@ -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;
+}
diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less
index ca8be90d4c2..71076470e7b 100644
--- a/doc/_themes/odoodoc/static/style.less
+++ b/doc/_themes/odoodoc/static/style.less
@@ -17,7 +17,11 @@
// 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;
+ }
+}
diff --git a/doc/conf.py b/doc/conf.py
index 34f77c9ab76..fa6ca77771b 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -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,
diff --git a/doc/modules.rst b/doc/modules.rst
index 16133d13413..cb5ed8d6ca1 100644
--- a/doc/modules.rst
+++ b/doc/modules.rst
@@ -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
index 00000000000..f6a51f0f9b8
--- /dev/null
+++ b/doc/modules/api_integration.rst
@@ -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 =
+ db =
+ username = 'admin'
+ password =
+
+ .. code-block:: ruby
+
+ url =
+ db =
+ username = "admin"
+ password =
+
+ .. code-block:: php
+
+ $url = ;
+ $db = ;
+ $username = "admin";
+ $password = ;
+
+ .. code-block:: java
+
+ final String url = ,
+ db = ,
+ username = "admin",
+ password = ;
+
+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 info = (Map)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 `_
+ library, which provides a simple XML-RPC API. Ripcord requires that
+ `XML-RPC support be enabled
+ `_ in your PHP
+ installation.
+
+ Since calls are performed over
+ `HTTPS `_, it also requires that
+ the `OpenSSL extension
+ `_ be enabled.
+
+ .. note::
+ :class: only-java
+
+ These examples use the `Apache XML-RPC library
+ `_
+
+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 ` 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 ` 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> fields =
+ (Map>)models.execute("execute_kw", Arrays.asList(
+ db, uid, password,
+ "res.partner", "fields_get",
+ Collections.emptyList()));
+ // filter keys of field attributes for display
+ final List allowed = Arrays.asList("string", "help", "type");
+ new HashMap>() {{
+ for(Entry> item: fields.entrySet()) {
+ put(item.getKey(), new HashMap() {{
+ for(Entry 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
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index e3c72f17241..aa14400aaa2 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -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 = []
diff --git a/openerp/fields.py b/openerp/fields.py
index ae8f4376593..52d594df659 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -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
diff --git a/openerp/models.py b/openerp/models.py
index 8f9e5ec0866..bfc768b52dd 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -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 `.
@@ -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():
diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py
index 679fae0cfc2..34672c44a78 100644
--- a/openerp/modules/registry.py
+++ b/openerp/modules/registry.py
@@ -23,13 +23,14 @@
"""
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. """
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index 30fe1f9a5eb..9506a5ca4fa 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -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
diff --git a/openerp/service/common.py b/openerp/service/common.py
index 97f36b7f964..16cbb5d2aa2 100644
--- a/openerp/service/common.py
+++ b/openerp/service/common.py
@@ -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]
diff --git a/openerp/sql_db.py b/openerp/sql_db.py
index 739c32a4f12..589da7ffe3b 100644
--- a/openerp/sql_db.py
+++ b/openerp/sql_db.py
@@ -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):
diff --git a/openerp/tools/func.py b/openerp/tools/func.py
index 03b4f76669a..f04bacd3ee8 100644
--- a/openerp/tools/func.py
+++ b/openerp/tools/func.py
@@ -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:
diff --git a/openerp/tools/image.py b/openerp/tools/image.py
index aa110d7825e..0b02d27a631 100644
--- a/openerp/tools/image.py
+++ b/openerp/tools/image.py
@@ -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