summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2014-11-17 17:31:12 +0100
committerChristophe Simonis <chs@odoo.com>2014-11-17 17:31:12 +0100
commitd64fe8279b4a1d3083a87b7e5fb211ddbbae50ca (patch)
tree207e10e9f6f69206862c6329a5dcd25be5d15f9b
parentb2563509ffd89fdadc1256cb8118d7106f36e2e3 (diff)
parentc92e70b929376ba742ea17187ec521cf706302e5 (diff)
[MERGE] forward port of branch 8.0 up to c92e70b
-rw-r--r--addons/account/account.py15
-rw-r--r--addons/account/account_assert_test.xml4
-rw-r--r--addons/account_voucher/account_voucher.py3
-rw-r--r--addons/crm/crm_lead.py2
-rw-r--r--addons/delivery/sale.py6
-rw-r--r--addons/delivery/stock.py21
-rw-r--r--addons/fleet/fleet.py6
-rw-r--r--addons/google_calendar/google_calendar.py6
-rw-r--r--addons/hr/static/src/xml/suggestions.xml2
-rw-r--r--addons/hr_recruitment/hr_recruitment.py2
-rw-r--r--addons/hr_timesheet/security/ir.model.access.csv1
-rw-r--r--addons/hr_timesheet_sheet/security/ir.model.access.csv1
-rw-r--r--addons/mail/static/src/xml/suggestions.xml2
-rw-r--r--addons/mrp/mrp.py7
-rw-r--r--addons/point_of_sale/security/point_of_sale_security.xml6
-rw-r--r--addons/point_of_sale/static/src/css/pos.css3
-rw-r--r--addons/point_of_sale/static/src/js/models.js7
-rw-r--r--addons/portal/__openerp__.py2
-rw-r--r--addons/product/product.py8
-rw-r--r--addons/project_timesheet/project_timesheet.py110
-rw-r--r--addons/purchase/purchase.py15
-rw-r--r--addons/sale/sale.py5
-rw-r--r--addons/sale_journal/sale_journal.py26
-rw-r--r--addons/sale_stock/sale_stock_view.xml14
-rw-r--r--addons/stock/procurement.py4
-rw-r--r--addons/stock/stock.py28
-rw-r--r--addons/stock/stock_view.xml4
-rw-r--r--addons/stock/wizard/stock_transfer_details.py2
-rw-r--r--addons/stock_account/stock_account.py12
-rw-r--r--addons/web/static/src/js/view_form.js1
-rw-r--r--addons/website/views/website_templates.xml2
-rw-r--r--addons/website_mail_group/views/website_mail_group.xml2
-rw-r--r--addons/website_sale/controllers/main.py1
-rw-r--r--addons/website_sale_delivery/models/sale_order.py8
-rw-r--r--doc/_themes/odoodoc/layout.html5
-rw-r--r--doc/_themes/odoodoc/sphinx_monkeypatch.py43
-rw-r--r--doc/_themes/odoodoc/static/style.css115
-rw-r--r--doc/_themes/odoodoc/static/style.less73
-rw-r--r--doc/api_integration.rst (renamed from doc/modules/api_integration.rst)6
-rw-r--r--doc/howtos/backend.rst2
-rw-r--r--doc/images/view-on-github.pngbin1493 -> 0 bytes
-rw-r--r--doc/index.rst31
-rw-r--r--doc/modules.rst8
-rw-r--r--doc/reference/cmdline.rst2
-rw-r--r--doc/reference/orm.rst332
-rw-r--r--openerp/addons/base/ir/ir_model.py10
-rw-r--r--openerp/addons/test_new_api/tests/test_new_fields.py3
-rw-r--r--openerp/api.py18
-rw-r--r--openerp/fields.py10
-rw-r--r--openerp/models.py207
-rw-r--r--openerp/modules/registry.py3
-rw-r--r--openerp/osv/fields.py5
-rw-r--r--openerp/tests/common.py20
53 files changed, 745 insertions, 486 deletions
diff --git a/addons/account/account.py b/addons/account/account.py
index d74bfce2f08..d4c34f49922 100644
--- a/addons/account/account.py
+++ b/addons/account/account.py
@@ -1154,6 +1154,19 @@ class account_move(osv.osv):
_description = "Account Entry"
_order = 'id desc'
+ def account_assert_balanced(self, cr, uid, context=None):
+ cr.execute("""\
+ SELECT move_id
+ FROM account_move_line
+ WHERE state = 'valid'
+ GROUP BY move_id
+ HAVING abs(sum(debit) - sum(credit)) > 0.00001
+ """)
+ assert len(cr.fetchall()) == 0, \
+ "For all Journal Items, the state is valid implies that the sum " \
+ "of credits equals the sum of debits"
+ return True
+
def account_move_prepare(self, cr, uid, journal_id, date=False, ref='', company_id=False, context=None):
'''
Prepares and returns a dictionary of values, ready to be passed to create() based on the parameters received.
@@ -2023,7 +2036,7 @@ class account_tax(osv.osv):
data['tax_amount']=quantity
# data['amount'] = quantity
elif tax.type=='code':
- localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner}
+ localdict = {'price_unit':cur_price_unit, 'product':product, 'partner':partner, 'quantity': quantity}
exec tax.python_compute in localdict
amount = localdict['result']
data['amount'] = amount
diff --git a/addons/account/account_assert_test.xml b/addons/account/account_assert_test.xml
index 88025514763..32e8afb1b50 100644
--- a/addons/account/account_assert_test.xml
+++ b/addons/account/account_assert_test.xml
@@ -1,8 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
- <assert model="account.move" search="[]" string="For all Journal Items, the state is valid implies that the sum of credits equals the sum of debits">
- <test expr="not len(line_id) or line_id[0].state != 'valid' or (sum([l.debit - l.credit for l in line_id]) &lt;= 0.00001)"/>
- </assert>
+ <function name="account_assert_balanced" model="account.move"/>
</data>
</openerp>
diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py
index 22bce5306be..9724451df5f 100644
--- a/addons/account_voucher/account_voucher.py
+++ b/addons/account_voucher/account_voucher.py
@@ -125,6 +125,9 @@ class account_voucher(osv.osv):
journal_pool = self.pool.get('account.journal')
journal_id = context.get('journal_id', False)
if journal_id:
+ if isinstance(journal_id, (list, tuple)):
+ # sometimes journal_id is a pair (id, display_name)
+ journal_id = journal_id[0]
journal = journal_pool.browse(cr, uid, journal_id, context=context)
if journal.currency:
return journal.currency.id
diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py
index 2c06865df44..e259de4cb44 100644
--- a/addons/crm/crm_lead.py
+++ b/addons/crm/crm_lead.py
@@ -789,7 +789,7 @@ class crm_lead(format_address, osv.osv):
partner_id = self._create_lead_partner(cr, uid, lead, context)
self.pool['res.partner'].write(cr, uid, partner_id, {'team_id': lead.team_id and lead.team_id.id or False})
if partner_id:
- lead.write({'partner_id': partner_id}, context=context)
+ lead.write({'partner_id': partner_id})
partner_ids[lead.id] = partner_id
return partner_ids
diff --git a/addons/delivery/sale.py b/addons/delivery/sale.py
index 4ef3224fc3e..a7e8b5d3716 100644
--- a/addons/delivery/sale.py
+++ b/addons/delivery/sale.py
@@ -53,10 +53,6 @@ class sale_order(osv.Model):
result['value']['carrier_id'] = dtype
return result
- def _prepare_order_picking(self, cr, uid, order, context=None):
- result = super(sale_order, self)._prepare_order_picking(cr, uid, order, context=context)
- result.update(carrier_id=order.carrier_id.id)
- return result
def _delivery_unset(self, cr, uid, ids, context=None):
sale_obj = self.pool['sale.order.line']
@@ -92,4 +88,4 @@ class sale_order(osv.Model):
'price_unit': grid_obj.get_price(cr, uid, grid.id, order, time.strftime('%Y-%m-%d'), context),
'tax_id': [(6, 0, taxes_ids)],
'is_delivery': True
- })
+ }) \ No newline at end of file
diff --git a/addons/delivery/stock.py b/addons/delivery/stock.py
index 7bfd52df420..c8781244fe0 100644
--- a/addons/delivery/stock.py
+++ b/addons/delivery/stock.py
@@ -120,9 +120,10 @@ class stock_picking(osv.osv):
}
def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
+ invoice_obj = self.pool.get('account.invoice')
invoice_line_obj = self.pool.get('account.invoice.line')
invoice_id = super(stock_picking, self)._create_invoice_from_picking(cr, uid, picking, vals, context=context)
- invoice = self.browse(cr, uid, invoice_id, context=context)
+ invoice = invoice_obj.browse(cr, uid, invoice_id, context=context)
invoice_line = self._prepare_shipping_invoice_line(cr, uid, picking, invoice, context=context)
if invoice_line:
invoice_line_obj.create(cr, uid, invoice_line)
@@ -170,6 +171,24 @@ class stock_move(osv.osv):
'weight_uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True,readonly="1",help="Unit of Measure (Unit of Measure) is the unit of measurement for Weight",),
}
+ def action_confirm(self, cr, uid, ids, context=None):
+ """
+ Pass the carrier to the picking from the sales order
+ (Should also work in case of Phantom BoMs when on explosion the original move is deleted)
+ """
+ procs_to_check = []
+ for move in self.browse(cr, uid, ids, context=context):
+ if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.carrier_id:
+ procs_to_check += [move.procurement_id]
+ res = super(stock_move, self).action_confirm(cr, uid, ids, context=context)
+ pick_obj = self.pool.get("stock.picking")
+ for proc in procs_to_check:
+ pickings = list(set([x.picking_id.id for x in proc.move_ids if x.picking_id and not x.picking_id.carrier_id]))
+ if pickings:
+ pick_obj.write(cr, uid, pickings, {'carrier_id': proc.sale_line_id.order_id.carrier_id.id}, context=context)
+ return res
+
+
def _get_default_uom(self, cr, uid, context=None):
uom_categ_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'product.product_uom_categ_kgm')
return self.pool.get('product.uom').search(cr, uid, [('category_id', '=', uom_categ_id),('factor','=',1)])[0]
diff --git a/addons/fleet/fleet.py b/addons/fleet/fleet.py
index 6f62d114c0d..1b2b96ffbfb 100644
--- a/addons/fleet/fleet.py
+++ b/addons/fleet/fleet.py
@@ -426,8 +426,10 @@ class fleet_vehicle_odometer(osv.Model):
res = {}
for record in self.browse(cr, uid, ids, context=context):
name = record.vehicle_id.name
- if record.date:
- name = name+ ' / '+ str(record.date)
+ if not name:
+ name = record.date
+ elif record.date:
+ name += ' / '+ record.date
res[record.id] = name
return res
diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py
index 03a311dd2ff..3d1a87661d2 100644
--- a/addons/google_calendar/google_calendar.py
+++ b/addons/google_calendar/google_calendar.py
@@ -494,7 +494,7 @@ class google_calendar(osv.AbstractModel):
all_my_attendees = self.pool['calendar.attendee'].search(cr, uid, [('partner_id', '=', current_user.partner_id.id)], context=context)
self.pool['calendar.attendee'].write(cr, uid, all_my_attendees, {'oe_synchro_date': False, 'google_internal_event_id': False}, context=context)
- current_user.write(reset_data, context=context)
+ current_user.write(reset_data)
return True
def synchronize_events_cron(self, cr, uid, context=None):
@@ -552,7 +552,7 @@ class google_calendar(osv.AbstractModel):
lastSync = False
_logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO FORCED" % user_to_sync)
else:
- current_user.write({'google_calendar_cal_id': current_google}, context=context)
+ current_user.write({'google_calendar_cal_id': current_google})
lastSync = False
_logger.info("[%s] Calendar Synchro - MODE FULL SYNCHRO - NEW CAL ID" % user_to_sync)
@@ -562,7 +562,7 @@ class google_calendar(osv.AbstractModel):
res = self.update_events(cr, uid, lastSync, context)
- current_user.write({'google_calendar_last_sync_date': ask_time}, context=context)
+ current_user.write({'google_calendar_last_sync_date': ask_time})
return {
"status": res and "need_refresh" or "no_new_event_form_google",
"url": ''
diff --git a/addons/hr/static/src/xml/suggestions.xml b/addons/hr/static/src/xml/suggestions.xml
index 816011852ca..6b7fbd43bf7 100644
--- a/addons/hr/static/src/xml/suggestions.xml
+++ b/addons/hr/static/src/xml/suggestions.xml
@@ -24,7 +24,7 @@
</div>
<div class="oe_suggested_item_content">
<a class="oe_suggestion_item_name" t-attf-href="#model=hr.employee&amp;id=#{result.id}"><t t-esc="result.name"/></a>
- <a class="oe_suggestion_remove_item oe_suggestion_employee oe_e" t-attf-id="{result.id}">X</a>
+ <a class="oe_suggestion_remove_item oe_suggestion_employee oe_e" t-att-id="result.id">X</a>
<br/>
<button class="oe_suggestion_follow" t-att-id="result.id">Follow</button>
</div>
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index 071441a2c81..833da7ca684 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -493,7 +493,7 @@ class hr_applicant(osv.Model):
address_id = self.pool.get('res.partner').address_get(cr, uid, [applicant.partner_id.id], ['contact'])['contact']
contact_name = self.pool.get('res.partner').name_get(cr, uid, [applicant.partner_id.id])[0][1]
if applicant.job_id and (applicant.partner_name or contact_name):
- applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1}, context=context)
+ applicant.job_id.write({'no_of_hired_employee': applicant.job_id.no_of_hired_employee + 1})
create_ctx = dict(context, mail_broadcast=True)
emp_id = hr_employee.create(cr, uid, {'name': applicant.partner_name or contact_name,
'job_id': applicant.job_id.id,
diff --git a/addons/hr_timesheet/security/ir.model.access.csv b/addons/hr_timesheet/security/ir.model.access.csv
index b48988700db..757cd84f4b5 100644
--- a/addons/hr_timesheet/security/ir.model.access.csv
+++ b/addons/hr_timesheet/security/ir.model.access.csv
@@ -10,3 +10,4 @@ access_product_uom_hr_timesheet,product.uom.hr.timesheet,product.model_product_u
access_account_fiscalyear_hr_user,account.account.fiscalyear.user,account.model_account_fiscalyear,base.group_hr_user,1,1,1,1
access_hr_analytic_timesheet_user,hr.analytic.timesheet.user,model_hr_analytic_timesheet,base.group_user,1,1,1,0
access_account_analytic_journal_user,account.account.analytic.journal.user,analytic.model_account_analytic_journal,base.group_user,1,0,0,0
+access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
diff --git a/addons/hr_timesheet_sheet/security/ir.model.access.csv b/addons/hr_timesheet_sheet/security/ir.model.access.csv
index 8eef30b35c4..2ad3c7d5ca2 100644
--- a/addons/hr_timesheet_sheet/security/ir.model.access.csv
+++ b/addons/hr_timesheet_sheet/security/ir.model.access.csv
@@ -3,6 +3,5 @@ access_hr_timesheet_sheet_sheet_user,hr_timesheet_sheet.sheet.user,model_hr_time
access_hr_timesheet_sheet_sheet_system_employee,hr_timesheet_sheet.sheet.system.employee,model_hr_timesheet_sheet_sheet,base.group_user,1,1,1,0
access_hr_timesheet_sheet_sheet_day,hr_timesheet_sheet.sheet.day,model_hr_timesheet_sheet_sheet_day,base.group_hr_user,1,1,1,1
access_hr_timesheet_sheet_sheet_account,hr_timesheet_sheet.sheet.account,model_hr_timesheet_sheet_sheet_account,base.group_hr_user,1,1,1,1
-access_hr_timesheet_report,hr.timesheet.report,model_hr_timesheet_report,base.group_hr_manager,1,1,0,0
access_hr_analytic_timesheet_system_user,hr.analytic.timesheet.system.user,model_hr_analytic_timesheet,base.group_user,1,0,0,0
access_hr_timesheet_sheet_sheet_day,hr.timesheet.sheet.sheet.day.user,model_hr_timesheet_sheet_sheet_day,base.group_user,1,1,1,0
diff --git a/addons/mail/static/src/xml/suggestions.xml b/addons/mail/static/src/xml/suggestions.xml
index 8279726ca45..52bc0aea4eb 100644
--- a/addons/mail/static/src/xml/suggestions.xml
+++ b/addons/mail/static/src/xml/suggestions.xml
@@ -25,7 +25,7 @@
</div>
<div class="oe_suggested_item_content">
<a class="oe_suggestion_item_name" t-attf-href="#model=mail.group&amp;id=#{result.id}"><t t-esc="result.name"/></a>
- <a class="oe_suggestion_remove_item oe_suggestion_group oe_e" t-attf-id="{result.id}">X</a>
+ <a class="oe_suggestion_remove_item oe_suggestion_group oe_e" t-att-id="result.id">X</a>
<br/>
<button t-att-id="result.id" class="oe_suggestion_join">Join Group</button>
</div>
diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py
index 8b3da7b49c1..914cfc376a6 100644
--- a/addons/mrp/mrp.py
+++ b/addons/mrp/mrp.py
@@ -334,6 +334,11 @@ class mrp_bom(osv.osv):
res['value'].update({'product_uom': product.uom_id.id})
return res
+ def unlink(self, cr, uid, ids, context=None):
+ if self.pool['mrp.production'].search(cr, uid, [('bom_id', 'in', ids), ('state', 'not in', ['done', 'cancel'])], context=context):
+ raise osv.except_osv(_('Warning!'), _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'))
+ return super(mrp_bom, self).unlink(cr, uid, ids, context=context)
+
def onchange_product_tmpl_id(self, cr, uid, ids, product_tmpl_id, product_qty=0, context=None):
""" Changes UoM and name if product_id changes.
@param product_id: Changed product_id
@@ -1200,7 +1205,7 @@ class mrp_production(osv.osv):
self._make_service_procurement(cr, uid, line, context=context)
if stock_moves:
self.pool.get('stock.move').action_confirm(cr, uid, stock_moves, context=context)
- production.write({'state': 'confirmed'}, context=context)
+ production.write({'state': 'confirmed'})
return 0
def action_assign(self, cr, uid, ids, context=None):
diff --git a/addons/point_of_sale/security/point_of_sale_security.xml b/addons/point_of_sale/security/point_of_sale_security.xml
index 58096a0b21d..1a472061220 100644
--- a/addons/point_of_sale/security/point_of_sale_security.xml
+++ b/addons/point_of_sale/security/point_of_sale_security.xml
@@ -25,5 +25,11 @@
<field name="global" eval="True" />
<field name="domain_force">[('company_id','child_of',[user.company_id.id])]</field>
</record>
+ <record id="rule_pos_order_report_multi_company" model="ir.rule">
+ <field name="name">Point Of Sale Order Analysis multi-company</field>
+ <field name="model_id" ref="model_report_pos_order"/>
+ <field name="global" eval="True"/>
+ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
+ </record>
</data>
</openerp>
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 2f647bbcb15..4e630488c96 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -1169,6 +1169,9 @@ td {
}
@media print {
+ * {
+ color: black !important;
+ }
body {
margin: 0;
}
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index c746d12c8fc..07be5f01bb2 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -811,10 +811,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_unit_price: function(){
return this.price;
},
- get_display_price: function(){
+ get_base_price: function(){
var rounding = this.pos.currency.rounding;
return round_pr(round_pr(this.get_unit_price() * this.get_quantity(),rounding) * (1- this.get_discount()/100.0),rounding);
},
+ get_display_price: function(){
+ return this.get_base_price();
+ },
get_price_without_tax: function(){
return this.get_all_prices().priceWithoutTax;
},
@@ -830,7 +833,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_all_prices: function(){
var self = this;
var currency_rounding = this.pos.currency.rounding;
- var base = round_pr(this.get_quantity() * this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)), currency_rounding);
+ var base = this.get_base_price();
var totalTax = base;
var totalNoTax = base;
diff --git a/addons/portal/__openerp__.py b/addons/portal/__openerp__.py
index 361971b0d92..042f2f20381 100644
--- a/addons/portal/__openerp__.py
+++ b/addons/portal/__openerp__.py
@@ -33,7 +33,7 @@
Customize access to your OpenERP database to external users by creating portals.
================================================================================
A portal defines a specific user menu and access rights for its members. This
-menu can ben seen by portal members, public users and any other user that
+menu can be seen by portal members, public users and any other user that
have the access to technical features (e.g. the administrator).
Also, each portal member is linked to a specific partner.
diff --git a/addons/product/product.py b/addons/product/product.py
index 3bcdf41cc2b..128aae5b1f7 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -847,7 +847,7 @@ class product_product(osv.osv):
context['uom'], value, uom.id)
value = value - product.price_extra
- return product.write({'list_price': value}, context=context)
+ return product.write({'list_price': value})
def _get_partner_code_name(self, cr, uid, ids, product, partner_id, context=None):
for supinfo in product.seller_ids:
@@ -898,8 +898,8 @@ class product_product(osv.osv):
res = self.write(cr, uid, [id], {'image_variant': image}, context=context)
product = self.browse(cr, uid, id, context=context)
if not product.product_tmpl_id.image:
- product.write({'image_variant': None}, context=context)
- product.product_tmpl_id.write({'image': image}, context=context)
+ product.write({'image_variant': None})
+ product.product_tmpl_id.write({'image': image})
return res
def _get_price_extra(self, cr, uid, ids, name, args, context=None):
@@ -1062,7 +1062,7 @@ class product_product(osv.osv):
if not limit or len(ids) < limit:
# we may underrun the limit because of dupes in the results, that's fine
limit2 = (limit - len(ids)) if limit else False
- ids.update(self.search(cr, user, args + [('name', operator, name)], limit=limit2, context=context))
+ ids.update(self.search(cr, user, args + [('name', operator, name), ('id', 'not in', list(ids))], limit=limit2, context=context))
ids = list(ids)
elif not ids and operator in expression.NEGATIVE_TERM_OPERATORS:
ids = self.search(cr, user, args + ['&', ('default_code', operator, name), ('name', operator, name)], limit=limit, context=context)
diff --git a/addons/project_timesheet/project_timesheet.py b/addons/project_timesheet/project_timesheet.py
index 39f48c47ea9..36e57341b2d 100644
--- a/addons/project_timesheet/project_timesheet.py
+++ b/addons/project_timesheet/project_timesheet.py
@@ -103,50 +103,57 @@ class project_work(osv.osv):
res['product_uom_id'] = emp.product_id.uom_id.id
return res
- def create(self, cr, uid, vals, *args, **kwargs):
- timesheet_obj = self.pool.get('hr.analytic.timesheet')
- task_obj = self.pool.get('project.task')
- uom_obj = self.pool.get('product.uom')
+ def _create_analytic_entries(self, cr, uid, vals, context):
+ """Create the hr analytic timesheet from project task work"""
+ timesheet_obj = self.pool['hr.analytic.timesheet']
+ task_obj = self.pool['project.task']
vals_line = {}
+ timeline_id = False
+ acc_id = False
+
+ task_obj = task_obj.browse(cr, uid, vals['task_id'], context=context)
+ result = self.get_user_related_details(cr, uid, vals.get('user_id', uid))
+ vals_line['name'] = '%s: %s' % (tools.ustr(task_obj.name), tools.ustr(vals['name'] or '/'))
+ vals_line['user_id'] = vals['user_id']
+ vals_line['product_id'] = result['product_id']
+ if vals.get('date'):
+ vals_line['date' ] = vals['date'][:10]
+
+ # Calculate quantity based on employee's product's uom
+ vals_line['unit_amount'] = vals['hours']
+
+ default_uom = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.project_time_mode_id.id
+ if result['product_uom_id'] != default_uom:
+ vals_line['unit_amount'] = self.pool['product.uom']._compute_qty(cr, uid, default_uom, vals['hours'], result['product_uom_id'])
+ acc_id = task_obj.project_id and task_obj.project_id.analytic_account_id.id or acc_id
+ if acc_id:
+ vals_line['account_id'] = acc_id
+ res = timesheet_obj.on_change_account_id(cr, uid, False, acc_id)
+ if res.get('value'):
+ vals_line.update(res['value'])
+ vals_line['general_account_id'] = result['general_account_id']
+ vals_line['journal_id'] = result['journal_id']
+ vals_line['amount'] = 0.0
+ vals_line['product_uom_id'] = result['product_uom_id']
+ amount = vals_line['unit_amount']
+ prod_id = vals_line['product_id']
+ unit = False
+ timeline_id = timesheet_obj.create(cr, uid, vals=vals_line, context=context)
+
+ # Compute based on pricetype
+ amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,
+ prod_id, amount, False, unit, vals_line['journal_id'], context=context)
+ if amount_unit and 'amount' in amount_unit.get('value',{}):
+ updv = { 'amount': amount_unit['value']['amount'] }
+ timesheet_obj.write(cr, uid, [timeline_id], updv, context=context)
+
+ return timeline_id
+
+ def create(self, cr, uid, vals, *args, **kwargs):
context = kwargs.get('context', {})
if not context.get('no_analytic_entry',False):
- task_obj = task_obj.browse(cr, uid, vals['task_id'])
- result = self.get_user_related_details(cr, uid, vals.get('user_id', uid))
- vals_line['name'] = '%s: %s' % (tools.ustr(task_obj.name), tools.ustr(vals['name'] or '/'))
- vals_line['user_id'] = vals['user_id']
- vals_line['product_id'] = result['product_id']
- vals_line['date'] = vals['date'][:10]
-
- # Calculate quantity based on employee's product's uom
- vals_line['unit_amount'] = vals['hours']
-
- default_uom = self.pool.get('res.users').browse(cr, uid, uid).company_id.project_time_mode_id.id
- if result['product_uom_id'] != default_uom:
- vals_line['unit_amount'] = uom_obj._compute_qty(cr, uid, default_uom, vals['hours'], result['product_uom_id'])
- acc_id = task_obj.project_id and task_obj.project_id.analytic_account_id.id or False
- if acc_id:
- vals_line['account_id'] = acc_id
- res = timesheet_obj.on_change_account_id(cr, uid, False, acc_id)
- if res.get('value'):
- vals_line.update(res['value'])
- vals_line['general_account_id'] = result['general_account_id']
- vals_line['journal_id'] = result['journal_id']
- vals_line['amount'] = 0.0
- vals_line['product_uom_id'] = result['product_uom_id']
- amount = vals_line['unit_amount']
- prod_id = vals_line['product_id']
- unit = False
- context = dict(context, recompute=True)
- timeline_id = timesheet_obj.create(cr, uid, vals_line, context=context)
-
- # Compute based on pricetype
- amount_unit = timesheet_obj.on_change_unit_amount(cr, uid, timeline_id,
- prod_id, amount, False, unit, vals_line['journal_id'], context=context)
- if amount_unit and 'amount' in amount_unit.get('value',{}):
- updv = { 'amount': amount_unit['value']['amount'] }
- timesheet_obj.write(cr, uid, [timeline_id], updv, context=context)
- vals['hr_analytic_timesheet_id'] = timeline_id
+ vals['hr_analytic_timesheet_id'] = self._create_analytic_entries(cr, uid, vals, context=context)
return super(project_work,self).create(cr, uid, vals, *args, **kwargs)
def write(self, cr, uid, ids, vals, context=None):
@@ -234,6 +241,10 @@ class task(osv.osv):
def write(self, cr, uid, ids, vals, context=None):
if context is None:
context = {}
+ task_work_obj = self.pool['project.task.work']
+ acc_id = False
+ missing_analytic_entries = {}
+
if vals.get('project_id',False) or vals.get('name',False):
vals_line = {}
hr_anlytic_timesheet = self.pool.get('hr.analytic.timesheet')
@@ -245,6 +256,16 @@ class task(osv.osv):
if len(task_obj.work_ids):
for task_work in task_obj.work_ids:
if not task_work.hr_analytic_timesheet_id:
+ if acc_id :
+ # missing timesheet activities to generate
+ missing_analytic_entries[task_work.id] = {
+ 'name' : task_work.name,
+ 'user_id' : task_work.user_id.id,
+ 'date' : task_work.date and task_work.date[:10] or False,
+ 'account_id': acc_id,
+ 'hours' : task_work.hours,
+ 'task_id' : task_obj.id
+ }
continue
line_id = task_work.hr_analytic_timesheet_id.id
if vals.get('project_id',False):
@@ -252,7 +273,14 @@ class task(osv.osv):
if vals.get('name',False):
vals_line['name'] = '%s: %s' % (tools.ustr(vals['name']), tools.ustr(task_work.name) or '/')
hr_anlytic_timesheet.write(cr, uid, [line_id], vals_line, {})
- return super(task,self).write(cr, uid, ids, vals, context)
+
+ res = super(task,self).write(cr, uid, ids, vals, context)
+
+ for task_work_id, analytic_entry in missing_analytic_entries.items():
+ timeline_id = task_work_obj._create_analytic_entries(cr, uid, analytic_entry, context=context)
+ task_work_obj.write(cr, uid, task_work_id, {'hr_analytic_timesheet_id' : timeline_id}, context=context)
+
+ return res
class res_partner(osv.osv):
diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py
index 338437a5ecf..5dd100c6905 100644
--- a/addons/purchase/purchase.py
+++ b/addons/purchase/purchase.py
@@ -150,7 +150,20 @@ class purchase_order(osv.osv):
def _get_picking_in(self, cr, uid, context=None):
obj_data = self.pool.get('ir.model.data')
- return obj_data.get_object_reference(cr, uid, 'stock','picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock','picking_type_in')[1] or False
+ type_obj = self.pool.get('stock.picking.type')
+ user_obj = self.pool.get('res.users')
+ company_id = user_obj.browse(cr, uid, uid, context=context).company_id.id
+ pick_type = obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in') and obj_data.get_object_reference(cr, uid, 'stock', 'picking_type_in')[1] or False
+ if pick_type:
+ type = type_obj.browse(cr, uid, pick_type, context=context)
+ if type and type.warehouse_id and type.warehouse_id.company_id.id == company_id:
+ return pick_type
+ types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)], context=context)
+ if not types:
+ types = type_obj.search(cr, uid, [('code', '=', 'incoming'), ('warehouse_id', '=', False)], context=context)
+ if not types:
+ raise osv.except_osv(_('Error!'), _("Make sure you have at least an incoming picking type defined"))
+ return types[0]
def _get_picking_ids(self, cr, uid, ids, field_names, args, context=None):
res = {}
diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index af257f8d22e..2cbc975ae4c 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -701,7 +701,7 @@ class sale_order(osv.osv):
vals = self._prepare_procurement_group(cr, uid, order, context=context)
if not order.procurement_group_id:
group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
- order.write({'procurement_group_id': group_id}, context=context)
+ order.write({'procurement_group_id': group_id})
for line in order.order_line:
#Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
@@ -711,10 +711,11 @@ class sale_order(osv.osv):
line.refresh()
#run again procurement that are in exception in order to trigger another move
proc_ids += [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
+ procurement_obj.reset_to_confirmed(cr, uid, proc_ids, context=context)
elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
if (line.state == 'done') or not line.product_id:
continue
- vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context)
+ vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
proc_id = procurement_obj.create(cr, uid, vals, context=context)
proc_ids.append(proc_id)
#Confirm procurement order such that rules will be applied on it
diff --git a/addons/sale_journal/sale_journal.py b/addons/sale_journal/sale_journal.py
index 076692cbf4a..f6e4abb6b53 100644
--- a/addons/sale_journal/sale_journal.py
+++ b/addons/sale_journal/sale_journal.py
@@ -61,17 +61,33 @@ class picking(osv.osv):
}
+class stock_move(osv.osv):
+ _inherit = "stock.move"
+
+ def action_confirm(self, cr, uid, ids, context=None):
+ """
+ Pass the invoice type to the picking from the sales order
+ (Should also work in case of Phantom BoMs when on explosion the original move is deleted, similar to carrier_id on delivery)
+ """
+ procs_to_check = []
+ for move in self.browse(cr, uid, ids, context=context):
+ if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.invoice_type_id:
+ procs_to_check += [move.procurement_id]
+ res = super(stock_move, self).action_confirm(cr, uid, ids, context=context)
+ pick_obj = self.pool.get("stock.picking")
+ for proc in procs_to_check:
+ pickings = list(set([x.picking_id.id for x in proc.move_ids if x.picking_id and not x.picking_id.invoice_type_id]))
+ if pickings:
+ pick_obj.write(cr, uid, pickings, {'invoice_type_id': proc.sale_line_id.order_id.invoice_type_id.id}, context=context)
+ return res
+
+
class sale(osv.osv):
_inherit = "sale.order"
_columns = {
'invoice_type_id': fields.many2one('sale_journal.invoice.type', 'Invoice Type', help="Generate invoice based on the selected option.")
}
- def _prepare_order_picking(self, cr, uid, order, context=None):
- result = super(sale,self)._prepare_order_picking(cr, uid, order, context=context)
- result.update(invoice_type_id=order.invoice_type_id and order.invoice_type_id.id or False)
- return result
-
def onchange_partner_id(self, cr, uid, ids, part, context=None):
result = super(sale, self).onchange_partner_id(cr, uid, ids, part, context=context)
if part:
diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml
index 0d5223a03c8..404316324f5 100644
--- a/addons/sale_stock/sale_stock_view.xml
+++ b/addons/sale_stock/sale_stock_view.xml
@@ -38,17 +38,11 @@
<xpath expr="//field[@name='order_line']/tree//field[@name='product_uom_qty']" position="replace">
<field context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
name="product_uom_qty"
- on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
- </xpath>
- <xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="replace">
- <field name="product_id"
- context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
- groups="base.group_user"
- on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position, False, parent.warehouse_id, context)"/>
+ on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,False,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
</xpath>
- <xpath expr="//field[@name='order_line']/form//field[@name='product_uom_qty']" position="replace">
- <field context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
- name="product_uom_qty" class="oe_inline"
+ <xpath expr="//field[@name='order_line']/form//field[@name='product_uos_qty']" position="replace">
+ <field context="{'partner_id':parent.partner_id, 'quantity':product_uos_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
+ name="product_uos_qty" class="oe_inline"
on_change="product_id_change_with_wh(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,product_uos,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, context)"/>
</xpath>
diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py
index 07024a2529e..c105ed6c017 100644
--- a/addons/stock/procurement.py
+++ b/addons/stock/procurement.py
@@ -273,7 +273,7 @@ class procurement_order(osv.osv):
@param context: A standard dictionary for contextual values
@return: Dictionary of values
'''
- super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, context=context)
+ super(procurement_order, self).run_scheduler(cr, uid, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
if context is None:
context = {}
try:
@@ -283,7 +283,7 @@ class procurement_order(osv.osv):
move_obj = self.pool.get('stock.move')
#Minimum stock rules
- self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company_id, context=context)
+ self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=use_new_cursor, company_id=company_id, context=context)
#Search all confirmed stock_moves and try to assign them
confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context)
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index ecaed0133a0..75def9b40d1 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -2640,13 +2640,27 @@ class stock_inventory(osv.osv):
def prepare_inventory(self, cr, uid, ids, context=None):
inventory_line_obj = self.pool.get('stock.inventory.line')
for inventory in self.browse(cr, uid, ids, context=context):
- #clean the existing inventory lines before redoing an inventory proposal
+ # If there are inventory lines already (e.g. from import), respect those and set their theoretical qty
line_ids = [line.id for line in inventory.line_ids]
- inventory_line_obj.unlink(cr, uid, line_ids, context=context)
- #compute the inventory lines and create them
- vals = self._get_inventory_lines(cr, uid, inventory, context=context)
- for product_line in vals:
- inventory_line_obj.create(cr, uid, product_line, context=context)
+ if not line_ids:
+ #compute the inventory lines and create them
+ vals = self._get_inventory_lines(cr, uid, inventory, context=context)
+ for product_line in vals:
+ inventory_line_obj.create(cr, uid, product_line, context=context)
+ else:
+ # On import calculate theoretical quantity
+ quant_obj = self.pool.get("stock.quant")
+ for line in inventory.line_ids:
+ dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id),
+ ('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id)]
+ if line.package_id:
+ dom += [('package_id', '=', line.package_id.id)]
+ quants = quant_obj.search(cr, uid, dom, context=context)
+ tot_qty = 0
+ for quant in quant_obj.browse(cr, uid, quants, context=context):
+ tot_qty += quant.qty
+ inventory_line_obj.write(cr, uid, [line.id], {'theoretical_qty': tot_qty}, context=context)
+
return self.write(cr, uid, ids, {'state': 'confirm', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
def _get_inventory_lines(self, cr, uid, inventory, context=None):
@@ -2712,7 +2726,7 @@ class stock_inventory_line(osv.osv):
'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
- 'theoretical_qty': fields.float('Theoretical Quantity', readonly=True),
+ 'theoretical_qty': fields.float('Theoretical Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), readonly=True),
'partner_id': fields.many2one('res.partner', 'Owner'),
'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index d923f5f6002..a95300d8744 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -1097,7 +1097,7 @@
</group>
<group string="Locations" groups="stock.group_locations">
<field name="location_id" domain="[('usage','&lt;&gt;','view')]"/>
- <field name="location_dest_id" domain="[('usage','=','internal')]"/>
+ <field name="location_dest_id" domain="[('usage','&lt;&gt;','view')]"/>
</group>
<group name="quants_grp" string="Reserved Quants" colspan="4" groups="base.group_no_one">
<field name="reserved_quant_ids"/>
@@ -1658,7 +1658,7 @@
<field name="model">stock.quant</field>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
- <form string="Quants">
+ <form string="Quants" create="false" edit="false">
<div class="oe_right oe_button_box">
<button name="action_view_quant_history" type="object" string="Quant History"/>
</div>
diff --git a/addons/stock/wizard/stock_transfer_details.py b/addons/stock/wizard/stock_transfer_details.py
index 831f225ca13..17f302f6317 100644
--- a/addons/stock/wizard/stock_transfer_details.py
+++ b/addons/stock/wizard/stock_transfer_details.py
@@ -133,7 +133,7 @@ class stock_transfer_details_items(models.TransientModel):
packop_id = fields.Many2one('stock.pack.operation', 'Operation')
product_id = fields.Many2one('product.product', 'Product')
product_uom_id = fields.Many2one('product.uom', 'Product Unit of Measure')
- quantity = fields.Float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), default = 1.0)
+ quantity = fields.Float('Quantity', digits=dp.get_precision('Product Unit of Measure'), default = 1.0)
package_id = fields.Many2one('stock.quant.package', 'Source package', domain="['|', ('location_id', 'child_of', sourceloc_id), ('location_id','=',False)]")
lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
sourceloc_id = fields.Many2one('stock.location', 'Source Location', required=True)
diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py
index c5308fde462..9707263391a 100644
--- a/addons/stock_account/stock_account.py
+++ b/addons/stock_account/stock_account.py
@@ -282,17 +282,25 @@ class stock_move(osv.osv):
def product_price_update_before_done(self, cr, uid, ids, context=None):
product_obj = self.pool.get('product.product')
+ tmpl_dict = {}
for move in self.browse(cr, uid, ids, context=context):
#adapt standard price on incomming moves if the product cost_method is 'average'
if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
product = move.product_id
- product_avail = product.qty_available
- if product.qty_available <= 0:
+ prod_tmpl_id = move.product_id.product_tmpl_id.id
+ qty_available = move.product_id.product_tmpl_id.qty_available
+ if tmpl_dict.get(prod_tmpl_id):
+ product_avail = qty_available + tmpl_dict[prod_tmpl_id]
+ else:
+ tmpl_dict[prod_tmpl_id] = 0
+ product_avail = qty_available
+ if product_avail <= 0:
new_std_price = move.price_unit
else:
# Get the standard price
amount_unit = product.standard_price
new_std_price = ((amount_unit * product_avail) + (move.price_unit * move.product_qty)) / (product_avail + move.product_qty)
+ tmpl_dict[prod_tmpl_id] += move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=context)
diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js
index 15f6e80f509..8d7f8869ae2 100644
--- a/addons/web/static/src/js/view_form.js
+++ b/addons/web/static/src/js/view_form.js
@@ -4372,7 +4372,6 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({
if (!this.fields_view || !this.editable()){
return true;
}
- this.o2m._dirty_flag = true;
var r;
return _.every(this.records.records, function(record){
r = record;
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index c01baa3098f..b8ff361cb35 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -166,7 +166,7 @@
<template id="layout_logo_show" inherit_id="website.layout" customize_show="True" name="Show Logo">
<xpath expr="//header//a[@class='navbar-brand']" position="replace">
<a href="/" class="navbar-brand logo">
- <img src="/logo.png"/>
+ <img src="/logo.png" t-att-alt="'Logo of %s' % res_company.name" t-att-title="res_company.name"/>
</a>
</xpath>
</template>
diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml
index b72f5f5c903..b8172e4015d 100644
--- a/addons/website_mail_group/views/website_mail_group.xml
+++ b/addons/website_mail_group/views/website_mail_group.xml
@@ -212,7 +212,7 @@
t-att-title="attachment.name"/>
</t>
<t t-if="attachment.file_type != 'webimage'">
- <img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type + '.png'"
+ <img t-att-src="'/mail/static/src/img/mimetypes/' + (attachment.file_type or 'disk') + '.png'"
class='oe_attachment_webimage'
t-att-title="attachment.name"/>
</t>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index ab50ac88cec..d776db029cd 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -179,6 +179,7 @@ class website_sale(http.Controller):
if search:
post["search"] = search
if category:
+ category = pool['product.public.category'].browse(cr, uid, int(category), context=context)
url = "/shop/category/%s" % slug(category)
style_obj = pool['product.style']
diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py
index 6e71d3fa574..2fbe081d768 100644
--- a/addons/website_sale_delivery/models/sale_order.py
+++ b/addons/website_sale_delivery/models/sale_order.py
@@ -63,7 +63,7 @@ class SaleOrder(orm.Model):
if not order:
return False
if all(line.product_id.type == "service" for line in order.website_order_line):
- order.write({'carrier_id': None}, context=context)
+ order.write({'carrier_id': None})
self.pool['sale.order']._delivery_unset(cr, SUPERUSER_ID, [order.id], context=context)
return True
else:
@@ -81,11 +81,11 @@ class SaleOrder(orm.Model):
if grid_id:
carrier_id = delivery_id
break
- order.write({'carrier_id': carrier_id}, context=context)
+ order.write({'carrier_id': carrier_id})
if carrier_id:
- order.delivery_set(context=context)
+ order.delivery_set()
else:
- order._delivery_unset(context=context)
+ order._delivery_unset()
return bool(carrier_id)
diff --git a/doc/_themes/odoodoc/layout.html b/doc/_themes/odoodoc/layout.html
index 7685d8af25f..1a68d31814a 100644
--- a/doc/_themes/odoodoc/layout.html
+++ b/doc/_themes/odoodoc/layout.html
@@ -32,7 +32,7 @@
<div class="sphinxsidebar">
<div class="sphinxsidebarwrapper">
{{ toctree(maxdepth=4, collapse=False, includehidden=True,
- main_navbar=False, titles_only=False) }}
+ navbar='side', titles_only=False) }}
{% if github_link %}
<p><a href="{{ github_link(mode='edit') }}" class="github">
Edit on GitHub
@@ -72,7 +72,8 @@
{% endif %}
</div>
<nav class="collapse navbar-collapse navbar-main" role="navigation">
- {{ toctree(titles_only=True, maxdepth=2, includehidden=True, collapse=False) }}
+ {{ toctree(titles_only=True, maxdepth=2, includehidden=True,
+ collapse=False, navbar='main') }}
</nav>
</div>
</header>
diff --git a/doc/_themes/odoodoc/sphinx_monkeypatch.py b/doc/_themes/odoodoc/sphinx_monkeypatch.py
index 13c3a305988..182fef89c8d 100644
--- a/doc/_themes/odoodoc/sphinx_monkeypatch.py
+++ b/doc/_themes/odoodoc/sphinx_monkeypatch.py
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
import sphinx.roles
import sphinx.environment
-from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.writers.html import HTMLTranslator
from docutils.writers.html4css1 import HTMLTranslator as DocutilsTranslator
@@ -9,27 +8,17 @@ def patch():
# navify toctree (oh god)
@monkey(sphinx.environment.BuildEnvironment)
def resolve_toctree(old_resolve, self, *args, **kwargs):
- """ If main_navbar, bootstrapify TOC to yield a navbar
+ """ If navbar, bootstrapify TOC to yield a navbar
"""
- main_navbar = kwargs.pop('main_navbar', False)
+ navbar = kwargs.pop('navbar', None)
toc = old_resolve(self, *args, **kwargs)
if toc is None:
return None
- navbarify(toc[0], main_navbar=main_navbar)
+ navbarify(toc[0], navbar=navbar)
return toc
- @monkey(StandaloneHTMLBuilder)
- def _get_local_toctree(old_local, self, *args, **kwargs):
- """ _get_local_toctree generates a documentation toctree for the local
- document (?), called from handle_page
- """
- # so can call toctree(main_navbar=False)
- d = {'main_navbar': True}
- d.update(kwargs)
- return old_local(self, *args, **d)
-
# monkeypatch visit_table to remove border and add .table
HTMLTranslator.visit_table = visit_table
# disable colspec crap
@@ -37,11 +26,18 @@ def patch():
# copy data- attributes straight from source to dest
HTMLTranslator.starttag = starttag_data
-def navbarify(node, main_navbar=False):
- # add classes to toplevel
- if not main_navbar:
- navify([node])
- else:
+def navbarify(node, navbar=None):
+ """
+ :param node: toctree node to navbarify
+ :param navbar: Whether this toctree is a 'main' navbar, a 'side' navbar or
+ not a navbar at all
+ """
+ if navbar == 'side':
+ for n in node.traverse():
+ if n.tagname == 'bullet_list':
+ n['classes'].append('nav')
+ elif navbar == 'main':
+ # add classes to just toplevel
node['classes'].extend(['nav', 'navbar-nav', 'navbar-right'])
for list_item in node.children:
# bullet_list
@@ -52,6 +48,10 @@ def navbarify(node, main_navbar=False):
# list_item
# compact_paragraph
# reference
+ # no bullet_list.list_item -> don't dropdownify
+ if not list_item.children[1].children:
+ return
+
list_item['classes'].append('dropdown')
# list_item.compact_paragraph.reference
link = list_item.children[0].children[0]
@@ -59,11 +59,6 @@ def navbarify(node, main_navbar=False):
link.attributes['data-toggle'] = 'dropdown'
# list_item.bullet_list
list_item.children[1]['classes'].append('dropdown-menu')
-def navify(nodes):
- for node in nodes:
- if node.tagname == 'bullet_list':
- node['classes'].append('nav')
- navify(node.children)
def visit_table(self, node):
"""
diff --git a/doc/_themes/odoodoc/static/style.css b/doc/_themes/odoodoc/static/style.css
index 19f5270109e..7c0b8e82959 100644
--- a/doc/_themes/odoodoc/static/style.css
+++ b/doc/_themes/odoodoc/static/style.css
@@ -5902,8 +5902,10 @@ button.close {
.panel-body:after,
.modal-footer:before,
.modal-footer:after,
-.document-super:before,
-.document-super:after,
+.document-super:not(.stripe):before,
+.document-super:not(.stripe):after,
+.document-super.stripe:before,
+.document-super.stripe:after,
.document:before,
.document:after {
content: " ";
@@ -5924,7 +5926,8 @@ button.close {
.pager:after,
.panel-body:after,
.modal-footer:after,
-.document-super:after,
+.document-super:not(.stripe):after,
+.document-super.stripe:after,
.document:after {
clear: both;
}
@@ -6181,35 +6184,53 @@ body {
overflow: auto;
position: relative;
}
-.document-super {
+.document-super:not(.stripe) {
margin-right: auto;
margin-left: auto;
padding-left: 15px;
padding-right: 15px;
}
@media (min-width: 768px) {
- .document-super {
+ .document-super:not(.stripe) {
width: 750px;
}
}
@media (min-width: 992px) {
- .document-super {
+ .document-super:not(.stripe) {
width: 970px;
}
}
@media (min-width: 1200px) {
- .document-super {
+ .document-super:not(.stripe) {
width: 1170px;
}
}
-.document-super > .navbar-header,
-.document-super > .navbar-collapse {
+.document-super:not(.stripe) > .navbar-header,
+.document-super:not(.stripe) > .navbar-collapse {
margin-right: -15px;
margin-left: -15px;
}
@media (min-width: 768px) {
- .document-super > .navbar-header,
- .document-super > .navbar-collapse {
+ .document-super:not(.stripe) > .navbar-header,
+ .document-super:not(.stripe) > .navbar-collapse {
+ margin-right: 0;
+ margin-left: 0;
+ }
+}
+.document-super.stripe {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px;
+}
+.document-super.stripe > .navbar-header,
+.document-super.stripe > .navbar-collapse {
+ margin-right: -15px;
+ margin-left: -15px;
+}
+@media (min-width: 768px) {
+ .document-super.stripe > .navbar-header,
+ .document-super.stripe > .navbar-collapse {
margin-right: 0;
margin-left: 0;
}
@@ -6296,7 +6317,7 @@ body {
-o-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
-.docs-nav .navbar-nav > li > a:after {
+.docs-nav .navbar-nav > li.dropdown > a:after {
content: " ";
display: inline-block;
width: 0;
@@ -6307,7 +6328,7 @@ body {
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
-.docs-nav .navbar-nav > li .dropdown-menu > li.current > a.current {
+.docs-nav .navbar-nav > li.dropdown .dropdown-menu > li.current > a.current {
background-color: #a24689;
color: white;
}
@@ -6709,6 +6730,74 @@ td.field-body > ul {
.descclassname {
opacity: 0.5;
}
+.index-tree ul,
+.index-tree li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.index-tree > ul > li {
+ margin-bottom: 0.5em;
+}
+.index-tree > ul > li > a {
+ font-family: Lato, Arial, sans-serif;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+ margin-top: 20px;
+ margin-bottom: 10px;
+ font-size: 24px;
+ text-align: center;
+}
+.index-tree > ul > li > a small,
+.index-tree > ul > li > a .small {
+ font-weight: normal;
+ line-height: 1;
+ color: #777777;
+}
+.index-tree > ul > li > a small,
+.index-tree > ul > li > a .small {
+ font-size: 65%;
+}
+.index-tree > ul > li > ul {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+}
+.index-tree > ul > li > ul > li {
+ width: 50%;
+}
+.index-tree > ul > li > ul > li a {
+ font-family: Lato, Arial, sans-serif;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 18px;
+ padding-left: 10%;
+ padding-right: 10%;
+}
+.index-tree > ul > li > ul > li a small,
+.index-tree > ul > li > ul > li a .small {
+ font-weight: normal;
+ line-height: 1;
+ color: #777777;
+}
+.index-tree > ul > li > ul > li a small,
+.index-tree > ul > li > ul > li a .small {
+ font-size: 75%;
+}
+.index-tree > ul > li a {
+ display: block;
+ color: #a24689 !important;
+ padding: 0.3em 0;
+ margin: 3px !important;
+}
+.index-tree > ul > li a:hover {
+ background-color: #eeeeee;
+}
.stripe .section {
margin-bottom: 2em;
}
diff --git a/doc/_themes/odoodoc/static/style.less b/doc/_themes/odoodoc/static/style.less
index 719fa6f10e3..ea53cd95e85 100644
--- a/doc/_themes/odoodoc/static/style.less
+++ b/doc/_themes/odoodoc/static/style.less
@@ -25,9 +25,12 @@ body {
position: relative;
}
-.document-super {
+.document-super:not(.stripe) {
.container();
}
+.document-super.stripe {
+ .container-fluid();
+}
.document {
.make-row();
@@ -89,20 +92,22 @@ body {
opacity: 0;
.transition(all 0.3s ease-out);
}
- > a:after {
- content: " ";
- display: inline-block;
- width: 0;
- height: 0;
- margin-left: 5px;
- vertical-align: middle;
- border-top: 4px solid @gray-light;
- border-right: 4px solid transparent;
- border-left: 4px solid transparent;
- }
- .dropdown-menu > li.current > a.current {
- background-color: @brand-primary;
- color: white;
+ &.dropdown {
+ > a:after {
+ content: " ";
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 5px;
+ vertical-align: middle;
+ border-top: 4px solid @gray-light;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent;
+ }
+ .dropdown-menu > li.current > a.current {
+ background-color: @brand-primary;
+ color: white;
+ }
}
}
/* version switcher */
@@ -547,6 +552,44 @@ td.field-body {
opacity: 0.5;
}
+
+.index-tree {
+ ul, li {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+ > ul > li {
+ margin-bottom: 0.5em;
+ > a {
+ .h3();
+ text-align: center;
+ }
+ > ul {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-wrap: wrap;
+ flex-wrap: wrap;
+ > li {
+ width: 50%;
+ a {
+ .h4();
+ padding-left: 10%;
+ padding-right: 10%;
+ }
+ }
+ }
+ a {
+ display: block;
+ color: @link-color !important;
+ padding: 0.3em 0;
+ margin: 3px !important;
+ &:hover {
+ background-color: @gray-lighter;
+ }
+ }
+ }
+}
// STRIPE-STYLE PAGES
.stripe {
.section {
diff --git a/doc/modules/api_integration.rst b/doc/api_integration.rst
index 21d0163ba46..12c5b56737e 100644
--- a/doc/modules/api_integration.rst
+++ b/doc/api_integration.rst
@@ -1,7 +1,7 @@
:classes: stripe
===========
-Odoo as API
+Web Service
===========
Odoo is mostly extended internally via modules, but much of its features and
@@ -250,8 +250,8 @@ the login.
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.
+The second endpoint is ``xmlrpc/2/object``, is used to call methods of odoo
+models via the ``execute_kw`` RPC function.
Each call to ``execute_kw`` takes the following parameters:
diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst
index bb300ae51c8..bbf173cb5c4 100644
--- a/doc/howtos/backend.rst
+++ b/doc/howtos/backend.rst
@@ -1384,7 +1384,7 @@ access rights are limited.
Here is an example of a rule that prevents the deletion of leads that are not
in state ``cancel``. Notice that the value of the field ``groups`` must follow
-the same convention as the method ``write`` of the ORM.
+the same convention as the method :meth:`~openerp.models.Model.write` of the ORM.
.. code-block:: xml
diff --git a/doc/images/view-on-github.png b/doc/images/view-on-github.png
deleted file mode 100644
index 65a8d42363d..00000000000
--- a/doc/images/view-on-github.png
+++ /dev/null
Binary files differ
diff --git a/doc/index.rst b/doc/index.rst
index 3c1696020d6..56f58b6aa0d 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -4,38 +4,17 @@ odoo developer documentation
Welcome to the Odoo developer documentation.
-This documentation is incomplete and may contain errors, if you wish to
-contribute, every page should have a :guilabel:`View on Github` link:
-
-.. image:: images/view-on-github.*
- :align: center
-
-Through this link you can edit documents and submit changes for review using
-`github's web interface
-<https://help.github.com/articles/editing-files-in-your-repository/>`_.
-Contributions are welcome and appreciated.
-
.. todo:: what's the documentation's license?
-The documentation is currently organized in four sections:
-
-* :doc:`tutorials`, aimed at introducing the primary areas of developing Odoo
- modules
-* :doc:`reference`, which ought be the complete and canonical documentation
- for Odoo subsystems
-* :doc:`modules`, documenting useful specialized modules and integration
- methods (and currently empty)
-
-.. hidden toctree w/o titlesonly otherwise the titlesonly "sticks" to
- in-document toctrees and we can't have a toctree showing both "sibling"
- pages and current document sections
-
+.. rst-class:: index-tree
+.. titlesonly breaks level 3 (~in-document) toc of left navbar, so use
+ maxdepth instead
.. toctree::
- :hidden:
+ :maxdepth: 2
tutorials
reference
- modules
+ api_integration
.. ifconfig:: todo_include_todos
diff --git a/doc/modules.rst b/doc/modules.rst
deleted file mode 100644
index cb5ed8d6ca1..00000000000
--- a/doc/modules.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-==============
-Module Objects
-==============
-
-.. toctree::
- :titlesonly:
-
- modules/api_integration
diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst
index 32c1ef48729..1ec7d96ae9d 100644
--- a/doc/reference/cmdline.rst
+++ b/doc/reference/cmdline.rst
@@ -71,7 +71,7 @@ Scaffolding is available via the :command:`odoo.py scaffold` subcommand.
.. option:: -t <template>
a template directory, files are passed through jinja2_ then copied to
- the :option:`destination` directory
+ the ``destination`` directory
.. option:: name
diff --git a/doc/reference/orm.rst b/doc/reference/orm.rst
index 94aa18b4863..cfc465078b1 100644
--- a/doc/reference/orm.rst
+++ b/doc/reference/orm.rst
@@ -421,10 +421,31 @@ added.
is not written to the database, just used to know which value to send back
to the client
+Low-level SQL
+-------------
+
+The :attr:`~openerp.api.Environment.cr` attribute on environments is the
+cursor for the current database transaction and allows executing SQL directly,
+either for queries which are difficult to express using the ORM (e.g. complex
+joins) or for performance reasons::
+
+ self.env.cr.execute("some_sql", param1, param2, param3)
+
+Because models use the same cursor and the :class:`~openerp.api.Environment`
+holds various caches, these caches must be invalidated when *altering* the
+database in raw SQL, or further uses of models may become incoherent. It is
+necessary to clear caches when using ``CREATE``, ``UPDATE`` or ``DELETE`` in
+SQL, but not ``SELECT`` (which simply reads the database).
+
+Clearing caches can be performed using the
+:meth:`~openerp.api.Environment.invalidate_all` method of the
+:class:`~openerp.api.Environment` object.
+
+
.. _reference/orm/oldapi:
Old API compatibility
----------------------
+=====================
Odoo is currently transitioning from an older (less regular) API, it can be
necessary to manually bridge from one to the other manually:
@@ -508,170 +529,6 @@ return lists of ids, there is also a decorator managing this:
>>> old_style_model.some_method(cr, uid, [1, 2, 3], context=context)
[1, 2, 3]
-Porting from the old API
-------------------------
-
-* methods still written in the old API should be automatically bridged by the
- ORM, no need to switch to the old API, just call them as if they were a new
- API method. See :ref:`reference/orm/oldapi/bridging` for more details.
-* ``search`` returns a recordset, no point in e.g. browsing its result
-* ``fields.related`` and ``fields.function`` are replaced by using a normal
- field type with either a ``related`` or a ``compute`` parameter
-* ``depends`` on field compute methods **must be complete**, it must list
- **all** the fields and sub-fields which the compute method uses. It is
- better to have too many dependencies (will recompute the field in cases
- where that is not needed) than not enough (will forget to recompute the
- field and then values will be incorrect)
-* **remove** all ``onchange`` methods on computed fields. Computed fields are
- automatically re-computed when one of their dependencies is changed, and
- that is used to auto-generate ``onchange`` by the client
-* the decorators :func:`~openerp.api.model` and :func:`~openerp.api.multi` are
- for bridging *when calling from the old API context*, for internal or pure
- new-api (e.g. compute) they are useless
-* remove :attr:`~openerp.models.Model._default`, replace by ``default=``
- parameter on corresponding fields
-* if a field's ``string`` is the titlecased version of the field name::
-
- name = fields.Char(string="Name")
-
- it is useless and should be removed
-* ``multi`` does not do anything on new API fields use the same ``compute``
- methods on all relevant fields for the same result
-* provide ``compute``, ``inverse`` and ``search`` methods by name (as a
- string), this makes them overridable (removes the need for an intermediate
- "trampoline" function)
-* double check that all fields and methods have different names, there is no
- warning in case of collision (because Python handles it before Odoo sees
- anything)
-* the normal new-api import is ``from openerp import fields, models``. If
- compatibility decorators are necessary, use ``from openerp import api,
- fields, models``
-* avoid the :func:`~openerp.api.one` decorator, it probably does not do what
- you expect
-* remove explicit definition of :attr:`~openerp.models.Model.create_uid`,
- :attr:`~openerp.models.Model.create_date`,
- :attr:`~openerp.models.Model.write_uid` and
- :attr:`~openerp.models.Model.write_date` fields: they are now created as
- regular "legitimate" fields, and can be read and written like any other
- field out-of-the-box
-* when straight conversion is impossible (semantics can not be bridged) or the
- "old API" version is not desirable and could be improved for the new API, it
- is possible to use completely different "old API" and "new API"
- implementations for the same method name using :func:`~openerp.api.v7` and
- :func:`~openerp.api.v8`. The method should first be defined using the
- old-API style and decorated with :func:`~openerp.api.v7`, it should then be
- re-defined using the exact same name but the new-API style and decorated
- with :func:`~openerp.api.v8`. Calls from an old-API context will be
- dispatched to the first implementation and calls from a new-API context will
- be dispatched to the second implementation. One implementation can call (and
- frequently does) call the other by switching context.
-
- .. danger:: using these decorators makes methods extremely difficult to
- override and harder to understand and document
-* uses of :attr:`~openerp.models.Model._columns` or
- :attr:`~openerp.models.Model._all_columns` should be replaced by
- :attr:`~openerp.models.Model._fields`, which provides access to instances of
- new-style :class:`openerp.fields.Field` instances (rather than old-style
- :class:`openerp.osv.fields._column`).
-
- Non-stored computed fields created using the new API style are *not*
- available in :attr:`~openerp.models.Model._columns` and can only be
- inspected through :attr:`~openerp.models.Model._fields`
-* reassigning ``self`` in a method is probably unnecessary and may break
- translation introspection
-* :class:`~openerp.api.Environment` objects rely on some threadlocal state,
- which has to be set up before using them. It is necessary to do so using the
- :meth:`openerp.api.Environment.manage` context manager when trying to use
- the new API in contexts where it hasn't been set up yet, such as new threads
- or a Python interactive environment::
-
- >>> from openerp import api, modules
- >>> r = modules.registry.RegistryManager.get('test')
- >>> cr = r.cursor()
- >>> env = api.Environment(cr, 1, {})
- Traceback (most recent call last):
- ...
- AttributeError: environments
- >>> with api.Environment.manage():
- ... env = api.Environment(cr, 1, {})
- ... print env['res.partner'].browse(1)
- ...
- res.partner(1,)
-
-.. _reference/orm/oldapi/bridging:
-
-Automatic bridging of old API methods
-'''''''''''''''''''''''''''''''''''''
-
-When models are initialized, all methods are automatically scanned and bridged
-if they look like models declared in the old API style. This bridging makes
-them transparently callable from new-API-style methods.
-
-Methods are matched as "old-API style" if their second positional parameter
-(after ``self``) is called either ``cr`` or ``cursor``. The system also
-recognizes the third positional parameter being called ``uid`` or ``user`` and
-the fourth being called ``id`` or ``ids``. It also recognizes the presence of
-any parameter called ``context``.
-
-When calling such methods from a new API context, the system will
-automatically fill matched parameters from the current
-:class:`~openerp.api.Environment` (for :attr:`~openerp.api.Environment.cr`,
-:attr:`~openerp.api.Environment.user` and
-:attr:`~openerp.api.Environment.context`) or the current recordset (for ``id``
-and ``ids``).
-
-In the rare cases where it is necessary, the bridging can be customized by
-decorating the old-style method:
-
-* disabling it entirely, by decorating a method with
- :func:`~openerp.api.noguess` there will be no bridging and methods will be
- called the exact same way from the new and old API styles
-* defining the bridge explicitly, this is mostly for methods which are matched
- incorrectly (because parameters are named in unexpected ways):
-
- :func:`~openerp.api.cr`
- will automatically prepend the current cursor to explicitly provided
- parameters, positionally
- :func:`~openerp.api.cr_uid`
- will automatically prepend the current cursor and user's id to explictly
- provided parameters
- :func:`~openerp.api.cr_uid_ids`
- will automatically prepend the current cursor, user's id and recordset's
- ids to explicitly provided parameters
- :func:`~openerp.api.cr_uid_id`
- will loop over the current recordset and call the method once for each
- record, prepending the current cursor, user's id and record's id to
- explicitly provided parameters.
-
- .. danger:: the result of this wrapper is *always a list* when calling
- from a new-API context
-
- All of these methods have a ``_context``-suffixed version
- (e.g. :func:`~openerp.api.cr_uid_context`) which also passes the current
- context *by keyword*.
-* dual implementations using :func:`~openerp.api.v7` and
- :func:`~openerp.api.v8` will be ignored as they provide their own "bridging"
-
-Low-level SQL
--------------
-
-The :attr:`~openerp.api.Environment.cr` attribute on environments is the
-cursor for the current database transaction and allows executing SQL directly,
-either for queries which are difficult to express using the ORM (e.g. complex
-joins) or for performance reasons::
-
- self.env.cr.execute("some_sql", param1, param2, param3)
-
-Because models use the same cursor and the :class:`~openerp.api.Environment`
-holds various caches, these caches must be invalidated when *altering* the
-database in raw SQL, or further uses of models may become incoherent. It is
-necessary to clear caches when using ``CREATE``, ``UPDATE`` or ``DELETE`` in
-SQL, but not ``SELECT`` (which simply reads the database).
-
-Clearing caches can be performed using the
-:meth:`~openerp.api.Environment.invalidate_all` method of the
-:class:`~openerp.api.Environment` object.
-
.. _reference/orm/model:
Model Reference
@@ -1168,3 +1025,148 @@ Domain criteria can be combined using logical operators in *prefix* form:
(name is 'ABC')
AND (language is NOT english)
AND (country is Belgium OR Germany)
+
+Porting from the old API
+========================
+
+* methods still written in the old API should be automatically bridged by the
+ ORM, no need to switch to the old API, just call them as if they were a new
+ API method. See :ref:`reference/orm/oldapi/bridging` for more details.
+* :meth:`~openerp.models.Model.search` returns a recordset, no point in e.g.
+ browsing its result
+* ``fields.related`` and ``fields.function`` are replaced by using a normal
+ field type with either a ``related=`` or a ``compute=`` parameter
+* :func:`~openerp.api.depends` on ``compute=`` methods **must be complete**,
+ it must list **all** the fields and sub-fields which the compute method
+ uses. It is better to have too many dependencies (will recompute the field
+ in cases where that is not needed) than not enough (will forget to recompute
+ the field and then values will be incorrect)
+* **remove** all ``onchange`` methods on computed fields. Computed fields are
+ automatically re-computed when one of their dependencies is changed, and
+ that is used to auto-generate ``onchange`` by the client
+* the decorators :func:`~openerp.api.model` and :func:`~openerp.api.multi` are
+ for bridging *when calling from the old API context*, for internal or pure
+ new-api (e.g. compute) they are useless
+* remove :attr:`~openerp.models.Model._default`, replace by ``default=``
+ parameter on corresponding fields
+* if a field's ``string=`` is the titlecased version of the field name::
+
+ name = fields.Char(string="Name")
+
+ it is useless and should be removed
+* the ``multi=`` parameter does not do anything on new API fields use the same
+ ``compute=`` methods on all relevant fields for the same result
+* provide ``compute=``, ``inverse=`` and ``search=`` methods by name (as a
+ string), this makes them overridable (removes the need for an intermediate
+ "trampoline" function)
+* double check that all fields and methods have different names, there is no
+ warning in case of collision (because Python handles it before Odoo sees
+ anything)
+* the normal new-api import is ``from openerp import fields, models``. If
+ compatibility decorators are necessary, use ``from openerp import api,
+ fields, models``
+* avoid the :func:`~openerp.api.one` decorator, it probably does not do what
+ you expect
+* remove explicit definition of :attr:`~openerp.models.Model.create_uid`,
+ :attr:`~openerp.models.Model.create_date`,
+ :attr:`~openerp.models.Model.write_uid` and
+ :attr:`~openerp.models.Model.write_date` fields: they are now created as
+ regular "legitimate" fields, and can be read and written like any other
+ field out-of-the-box
+* when straight conversion is impossible (semantics can not be bridged) or the
+ "old API" version is not desirable and could be improved for the new API, it
+ is possible to use completely different "old API" and "new API"
+ implementations for the same method name using :func:`~openerp.api.v7` and
+ :func:`~openerp.api.v8`. The method should first be defined using the
+ old-API style and decorated with :func:`~openerp.api.v7`, it should then be
+ re-defined using the exact same name but the new-API style and decorated
+ with :func:`~openerp.api.v8`. Calls from an old-API context will be
+ dispatched to the first implementation and calls from a new-API context will
+ be dispatched to the second implementation. One implementation can call (and
+ frequently does) call the other by switching context.
+
+ .. danger:: using these decorators makes methods extremely difficult to
+ override and harder to understand and document
+* uses of :attr:`~openerp.models.Model._columns` or
+ :attr:`~openerp.models.Model._all_columns` should be replaced by
+ :attr:`~openerp.models.Model._fields`, which provides access to instances of
+ new-style :class:`openerp.fields.Field` instances (rather than old-style
+ :class:`openerp.osv.fields._column`).
+
+ Non-stored computed fields created using the new API style are *not*
+ available in :attr:`~openerp.models.Model._columns` and can only be
+ inspected through :attr:`~openerp.models.Model._fields`
+* reassigning ``self`` in a method is probably unnecessary and may break
+ translation introspection
+* :class:`~openerp.api.Environment` objects rely on some threadlocal state,
+ which has to be set up before using them. It is necessary to do so using the
+ :meth:`openerp.api.Environment.manage` context manager when trying to use
+ the new API in contexts where it hasn't been set up yet, such as new threads
+ or a Python interactive environment::
+
+ >>> from openerp import api, modules
+ >>> r = modules.registry.RegistryManager.get('test')
+ >>> cr = r.cursor()
+ >>> env = api.Environment(cr, 1, {})
+ Traceback (most recent call last):
+ ...
+ AttributeError: environments
+ >>> with api.Environment.manage():
+ ... env = api.Environment(cr, 1, {})
+ ... print env['res.partner'].browse(1)
+ ...
+ res.partner(1,)
+
+.. _reference/orm/oldapi/bridging:
+
+Automatic bridging of old API methods
+-------------------------------------
+
+When models are initialized, all methods are automatically scanned and bridged
+if they look like models declared in the old API style. This bridging makes
+them transparently callable from new-API-style methods.
+
+Methods are matched as "old-API style" if their second positional parameter
+(after ``self``) is called either ``cr`` or ``cursor``. The system also
+recognizes the third positional parameter being called ``uid`` or ``user`` and
+the fourth being called ``id`` or ``ids``. It also recognizes the presence of
+any parameter called ``context``.
+
+When calling such methods from a new API context, the system will
+automatically fill matched parameters from the current
+:class:`~openerp.api.Environment` (for :attr:`~openerp.api.Environment.cr`,
+:attr:`~openerp.api.Environment.user` and
+:attr:`~openerp.api.Environment.context`) or the current recordset (for ``id``
+and ``ids``).
+
+In the rare cases where it is necessary, the bridging can be customized by
+decorating the old-style method:
+
+* disabling it entirely, by decorating a method with
+ :func:`~openerp.api.noguess` there will be no bridging and methods will be
+ called the exact same way from the new and old API styles
+* defining the bridge explicitly, this is mostly for methods which are matched
+ incorrectly (because parameters are named in unexpected ways):
+
+ :func:`~openerp.api.cr`
+ will automatically prepend the current cursor to explicitly provided
+ parameters, positionally
+ :func:`~openerp.api.cr_uid`
+ will automatically prepend the current cursor and user's id to explictly
+ provided parameters
+ :func:`~openerp.api.cr_uid_ids`
+ will automatically prepend the current cursor, user's id and recordset's
+ ids to explicitly provided parameters
+ :func:`~openerp.api.cr_uid_id`
+ will loop over the current recordset and call the method once for each
+ record, prepending the current cursor, user's id and record's id to
+ explicitly provided parameters.
+
+ .. danger:: the result of this wrapper is *always a list* when calling
+ from a new-API context
+
+ All of these methods have a ``_context``-suffixed version
+ (e.g. :func:`~openerp.api.cr_uid_context`) which also passes the current
+ context *by keyword*.
+* dual implementations using :func:`~openerp.api.v7` and
+ :func:`~openerp.api.v8` will be ignored as they provide their own "bridging"
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index 06a82081051..78672e0a530 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -228,12 +228,7 @@ class ir_model(osv.osv):
_module = False
_custom = True
- obj = CustomModel._build_model(self.pool, cr)
- obj._rec_name = CustomModel._rec_name = (
- 'x_name' if 'x_name' in obj._columns else
- list(obj._columns)[0] if obj._columns else
- 'id'
- )
+ CustomModel._build_model(self.pool, cr)
class ir_model_fields(osv.osv):
_name = 'ir.model.fields'
@@ -501,6 +496,7 @@ class ir_model_fields(osv.osv):
# we want to change the key of field in obj._fields and obj._columns
field = obj._pop_field(rename[1])
obj._add_field(rename[2], field)
+ self.pool.setup_models(cr, partial=(not self.pool.ready))
if patches:
# We have to update _columns of the model(s) and then call their
@@ -521,11 +517,11 @@ class ir_model_fields(osv.osv):
obj._add_field(field_name, field.new(**attrs))
# update database schema
+ self.pool.setup_models(cr, partial=(not self.pool.ready))
obj._auto_init(cr, ctx)
obj._auto_end(cr, ctx) # actually create FKs!
if column_rename or patches:
- self.pool.setup_models(cr, partial=(not self.pool.ready))
RegistryManager.signal_registry_change(cr.dbname)
return res
diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py
index 74f85fa6d68..126a967ba48 100644
--- a/openerp/addons/test_new_api/tests/test_new_fields.py
+++ b/openerp/addons/test_new_api/tests/test_new_fields.py
@@ -185,6 +185,9 @@ class TestNewFields(common.TransactionCase):
with self.assertRaises(Exception):
self.env['test_new_api.message'].create({'discussion': discussion.id, 'body': 'Whatever'})
+ # make sure that assertRaises() does not leave fields to recompute
+ self.assertFalse(self.env.has_todo())
+
# put back oneself into discussion participants: now we can create
# messages in discussion
discussion.participants += self.env.user
diff --git a/openerp/api.py b/openerp/api.py
index da9e62f1549..94ed98c1265 100644
--- a/openerp/api.py
+++ b/openerp/api.py
@@ -815,6 +815,24 @@ class Environment(object):
env.computed.clear()
env.dirty.clear()
+ def clear(self):
+ """ Clear all record caches, and discard all fields to recompute.
+ This may be useful when recovering from a failed ORM operation.
+ """
+ self.invalidate_all()
+ self.all.todo.clear()
+
+ @contextmanager
+ def clear_upon_failure(self):
+ """ Context manager that clears the environments (caches and fields to
+ recompute) upon exception.
+ """
+ try:
+ yield
+ except Exception:
+ self.clear()
+ raise
+
def field_todo(self, field):
""" Check whether `field` must be recomputed, and returns a recordset
with all records to recompute for `field`.
diff --git a/openerp/fields.py b/openerp/fields.py
index 58838bf0531..43b63f84b13 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -426,6 +426,7 @@ class Field(object):
# put invalidation triggers on model dependencies
for dep_model_name, field_names in model._depends.iteritems():
dep_model = env[dep_model_name]
+ dep_model._setup_fields()
for field_name in field_names:
field = dep_model._fields[field_name]
field._triggers.add((self, None))
@@ -444,8 +445,8 @@ class Field(object):
recs = env[self.model_name]
fields = []
for name in self.related:
+ recs._setup_fields()
field = recs._fields[name]
- field.setup(env)
recs = recs[name]
fields.append(field)
@@ -551,6 +552,7 @@ class Field(object):
env = model.env
head, tail = path1[0], path1[1:]
+ model._setup_fields()
if head == '*':
# special case: add triggers on all fields of model (except self)
fields = set(model._fields.itervalues()) - set([self])
@@ -563,8 +565,6 @@ class Field(object):
self.recursive = True
continue
- field.setup(env)
-
#_logger.debug("Add trigger on %s to recompute %s", field, self)
field._triggers.add((self, '.'.join(path0 or ['id'])))
@@ -1648,7 +1648,9 @@ class One2many(_RelationalMulti):
if self.inverse_name:
# link self to its inverse field and vice-versa
- invf = env[self.comodel_name]._fields[self.inverse_name]
+ comodel = env[self.comodel_name]
+ comodel._setup_fields()
+ invf = comodel._fields[self.inverse_name]
# In some rare cases, a `One2many` field can link to `Int` field
# (res_model/res_id pattern). Only inverse the field if this is
# a `Many2one` field.
diff --git a/openerp/models.py b/openerp/models.py
index 8c2e5ad0bd1..d64976bfe0d 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -516,7 +516,7 @@ class BaseModel(object):
"""
def add(name, field):
""" add `field` with the given `name` if it does not exist yet """
- if name not in cls._columns and name not in cls._fields:
+ if name not in cls._fields:
cls._add_field(name, field)
# cyclic import
@@ -700,50 +700,46 @@ class BaseModel(object):
pool._store_function[model].sort(key=lambda x: x[4])
@classmethod
- def _init_manual_fields(cls, pool, cr):
+ def _init_manual_fields(cls, cr):
# Check whether the query is already done
- if pool.fields_by_model is not None:
- manual_fields = pool.fields_by_model.get(cls._name, [])
+ if cls.pool.fields_by_model is not None:
+ manual_fields = cls.pool.fields_by_model.get(cls._name, [])
else:
cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (cls._name, 'manual'))
manual_fields = cr.dictfetchall()
for field in manual_fields:
- if field['name'] in cls._columns:
+ if field['name'] in cls._fields:
continue
attrs = {
+ 'manual': True,
'string': field['field_description'],
'required': bool(field['required']),
'readonly': bool(field['readonly']),
- 'domain': eval(field['domain']) if field['domain'] else None,
- 'size': field['size'] or None,
- 'ondelete': field['on_delete'],
- 'translate': (field['translate']),
- 'manual': True,
- '_prefetch': False,
- #'select': int(field['select_level'])
}
- if field['serialization_field_id']:
- cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
- attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
- if field['ttype'] in ['many2one', 'one2many', 'many2many']:
- attrs.update({'relation': field['relation']})
- cls._columns[field['name']] = fields.sparse(**attrs)
- elif field['ttype'] == 'selection':
- cls._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
- elif field['ttype'] == 'reference':
- cls._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
+ # FIXME: ignore field['serialization_field_id']
+ if field['ttype'] in ('char', 'text', 'html'):
+ attrs['translate'] = bool(field['translate'])
+ attrs['size'] = field['size'] or None
+ elif field['ttype'] in ('selection', 'reference'):
+ attrs['selection'] = eval(field['selection'])
elif field['ttype'] == 'many2one':
- cls._columns[field['name']] = fields.many2one(field['relation'], **attrs)
+ attrs['comodel_name'] = field['relation']
+ attrs['ondelete'] = field['on_delete']
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
elif field['ttype'] == 'one2many':
- cls._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
+ attrs['comodel_name'] = field['relation']
+ attrs['inverse_name'] = field['relation_field']
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
elif field['ttype'] == 'many2many':
+ attrs['comodel_name'] = field['relation']
_rel1 = field['relation'].replace('.', '_')
_rel2 = field['model'].replace('.', '_')
- _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
- cls._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
- else:
- cls._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
+ attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
+ attrs['column1'] = 'id1'
+ attrs['column2'] = 'id2'
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
+ cls._add_field(field['name'], Field.by_type[field['ttype']](**attrs))
@classmethod
def _init_constraints_onchanges(cls):
@@ -756,12 +752,8 @@ class BaseModel(object):
cls._onchange_methods = defaultdict(list)
for attr, func in getmembers(cls, callable):
if hasattr(func, '_constrains'):
- if not all(name in cls._fields for name in func._constrains):
- _logger.warning("@constrains%r parameters must be field names", func._constrains)
cls._constraint_methods.append(func)
if hasattr(func, '_onchange'):
- if not all(name in cls._fields for name in func._onchange):
- _logger.warning("@onchange%r parameters must be field names", func._onchange)
for name in func._onchange:
cls._onchange_methods[name].append(func)
@@ -815,39 +807,17 @@ class BaseModel(object):
cls._fields = {}
above = cls.__bases__[0]
for attr, field in getmembers(above, Field.__instancecheck__):
- if not field.inherited:
- cls._add_field(attr, field.new())
+ cls._add_field(attr, field.new())
# introduce magic fields
cls._add_magic_fields()
# register stuff about low-level function fields and custom fields
cls._init_function_fields(pool, cr)
- cls._init_manual_fields(pool, cr)
-
- # process _inherits
- cls._inherits_check()
- cls._inherits_reload()
# register constraints and onchange methods
cls._init_constraints_onchanges()
- # check defaults
- for k in cls._defaults:
- assert k in cls._fields, \
- "Model %s has a default for non-existing field %s" % (cls._name, k)
-
- # restart columns
- for column in cls._columns.itervalues():
- column.restart()
-
- # validate rec_name
- if cls._rec_name:
- assert cls._rec_name in cls._fields, \
- "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
- elif 'name' in cls._fields:
- cls._rec_name = 'name'
-
# prepare ormcache, which must be shared by all instances of the model
cls._ormcache = {}
@@ -2903,46 +2873,33 @@ class BaseModel(object):
#
@classmethod
- def _inherits_reload_src(cls):
- """ Recompute the _inherit_fields mapping on each _inherits'd child model."""
- for model in cls.pool.values():
- if cls._name in model._inherits:
- model._inherits_reload()
-
- @classmethod
def _inherits_reload(cls):
- """ Recompute the _inherit_fields mapping.
-
- This will also call itself on each inherits'd child model.
+ """ Recompute the _inherit_fields mapping, and inherited fields. """
+ struct = {}
+ fields = {}
+ for parent_model, parent_field in cls._inherits.iteritems():
+ parent = cls.pool[parent_model]
+ # old-api struct for _inherit_fields
+ for name, column in parent._columns.iteritems():
+ struct[name] = (parent_model, parent_field, column, parent_model)
+ for name, source in parent._inherit_fields.iteritems():
+ struct[name] = (parent_model, parent_field, source[2], source[3])
+ # new-api fields for _fields
+ for name, field in parent._fields.iteritems():
+ fields[name] = field.new(
+ inherited=True,
+ related=(parent_field, name),
+ related_sudo=False,
+ )
- """
- res = {}
- for table in cls._inherits:
- other = cls.pool[table]
- for col in other._columns.keys():
- res[col] = (table, cls._inherits[table], other._columns[col], table)
- for col in other._inherit_fields.keys():
- res[col] = (table, cls._inherits[table], other._inherit_fields[col][2], other._inherit_fields[col][3])
- cls._inherit_fields = res
+ # old-api stuff
+ cls._inherit_fields = struct
cls._all_columns = cls._get_column_infos()
- # interface columns with new-style fields
- for attr, column in cls._columns.items():
- if attr not in cls._fields:
- cls._add_field(attr, column.to_field())
-
- # interface inherited fields with new-style fields (note that the
- # reverse order is for being consistent with _all_columns above)
- for parent_model, parent_field in reversed(cls._inherits.items()):
- for attr, field in cls.pool[parent_model]._fields.iteritems():
- if attr not in cls._fields:
- cls._add_field(attr, field.new(
- inherited=True,
- related=(parent_field, attr),
- related_sudo=False,
- ))
-
- cls._inherits_reload_src()
+ # add inherited fields that are not redefined locally
+ for name, field in fields.iteritems():
+ if name not in cls._fields:
+ cls._add_field(name, field)
@classmethod
def _get_column_infos(cls):
@@ -2982,34 +2939,74 @@ class BaseModel(object):
@api.model
def _prepare_setup_fields(self):
""" Prepare the setup of fields once the models have been loaded. """
- for field in self._fields.itervalues():
- field.reset()
+ type(self)._setup_done = False
+ for name, field in self._fields.items():
+ if field.inherited:
+ del self._fields[name]
+ else:
+ field.reset()
@api.model
- def _setup_fields(self, partial=False):
+ def _setup_fields(self):
""" Setup the fields (dependency triggers, etc). """
- for field in self._fields.itervalues():
- try:
- field.setup(self.env)
- except Exception:
- if not partial:
- raise
+ cls = type(self)
+ if cls._setup_done:
+ return
+ cls._setup_done = True
+
+ # first make sure that parent models are all set up
+ for parent in self._inherits:
+ self.env[parent]._setup_fields()
+
+ # retrieve custom fields
+ if not self._context.get('_setup_fields_partial'):
+ cls._init_manual_fields(self._cr)
+
+ # retrieve inherited fields
+ cls._inherits_check()
+ cls._inherits_reload()
+
+ # set up fields
+ for field in cls._fields.itervalues():
+ field.setup(self.env)
- # update columns (fields may have changed), and column_infos
- for name, field in self._fields.iteritems():
+ # update columns (fields may have changed)
+ for name, field in cls._fields.iteritems():
if field.column:
- self._columns[name] = field.to_column()
- self._inherits_reload()
+ cls._columns[name] = field.to_column()
# group fields by compute to determine field.computed_fields
fields_by_compute = defaultdict(list)
- for field in self._fields.itervalues():
+ for field in cls._fields.itervalues():
if field.compute:
field.computed_fields = fields_by_compute[field.compute]
field.computed_fields.append(field)
else:
field.computed_fields = []
+ # check constraints
+ for func in cls._constraint_methods:
+ if not all(name in cls._fields for name in func._constrains):
+ _logger.warning("@constrains%r parameters must be field names", func._constrains)
+ for name in cls._onchange_methods:
+ if name not in cls._fields:
+ func = cls._onchange_methods[name]
+ _logger.warning("@onchange%r parameters must be field names", func._onchange)
+
+ # check defaults
+ for name in cls._defaults:
+ assert name in cls._fields, \
+ "Model %s has a default for nonexiting field %s" % (cls._name, name)
+
+ # validate rec_name
+ if cls._rec_name:
+ assert cls._rec_name in cls._fields, \
+ "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
+ elif 'name' in cls._fields:
+ cls._rec_name = 'name'
+ elif 'x_name' in cls._fields:
+ cls._rec_name = 'x_name'
+
def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
""" fields_get([fields])
diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py
index 34672c44a78..7cf734d792e 100644
--- a/openerp/modules/registry.py
+++ b/openerp/modules/registry.py
@@ -164,8 +164,9 @@ class Registry(Mapping):
# do the actual setup from a clean state
self._m2m = {}
+ context = {'_setup_fields_partial': partial}
for model in self.models.itervalues():
- model._setup_fields(cr, SUPERUSER_ID, partial=partial)
+ model._setup_fields(cr, SUPERUSER_ID, context=context)
def clear_caches(self):
""" Clear the caches
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index e8d416652bb..59e607b4510 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -136,9 +136,8 @@ class _column(object):
for key, val in args.iteritems():
setattr(self, key, val)
- # prefetch only if self._classic_write, not self.groups, and not
- # self.deprecated
- if not self._classic_write or self.deprecated:
+ # prefetch only if _classic_write, not deprecated and not manual
+ if not self._classic_write or self.deprecated or self.manual:
self._prefetch = False
def new(self, **args):
diff --git a/openerp/tests/common.py b/openerp/tests/common.py
index b3f6dbe81a3..f058436ad33 100644
--- a/openerp/tests/common.py
+++ b/openerp/tests/common.py
@@ -16,6 +16,7 @@ import time
import unittest2
import urllib2
import xmlrpclib
+from contextlib import contextmanager
from datetime import datetime, timedelta
import werkzeug
@@ -104,6 +105,20 @@ class BaseCase(unittest2.TestCase):
module, xid = xid.split('.')
return self.registry('ir.model.data').get_object(self.cr, self.uid, module, xid)
+ @contextmanager
+ def _assertRaises(self, exception):
+ """ Context manager that clears the environment upon failure. """
+ with super(BaseCase, self).assertRaises(exception) as cm:
+ with self.env.clear_upon_failure():
+ yield cm
+
+ def assertRaises(self, exception, func=None, *args, **kwargs):
+ if func:
+ with self._assertRaises(exception):
+ func(*args, **kwargs)
+ else:
+ return self._assertRaises(exception)
+
class TransactionCase(BaseCase):
""" TestCase in which each test method is run in its own transaction,
@@ -120,6 +135,8 @@ class TransactionCase(BaseCase):
self.env = api.Environment(self.cr, self.uid, {})
def tearDown(self):
+ # rollback and close the cursor, and reset the environments
+ self.env.reset()
self.cr.rollback()
self.cr.close()
@@ -139,9 +156,12 @@ class SingleTransactionCase(BaseCase):
@classmethod
def tearDownClass(cls):
+ # rollback and close the cursor, and reset the environments
+ cls.env.reset()
cls.cr.rollback()
cls.cr.close()
+
class RedirectHandler(urllib2.HTTPRedirectHandler):
"""
HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and