diff options
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>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>product_uom_qty" decoration-success="quantity_done==product_uom_qty" decoration-danger="quantity_available < 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>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>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 > 0) and (quantity_done>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&value=%s&width=%s&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) |
