summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/data/invoice_action_data.xml3
-rw-r--r--addons/account/models/account_bank_statement.py2
-rw-r--r--addons/account/models/account_invoice.py4
-rw-r--r--addons/auth_oauth/controllers/main.py2
-rw-r--r--addons/crm/wizard/base_partner_merge.py5
-rw-r--r--addons/crm/wizard/base_partner_merge_views.xml7
-rw-r--r--addons/l10n_be_intrastat/wizard/xml_decl.py4
-rw-r--r--addons/mrp/models/mrp_bom.py13
-rw-r--r--addons/mrp/models/mrp_production.py18
-rw-r--r--addons/mrp/models/mrp_unbuild.py1
-rw-r--r--addons/mrp/models/procurement.py1
-rw-r--r--addons/mrp/models/stock_move.py9
-rw-r--r--addons/mrp/report/mrp_bom_cost_report.py9
-rw-r--r--addons/mrp/report/mrp_bom_cost_report_templates.xml36
-rw-r--r--addons/mrp/report/mrp_report_views_main.xml2
-rw-r--r--addons/mrp/views/mrp_bom_views.xml6
-rw-r--r--addons/mrp/views/mrp_message_views.xml2
-rw-r--r--addons/mrp/views/mrp_production_views.xml4
-rw-r--r--addons/mrp/views/mrp_unbuild_views.xml3
-rw-r--r--addons/mrp/views/product_views.xml2
-rw-r--r--addons/mrp/views/stock_move_views.xml3
-rw-r--r--addons/mrp/views/stock_scrap_views.xml13
-rw-r--r--addons/payment_paypal/models/__init__.py1
-rw-r--r--addons/payment_paypal/models/account_invoice.py27
-rw-r--r--addons/portal_sale/__openerp__.py1
-rw-r--r--addons/portal_sale/models/account_invoice.py9
-rw-r--r--addons/portal_sale/models/sale_order.py10
-rw-r--r--addons/project/models/project.py17
-rw-r--r--addons/project_issue/models/project_issue.py18
-rw-r--r--addons/purchase/models/account_invoice.py3
-rw-r--r--addons/purchase/models/purchase.py7
-rw-r--r--addons/sale/data/mail_template_data.xml2
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/sale_timesheet/models/procurement.py8
-rw-r--r--addons/stock/models/product.py14
-rw-r--r--addons/stock/models/stock_move.py14
-rw-r--r--addons/stock/models/stock_pack_operation.py4
-rw-r--r--addons/stock/models/stock_quant.py34
-rw-r--r--addons/stock/report/report_stockpicking_operations.xml5
-rw-r--r--addons/stock_dropshipping/__init__.py1
-rw-r--r--addons/stock_dropshipping/models/__init__.py4
-rw-r--r--addons/stock_dropshipping/models/stock_dropshipping.py19
-rw-r--r--addons/subscription/models/subscription.py6
-rw-r--r--addons/web/controllers/main.py4
-rw-r--r--addons/web/static/src/js/views/form_widgets.js3
-rw-r--r--addons/web/static/src/js/views/pivot_view.js1
-rw-r--r--addons/web/static/src/xml/base.xml1
-rw-r--r--addons/web/views/database_manager.html2
-rw-r--r--addons/web_editor/static/src/js/snippets.editor.js15
-rw-r--r--addons/web_editor/static/src/js/snippets.options.js1
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.components.less3
-rw-r--r--addons/website/static/src/less/website.ui.components.less2
-rw-r--r--addons/website_crm_partner_assign/controllers/main.py1
-rw-r--r--addons/website_portal_sale/__openerp__.py1
-rw-r--r--addons/website_portal_sale/controllers/main.py6
-rw-r--r--addons/website_portal_sale/security/ir.model.access.csv (renamed from addons/portal_sale/security/ir.model.access.csv)0
-rw-r--r--addons/website_portal_sale/views/website_portal_sale_templates.xml15
-rw-r--r--addons/website_project/__init__.py1
-rw-r--r--addons/website_project/models/__init__.py4
-rw-r--r--addons/website_project/models/project.py58
-rw-r--r--addons/website_project_issue/models/project_issue.py16
-rw-r--r--addons/website_sale/models/__init__.py1
-rw-r--r--addons/website_sale/models/account_invoice.py31
-rw-r--r--addons/website_sale/models/sale_order.py11
-rw-r--r--doc/cla/corporate/akretion.md1
-rw-r--r--doc/reference/upgrade_api.rst200
-rw-r--r--odoo/addons/base/ir/ir_qweb/qweb.py16
-rw-r--r--odoo/addons/base/ir/ir_ui_view.py11
-rw-r--r--odoo/addons/base/tests/test_mimetypes.py14
-rw-r--r--odoo/tools/mimetypes.py4
70 files changed, 570 insertions, 208 deletions
diff --git a/addons/account/data/invoice_action_data.xml b/addons/account/data/invoice_action_data.xml
index 0aa82409d95..8a9f4bcb139 100644
--- a/addons/account/data/invoice_action_data.xml
+++ b/addons/account/data/invoice_action_data.xml
@@ -19,6 +19,7 @@
<p>Dear ${object.partner_id.name}
% set access_action = object.get_access_action()
% set access_url = access_action['type'] == 'ir.actions.act_url' and access_action['url'] or '/report/pdf/account.report_invoice/' + str(object.id)
+% set is_online = access_action and access_action['type'] == 'ir.actions.act_url'
% if object.partner_id.parent_id:
(<i>${object.partner_id.parent_id.name}</i>)
@@ -32,10 +33,12 @@ amounting in <strong>${object.amount_total} ${object.currency_id.name}</strong>
from ${object.company_id.name}.
</p>
+% if is_inline:
<br/><br/>
<center>
<a href="${access_url}" style="background-color: #1abc9c; padding: 20px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;" class="o_default_snippet_text">View Invoice</a>
</center>
+% endif
<br/><br/>
% if object.state=='paid':
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 673bbc6d6b6..bb440f7a516 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -101,7 +101,7 @@ class AccountBankStatement(models.Model):
journals = self.env['account.journal'].search([('type', '=', journal_type), ('company_id', '=', company_id)])
if journals:
return journals[0]
- return False
+ return self.env['account.journal']
@api.multi
def _get_opening_balance(self, journal_id):
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index bf4604cd21a..2512fe8ac30 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -534,7 +534,7 @@ class AccountInvoice(models.Model):
report_invoice = self.env['report']._get_report_from_name('account.report_invoice')
except IndexError:
report_invoice = False
- if report_invoice:
+ if report_invoice and report_invoice.attachment:
for invoice in self:
with invoice.env.do_in_draft():
invoice.number, invoice.state = invoice.move_name, 'open'
@@ -1108,6 +1108,8 @@ class AccountInvoice(models.Model):
continue
if not recipient.user_ids:
group_data['partner'] |= recipient
+ elif all(recipient.user_ids.mapped('share')):
+ group_data['partner'] |= recipient
else:
group_data['user'] |= recipient
done_ids.add(recipient.id)
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index 6dd0643b4fb..233cd839e6b 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -120,8 +120,6 @@ class OAuthLogin(Home):
@http.route()
def web_auth_reset_password(self, *args, **kw):
providers = self.list_providers()
- if len(providers) == 1:
- werkzeug.exceptions.abort(werkzeug.utils.redirect(providers[0]['auth_link'], 303))
response = super(OAuthLogin, self).web_auth_reset_password(*args, **kw)
response.qcontext.update(providers=providers)
return response
diff --git a/addons/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py
index 71799d265de..d2ddf32afcf 100644
--- a/addons/crm/wizard/base_partner_merge.py
+++ b/addons/crm/wizard/base_partner_merge.py
@@ -435,11 +435,6 @@ class MergePartnerAutomatic(models.TransientModel):
# ----------------------------------------
@api.multi
- def action_close(self):
- self.ensure_one()
- return {'type': 'ir.actions.act_window_close'}
-
- @api.multi
def action_skip(self):
""" Skip this wizard line. Don't compute any thing, and simply redirect to the new step."""
if self.current_line_id:
diff --git a/addons/crm/wizard/base_partner_merge_views.xml b/addons/crm/wizard/base_partner_merge_views.xml
index 67fbf2c4f50..6c86442db32 100644
--- a/addons/crm/wizard/base_partner_merge_views.xml
+++ b/addons/crm/wizard/base_partner_merge_views.xml
@@ -93,13 +93,10 @@
confirm="Are you sure to execute the list of automatic merges of your contacts ?"
attrs="{'invisible': [('state', '!=', 'option')]}" />
<span class="or_cancel" attrs="{'invisible': [('state', '=', 'finished')]} ">or
- <button name="action_close" special="nosave" string="Cancel" type="object" class="oe_link oe_inline"/>
+ <button special="cancel" string="Cancel" type="object" class="oe_link oe_inline"/>
</span>
<span class="or_cancel" attrs="{'invisible': [('state', '!=', 'finished')]} ">
- <button name="action_close" special="nosave"
- string="Close"
- type="object"
- class="oe_link oe_inline"/>
+ <button special="cancel" string="Close" type="object" class="oe_link oe_inline"/>
</span>
</footer>
</form>
diff --git a/addons/l10n_be_intrastat/wizard/xml_decl.py b/addons/l10n_be_intrastat/wizard/xml_decl.py
index 50b457361eb..ea49dc3887c 100644
--- a/addons/l10n_be_intrastat/wizard/xml_decl.py
+++ b/addons/l10n_be_intrastat/wizard/xml_decl.py
@@ -259,8 +259,10 @@ class XmlDeclaration(models.TransientModel):
numlgn = 0
for linekey in entries:
- numlgn += 1
amounts = entries[linekey]
+ if round(amounts[0], 0) == 0:
+ continue
+ numlgn += 1
item = ET.SubElement(datas, 'Item')
self._set_Dim(item, 'EXSEQCODE', unicode(numlgn))
self._set_Dim(item, 'EXTRF', unicode(linekey.EXTRF))
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index 2dfec8d1e8f..0fbc3c030e1 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -111,13 +111,13 @@ class MrpBom(models.Model):
return self.search(domain, order='sequence, product_id', limit=1)
def explode(self, product, quantity, picking_type=False):
- boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity})]
+ boms_done = [(self, {'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False})]
lines_done = []
templates_done = product.product_tmpl_id
- bom_lines = [(bom_line, product, quantity) for bom_line in self.bom_line_ids]
+ bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids]
while bom_lines:
- current_line, current_product, current_qty = bom_lines[0]
+ current_line, current_product, current_qty, parent_line = bom_lines[0]
bom_lines = bom_lines[1:]
if current_line._skip_bom_line(current_product):
@@ -126,15 +126,14 @@ class MrpBom(models.Model):
raise UserError(_('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!'))
line_quantity = current_qty * current_line.product_qty / current_line.bom_id.product_qty
-
bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id)
if bom.type == 'phantom':
converted_line_quantity = current_line.product_uom_id._compute_quantity(line_quantity, bom.product_uom_id)
- bom_lines = [(line, current_line.product_id, line_quantity) for line in bom.bom_line_ids] + bom_lines
+ bom_lines = [(line, current_line.product_id, line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines
templates_done |= current_line.product_id.product_tmpl_id
- boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity}))
+ boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line}))
else:
- lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity}))
+ lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line}))
return boms_done, lines_done
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index a3e30cab848..7d6bc5d881b 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -149,6 +149,8 @@ class MrpProduction(models.Model):
has_moves = fields.Boolean(compute='_has_moves')
scrap_ids = fields.One2many('stock.scrap', 'production_id', 'Scraps')
scrap_count = fields.Integer(compute='_compute_scrap_move_count', string='Scrap Move')
+ priority = fields.Selection([('0', 'Not urgent'), ('1', 'Normal'), ('2', 'Urgent'), ('3', 'Very Urgent')], 'Priority',
+ readonly=True, states={'confirmed': [('readonly', False)]}, default='1')
@api.multi
@api.depends('workorder_ids')
@@ -294,10 +296,13 @@ class MrpProduction(models.Model):
self.ensure_one()
moves = self.env['stock.move']
for bom_line, line_data in exploded_lines:
- moves += self._generate_raw_move(bom_line, line_data['qty'])
+ moves += self._generate_raw_move(bom_line, line_data)
return moves
- def _generate_raw_move(self, bom_line, quantity):
+ def _generate_raw_move(self, bom_line, line_data):
+ quantity = line_data['qty']
+ # alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line
+ alt_op = line_data['parent_line'] and line_data['parent_line'].operation_id.id or False
if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom':
return self.env['stock.move']
if bom_line.product_id.type not in ['product', 'consu']:
@@ -319,7 +324,7 @@ class MrpProduction(models.Model):
'location_dest_id': self.product_id.property_stock_production.id,
'raw_material_production_id': self.id,
'company_id': self.company_id.id,
- 'operation_id': bom_line.operation_id.id,
+ 'operation_id': bom_line.operation_id.id or alt_op,
'price_unit': bom_line.product_id.standard_price,
'procure_method': 'make_to_stock',
'origin': self.name,
@@ -385,15 +390,18 @@ class MrpProduction(models.Model):
def _generate_workorders(self, exploded_boms):
workorders = self.env['mrp.workorder']
for bom, bom_data in exploded_boms:
- workorders += self._workorders_create(bom, bom_data['qty'])
+ # If the routing of the parent BoM and phantom BoM are the same, don't recreate work orders, but use one master routing
+ if bom.routing_id.id and (not bom_data['parent_line'] or bom_data['parent_line'].bom_id.routing_id.id != bom.routing_id.id):
+ workorders += self._workorders_create(bom, bom_data)
return workorders
- def _workorders_create(self, bom, qty):
+ def _workorders_create(self, bom, bom_data):
"""
:param bom: in case of recursive boms: we could create work orders for child
BoMs
"""
workorders = self.env['mrp.workorder']
+ qty = bom_data['qty']
use_serial = any(product.tracking == 'serial' for product in self.mapped('move_finished_ids.product_id'))
if use_serial:
diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py
index b5d1c44495a..0abd787fbf0 100644
--- a/addons/mrp/models/mrp_unbuild.py
+++ b/addons/mrp/models/mrp_unbuild.py
@@ -39,6 +39,7 @@ class MrpUnbuild(models.Model):
'stock.production.lot', 'Lot',
domain="[('product_id', '=', product_id)]",
states={'done': [('readonly', True)]})
+ has_tracking=fields.Selection(related='product_id.tracking', readonly=True)
location_id = fields.Many2one(
'stock.location', 'Location',
default=_get_default_location_id,
diff --git a/addons/mrp/models/procurement.py b/addons/mrp/models/procurement.py
index a1f25ce0b07..1cfb047d856 100644
--- a/addons/mrp/models/procurement.py
+++ b/addons/mrp/models/procurement.py
@@ -84,7 +84,6 @@ class ProcurementOrder(models.Model):
# create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
production = ProductionSudo.create(procurement._prepare_mo_vals(bom))
res[procurement.id] = production.id
-
procurement.message_post(body=_("Manufacturing Order <em>%s</em> created.") % (production.name))
else:
res[procurement.id] = False
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index ae0c1d411b3..e3dd9e07994 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -72,6 +72,7 @@ class StockMove(models.Model):
'Quantity', compute='_qty_done_compute', inverse='_qty_done_set',
digits=dp.get_precision('Product Unit of Measure'))
move_lot_ids = fields.One2many('stock.move.lots', 'move_id', string='Lots')
+ active_move_lot_ids = fields.One2many('stock.move.lots', 'move_id', domain=[('done_wo', '=', True)], string='Lots')
bom_line_id = fields.Many2one('mrp.bom.line', 'BoM Line')
unit_factor = fields.Float('Unit Factor')
is_done = fields.Boolean(
@@ -93,7 +94,7 @@ class StockMove(models.Model):
def _qty_done_compute(self):
for move in self:
if move.has_tracking != 'none':
- move.quantity_done = sum(move.move_lot_ids.mapped('quantity_done'))
+ move.quantity_done = sum(move.move_lot_ids.filtered(lambda x: x.done_wo).mapped('quantity_done')) #TODO: change with active_move_lot_ids?
else:
move.quantity_done = move.quantity_done_store
@@ -169,6 +170,7 @@ class StockMove(models.Model):
moves = self._filter_closed_moves()
quant_obj = self.env['stock.quant']
moves_todo = self.env['stock.move']
+ moves_to_unreserve = self.env['stock.move']
for move in moves:
rounding = move.product_uom.rounding
if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0:
@@ -186,6 +188,8 @@ class StockMove(models.Model):
# Need to do some kind of conversion here
qty_split = move.product_uom._compute_quantity(move.product_uom_qty - move.quantity_done, move.product_id.uom_id)
new_move = move.split(qty_split)
+ # If you were already putting stock.move.lots on the next one in the work order, transfer those to the new move
+ move.move_lot_ids.filtered(lambda x: not x.done_wo).write({'move_id': new_move})
self.browse(new_move).quantity_done = 0.0
main_domain = [('qty', '>', 0)]
preferred_domain = [('reservation_id', '=', move.id)]
@@ -201,10 +205,11 @@ class StockMove(models.Model):
qty = move.product_uom._compute_quantity(movelot.quantity_done, move.product_id.uom_id)
quants = quant_obj.quants_get_preferred_domain(qty, move, lot_id=movelot.lot_id.id, domain=main_domain, preferred_domain_list=preferred_domain_list)
self.env['stock.quant'].quants_move(quants, move, move.location_dest_id, lot_id = movelot.lot_id.id)
- move.quants_unreserve()
+ moves_to_unreserve |= move
# Next move in production order
if move.move_dest_id:
move.move_dest_id.action_assign()
+ moves_to_unreserve.quants_unreserve()
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
return moves_todo
diff --git a/addons/mrp/report/mrp_bom_cost_report.py b/addons/mrp/report/mrp_bom_cost_report.py
index acdd977f777..93ae3ad64e1 100644
--- a/addons/mrp/report/mrp_bom_cost_report.py
+++ b/addons/mrp/report/mrp_bom_cost_report.py
@@ -19,18 +19,17 @@ class MrpBomCost(models.AbstractModel):
for value in product.attribute_value_ids:
attributes += [(value.attribute_id.name, value.name)]
result, result2 = bom.explode(product, 1)
- product_line = {'name': product.name, 'lines': [], 'total': 0.0,
+ product_line = {'bom': bom, 'name': product.name, 'lines': [], 'total': 0.0,
'currency': self.env.user.company_id.currency_id,
'product_uom_qty': bom.product_qty,
'product_uom': bom.product_uom_id,
'attributes': attributes}
total = 0.0
for bom_line, line_data in result2:
- line_product = line_data['product']
- price_uom = line_product.uom_id._compute_quantity(line_product.standard_price, bom_line.product_uom_id)
+ price_uom = self.env['product.uom']._compute_qty(bom_line.product_id.uom_id.id, bom_line.product_id.standard_price, bom_line.product_uom_id.id)
line = {
- 'product_id': line_product,
- 'product_uom_qty': line_data['qty'],
+ 'product_id': bom_line.product_id,
+ 'product_uom_qty': line_data['qty'], #line_data needed for phantom bom explosion
'product_uom': bom_line.product_uom_id,
'price_unit': price_uom,
'total_price': price_uom * line_data['qty'],
diff --git a/addons/mrp/report/mrp_bom_cost_report_templates.xml b/addons/mrp/report/mrp_bom_cost_report_templates.xml
index 87de18872bf..14f51211aa6 100644
--- a/addons/mrp/report/mrp_bom_cost_report_templates.xml
+++ b/addons/mrp/report/mrp_bom_cost_report_templates.xml
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="mrp_bom_cost_report">
- <div class="o_form_view">
- <div class="page o_form_sheet_bg">
- <div class="o_form_sheet">
- <t t-foreach="lines" t-as="line">
+ <t t-call="report.html_container">
+ <t t-foreach="lines" t-as="line">
+ <t t-call="report.internal_layout">
+ <div class="page">
<t t-set="currency" t-value="line['currency']"/>
<table class="table table-condensed">
<thead>
@@ -24,7 +24,7 @@
</th>
</tr>
<tr>
- <th colspan="2">Raw Materials</th>
+ <th colspan="3">Raw Materials</th>
<th class="col-sm-2 text-right">Quantity</th>
<th class="col-sm-2 text-right">Unit Cost</th>
<th class="col-sm-2 text-right">Total Cost</th>
@@ -32,24 +32,30 @@
</thead>
<tbody>
<tr t-foreach="line['lines']" t-as="bom_line">
- <td colspan="2">
- <span t-esc="bom_line['product_id'].name"/>
+ <td colspan="3">
+ <span t-att-res-id="bom_line['product_id'].id" res-model="product.product" view-type="form" t-esc="bom_line['product_id'].name"/>
</td>
<td class="text-right">
<span t-esc="bom_line['product_uom_qty']"/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/>
</td>
- <td class="text-right"><span t-esc="bom_line['price_unit']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></td>
- <td class="text-right"><span t-esc="bom_line['total_price']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></td>
+ <td class="text-right">
+ <span t-esc="bom_line['price_unit']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
+ <td class="text-right">
+ <span t-esc="bom_line['total_price']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </td>
</tr>
<tr>
- <th colspan="4" class="text-right">Total Cost of Raw Materials</th>
- <th class="text-right"><t t-esc="line['total']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></th>
+ <th colspan="5" class="text-right">Total Cost of Raw Materials</th>
+ <th class="text-right">
+ <span t-esc="line['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+ </th>
</tr>
</tbody>
</table>
- </t>
- </div>
- </div>
- </div>
+ </div>
+ </t>
+ </t>
+ </t>
</template>
</odoo>
diff --git a/addons/mrp/report/mrp_report_views_main.xml b/addons/mrp/report/mrp_report_views_main.xml
index 639666e150b..06fece65865 100644
--- a/addons/mrp/report/mrp_report_views_main.xml
+++ b/addons/mrp/report/mrp_report_views_main.xml
@@ -25,7 +25,7 @@
model="mrp.bom"
name="mrp_bom_cost"
file="mrp_bom_cost"
- report_type="qweb-pdf"
+ report_type="qweb-html"
/>
</data>
</odoo>
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 9c97f394931..07b851d168d 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -17,8 +17,8 @@
</div>
<group>
<group>
- <field name="product_tmpl_id"/>
- <field name="product_id" groups="product.group_product_variant"/>
+ <field name="product_tmpl_id" context="{'default_type': 'product'}"/>
+ <field name="product_id" groups="product.group_product_variant" context="{'default_type': 'product'}"/>
<label for="product_qty" string="Quantity"/>
<div class="o_row">
<field name="product_qty"/>
@@ -41,7 +41,7 @@
<field name="bom_line_ids" widget="one2many_list">
<tree string="Components" editable="bottom">
<field name="sequence" widget="handle"/>
- <field name="product_id"/>
+ <field name="product_id" context="{'default_type': 'product'}"/>
<field name="has_attachments" invisible="1"/>
<button name="action_see_attachments" type="object" icon="fa-files-o" attrs="{'invisible': [('has_attachments', '=', False)]}"/>
<button name="action_see_attachments" type="object" icon="fa-file" attrs="{'invisible': [('has_attachments', '=', True)]}"/>
diff --git a/addons/mrp/views/mrp_message_views.xml b/addons/mrp/views/mrp_message_views.xml
index b577d7839de..b3849aa61a2 100644
--- a/addons/mrp/views/mrp_message_views.xml
+++ b/addons/mrp/views/mrp_message_views.xml
@@ -109,7 +109,7 @@
products, process, or actions to take care of. (e.g; Warning, the bill of material changed,
check the details in new worksheet)
</p><p>
- Messages can be configurred to appear during any operation: receptions, delivery orders,
+ Messages can be configured to appear during any operation: receptions, delivery orders,
manufacturing orders, work orders, etc.
</p>
</field>
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 69fa2c97943..ed1284cf2c8 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -117,7 +117,7 @@
<notebook>
<page string="Consumed Materials">
<field name="move_raw_ids" options="{'reload_on_button': True}" context="{'default_location_id': location_src_id, 'default_location_dest_id': location_dest_id}">
- <tree editable="bottom" delete="0" default_order="is_done desc,sequence" decoration-muted="is_done" decoration-danger="quantity_done&gt;product_uom_qty" decoration-success="quantity_done==product_uom_qty" create="0">
+ <tree editable="bottom" delete="0" default_order="is_done desc,sequence" decoration-muted="is_done" decoration-warning="quantity_done&gt;product_uom_qty" decoration-success="quantity_done==product_uom_qty" decoration-danger="quantity_available &lt; product_uom_qty" create="0">
<field name="product_id" required="1"/>
<field name="product_uom" groups="product.group_uom"/>
<field name="has_tracking" invisible="1"/>
@@ -136,7 +136,7 @@
</page>
<page string="Finished Products">
<field name="move_finished_ids" options="{'reload_on_button': True}" context="{'default_location_id': location_src_id, 'default_location_dest_id': location_dest_id}">
- <tree editable="bottom" delete="0" default_order="is_done desc,sequence" decoration-muted="is_done" decoration-danger="quantity_done&gt;product_uom_qty" decoration-success="quantity_done==product_uom_qty" create="0">
+ <tree editable="bottom" delete="0" default_order="is_done desc,sequence" decoration-muted="is_done" decoration-warning="quantity_done&gt;product_uom_qty" decoration-success="quantity_done==product_uom_qty" create="0">
<field name="product_id" required="1"/>
<field name="product_uom" groups="product.group_uom"/>
<field name="is_done" invisible="1"/>
diff --git a/addons/mrp/views/mrp_unbuild_views.xml b/addons/mrp/views/mrp_unbuild_views.xml
index 6769ddca8d4..8630b7a57bb 100644
--- a/addons/mrp/views/mrp_unbuild_views.xml
+++ b/addons/mrp/views/mrp_unbuild_views.xml
@@ -76,7 +76,8 @@
<field name="mo_id"/>
<field name="location_id" groups="stock.group_stock_multi_locations"/>
<field name="location_dest_id" groups="stock.group_stock_multi_locations"/>
- <field name="lot_id" groups="stock.group_production_lot"/>
+ <field name="has_tracking" invisible="1"/>
+ <field name="lot_id" attrs="{'invisible': [('has_tracking', '=', 'none')]}" groups="stock.group_production_lot"/>
</group>
</group>
</sheet>
diff --git a/addons/mrp/views/product_views.xml b/addons/mrp/views/product_views.xml
index aa6c13f10e9..aab72436df9 100644
--- a/addons/mrp/views/product_views.xml
+++ b/addons/mrp/views/product_views.xml
@@ -31,7 +31,7 @@
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
- <field name="context">{}</field>
+ <field name="context">{"search_default_products": 1, 'default_type': 'product'}</field>
</record>
<menuitem id="menu_mrp_product_form"
name="Products"
diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml
index 41cf5d4fd38..6cd71dac6b2 100644
--- a/addons/mrp/views/stock_move_views.xml
+++ b/addons/mrp/views/stock_move_views.xml
@@ -28,7 +28,7 @@
<field name="workorder_id" invisible="1"/>
</group>
</group>
- <field name="move_lot_ids" attrs="{'readonly': [('is_done', '=', True)]}" context="{'default_workorder_id': workorder_id}">
+ <field name="active_move_lot_ids" attrs="{'readonly': [('is_done', '=', True)]}" context="{'default_workorder_id': workorder_id}">
<tree editable="bottom" decoration-success="quantity==quantity_done" decoration-danger="(quantity &gt; 0) and (quantity_done&gt;quantity)">
<field name="lot_id" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
<field name="quantity" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
@@ -37,6 +37,7 @@
<button name="do_plus" type="object" icon="fa-plus-square" attrs="{'invisible': [('plus_visible', '=', False)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
<field name="plus_visible" invisible="1"/>
<field name="workorder_id" invisible="1"/>
+ <field name="done_wo" invisible="1"/>
</tree>
</field>
<footer class="oe_edit_only">
diff --git a/addons/mrp/views/stock_scrap_views.xml b/addons/mrp/views/stock_scrap_views.xml
index 637c7d2a159..22021e9d475 100644
--- a/addons/mrp/views/stock_scrap_views.xml
+++ b/addons/mrp/views/stock_scrap_views.xml
@@ -1,9 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
+ <record id="stock_scrap_view_form2_mrp_inherit_mrp" model="ir.ui.view">
+ <field name="name">stock.scrap.view.form2.inherit.mrp</field>
+ <field name="model">stock.scrap</field>
+ <field name="inherit_id" ref="stock.stock_scrap_form_view2"/>
+ <field name="arch" type="xml">
+ <field name="owner_id" position="after">
+ <field name="workorder_id" attrs="{'invisible': [('workorder_id', '=', False)]}"/>
+ <field name="production_id" attrs="{'invisible': [('production_id', '=', False)]}"/>
+ </field>
+ </field>
+ </record>
<record id="stock_scrap_view_form_mrp_inherit_mrp" model="ir.ui.view">
<field name="name">stock.scrap.view.form.inherit.mrp</field>
<field name="model">stock.scrap</field>
- <field name="inherit_id" ref="stock.stock_scrap_form_view2"/>
+ <field name="inherit_id" ref="stock.stock_scrap_form_view"/>
<field name="arch" type="xml">
<field name="owner_id" position="after">
<field name="workorder_id" attrs="{'invisible': [('workorder_id', '=', False)]}"/>
diff --git a/addons/payment_paypal/models/__init__.py b/addons/payment_paypal/models/__init__.py
index 15cb180448e..10b988e0c28 100644
--- a/addons/payment_paypal/models/__init__.py
+++ b/addons/payment_paypal/models/__init__.py
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
-import account_invoice
import payment
diff --git a/addons/payment_paypal/models/account_invoice.py b/addons/payment_paypal/models/account_invoice.py
deleted file mode 100644
index 7b7f3e377b7..00000000000
--- a/addons/payment_paypal/models/account_invoice.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from openerp import models, fields, api
-
-import werkzeug.urls
-
-class AccountInvoice(models.Model):
- _inherit = 'account.invoice'
-
- paypal_url = fields.Char('Paypal Url', compute='_compute_paypal_url')
-
- @api.depends('type', 'number', 'company_id', 'currency_id')
- def _compute_paypal_url(self):
- for inv in self:
- if inv.type == 'out_invoice' and inv.company_id.paypal_account:
- params = {
- "cmd": "_xclick",
- "business": inv.company_id.paypal_account,
- "item_name": "%s Invoice %s" % (inv.company_id.name, inv.number or ''),
- "invoice": inv.number,
- "amount": inv.residual,
- "currency_code": inv.currency_id.name,
- "button_subtype": "services",
- "no_note": "1",
- "bn": "Odoo_Invoice_PayNow_" + inv.currency_id.name,
- }
- inv.paypal_url = "https://www.paypal.com/cgi-bin/webscr?" + werkzeug.url_encode(params)
diff --git a/addons/portal_sale/__openerp__.py b/addons/portal_sale/__openerp__.py
index 828a41cb232..1095b637c22 100644
--- a/addons/portal_sale/__openerp__.py
+++ b/addons/portal_sale/__openerp__.py
@@ -28,7 +28,6 @@ by default, you simply need to configure a Paypal account in the Accounting/Invo
'depends': ['sale', 'portal', 'payment'],
'data': [
'security/portal_sale_security.xml',
- 'security/ir.model.access.csv',
'views/sale_order_views.xml',
],
'auto_install': True,
diff --git a/addons/portal_sale/models/account_invoice.py b/addons/portal_sale/models/account_invoice.py
index 75de697483e..628d79d27d3 100644
--- a/addons/portal_sale/models/account_invoice.py
+++ b/addons/portal_sale/models/account_invoice.py
@@ -22,12 +22,3 @@ class AccountInvoice(models.Model):
action='/mail/view',
model=self._name,
res_id=self.id)[self.partner_id.id]
-
- @api.multi
- def get_formview_action(self):
- # NOTE : if this method is call as sudo, the only to determine if the user is portal is to check the uid from context
- # This was introduced with https://github.com/odoo/odoo/commit/f5fedbcb18a57ee9aeab952f3ff95f692f7a863c, and should be better fixed.
- uid = self.env.context.get('uid', self.env.user.id)
- if self.env['res.users'].sudo().browse(uid).share:
- return self.env['ir.actions.act_window'].for_xml_id('portal_sale', 'portal_action_invoices')
- return super(AccountInvoice, self).get_formview_action()
diff --git a/addons/portal_sale/models/sale_order.py b/addons/portal_sale/models/sale_order.py
index f3dc5444d4c..b11d000c125 100644
--- a/addons/portal_sale/models/sale_order.py
+++ b/addons/portal_sale/models/sale_order.py
@@ -22,13 +22,3 @@ class SaleOrder(models.Model):
action='/mail/view',
model=self._name,
res_id=self.id)[self.partner_id.id]
-
- @api.multi
- def get_formview_action(self):
- # NOTE : if this method is call as sudo, the only to determine if the user is portal is to check the uid from context
- # This was introduced with https://github.com/odoo/odoo/commit/f5fedbcb18a57ee9aeab952f3ff95f692f7a863c, and should be better fixed.
- uid = self.env.context.get('uid', self.env.user.id)
- if self.env['res.users'].sudo().browse(uid).share:
- action_xmlid = 'action_quotations_portal' if self.state in ('draft', 'sent') else 'action_orders_portal'
- return self.env['ir.actions.act_window'].for_xml_id('portal_sale', action_xmlid)
- return super(SaleOrder, self).get_formview_action()
diff --git a/addons/project/models/project.py b/addons/project/models/project.py
index 1ae65b18c99..c226e135967 100644
--- a/addons/project/models/project.py
+++ b/addons/project/models/project.py
@@ -299,7 +299,10 @@ class Task(models.Model):
def _get_default_stage_id(self):
""" Gives default stage_id """
- return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', False)])
+ project_id = self.env.context.get('default_project_id')
+ if not project_id:
+ return False
+ return self.stage_find(project_id, [('fold', '=', False)])
@api.multi
def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
@@ -378,7 +381,7 @@ class Task(models.Model):
partner_id = fields.Many2one('res.partner',
string='Customer',
default=_get_default_partner)
- manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id')
+ manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True)
company_id = fields.Many2one('res.company',
string='Company',
default=lambda self: self.env['res.company']._company_default_get())
@@ -387,14 +390,18 @@ class Task(models.Model):
attachment_ids = fields.One2many('ir.attachment', 'res_id', domain=lambda self: [('res_model', '=', self._name)], auto_join=True, string='Attachments')
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Displayed Image')
- legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation')
- legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation')
- legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation')
+ legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True)
+ legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True)
+ legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True)
@api.onchange('project_id')
def _onchange_project(self):
if self.project_id:
self.partner_id = self.project_id.partner_id
+ self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
+ else:
+ self.partner_id = False
+ self.stage_id = False
@api.onchange('user_id')
def _onchange_user(self):
diff --git a/addons/project_issue/models/project_issue.py b/addons/project_issue/models/project_issue.py
index 0b38b972494..64e027d6b35 100644
--- a/addons/project_issue/models/project_issue.py
+++ b/addons/project_issue/models/project_issue.py
@@ -15,7 +15,10 @@ class ProjectIssue(models.Model):
@api.model
def _get_default_stage_id(self):
- return self.stage_find(self.env.context.get('default_project_id'), [('fold', '=', False)])
+ project_id = self.env.context.get('default_project_id')
+ if not project_id:
+ return False
+ return self.stage_find(project_id, [('fold', '=', False)])
name = fields.Char(string='Issue', required=True)
active = fields.Boolean(default=True)
@@ -60,9 +63,9 @@ class ProjectIssue(models.Model):
user_email = fields.Char(related='user_id.email', string='User Email', readonly=True)
date_action_last = fields.Datetime(string='Last Action', readonly=True)
date_action_next = fields.Datetime(string='Next Action', readonly=True)
- legend_blocked = fields.Char(related="stage_id.legend_blocked", string='Kanban Blocked Explanation')
- legend_done = fields.Char(related="stage_id.legend_done", string='Kanban Valid Explanation')
- legend_normal = fields.Char(related="stage_id.legend_normal", string='Kanban Ongoing Explanation')
+ legend_blocked = fields.Char(related="stage_id.legend_blocked", string='Kanban Blocked Explanation', readonly=True)
+ legend_done = fields.Char(related="stage_id.legend_done", string='Kanban Valid Explanation', readonly=True)
+ legend_normal = fields.Char(related="stage_id.legend_normal", string='Kanban Ongoing Explanation', readonly=True)
@api.multi
def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
@@ -136,6 +139,11 @@ class ProjectIssue(models.Model):
if self.project_id:
self.partner_id = self.project_id.partner_id.id
self.email_from = self.project_id.partner_id.email
+ self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
+ else:
+ self.partner_id = False
+ self.email_from = False
+ self.stage_id = False
@api.onchange('task_id')
def _onchange_task_id(self):
@@ -245,6 +253,8 @@ class ProjectIssue(models.Model):
group_data['group_project_user'] |= recipient
elif not recipient.user_ids:
group_data['partner'] |= recipient
+ elif all(recipient.user_ids.mapped('share')):
+ group_data['partner'] |= recipient
else:
group_data['user'] |= recipient
done_ids.add(recipient.id)
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 5418ec78a09..fdfdf7c49e1 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -160,13 +160,14 @@ class AccountInvoice(models.Model):
#line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...]
taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']])
price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded']
+ price_before = line.get('price', 0.0)
line.update({'price': round(valuation_price_unit * line['quantity'], account_prec)})
diff_res.append({
'type': 'src',
'name': i_line.name[:64],
'price_unit': round(price_unit - valuation_price_unit, account_prec),
'quantity': line['quantity'],
- 'price': round((price_unit - valuation_price_unit) * line['quantity'], account_prec),
+ 'price': round(price_before - line.get('price', 0.0), account_prec),
'account_id': acc,
'product_id': line['product_id'],
'uom_id': line['uom_id'],
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index eacd6249373..52d229058c7 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -75,12 +75,15 @@ class PurchaseOrder(models.Model):
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
return types[:1]
- @api.depends('order_line.move_ids.picking_id')
+ @api.depends('order_line.move_ids')
def _compute_picking(self):
for order in self:
pickings = self.env['stock.picking']
for line in order.order_line:
- moves = line.move_ids.filtered(lambda r: r.state != 'cancel')
+ # We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
+ # do some recursive search, but that could be prohibitive if not done correctly.
+ moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
+ moves = moves.filtered(lambda r: r.state != 'cancel')
pickings |= moves.mapped('picking_id')
order.picking_ids = pickings
order.picking_count = len(pickings)
diff --git a/addons/sale/data/mail_template_data.xml b/addons/sale/data/mail_template_data.xml
index b20487d371d..25ddab813e8 100644
--- a/addons/sale/data/mail_template_data.xml
+++ b/addons/sale/data/mail_template_data.xml
@@ -35,12 +35,14 @@ from ${object.company_id.name}.
</p>
<br/><br/>
+% if is_online:
<center>
<a href="${access_url}" style="background-color: #1abc9c; padding: 20px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;" class="o_default_snippet_text">${access_name}</a>
<br/><br/>
<span style="color:#888888">(or view attached PDF)</span>
</center>
<br/>
+% endif
<p>You can reply to this email if you have any questions.</p>
<p>Thank you,</p>
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index b9da002bff1..d872c524582 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -486,6 +486,8 @@ class SaleOrder(models.Model):
continue
if not recipient.user_ids:
group_data['partner'] |= recipient
+ elif all(recipient.user_ids.mapped('share')):
+ group_data['partner'] |= recipient
else:
group_data['user'] |= recipient
done_ids.add(recipient.id)
diff --git a/addons/sale_timesheet/models/procurement.py b/addons/sale_timesheet/models/procurement.py
index 60d3a3a9600..63df3e4a890 100644
--- a/addons/sale_timesheet/models/procurement.py
+++ b/addons/sale_timesheet/models/procurement.py
@@ -25,6 +25,14 @@ class ProcurementOrder(models.Model):
def _run(self):
self.ensure_one()
if self._is_procurement_task() and not self.task_id:
+ # If the SO was confirmed, cancelled, set to draft then confirmed, avoid creating a new
+ # task.
+ if self.sale_line_id:
+ existing_task = self.env['project.task'].search(
+ [('sale_line_id', '=', self.sale_line_id.id)]
+ )
+ if existing_task:
+ return existing_task
# create a task for the procurement
return self._create_service_task()
return super(ProcurementOrder, self)._run()
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 394218f184d..a2df8cb74c6 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -4,10 +4,18 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
-from odoo.tools.safe_eval import safe_eval
from odoo.tools.float_utils import float_round
from datetime import datetime
+import operator as py_operator
+OPERATORS = {
+ '<': py_operator.lt,
+ '>': py_operator.gt,
+ '<=': py_operator.le,
+ '>=': py_operator.ge,
+ '==': py_operator.eq,
+ '!=': py_operator.ne
+}
class Product(models.Model):
_inherit = "product.product"
@@ -258,7 +266,7 @@ class Product(models.Model):
# TODO: Still optimization possible when searching virtual quantities
ids = []
for product in self.search([]):
- if safe_eval(str(product[field]) + operator + str(value)):
+ if OPERATORS[operator](product[field], value):
ids.append(product.id)
return [('id', 'in', ids)]
@@ -281,7 +289,7 @@ class Product(models.Model):
domain_quant.append(('package_id', '=', package_id))
quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'qty'], ['product_id'])
for quant in quants_groupby:
- if safe_eval('%s %s %s' % (quant['qty'], operator, value)):
+ if OPERATORS[operator](quant['qty'], value):
product_ids.add(quant['product_id'][0])
return list(product_ids)
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 0ecee8cac4d..34d98ba27ef 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -910,6 +910,7 @@ class StockMove(models.Model):
:param restrict_partner_id: optional partner that can be given in order to force the new move to restrict its choice of quants to the ones belonging to this partner.
:param context: dictionay. can contains the special key 'source_location_id' in order to force the source location when copying the move
:returns: id of the backorder move created """
+ self = self.with_prefetch() # This makes the ORM only look for one record and not 300 at a time, which improves performance
if self.state in ('done', 'cancel'):
raise UserError(_('You cannot split a move done'))
elif self.state == 'draft':
@@ -918,7 +919,6 @@ class StockMove(models.Model):
raise UserError(_('You cannot split a draft move. It needs to be confirmed first.'))
if float_is_zero(qty, precision_rounding=self.product_id.uom_id.rounding) or self.product_qty <= qty:
return self.id
-
# HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
uom_qty = self.product_id.uom_id._compute_quantity(qty, self.product_uom, rounding_method='HALF-UP')
defaults = {
@@ -937,12 +937,11 @@ class StockMove(models.Model):
if self.env.context.get('source_location_id'):
defaults['location_id'] = self.env.context['source_location_id']
new_move = self.copy(defaults)
-
# ctx = context.copy()
# TDE CLEANME: used only in write in this file, to clean
# ctx['do_not_propagate'] = True
self.with_context(do_not_propagate=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
-
+
if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
new_move_prop = self.move_dest_id.split(qty)
new_move.write({'move_dest_id': new_move_prop})
@@ -971,10 +970,5 @@ class StockMove(models.Model):
# ----------------------------------------------------------------------
def quants_unreserve(self):
- # TDE FIXME: do in batch
- for move in self:
- if move.reserved_quant_ids:
- # if move has a picking_id, write on that picking that pack_operation might have changed and need to be recomputed
- if move.partially_available:
- move.write({'partially_available': False})
- move.reserved_quant_ids.sudo().write({'reservation_id': False})
+ self.filtered(lambda x: x.partially_available).write({'partially_available': False})
+ self.mapped('reserved_quant_ids').sudo().write({'reservation_id': False}) \ No newline at end of file
diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py
index 207c4565a36..a581d4028d3 100644
--- a/addons/stock/models/stock_pack_operation.py
+++ b/addons/stock/models/stock_pack_operation.py
@@ -77,9 +77,9 @@ class PackOperation(models.Model):
def _set_is_done(self):
# TDE FIXME: whuuuut ???
if not self.product_id:
- if self.processed_boolean and self.qty_done == 0:
+ if self.is_done and self.qty_done == 0:
self.qty_done = 1.0
- if not self.processed_boolean and self.qty_done != 0:
+ if not self.is_done and self.qty_done != 0:
self.qty_done = 0.0
def _get_remaining_prod_quantities(self):
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index f9ece848bd2..5c86fd93d77 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -177,11 +177,13 @@ class Quant(models.Model):
quants_reconcile_sudo = self.env['stock.quant'].sudo()
quants_move_sudo = self.env['stock.quant'].sudo()
+ check_lot = False
for quant, qty in quants:
if not quant:
#If quant is None, we will create a quant to move (and potentially a negative counterpart too)
quant = self._quant_create_from_move(
qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to)
+ check_lot = True
else:
quant._quant_split(qty)
quants_move_sudo |= quant
@@ -201,6 +203,24 @@ class Quant(models.Model):
if self._cr.fetchone():
quants_reconcile_sudo._quant_reconcile_negative(move)
+ # In case of serial tracking, check if the product does not exist somewhere internally already
+ # Checking that a positive quant already exists in an internal location is too restrictive.
+ # Indeed, if a warehouse is configured with several steps (e.g. "Pick + Pack + Ship") and
+ # one step is forced (creates a quant of qty = -1.0), it is not possible afterwards to
+ # correct the inventory unless the product leaves the stock.
+ picking_type = move.picking_id and move.picking_id.picking_type_id or False
+ if check_lot and lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)):
+ other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
+ ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')])
+ if other_quants:
+ # We raise an error if:
+ # - the total quantity is strictly larger than 1.0
+ # - there are more than one negative quant, to avoid situations where the user would
+ # force the quantity at several steps of the process
+ if sum(other_quants.mapped('qty')) > 1.0 or len([q for q in other_quants.mapped('qty') if q < 0]) > 1:
+ lot_name = self.env['stock.production.lot'].browse(lot_id).name
+ raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set."))
+
@api.model
def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False,
src_package_id=False, dest_package_id=False,
@@ -234,16 +254,10 @@ class Quant(models.Model):
negative_quant_id = self.sudo().create(negative_vals)
vals.update({'propagated_from_id': negative_quant_id.id})
- # In case of serial tracking, check if the product does not exist somewhere internally already
picking_type = move.picking_id and move.picking_id.picking_type_id or False
if lot_id and move.product_id.tracking == 'serial' and (not picking_type or (picking_type.use_create_lots or picking_type.use_existing_lots)):
if qty != 1.0:
raise UserError(_('You should only receive by the piece with the same serial number'))
- other_quants = self.search([('product_id', '=', move.product_id.id), ('lot_id', '=', lot_id),
- ('qty', '>', 0.0), ('location_id.usage', '=', 'internal')])
- if other_quants:
- lot_name = self.env['stock.production.lot'].browse(lot_id).name
- raise UserError(_('The serial number %s is already in stock.') % lot_name + _("Otherwise make sure the right stock/owner is set."))
# create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
return self.sudo().create(vals)
@@ -423,7 +437,9 @@ class Quant(models.Model):
if pack_operation_id:
pack_operation = self.env['stock.pack.operation'].browse(pack_operation_id)
- domain += [('owner_id', '=', pack_operation.owner_id.id), ('location_id', '=', pack_operation.location_id.id)]
+ domain += [('location_id', '=', pack_operation.location_id.id)]
+ if pack_operation.owner_id:
+ domain += [('owner_id', '=', pack_operation.owner_id.id)]
if pack_operation.package_id and not pack_operation.product_id:
domain += [('package_id', 'child_of', pack_operation.package_id.id)]
elif pack_operation.package_id and pack_operation.product_id:
@@ -431,7 +447,9 @@ class Quant(models.Model):
else:
domain += [('package_id', '=', False)]
else:
- domain += [('owner_id', '=', move.restrict_partner_id.id), ('location_id', 'child_of', move.location_id.id)]
+ domain += [('location_id', 'child_of', move.location_id.id)]
+ if move.restrict_partner_id:
+ domain += [('owner_id', '=', move.restrict_partner_id.id)]
if company_id:
domain += [('company_id', '=', company_id)]
diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml
index 112ae7ad229..c32e819dbe2 100644
--- a/addons/stock/report/report_stockpicking_operations.xml
+++ b/addons/stock/report/report_stockpicking_operations.xml
@@ -80,13 +80,14 @@
<br/>
<br/>
<table class="table table-condensed" t-if="o.pack_operation_ids">
+ <t t-set="has_barcode" t-value="any([pack_operation.product_id and pack_operation.product_id.barcode or pack_operation.package_id for pack_operation in o.pack_operation_ids])"/>
<t t-set="has_serial_number" t-value="o.pack_operation_ids.filtered('pack_lot_ids')" groups="stock.group_production_lot"/>
<thead>
<tr>
<th><strong>Product</strong></th>
<th class="text-right"><strong>Quantity</strong></th>
<t t-if="o.picking_type_id.code != 'incoming'"><th><strong>Source</strong></th></t>
- <th t-if="any([pack_operation.product_id and pack_operation.product_id.barcode or pack_operation.package_id for pack_operation in o.pack_operation_ids])" class="text-center">
+ <th t-if="has_barcode" class="text-center">
<strong>Barcode</strong>
</th>
<th t-if="has_serial_number">
@@ -108,7 +109,7 @@
<span t-field="pack_operation.package_id"/>
</td>
</t>
- <td>
+ <td t-if="has_barcode">
<span t-if="pack_operation.product_id and pack_operation.product_id.barcode">
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pack_operation.product_id.barcode, 600, 100)" style="width:300px;height:50px"/>
</span>
diff --git a/addons/stock_dropshipping/__init__.py b/addons/stock_dropshipping/__init__.py
index 67dee8c60db..d7156f09070 100644
--- a/addons/stock_dropshipping/__init__.py
+++ b/addons/stock_dropshipping/__init__.py
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import models # noqa
diff --git a/addons/stock_dropshipping/models/__init__.py b/addons/stock_dropshipping/models/__init__.py
new file mode 100644
index 00000000000..16c708eba5a
--- /dev/null
+++ b/addons/stock_dropshipping/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import stock_dropshipping # noqa
diff --git a/addons/stock_dropshipping/models/stock_dropshipping.py b/addons/stock_dropshipping/models/stock_dropshipping.py
new file mode 100644
index 00000000000..0c2e2424fa0
--- /dev/null
+++ b/addons/stock_dropshipping/models/stock_dropshipping.py
@@ -0,0 +1,19 @@
+# coding: utf-8
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import models, api
+
+class AccountInvoice(models.Model):
+ _inherit = 'account.invoice'
+
+ @api.model
+ def _anglo_saxon_sale_move_lines(self, i_line):
+ for sale_line in i_line.sale_line_ids:
+ for proc in sale_line.procurement_ids:
+ if proc.purchase_line_id:
+ # if the invoice line is related to sale order lines having one of its
+ # procurement_ids with a purchase_line_id set, it means that it is a
+ # confirmed dropship and in that case we mustn't create the cost of
+ # sale line (because the product won't enter the stock)
+ return []
+ return super(AccountInvoice, self)._anglo_saxon_sale_move_lines(i_line)
diff --git a/addons/subscription/models/subscription.py b/addons/subscription/models/subscription.py
index be3d24e00f4..3cf18f64b05 100644
--- a/addons/subscription/models/subscription.py
+++ b/addons/subscription/models/subscription.py
@@ -74,13 +74,17 @@ class Subscription(models.Model):
'nextcall': subscription.date_init,
'model': self._name,
'args': repr([[subscription.id]]),
- 'function': 'model_copy',
+ 'function': '_cron_model_copy',
'priority': 6,
'user_id': subscription.user_id.id
}
cron = self.env['ir.cron'].sudo().create(cron_data)
subscription.write({'cron_id': cron.id, 'state': 'running'})
+ @api.model
+ def _cron_model_copy(self, ids):
+ self.browse(ids).model_copy()
+
@api.multi
def model_copy(self):
for subscription in self.filtered(lambda sub: sub.cron_id):
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 23a0f7d8327..39ac40a35c2 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -632,8 +632,8 @@ class Database(http.Controller):
try:
# country code could be = "False" which is actually True in python
country_code = post.get('country_code') or False
- dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post.get('login'), country_code])
- request.session.authenticate(name, 'admin', password)
+ dispatch_rpc('db', 'create_database', [master_pwd, name, bool(post.get('demo')), lang, password, post['login'], country_code])
+ request.session.authenticate(name, post['login'], password)
return http.local_redirect('/web/')
except Exception, e:
error = "Database creation error: %s" % e
diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js
index f669a047ab3..bc649e65cbd 100644
--- a/addons/web/static/src/js/views/form_widgets.js
+++ b/addons/web/static/src/js/views/form_widgets.js
@@ -1078,7 +1078,6 @@ var FieldBinary = common.AbstractField.extend(common.ReinitializeFieldMixin, {
this.$('.o_select_file_button').click(function() {
self.$inputFile.click();
});
- this.$('.o_save_file_button').click(this.on_save_as);
this.$('.o_clear_file_button').click(this.on_clear);
},
on_file_change: function(e) {
@@ -1432,7 +1431,7 @@ var FieldStatus = common.AbstractField.extend({
});
}
}
- }, 300),
+ }, 300, true),
});
var FieldMonetary = FieldFloat.extend({
diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js
index f0fd2eaa0b8..df8e3e49b66 100644
--- a/addons/web/static/src/js/views/pivot_view.js
+++ b/addons/web/static/src/js/views/pivot_view.js
@@ -692,7 +692,6 @@ var PivotView = View.extend({
if (width > 1) {
var total_cell = {width:nbr_measures, height: depth, title:""};
if (nbr_measures === 1) {
- total_cell.title = this.measures[this.active_measures[0]].string;
total_cell.total = true;
}
result[0].push(total_cell);
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index ad54ed6a1f3..11af5272077 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -839,7 +839,6 @@
<button type="button" class="btn btn-sm btn-primary o_select_file_button" title="Select">Upload your file</button>
<button type="button" class="btn btn-sm btn-default fa fa-pencil o_select_file_button" title="Select"/>
- <button type="button" class="btn btn-sm btn-default fa fa-download o_save_file_button" title="Save As..."/>
<button type="button" class="btn btn-sm btn-default fa fa-trash-o o_clear_file_button" title="Clear"/>
<span class="o_form_binary_progress">Uploading...</span>
diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html
index dfcb95d4d05..7cef4678e8d 100644
--- a/addons/web/views/database_manager.html
+++ b/addons/web/views/database_manager.html
@@ -69,7 +69,7 @@
<div class="row">
<div class="col-md-12">
<label for="login" class="control-label">Email</label>
- <input id="login" type="text" name="login" class="form-control" autocomplete="off"/>
+ <input id="login" type="text" name="login" class="form-control" required="required" autocomplete="off"/>
</div>
</div>
</div>
diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js
index 1549db18689..e4885593029 100644
--- a/addons/web_editor/static/src/js/snippets.editor.js
+++ b/addons/web_editor/static/src/js/snippets.editor.js
@@ -1082,28 +1082,35 @@ data.Editor = Class.extend({
var $style_button = this.$overlay.find(".oe_options");
var $ul = $style_button.find("ul:first");
var $headers = $ul.find(".dropdown-header:data(editor)");
- _.each($headers, function (el) {
+ _.each($headers, (function (el) {
var $el = $(el);
var styles = _.values($el.data("editor").styles);
if ($el.data("editor") !== this) {
styles = _.filter(styles, function (option) { return !option.preventChildPropagation; });
}
+
+ var count = 0;
_.each(_.sortBy(styles, "__order").reverse(), function (style) {
- do_action(style, $el);
+ if (do_action(style, $el)) {
+ count++;
+ }
});
- });
+ $el.toggleClass("hidden", count === 0);
+ }).bind(this));
// Activate the overlay
- $style_button.toggleClass("hidden", $ul.children(":not(.dropdown-header):not(.divider):not(.hidden)").length === 0);
+ $style_button.toggleClass("hidden", $ul.children(":not(.divider):not(.hidden)").length === 0);
this.$overlay.toggleClass("oe_active", !!focus);
function _do_action_focus(style, $dest) {
style.$el.insertAfter($dest);
style.on_focus();
+ return (style.$el.length > 0);
}
function _do_action_blur(style, $dest) {
style.$el.detach();
style.on_blur();
+ return false;
}
},
});
diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js
index 3d16c96dc01..cd5c5de71b4 100644
--- a/addons/web_editor/static/src/js/snippets.options.js
+++ b/addons/web_editor/static/src/js/snippets.options.js
@@ -318,6 +318,7 @@ odoo.define('web_editor.snippets.options', function (require) {
var res = this._super.apply(this, arguments);
this.$target.off(".background-option")
.on("background-color-event.background-option", (function (e, type) {
+ e.stopPropagation();
this.$el.find("li:first > a").trigger(type);
}).bind(this));
return res;
diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less
index d89929bb13d..0c03c2cbb8e 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.components.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.components.less
@@ -244,7 +244,8 @@ body .modal {
font-weight: bold;
}
.o_bg_img_opt_object {
- width: 100%;
+ display: inline-block;
+ max-width: 100%;
background: white;
min-height: 10px;
position: relative;
diff --git a/addons/website/static/src/less/website.ui.components.less b/addons/website/static/src/less/website.ui.components.less
index e9aae939eef..c94aba0a245 100644
--- a/addons/website/static/src/less/website.ui.components.less
+++ b/addons/website/static/src/less/website.ui.components.less
@@ -267,7 +267,7 @@ body .modal {
}
// LOGIN FORM
-.oe_login_form {
+.oe_login_form, .oe_signup_form {
max-width: 300px;
position: relative;
margin: 50px auto;
diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py
index 4bceb62fbf9..7215231c665 100644
--- a/addons/website_crm_partner_assign/controllers/main.py
+++ b/addons/website_crm_partner_assign/controllers/main.py
@@ -279,7 +279,6 @@ class WebsiteCrmPartnerAssign(WebsitePartnerPage):
current_grade, current_country = None, None
grade_id = post.get('grade_id')
country_id = post.get('country_id')
- print post
if grade_id:
current_grade = request.env['res.partner.grade'].browse(int(grade_id)).exists()
if country_id:
diff --git a/addons/website_portal_sale/__openerp__.py b/addons/website_portal_sale/__openerp__.py
index c2b2263bc45..f7fb153ab01 100644
--- a/addons/website_portal_sale/__openerp__.py
+++ b/addons/website_portal_sale/__openerp__.py
@@ -16,6 +16,7 @@ Add your sales document in the frontend portal. Your customers will be able to c
],
'data': [
'views/website_portal_sale_templates.xml',
+ 'security/ir.model.access.csv',
],
'demo': [
'data/sale_demo.xml'
diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py
index db93dd58fbc..85ff187cdf3 100644
--- a/addons/website_portal_sale/controllers/main.py
+++ b/addons/website_portal_sale/controllers/main.py
@@ -55,7 +55,7 @@ class website_account(website_account):
archive_groups = self._get_archive_groups('sale.order', domain)
if date_begin and date_end:
- domain += [('create_date', '>=', date_begin), ('create_date', '<', date_end)]
+ domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# count for pager
quotation_count = SaleOrder.search_count(domain)
@@ -91,7 +91,7 @@ class website_account(website_account):
]
archive_groups = self._get_archive_groups('sale.order', domain)
if date_begin and date_end:
- domain += [('create_date', '>=', date_begin), ('create_date', '<', date_end)]
+ domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# count for pager
order_count = SaleOrder.search_count(domain)
@@ -146,7 +146,7 @@ class website_account(website_account):
]
archive_groups = self._get_archive_groups('account.invoice', domain)
if date_begin and date_end:
- domain += [('create_date', '>=', date_begin), ('create_date', '<', date_end)]
+ domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# count for pager
invoice_count = AccountInvoice.search_count(domain)
diff --git a/addons/portal_sale/security/ir.model.access.csv b/addons/website_portal_sale/security/ir.model.access.csv
index 4ca1fb4c2b9..4ca1fb4c2b9 100644
--- a/addons/portal_sale/security/ir.model.access.csv
+++ b/addons/website_portal_sale/security/ir.model.access.csv
diff --git a/addons/website_portal_sale/views/website_portal_sale_templates.xml b/addons/website_portal_sale/views/website_portal_sale_templates.xml
index de449b76de8..000ce6c416d 100644
--- a/addons/website_portal_sale/views/website_portal_sale_templates.xml
+++ b/addons/website_portal_sale/views/website_portal_sale_templates.xml
@@ -381,19 +381,4 @@
</t>
</template>
- <template id="sale_backend_link" inherit_id='website_portal_sale.portal_my_orders' customize_show="True" name="Link to Sales Backend" active="False">
- <xpath expr="//h3[@class='page-header']" position="inside">
- <a href="/web#action=portal_sale.action_orders_portal" target="_blank" class="btn btn-default btn-xs pull-right"> Access the complete Sale application</a>
- </xpath>
- </template>
-
- <template id="sales_backend_access" inherit_id='website_portal_sale.orders_followup' customize_show="True" name="Backend Access" active="False">
- <xpath expr="//div[@id='optional_placeholder']" position="inside">
- <h4 class="text-muted text-center mt64 mb64">
- If you wish to access the complete application to manage your orders, you can follow
- <a href="/web#action=portal_sale.action_orders_portal">this link</a>.
- </h4>
- </xpath>
- </template>
-
</odoo>
diff --git a/addons/website_project/__init__.py b/addons/website_project/__init__.py
index bf7fb7b45c1..805c516d376 100644
--- a/addons/website_project/__init__.py
+++ b/addons/website_project/__init__.py
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
+import models \ No newline at end of file
diff --git a/addons/website_project/models/__init__.py b/addons/website_project/models/__init__.py
new file mode 100644
index 00000000000..f08438b04dc
--- /dev/null
+++ b/addons/website_project/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import project
diff --git a/addons/website_project/models/project.py b/addons/website_project/models/project.py
new file mode 100644
index 00000000000..d9a0ae1d0a4
--- /dev/null
+++ b/addons/website_project/models/project.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+class Project(models.Model):
+ _inherit = ['project.project']
+
+ @api.multi
+ def get_access_action(self):
+ """ Override method that generated the link to access the document. Instead
+ of the classic form view, redirect to the post on the website directly """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/my/project/%s' % self.id,
+ 'target': 'self',
+ 'res_id': self.id,
+ }
+
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the method to place the portal customers in the 'user' group data as a portal view now exists"""
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ if recipient.user_ids and all(recipient.user_ids.mapped('share')):
+ group_data['user'] |= recipient
+ done_ids.add(recipient.id)
+ return super(Project, self)._notification_group_recipients(message, recipients, done_ids, group_data)
+
+
+class Task(models.Model):
+ _inherit = ['project.task']
+
+ @api.multi
+ def get_access_action(self):
+ """ Override method that generated the link to access the document. Instead
+ of the classic form view, redirect to the post on the website directly """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/my/task/%s' % self.id,
+ 'target': 'self',
+ 'res_id': self.id,
+ }
+
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the method to place the portal customers in the 'user' group data as a portal view now exists"""
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ if recipient.user_ids and all(recipient.user_ids.mapped('share')):
+ group_data['user'] |= recipient
+ done_ids.add(recipient.id)
+ return super(Task, self)._notification_group_recipients(message, recipients, done_ids, group_data)
+
diff --git a/addons/website_project_issue/models/project_issue.py b/addons/website_project_issue/models/project_issue.py
index 8e7e0581bbe..31bafc1854f 100644
--- a/addons/website_project_issue/models/project_issue.py
+++ b/addons/website_project_issue/models/project_issue.py
@@ -19,3 +19,19 @@ class Issue(models.Model):
'target': 'self',
'res_id': self.id,
}
+
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the mail.thread method to handle project users and officers
+ recipients. Indeed those will have specific action in their notification
+ emails: creating tasks, assigning it. """
+ group_project_user_id = self.env.ref('project.group_project_user').id
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ if recipient.user_ids and group_project_user_id in recipient.user_ids[0].groups_id.ids:
+ group_data['group_project_user'] |= recipient
+ elif recipient.user_ids and all(recipient.user_ids.mapped('share')):
+ group_data['user'] |= recipient
+ done_ids.add(recipient.id)
+ return super(Issue, self)._notification_group_recipients(message, recipients, done_ids, group_data)
diff --git a/addons/website_sale/models/__init__.py b/addons/website_sale/models/__init__.py
index a40ad2379a6..3f8dbc63ee7 100644
--- a/addons/website_sale/models/__init__.py
+++ b/addons/website_sale/models/__init__.py
@@ -1,3 +1,4 @@
+import account_invoice
import ir_http
import rating
import product
diff --git a/addons/website_sale/models/account_invoice.py b/addons/website_sale/models/account_invoice.py
new file mode 100644
index 00000000000..83fc8f1393a
--- /dev/null
+++ b/addons/website_sale/models/account_invoice.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models
+
+
+class AccountInvoice(models.Model):
+ _inherit = 'account.invoice'
+
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the method to place the portal customers in the 'user' group data as a portal view now exists"""
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ if recipient.user_ids and all(recipient.user_ids.mapped('share')):
+ group_data['user'] |= recipient
+ done_ids.add(recipient.id)
+ return super(AccountInvoice, self)._notification_group_recipients(message, recipients, done_ids, group_data)
+
+ @api.multi
+ def get_access_action(self):
+ """ Override method that generated the link to access the document. Instead
+ of the classic form view, redirect to the online invoice if exists. """
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': '/my/invoices', # No controller /my/invoices/<int>, only a report pdf
+ 'target': 'self',
+ 'res_id': self.id,
+ }
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 031989c5585..75e38e666b0 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -153,6 +153,17 @@ class SaleOrder(models.Model):
accessory_products -= order.website_order_line.mapped('product_id')
return random.sample(accessory_products, len(accessory_products))
+ @api.multi
+ def _notification_group_recipients(self, message, recipients, done_ids, group_data):
+ """ Override the method to place the portal customers in the 'user' group data as a portal view now exists"""
+ for recipient in recipients:
+ if recipient.id in done_ids:
+ continue
+ if recipient.user_ids and all(recipient.user_ids.mapped('share')):
+ group_data['user'] |= recipient
+ done_ids.add(recipient.id)
+ return super(SaleOrder, self)._notification_group_recipients(message, recipients, done_ids, group_data)
+
class Website(models.Model):
_inherit = 'website'
diff --git a/doc/cla/corporate/akretion.md b/doc/cla/corporate/akretion.md
index 61932b3fe8b..156b24ff4d5 100644
--- a/doc/cla/corporate/akretion.md
+++ b/doc/cla/corporate/akretion.md
@@ -33,6 +33,7 @@ Cilene Oliveira cilene.oliveira@akretion.com https://github.com/cileneoliveira
David Beal david.beal@akretion.com https://github.com/bealdav
Florian da Costa florian.dacosta@akretion.com https://github.com/florian-dacosta
Magno Barcelo da Costa magno.costa@akretion.com.br https://github.com/mbcosta
+Raphaël Reverdy raphael.reverdy@akretion.com https://github.com/hparfr
Raphaël Valyi raphael.valyi@akretion.com https://github.com/rvalyi
Renato Lima renato.lima@akretion.com https://github.com/renatonlima
Sebastien Beau sebastien.beau@akretion.com https://github.com/sebastienbeau
diff --git a/doc/reference/upgrade_api.rst b/doc/reference/upgrade_api.rst
index dd03f4f3f5d..567d8aa29fc 100644
--- a/doc/reference/upgrade_api.rst
+++ b/doc/reference/upgrade_api.rst
@@ -23,11 +23,11 @@ Although the database will follow the same process described on that form.
The required steps are:
-* creating a request
-* uploading a database dump
-* running the upgrade process
-* obtaining the status of the database request
-* downloading the upgraded database dump
+* :ref:`creating a request <upgrade-api-create-method>`
+* :ref:`uploading a database dump <upgrade-api-upload-method>`
+* :ref:`running the upgrade process <upgrade-api-process-method>`
+* :ref:`obtaining the status of the database request <upgrade-api-status-method>`
+* :ref:`downloading the upgraded database dump <upgrade-api-download-method>`
The methods
~~~~~~~~~~~
@@ -60,9 +60,11 @@ The ``create`` method
:param str filename: (required) a purely informative name for you database dump file
:param str timezone: (optional) the timezone used by your server. Only for Odoo source version < 6.1
:return: request result
- :rtype: json dictionary
+ :rtype: JSON dictionary
-The *create* method returns a json dictionary containing the following keys:
+The *create* method returns a JSON dictionary containing the following keys:
+
+.. _upgrade-api-json-failure:
``failures``
''''''''''''
@@ -201,11 +203,17 @@ Here are 2 examples of database upgrade request creation using:
Uploading your database dump
============================
-This action upload your database dump.
+There are 2 methods to upload your database dump:
+
+* the ``upload`` method using the HTTPS protocol
+* the ``request_sftp_access`` method using the SFTP protocol
The ``upload`` method
---------------------
+It's the most simple and most straightforward way of uploading your database dump.
+It uses the HTTPS protocol.
+
.. py:function:: https://upgrade.odoo.com/database/v1/upload
Uploads a database dump
@@ -213,12 +221,12 @@ The ``upload`` method
:param str key: (required) your private key
:param str request: (required) your request id
:return: request result
- :rtype: json dictionary
+ :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method
<upgrade-api-create-method>`
-The result is a json dictionary containing the list of ``failures``, which
+The result is a JSON dictionary containing the list of ``failures``, which
should be empty if everything went fine.
.. rst-class:: setup doc-aside
@@ -230,8 +238,6 @@ should be empty if everything went fine.
import os
import pycurl
from urllib import urlencode
- from io import BytesIO
- import json
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload"
DUMPFILE = "openchs.70.cdump"
@@ -267,6 +273,144 @@ should be empty if everything went fine.
HEADER="Content-Type: application/octet-stream"
curl -H $HEADER --data-binary "@${DUMPFILE}" "${UPLOAD_URL}?${URL_PARAMS}"
+.. _upgrade-api-request-sftp-access-method:
+
+The ``request_sftp_access`` method
+----------------------------------
+
+This method is recommanded for big database dumps.
+It uses the SFTP protocol and supports resuming.
+
+It will create a temporary SFTP server where you can connect to and allow you
+to upload your database dump using an SFTP client.
+
+.. py:function:: https://upgrade.odoo.com/database/v1/request_sftp_access
+
+ Creates an SFTP server
+
+ :param str key: (required) your private key
+ :param str request: (required) your request id
+ :param str ssh_keys: (required) the path to a file listing the ssh public keys you'd like to use
+ :return: request result
+ :rtype: JSON dictionary
+
+The request id and the private key are obtained using the :ref:`create method
+<upgrade-api-create-method>`
+
+The file listing your ssh public keys should be roughly similar to a standard ``authorized_keys`` file.
+This file should only contains public keys, blank lines or comments (lines starting with the ``#`` character)
+
+Your database upgrade request should be in the ``draft`` state.
+
+The ``request_sftp_access`` method returns a JSON dictionary containing the following keys:
+
+
+.. rst-class:: setup doc-aside
+
+.. switcher::
+
+ .. code-block:: python
+
+ import os
+ import pycurl
+ from urllib import urlencode
+
+ UPLOAD_URL = "https://upgrade.odoo.com/database/v1/request_sftp_access"
+ SSH_KEYS="/path/to/your/authorized_keys"
+
+ fields = dict([
+ ('request', '10534'),
+ ('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
+ ])
+ postfields = urlencode(fields)
+
+ c = pycurl.Curl()
+ c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields)
+ c.setopt(pycurl.POST, 1)
+ c.setopt(c.HTTPPOST,[("ssh_keys",
+ (c.FORM_FILE, SSH_KEYS,
+ c.FORM_CONTENTTYPE, "text/plain"))
+ ])
+
+ c.perform()
+ c.close()
+
+ .. code-block:: bash
+
+ REQUEST_SFTP_ACCESS_URL="https://upgrade.odoo.com/database/v1/request_sftp_access"
+ SSH_KEYS=/path/to/your/authorized_keys
+ KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
+ REQUEST_ID="10534"
+ URL_PARAMS="key=${KEY}&request=${REQUEST}"
+
+ curl -sS "${REQUEST_SFTP_ACCESS_URL}?${URL_PARAMS}" -F ssh_keys=@${SSH_KEYS} > request_sftp_result.json
+
+ # check for failures
+ failures=$(cat request_sftp_result.json | jq -r '.failures[]')
+ if [ "$failures" != "" ]; then
+ echo $failures | jq -r '.'
+ exit 1
+ fi
+
+
+``failures``
+''''''''''''
+
+The list of errors. See :ref:`failures <upgrade-api-json-failure>` for an
+explanation about the JSON dictionary returned in case of failure.
+
+``request``
+'''''''''''
+
+If the call is successful, the value associated to the *request* key
+will be a dictionary containing your SFTP connexion parameters:
+
+* ``hostname``: the host address to connect to
+* ``sftp_port``: the port to connect to
+* ``sftp_user``: the SFTP user to use for connecting
+* ``shared_file``: the filename you need to use (identical to the ``filename`` value you have used when creating the request in the :ref:`create method <upgrade-api-create-method>`.)
+* ``request_id``: the related upgrade request id (only informative, ,not required for the connection)
+* ``sample_command``: a sample command using the 'sftp' client
+
+You should normally be able to connect using the sample command as is.
+
+You will only have access to the ``shared_file``. No other files will be
+accessible and you will not be able to create new files in your shared
+environment on the SFTP server.
+
+Using the 'sftp' client
++++++++++++++++++++++++
+
+Once you have successfully connected using your SFTP client, you can upload
+your database dump. Here is a sample session using the 'sftp' client:
+
+::
+
+ $ sftp -P 2200 user_10534@upgrade.odoo.com
+ Connected to upgrade.odoo.com.
+ sftp> put /path/to/openchs.70.cdump openchs.70.cdump
+ Uploading /path/to/openchs.70.cdump to /openchs.70.cdump
+ sftp> ls -l openchs.70.cdump
+ -rw-rw-rw- 0 0 0 849920 Aug 30 15:58 openchs.70.cdump
+
+If your connection is interrupted, you can continue your file transfer using
+the ``-a`` command line switch:
+
+.. code-block:: text
+
+ sftp> put -a /path/to/openchs.70.cdump openchs.70.cdump
+ Resuming upload of /path/to/openchs.70.cdump to /openchs.70.cdump
+
+If you don't want to manually type the command and need to automate your
+database upgrade using a script, you can use a batch file or pipe your commands to 'sftp':
+
+::
+
+ echo "put /path/to/openchs.70.cdump openchs.70.cdump" | sftp -b - -P 2200 user_10534@upgrade.odoo.com
+
+The ``-b`` parameter takes a filename. If the filename is ``-``, it reads the commands from standard input.
+
+
.. _upgrade-api-process-method:
Asking to process your request
@@ -284,12 +428,12 @@ The ``process`` method
:param str key: (required) your private key
:param str request: (required) your request id
:return: request result
- :rtype: json dictionary
+ :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method
<upgrade-api-create-method>`
-The result is a json dictionary containing the list of ``failures``, which
+The result is a JSON dictionary containing the list of ``failures``, which
should be empty if everything went fine.
.. rst-class:: setup doc-aside
@@ -350,12 +494,12 @@ The ``status`` method
:param str key: (required) your private key
:param str request: (required) your request id
:return: request result
- :rtype: json dictionary
+ :rtype: JSON dictionary
The request id and the private key are obtained using the :ref:`create method
<upgrade-api-create-method>`
-The result is a json dictionary containing various information about your
+The result is a JSON dictionary containing various information about your
database upgrade request.
.. rst-class:: setup doc-aside
@@ -502,4 +646,28 @@ The ``request`` key contains various useful information about your request:
}
}
+.. _upgrade-api-download-method:
+
+Downloading your database dump
+==============================
+
+Beside downloading your migrated database using the URL provided by the
+:ref:`status method <upgrade-api-status-method>`, you can also use the SFTP
+protocol as described in the :ref:`request_sftp_access method
+<upgrade-api-request-sftp-access-method>`
+
+The diffence is that you'll only be able to download the migrated database. No
+uploading will be possible.
+
+Your database upgrade request should be in the ``done`` state.
+
+Once you have successfully connected using your SFTP client, you can download
+your database dump. Here is a sample session using the 'sftp' client:
+
+::
+
+ $ sftp -P 2200 user_10534@upgrade.odoo.com
+ Connected to upgrade.odoo.com.
+ sftp> get upgraded_openchs.70.cdump /path/to/upgraded_openchs.70.cdump
+ Downloading /upgraded_openchs.70.cdump to /path/to/upgraded_openchs.70.cdump
diff --git a/odoo/addons/base/ir/ir_qweb/qweb.py b/odoo/addons/base/ir/ir_qweb/qweb.py
index 8c66c4fb11e..703c002acc4 100644
--- a/odoo/addons/base/ir/ir_qweb/qweb.py
+++ b/odoo/addons/base/ir/ir_qweb/qweb.py
@@ -275,7 +275,10 @@ class QWeb(object):
astmod = self._base_module()
try:
body = self._compile_node(element, _options)
+ ast_calls = _options['ast_calls']
+ _options['ast_calls'] = []
def_name = self._create_def(_options, body, prefix='template_%s' % name.replace('.', '_'))
+ _options['ast_calls'] += ast_calls
except QWebException, e:
raise e
except Exception, e:
@@ -371,6 +374,9 @@ class QWeb(object):
def _profiling(self, astmod, options):
""" Add profiling code into the givne module AST. """
+ if not astor:
+ _logger.warning("Please install astor to display the code profiling")
+ return
code_line = astor.to_source(astmod)
# code = $code_lines.split(u"\n")
@@ -887,18 +893,16 @@ class QWeb(object):
if body:
def_name = self._create_def(options, body, prefix='set', lineno=el.sourceline)
return [
- # $varset = []
+ # content = []
ast.Assign(
- targets=[
- self._values_var(ast.Str(varname), ctx=ast.Store())
- ],
+ targets=[ast.Name(id='content', ctx=ast.Store())],
value=ast.List(elts=[], ctx=ast.Load())
),
# set(self, $varset.append)
ast.Expr(self._call_def(
def_name,
append=ast.Attribute(
- value=self._values_var(ast.Str(varname), ctx=ast.Load()),
+ value=ast.Name(id='content', ctx=ast.Load()),
attr='append',
ctx=ast.Load()
)
@@ -908,7 +912,7 @@ class QWeb(object):
targets=[self._values_var(ast.Str(varname), ctx=ast.Store())],
value=ast.Call(
func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
- args=[self._values_var(ast.Str(varname), ctx=ast.Load())], keywords=[],
+ args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
starargs=None, kwargs=None
)
)
diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py
index ef565dcfb47..cf8764403f9 100644
--- a/odoo/addons/base/ir/ir_ui_view.py
+++ b/odoo/addons/base/ir/ir_ui_view.py
@@ -14,6 +14,7 @@ from operator import itemgetter
import json
import werkzeug
from lxml import etree
+from lxml.etree import LxmlError
from lxml.builder import E
from odoo import api, fields, models, tools, SUPERUSER_ID, _
@@ -330,7 +331,13 @@ actual arch.
if values.get('inherit_id'):
values['type'] = self.browse(values['inherit_id']).type
else:
- values['type'] = etree.fromstring(values['arch']).tag
+
+ try:
+ values['type'] = etree.fromstring(values.get('arch') or values.get('arch_base')).tag
+ except LxmlError:
+ # don't raise here, the constraint that runs `self._check_xml` will
+ # do the job properly.
+ pass
if not values.get('name'):
values['name'] = "%s %s" % (values.get('model'), values['type'])
@@ -342,7 +349,7 @@ actual arch.
def write(self, vals):
# If view is modified we remove the arch_fs information thus activating the arch_db
# version. An `init` of the view will restore the arch_fs for the --dev mode
- if 'arch' in vals and 'install_mode_data' not in self._context:
+ if ('arch' in vals or 'arch_base' in vals) and 'install_mode_data' not in self._context:
vals['arch_fs'] = False
# drop the corresponding view customizations (used for dashboards for example), otherwise
diff --git a/odoo/addons/base/tests/test_mimetypes.py b/odoo/addons/base/tests/test_mimetypes.py
index 1273bb3942a..2ba8cc05972 100644
--- a/odoo/addons/base/tests/test_mimetypes.py
+++ b/odoo/addons/base/tests/test_mimetypes.py
@@ -21,11 +21,18 @@ class test_guess_mimetype(unittest.TestCase):
def test_default_mimetype_empty(self):
mimetype = guess_mimetype('')
- self.assertEqual(mimetype, 'application/octet-stream')
+ # odoo implementation returns application/octet-stream by default
+ # if available, python-magic returns application/x-empty
+ self.assertIn(mimetype, ('application/octet-stream', 'application/x-empty'))
def test_default_mimetype(self):
mimetype = guess_mimetype('', default='test')
- self.assertEqual(mimetype, 'test')
+ # if available, python-magic returns application/x-empty
+ self.assertIn(mimetype, ('test', 'application/x-empty'))
+
+ def test_mimetype_octet_stream(self):
+ mimetype = guess_mimetype('\0')
+ self.assertEqual(mimetype, 'application/octet-stream')
def test_mimetype_png(self):
content = base64.b64decode(PNG)
@@ -35,7 +42,8 @@ class test_guess_mimetype(unittest.TestCase):
def test_mimetype_bmp(self):
content = base64.b64decode(BMP)
mimetype = guess_mimetype(content, default='test')
- self.assertEqual(mimetype, 'image/bmp')
+ # mimetype should match image/bmp, image/x-ms-bmp, ...
+ self.assertRegexpMatches(mimetype, r'image/.*\bbmp')
def test_mimetype_jpg(self):
content = base64.b64decode(JPG)
diff --git a/odoo/tools/mimetypes.py b/odoo/tools/mimetypes.py
index 581dcb27bd6..370c4b2e85b 100644
--- a/odoo/tools/mimetypes.py
+++ b/odoo/tools/mimetypes.py
@@ -153,9 +153,9 @@ else:
# magic from pypi https://pypi.python.org/pypi/python-magic/
if hasattr(magic,'from_buffer'):
- guess_mimetype = lambda bin_data: magic.from_buffer(bin_data, mime=True)
+ guess_mimetype = lambda bin_data, default=None: magic.from_buffer(bin_data, mime=True)
# magic from file(1) https://packages.debian.org/squeeze/python-magic
elif hasattr(magic,'open'):
ms = magic.open(magic.MAGIC_MIME_TYPE)
ms.load()
- guess_mimetype = ms.buffer
+ guess_mimetype = lambda bin_data, default=None: ms.buffer(bin_data)