From a329aa19499c4e59e97abd02401196bab66687c2 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 18 Mar 2015 14:05:02 +0100 Subject: [PATCH] [MERGE] forward port of branch 8.0 up to e27744d --- addons/account/account_pre_install.yml | 2 +- addons/account/views/report_invoice.xml | 2 +- addons/account_asset/account_asset.py | 9 ++- addons/account_payment/account_move_line.py | 4 +- addons/mail/res_users_view.xml | 12 ++++ addons/mrp/mrp.py | 25 +++++-- addons/point_of_sale/point_of_sale.py | 15 +++-- addons/point_of_sale/views/templates.xml | 1 + addons/product/product.py | 13 +++- addons/purchase/purchase.py | 6 +- addons/purchase/purchase_view.xml | 2 +- .../purchase/test/process/run_scheduler.yml | 2 +- addons/sale/sale.py | 2 + addons/sale_stock/sale_stock.py | 13 ---- addons/share/security/share_security.xml | 2 +- addons/stock/product.py | 2 +- addons/stock/stock.py | 65 +++++++------------ addons/stock/tests/test_stock_flow.py | 52 +++++++++++++++ addons/stock/views/report_stockpicking.xml | 27 +++++--- addons/web_kanban/static/src/js/kanban.js | 10 ++- addons/website/controllers/main.py | 5 +- addons/website/static/src/js/website.js | 4 +- addons/website_report/controllers/main.py | 5 +- doc/cla/corporate/bloopark.md | 20 ++++++ doc/cla/corporate/therp.md | 21 ++++++ doc/cla/individual/eLBati.md | 11 ++++ doc/cla/individual/matthieuchoplin.md | 11 ++++ openerp/addons/base/ir/ir_actions.py | 2 +- openerp/addons/base/ir/ir_model.py | 4 +- openerp/addons/base/ir/ir_sequence.py | 2 +- openerp/addons/base/tests/test_ir_sequence.py | 30 ++++++++- openerp/addons/base/workflow/workflow.py | 2 +- openerp/fields.py | 8 +-- openerp/models.py | 9 ++- openerp/tools/misc.py | 2 +- 35 files changed, 293 insertions(+), 109 deletions(-) create mode 100644 doc/cla/corporate/bloopark.md create mode 100644 doc/cla/corporate/therp.md create mode 100644 doc/cla/individual/eLBati.md create mode 100644 doc/cla/individual/matthieuchoplin.md diff --git a/addons/account/account_pre_install.yml b/addons/account/account_pre_install.yml index e295989e698..798c0d954a5 100644 --- a/addons/account/account_pre_install.yml +++ b/addons/account/account_pre_install.yml @@ -7,7 +7,7 @@ wiz = wizards.browse(cr, uid, ref('account.account_configuration_installer_todo')) part = self.pool.get('res.partner').browse(cr, uid, ref('base.main_partner')) # if we know the country and the wizard has not yet been executed, we do it - if (part.country_id.id) and (wiz.state=='open'): + if (part.country_id.id) and (part.country_id.code) and (wiz.state=='open'): mod = 'l10n_'+part.country_id.code.lower() ids = modules.search(cr, uid, [ ('name','=',mod) ], context=context) if ids: diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index 1e244d5a40c..79174cd77dd 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -8,7 +8,7 @@
- + VAT:
diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index e3f69d79e26..d1dfa90901f 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -171,10 +171,6 @@ class account_asset_asset(osv.osv): for x in range(len(posted_depreciation_line_ids), undone_dotation_number): i = x + 1 amount = self._compute_board_amount(cr, uid, asset, i, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date, context=context) - company_currency = asset.company_id.currency_id.id - current_currency = asset.currency_id.id - # compute amount into company currency - amount = currency_obj.compute(cr, uid, current_currency, company_currency, amount, context=context) residual_amount -= amount vals = { 'amount': amount, @@ -215,7 +211,10 @@ class account_asset_asset(osv.osv): l.asset_id IN %s GROUP BY l.asset_id """, (tuple(ids),)) res=dict(cr.fetchall()) for asset in self.browse(cr, uid, ids, context): - res[asset.id] = asset.purchase_value - res.get(asset.id, 0.0) - asset.salvage_value + company_currency = asset.company_id.currency_id.id + current_currency = asset.currency_id.id + amount = self.pool['res.currency'].compute(cr, uid, company_currency, current_currency, res.get(asset.id, 0.0), context=context) + res[asset.id] = asset.purchase_value - amount - asset.salvage_value for id in ids: res.setdefault(id, 0.0) return res diff --git a/addons/account_payment/account_move_line.py b/addons/account_payment/account_move_line.py index 8bfe07573a0..d13eba17c35 100644 --- a/addons/account_payment/account_move_line.py +++ b/addons/account_payment/account_move_line.py @@ -27,8 +27,10 @@ from operator import itemgetter class account_move_line(osv.osv): _inherit = "account.move.line" + # delegate to parent, used for local fields.function redefinition def _amount_residual(self, cr, uid, ids, field_names, args, context=None): - return self._amount_residual(self, cr, uid, ids, field_names, args, context=context) + return super(account_move_line, self)._amount_residual( + cr, uid, ids, field_names, args, context=context) def _to_pay_search(self, cr, uid, obj, name, args, context=None): if not args: diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml index eef2924134f..7bbe369c4d7 100644 --- a/addons/mail/res_users_view.xml +++ b/addons/mail/res_users_view.xml @@ -16,6 +16,18 @@ + + + res.users.form.mail diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index acfcb9a1ab9..6532b32e167 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -1094,14 +1094,27 @@ class mrp_production(osv.osv): #is 1 element long, so we can take the first. return stock_move.action_confirm(cr, uid, [move_id], context=context)[0] - def _get_raw_material_procure_method(self, cr, uid, product, context=None): - '''This method returns the procure_method to use when creating the stock move for the production raw materials''' + def _get_raw_material_procure_method(self, cr, uid, product, location_id=False, location_dest_id=False, context=None): + '''This method returns the procure_method to use when creating the stock move for the production raw materials + Besides the standard configuration of looking if the product or product category has the MTO route, + you can also define a rule e.g. from Stock to Production (which might be used in the future like the sale orders) + ''' warehouse_obj = self.pool['stock.warehouse'] + routes = product.route_ids + product.categ_id.total_route_ids + + if location_id and location_dest_id: + pull_obj = self.pool['procurement.rule'] + pulls = pull_obj.search(cr, uid, [('route_id', 'in', [x.id for x in routes]), + ('location_id', '=', location_dest_id), + ('location_src_id', '=', location_id)], limit=1, context=context) + if pulls: + return pull_obj.browse(cr, uid, pulls[0], context=context).procure_method + try: mto_route = warehouse_obj._get_mto_route(cr, uid, context=context) except: return "make_to_stock" - routes = product.route_ids + product.categ_id.total_route_ids + if mto_route in [x.id for x in routes]: return "make_to_order" return "make_to_stock" @@ -1133,7 +1146,8 @@ class mrp_production(osv.osv): move = stock_move.copy(cr, uid, move_id, default = { 'location_id': source_location_id, 'location_dest_id': dest_location_id, - 'procure_method': self._get_raw_material_procure_method(cr, uid, product, context=context), + 'procure_method': self._get_raw_material_procure_method(cr, uid, product, location_id=source_location_id, + location_dest_id=dest_location_id, context=context), 'raw_material_production_id': False, 'move_dest_id': move_id, 'picking_type_id': types and types[0] or False, @@ -1166,7 +1180,8 @@ class mrp_production(osv.osv): 'location_id': source_location_id, 'location_dest_id': destination_location_id, 'company_id': production.company_id.id, - 'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, context=context), #Make_to_stock avoids creating procurement + 'procure_method': prev_move and 'make_to_stock' or self._get_raw_material_procure_method(cr, uid, product, location_id=source_location_id, + location_dest_id=destination_location_id, context=context), #Make_to_stock avoids creating procurement 'raw_material_production_id': production.id, #this saves us a browse in create() 'price_unit': product.standard_price, diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 19dc7f09b69..bd05695fbe6 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -1128,11 +1128,16 @@ class pos_order(osv.osv): if not grouped_data[key]: grouped_data[key].append(values) else: - current_value = grouped_data[key][0] - current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) - current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) - current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) - current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + for line in grouped_data[key]: + if line.get('tax_code_id') == values.get('tax_code_id'): + current_value = line + current_value['quantity'] = current_value.get('quantity', 0.0) + values.get('quantity', 0.0) + current_value['credit'] = current_value.get('credit', 0.0) + values.get('credit', 0.0) + current_value['debit'] = current_value.get('debit', 0.0) + values.get('debit', 0.0) + current_value['tax_amount'] = current_value.get('tax_amount', 0.0) + values.get('tax_amount', 0.0) + break + else: + grouped_data[key].append(values) else: grouped_data[key].append(values) diff --git a/addons/point_of_sale/views/templates.xml b/addons/point_of_sale/views/templates.xml index 9a9f7f1d5c1..d45d124e8ea 100644 --- a/addons/point_of_sale/views/templates.xml +++ b/addons/point_of_sale/views/templates.xml @@ -35,6 +35,7 @@ var s = new openerp.init(); var wc = new s.web.WebClient(); + wc._title_changed = function() {}; wc.show_application = function() { wc.action_manager.do_action("pos.ui"); }; diff --git a/addons/product/product.py b/addons/product/product.py index 0d442bb36fa..83dd371bfb6 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -791,6 +791,12 @@ class product_template(osv.osv): return super(product_template, self).name_get(cr, user, ids, context) def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): + # Only use the product.product heuristics if there is a search term and the domain + # does not specify a match on `product.template` IDs. + if not name or any(term[0] == 'id' for term in (args or [])): + return super(product_template, self).name_search( + cr, user, name=name, args=args, operator=operator, context=context, limit=limit) + product_product = self.pool['product.product'] results = product_product.name_search( cr, user, name, args, operator=operator, context=context, limit=limit) @@ -798,8 +804,11 @@ class product_template(osv.osv): template_ids = [p.product_tmpl_id.id for p in product_product.browse( cr, user, product_ids, context=context)] - uniq_ids = OrderedDict.fromkeys(template_ids).keys() - return self.name_get(cr, user, uniq_ids, context=context) + + # re-apply product.template order + name_get + return super(product_template, self).name_search( + cr, user, '', args=[('id', 'in', template_ids)], + operator=operator, context=context, limit=limit) class product_product(osv.osv): _name = "product.product" diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 80c8b370101..c8ba0ce6ee9 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1019,9 +1019,9 @@ class purchase_order_line(osv.osv): if line.state not in ['draft', 'cancel']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,)) procurement_obj = self.pool.get('procurement.order') - procurement_ids_to_cancel = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) - if procurement_ids_to_cancel: - self.pool['procurement.order'].cancel(cr, uid, procurement_ids_to_cancel) + procurement_ids_to_except = procurement_obj.search(cr, uid, [('purchase_line_id', 'in', ids)], context=context) + if procurement_ids_to_except: + self.pool['procurement.order'].write(cr, uid, procurement_ids_to_except, {'state': 'exception'}, context=context) return super(purchase_order_line, self).unlink(cr, uid, ids, context=context) def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id, diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index ff5ca396609..ade148a5e71 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -550,7 +550,7 @@ On Purchase Order Lines ir.actions.act_window purchase.order.line - [('order_id.invoice_method','<>','picking'), ('state','in',('confirmed','done')), ('invoiced', '=', False)] + [('order_id.invoice_method','=','manual'), ('state','in',('confirmed','done')), ('invoiced', '=', False)] form tree,form diff --git a/addons/purchase/test/process/run_scheduler.yml b/addons/purchase/test/process/run_scheduler.yml index 01aa497311d..83c5c6a099b 100644 --- a/addons/purchase/test/process/run_scheduler.yml +++ b/addons/purchase/test/process/run_scheduler.yml @@ -39,4 +39,4 @@ procurement = self.browse(cr, uid, ref('procurement_order_testcase0')) procurement.purchase_line_id.unlink() procurement.refresh() - assert procurement.state == 'cancel', 'Procurement should be cancelled' + assert procurement.state == 'exception', 'Procurement should be in exception' diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 2e181782775..7a7fd777e49 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -578,11 +578,13 @@ class sale_order(osv.osv): context = {} sale_order_line_obj = self.pool.get('sale.order.line') account_invoice_obj = self.pool.get('account.invoice') + procurement_obj = self.pool.get('procurement.order') for sale in self.browse(cr, uid, ids, context=context): for inv in sale.invoice_ids: if inv.state not in ('draft', 'cancel'): raise UserError(_('Cannot cancel this sales order!') + ':' + _('First cancel all invoices attached to this sales order.')) inv.signal_workflow('invoice_cancel') + procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in sale.order_line],[])) sale_order_line_obj.write(cr, uid, [l.id for l in sale.order_line], {'state': 'cancel'}) self.write(cr, uid, ids, {'state': 'cancel'}) diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index 35827cbc04d..0f99767a317 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -155,19 +155,6 @@ class sale_order(osv.osv): move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context) return res - def action_cancel(self, cr, uid, ids, context=None): - if context is None: - context = {} - sale_order_line_obj = self.pool.get('sale.order.line') - proc_obj = self.pool.get('procurement.order') - stock_obj = self.pool.get('stock.picking') - for sale in self.browse(cr, uid, ids, context=context): - for pick in sale.picking_ids: - if pick.state not in ('draft', 'cancel'): - raise UserError(_('Cannot cancel sales order!') + '\n' + _('You must first cancel all delivery order(s) attached to this sales order.')) - stock_obj.signal_workflow(cr, uid, [p.id for p in sale.picking_ids], 'button_cancel') - return super(sale_order, self).action_cancel(cr, uid, ids, context=context) - def action_wait(self, cr, uid, ids, context=None): res = super(sale_order, self).action_wait(cr, uid, ids, context=context) for o in self.browse(cr, uid, ids): diff --git a/addons/share/security/share_security.xml b/addons/share/security/share_security.xml index 8619cfa4ca3..38edcdf2e89 100644 --- a/addons/share/security/share_security.xml +++ b/addons/share/security/share_security.xml @@ -11,7 +11,7 @@ Shared Group This group is a template group for shared users, giving basic access needs, the menus for instance. - + 1 diff --git a/addons/stock/product.py b/addons/stock/product.py index 42fecb062f0..c0a0b5d1a5b 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -186,7 +186,7 @@ class product_product(osv.osv): operator = '==' ids = [] - if name == 'qty_available': + if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')): res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context))) else: product_ids = self.search(cr, uid, [], context=context) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index d73edc3adcd..829be277793 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2660,14 +2660,6 @@ class stock_inventory(osv.osv): self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0}) return True - def _inventory_line_hook(self, cr, uid, inventory_line, move_vals): - """ Creates a stock move from an inventory line - @param inventory_line: - @param move_vals: - @return: - """ - return self.pool.get('stock.move').create(cr, uid, move_vals) - def action_done(self, cr, uid, ids, context=None): """ Finish the inventory @return: True @@ -2686,35 +2678,7 @@ class stock_inventory(osv.osv): #as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior #as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want). move_obj = self.pool.get('stock.move') - move_obj.action_done(cr, uid, [x.id for x in inv.move_ids], context=context) - - def _create_stock_move(self, cr, uid, inventory, todo_line, context=None): - stock_move_obj = self.pool.get('stock.move') - product_obj = self.pool.get('product.product') - inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id - vals = { - 'name': _('INV:') + (inventory.name or ''), - 'product_id': todo_line['product_id'], - 'product_uom': todo_line['product_uom_id'], - 'date': inventory.date, - 'company_id': inventory.company_id.id, - 'inventory_id': inventory.id, - 'state': 'assigned', - 'restrict_lot_id': todo_line.get('prod_lot_id'), - 'restrict_partner_id': todo_line.get('partner_id'), - } - - if todo_line['product_qty'] < 0: - #found more than expected - vals['location_id'] = inventory_location_id - vals['location_dest_id'] = todo_line['location_id'] - vals['product_uom_qty'] = -todo_line['product_qty'] - else: - #found less than expected - vals['location_id'] = todo_line['location_id'] - vals['location_dest_id'] = inventory_location_id - vals['product_uom_qty'] = todo_line['product_qty'] - return stock_move_obj.create(cr, uid, vals, context=context) + move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.state != 'done'], context=context) def action_check(self, cr, uid, ids, context=None): """ Checks the inventory and computes the stock move to do @@ -2728,13 +2692,14 @@ class stock_inventory(osv.osv): stock_move_obj.unlink(cr, uid, move_ids, context=context) for line in inventory.line_ids: #compare the checked quantities on inventory lines to the theorical one - inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context) + stock_move = inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context) def action_cancel_draft(self, cr, uid, ids, context=None): """ Cancels the stock move and change inventory state to draft. @return: True """ for inv in self.browse(cr, uid, ids, context=context): + self.write(cr, uid, [inv.id], {'line_ids': [(5,)]}, context=context) self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context) self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context) return True @@ -2862,7 +2827,7 @@ class stock_inventory_line(osv.osv): def _get_quants(self, cr, uid, line, context=None): quant_obj = self.pool["stock.quant"] - dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id), + dom = [('company_id', '=', line.company_id.id), ('location_id', '=', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id), ('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id), ('package_id', '=', line.package_id.id)] quants = quant_obj.search(cr, uid, dom, context=context) return quants @@ -2882,7 +2847,7 @@ class stock_inventory_line(osv.osv): product = self.pool.get('product.product').browse(cr, uid, product_id, context=context) if not company_id: company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id - dom = [('company_id', '=', company_id), ('location_id', 'child_of', location_id), ('lot_id', '=', prod_lot_id), + dom = [('company_id', '=', company_id), ('location_id', '=', location_id), ('lot_id', '=', prod_lot_id), ('product_id','=', product_id), ('owner_id', '=', partner_id), ('package_id', '=', package_id)] quants = quant_obj.search(cr, uid, dom, context=context) th_qty = sum([x.qty for x in quant_obj.browse(cr, uid, quants, context=context)]) @@ -2893,6 +2858,7 @@ class stock_inventory_line(osv.osv): def _resolve_inventory_line(self, cr, uid, inventory_line, context=None): stock_move_obj = self.pool.get('stock.move') + quant_obj = self.pool.get('stock.quant') diff = inventory_line.theoretical_qty - inventory_line.product_qty if not diff: return @@ -2919,7 +2885,24 @@ class stock_inventory_line(osv.osv): vals['location_id'] = inventory_line.location_id.id vals['location_dest_id'] = inventory_location_id vals['product_uom_qty'] = diff - return stock_move_obj.create(cr, uid, vals, context=context) + move_id = stock_move_obj.create(cr, uid, vals, context=context) + move = stock_move_obj.browse(cr, uid, move_id, context=context) + if diff > 0: + domain = [('qty', '>', 0.0), ('package_id', '=', inventory_line.package_id.id), ('lot_id', '=', inventory_line.prod_lot_id.id), ('location_id', '=', inventory_line.location_id.id)] + preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', inventory_line.inventory_id.id)]] + quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, move.product_qty, domain=domain, prefered_domain_list=preferred_domain_list, restrict_partner_id=move.restrict_partner_id.id, context=context) + quant_obj.quants_reserve(cr, uid, quants, move, context=context) + elif inventory_line.package_id: + stock_move_obj.action_done(cr, uid, move_id, context=context) + quants = [x.id for x in move.quant_ids] + quant_obj.write(cr, uid, quants, {'package_id': inventory_line.package_id.id}, context=context) + res = quant_obj.search(cr, uid, [('qty', '<', 0.0), ('product_id', '=', move.product_id.id), + ('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1, context=context) + if res: + for quant in move.quant_ids: + if quant.location_id.id == move.location_dest_id.id: #To avoid we take a quant that was reconcile already + quant_obj._quant_reconcile_negative(cr, uid, quant, move, context=context) + return move_id # Should be left out in next version def restrict_change(self, cr, uid, ids, theoretical_qty, context=None): diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 32c9e5e3eb0..bd3302b5be9 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1095,3 +1095,55 @@ class TestStockFlow(TestStockCommon): total_qty = [quant.qty for quant in quants] self.assertEqual(sum(total_qty), 4000, 'Expecting 4000 kg , got %.4f on location stock!' % (sum(total_qty))) self.assertEqual(productKG.qty_available, 4000, 'Expecting 4000 kg , got %.4f of quantity available!' % (productKG.qty_available)) + + + #-------------------------------------------------------- + # TEST PARTIAL INVENTORY WITH PACKS and LOTS + #--------------------------------------------------------- + + packproduct = self.ProductObj.create({'name': 'Pack Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id}) + lotproduct = self.ProductObj.create({'name': 'Lot Product', 'uom_id': self.uom_unit.id, 'uom_po_id': self.uom_unit.id}) + inventory = self.InvObj.create({'name': 'Test Partial and Pack', + 'filter': 'partial', + 'location_id': self.stock_location}) + inventory.prepare_inventory() + pack_obj = self.env['stock.quant.package'] + lot_obj = self.env['stock.production.lot'] + pack1 = pack_obj.create({'name': 'PACK00TEST1'}) + pack2 = pack_obj.create({'name': 'PACK00TEST2'}) + lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id}) + move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) + self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare") + + line_vals = [] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id, 'package_id': pack1.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 30, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 25, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] + inventory.write({'line_ids': [(0, 0, x) for x in line_vals]}) + inventory.action_done() + self.assertEqual(packproduct.qty_available, 30, "Wrong qty available for packproduct") + self.assertEqual(lotproduct.qty_available, 55, "Wrong qty available for lotproduct") + quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty)) + + #Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack + inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2', + 'filter': 'partial', + 'location_id': self.stock_location}) + inventory2.prepare_inventory() + line_vals = [] + line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 20, 'product_uom_id': packproduct.uom_id.id}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 0, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': False}] + line_vals += [{'location_id': self.stock_location, 'product_id': lotproduct.id, 'product_qty': 10, 'product_uom_id': lotproduct.uom_id.id, 'prod_lot_id': lot1.id}] + inventory2.write({'line_ids': [(0, 0, x) for x in line_vals]}) + inventory2.action_done() + self.assertEqual(packproduct.qty_available, 40, "Wrong qty available for packproduct") + self.assertEqual(lotproduct.qty_available, 10, "Wrong qty available for lotproduct") + quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', lot1.id)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 10, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) + quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', False)]) + total_qty = sum([quant.qty for quant in quants]) + self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) \ No newline at end of file diff --git a/addons/stock/views/report_stockpicking.xml b/addons/stock/views/report_stockpicking.xml index 8f42b94a3d3..fb7bff3f23d 100644 --- a/addons/stock/views/report_stockpicking.xml +++ b/addons/stock/views/report_stockpicking.xml @@ -83,15 +83,24 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 4ad9c893097..3bc5684486a 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -28,6 +28,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ this.group_by_field = {}; this.grouped_by_m2o = false; this.many2manys = []; + this.m2m_context = {}; this.state = { groups : {}, records : {} @@ -107,6 +108,11 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ this.transform_qweb_template(child); this.qweb.add_template(instance.web.json_node_to_xml(child)); break; + } else if (child.tag === 'field') { + var ftype = child.attrs.widget || this.fields_view.fields[child.attrs.name].type; + if(ftype == "many2many" && "context" in child.attrs) { + this.m2m_context[child.attrs.name] = child.attrs.context; + } } } }, @@ -570,7 +576,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ return; } if (!relations[field.relation]) { - relations[field.relation] = { ids: [], elements: {}}; + relations[field.relation] = { ids: [], elements: {}, context: self.m2m_context[name]}; } var rel = relations[field.relation]; field.raw_value.forEach(function(id) { @@ -584,7 +590,7 @@ instance.web_kanban.KanbanView = instance.web.View.extend({ }); }); _.each(relations, function(rel, rel_name) { - var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context()); + var dataset = new instance.web.DataSetSearch(self, rel_name, self.dataset.get_context(rel.context)); dataset.name_get(_.uniq(rel.ids)).done(function(result) { result.forEach(function(nameget) { $(rel.elements[nameget[0]]).append('' + _.str.escapeHTML(nameget[1]) + ''); diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index d7262d54c6d..ba7166b45a8 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -480,7 +480,10 @@ class Website(openerp.addons.web.controllers.main.Home): #------------------------------------------------------ # Server actions #------------------------------------------------------ - @http.route('/website/action/', type='http', auth="public", website=True) + @http.route([ + '/website/action/', + '/website/action//', + ], type='http', auth="public", website=True) def actions_server(self, path_or_xml_id_or_id, **post): cr, uid, context = request.cr, request.uid, request.context res, action_id, action = None, None, None diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 8c8dfcc50c7..26a0ce0477e 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -323,7 +323,9 @@ function treat_node(node){ if(node.nodeType === 3) { if(node.nodeValue.match(/\S/)){ - node.nodeValue = openerp._t($.trim(node.nodeValue)); + var text_value = $.trim(node.nodeValue); + var spaces = node.nodeValue.split(text_value); + node.nodeValue = spaces[0] + openerp._t(text_value) + spaces[1]; } } else if(node.nodeType === 1 && node.hasChildNodes()) { diff --git a/addons/website_report/controllers/main.py b/addons/website_report/controllers/main.py index 5f79e259c60..5350bc4d2b0 100644 --- a/addons/website_report/controllers/main.py +++ b/addons/website_report/controllers/main.py @@ -28,6 +28,7 @@ class Website(Website): @route() def customize_template_get(self, key, full=False, bundles=False): res = super(Website, self).customize_template_get(key, full=full, bundles=bundles) - if full and request.session.get('report_view_ids'): - res += request.session['report_view_ids'] + if full: + for r in request.session.get('report_view_ids', []): + res += super(Website, self).customize_template_get(r.get('xml_id'), full=full) return res diff --git a/doc/cla/corporate/bloopark.md b/doc/cla/corporate/bloopark.md new file mode 100644 index 00000000000..2664bbe3006 --- /dev/null +++ b/doc/cla/corporate/bloopark.md @@ -0,0 +1,20 @@ +Germany 2015-02-25 + +bloopark systems GmbH & Co. KG agrees to the terms of the Odoo Corporate +Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Jens Holze jholze@bloopark.de https://github.com/jholze + +List of contributors: + +Jens Holze jholze@bloopark.de https://github.com/jholze +Christoph Giesel cgiesel@bloopark.de https://github.com/christophlsa +Benjamin Bachmann bBachmann@bloopark.de https://github.com/benniphx +Robert Rübner rruebner@bloopark.de https://github.com/rruebner +Florian Fischer ffischer@bloopark.de https://github.com/florianfischer +Mercerdes Scenna mscenna@bloopark.de https://github.com/mscenna diff --git a/doc/cla/corporate/therp.md b/doc/cla/corporate/therp.md new file mode 100644 index 00000000000..428ecb2b411 --- /dev/null +++ b/doc/cla/corporate/therp.md @@ -0,0 +1,21 @@ +Netherlands, 2015-03-09 + +Therp BV agrees to the terms of the Odoo Corporate +Contributor License Agreement v1.0. + +We declare that we are authorized and able to make this agreement and sign +this declaration. + +Signed, + +* Ronald Portier rportier@therp.nl https://github.com/nl66278 +* Stefan Rijnhart stefan@therp.nl https://github.com/stefanrijnhart + +List of contributors: + +* Giovanni Capalbo giovanni@therp.nl https://github.com/gfcapalbo +* Holger Brunn hbrunn@therp.nl https://github.com/hbrunn +* Lara Freeke lfreeke@therp.nl https://github.com/lfreeke +* Ronald Portier rportier@therp.nl https://github.com/nl66278 +* Stefan Rijnhart stefan@therp.nl https://github.com/stefanrijnhart + diff --git a/doc/cla/individual/eLBati.md b/doc/cla/individual/eLBati.md new file mode 100644 index 00000000000..50104556d80 --- /dev/null +++ b/doc/cla/individual/eLBati.md @@ -0,0 +1,11 @@ +Italy, 2015-02-18 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Lorenzo Battistini lorenzo.battistini@agilebg.com https://github.com/eLBati diff --git a/doc/cla/individual/matthieuchoplin.md b/doc/cla/individual/matthieuchoplin.md new file mode 100644 index 00000000000..4533005bfd0 --- /dev/null +++ b/doc/cla/individual/matthieuchoplin.md @@ -0,0 +1,11 @@ +United Kingdom, 13.03.2015 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Matthieu Choplin choplin.mat@gmail.com https://github.com/matthieuchoplin diff --git a/openerp/addons/base/ir/ir_actions.py b/openerp/addons/base/ir/ir_actions.py index 2b7e2616151..cb71de52b71 100644 --- a/openerp/addons/base/ir/ir_actions.py +++ b/openerp/addons/base/ir/ir_actions.py @@ -1086,7 +1086,7 @@ class ir_server_object_lines(osv.osv): _sequence = 'ir_actions_id_seq' _columns = { - 'server_id': fields.many2one('ir.actions.server', 'Related Server Action'), + 'server_id': fields.many2one('ir.actions.server', 'Related Server Action', ondelete='cascade'), 'col1': fields.many2one('ir.model.fields', 'Field', required=True), 'value': fields.text('Value', required=True, help="Expression containing a value specification. \n" "When Formula type is selected, this field may be a Python expression " diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index aeead6e664e..798389ecc28 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -382,13 +382,13 @@ class ir_model_fields(osv.osv): if vals.get('relation',False) and not self.pool['ir.model'].search(cr, user, [('model','=',vals['relation'])]): raise UserError(_("Model %s does not exist!") % vals['relation']) + self.pool.clear_manual_fields() + if vals['model'] in self.pool: model = self.pool[vals['model']] if vals['model'].startswith('x_') and vals['name'] == 'x_name': model._rec_name = 'x_name' - self.pool.clear_manual_fields() - # re-initialize model in registry model.__init__(self.pool, cr) self.pool.setup_models(cr, partial=(not self.pool.ready)) diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index 32bde90e664..a9a2dcd073c 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -179,7 +179,7 @@ class ir_sequence(models.Model): if new_implementation in ('standard', None): # Implementation has NOT changed. # Only change sequence if really requested. - if seq.number_next != n: + if values.get('number_next'): _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_next=n) if seq.number_increment != i: _alter_sequence(self.env.cr, "ir_sequence_%03d" % seq.id, number_increment=i) diff --git a/openerp/addons/base/tests/test_ir_sequence.py b/openerp/addons/base/tests/test_ir_sequence.py index b0d106600d2..04a3c256ff5 100644 --- a/openerp/addons/base/tests/test_ir_sequence.py +++ b/openerp/addons/base/tests/test_ir_sequence.py @@ -196,5 +196,33 @@ class test_ir_sequence_generate(unittest2.TestCase): drop_sequence('test_sequence_type_6') -if __name__ == '__main__': +class Test_ir_sequence_init(common.TransactionCase): + + def test_00(self): + registry, cr, uid = self.registry, self.cr, self.uid + + # test if read statement return the good number_next value (from postgreSQL sequence and not ir_sequence value) + sequence = registry('ir.sequence') + # first creation of sequence (normal) + values = {'number_next': 1, + 'company_id': 1, + 'padding': 4, + 'number_increment': 1, + 'implementation': 'standard', + 'name': 'test-sequence-00'} + seq_id = sequence.create(cr, uid, values) + # Call get next 4 times + sequence.next_by_id(cr, uid, seq_id) + sequence.next_by_id(cr, uid, seq_id) + sequence.next_by_id(cr, uid, seq_id) + read_sequence = sequence.next_by_id(cr, uid, seq_id) + # Read the value of the current sequence + assert read_sequence == "0004", 'The actual sequence value must be 4. reading : %s' % read_sequence + # reset sequence to 1 by write method calling + sequence.write(cr, uid, [seq_id], {'number_next': 1}) + # Read the value of the current sequence + read_sequence = sequence.next_by_id(cr, uid, seq_id) + assert read_sequence == "0001", 'The actual sequence value must be 1. reading : %s' % read_sequence + +if __name__ == "__main__": unittest2.main() diff --git a/openerp/addons/base/workflow/workflow.py b/openerp/addons/base/workflow/workflow.py index 4d23ea1bb53..63a1abf5d10 100644 --- a/openerp/addons/base/workflow/workflow.py +++ b/openerp/addons/base/workflow/workflow.py @@ -177,7 +177,7 @@ class wkf_workitem(osv.osv): _columns = { 'act_id': fields.many2one('workflow.activity', 'Activity', required=True, ondelete="cascade", select=True), 'wkf_id': fields.related('act_id','wkf_id', type='many2one', relation='workflow', string='Workflow'), - 'subflow_id': fields.many2one('workflow.instance', 'Subflow', ondelete="cascade", select=True), + 'subflow_id': fields.many2one('workflow.instance', 'Subflow', ondelete="set null", select=True), 'inst_id': fields.many2one('workflow.instance', 'Instance', required=True, ondelete="cascade", select=True), 'state': fields.char('Status', select=True), } diff --git a/openerp/fields.py b/openerp/fields.py index 7be703b3ae5..82ecce34d74 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -1267,20 +1267,18 @@ class Selection(Field): def set_class_name(self, cls, name): super(Selection, self).set_class_name(cls, name) # determine selection (applying 'selection_add' extensions) - selection = None for field in resolve_all_mro(cls, name, reverse=True): if isinstance(field, type(self)): # We cannot use field.selection or field.selection_add here # because those attributes are overridden by `set_class_name`. if 'selection' in field._attrs: - selection = field._attrs['selection'] + self.selection = field._attrs['selection'] if 'selection_add' in field._attrs: # use an OrderedDict to update existing values selection_add = field._attrs['selection_add'] - selection = OrderedDict(selection + selection_add).items() + self.selection = OrderedDict(self.selection + selection_add).items() else: - selection = None - self.selection = selection + self.selection = None def _description_selection(self, env): """ return the selection list (pairs (value, label)); labels are diff --git a/openerp/models.py b/openerp/models.py index 13e829c186b..b59c6374f47 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -1599,6 +1599,7 @@ class BaseModel(object): 'views': [(view_id, 'form')], 'target': 'current', 'res_id': id, + 'context': context, } def get_access_action(self, cr, uid, id, context=None): @@ -2472,7 +2473,9 @@ class BaseModel(object): self._o2m_raise_on_missing_reference(cr, f) elif isinstance(f, fields.many2many): - self._m2m_raise_or_create_relation(cr, f) + res = self._m2m_raise_or_create_relation(cr, f) + if res and self._fields[k].depends: + stored_fields.append(self._fields[k]) else: res = column_data.get(k) @@ -2772,6 +2775,9 @@ class BaseModel(object): raise UserError(_("There is no reference field '%s' found for '%s'") % (f._fields_id, f._obj)) def _m2m_raise_or_create_relation(self, cr, f): + """ Create the table for the relation if necessary. + Return ``True`` if the relation had to be created. + """ m2m_tbl, col1, col2 = f._sql_names(self) # do not create relations for custom fields as they do not belong to a module # they will be automatically removed when dropping the corresponding ir.model.field @@ -2798,6 +2804,7 @@ class BaseModel(object): cr.execute("COMMENT ON TABLE \"%s\" IS 'RELATION BETWEEN %s AND %s'" % (m2m_tbl, self._table, ref)) cr.commit() _schema.debug("Create table '%s': m2m relation between '%s' and '%s'", m2m_tbl, self._table, ref) + return True def _add_sql_constraints(self, cr): diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index f464d57ac72..16e552fc681 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -533,7 +533,7 @@ ALL_LANGUAGES = { 'mn_MN': u'Mongolian / монгол', 'nb_NO': u'Norwegian Bokmål / Norsk bokmål', 'nl_NL': u'Dutch / Nederlands', - 'nl_BE': u'Flemish (BE) / Vlaams (BE)', + 'nl_BE': u'Dutch (BE) / Nederlands (BE)', 'oc_FR': u'Occitan (FR, post 1500) / Occitan', 'pl_PL': u'Polish / Język polski', 'pt_BR': u'Portuguese (BR) / Português (BR)',