diff options
| author | Christophe Simonis <chs@odoo.com> | 2014-11-17 17:31:12 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2014-11-17 17:31:12 +0100 |
| commit | d64fe8279b4a1d3083a87b7e5fb211ddbbae50ca (patch) | |
| tree | 207e10e9f6f69206862c6329a5dcd25be5d15f9b | |
| parent | b2563509ffd89fdadc1256cb8118d7106f36e2e3 (diff) | |
| parent | c92e70b929376ba742ea17187ec521cf706302e5 (diff) | |
[MERGE] forward port of branch 8.0 up to c92e70b
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]) <= 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&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&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','<>','view')]"/> - <field name="location_dest_id" domain="[('usage','=','internal')]"/> + <field name="location_dest_id" domain="[('usage','<>','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 Binary files differdeleted file mode 100644 index 65a8d42363d..00000000000 --- a/doc/images/view-on-github.png +++ /dev/null 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 |
