diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-08-22 17:20:15 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-08-22 17:20:15 +0200 |
| commit | d5f2a38b50ceb2fb9f5dcecb5eeaad3188555705 (patch) | |
| tree | 5ad6be44887a1980ca1703f3fd80c38c10ddf1e5 | |
| parent | e38de9dfa4c09f861253cb85643a47153d448e3c (diff) | |
| parent | 6956ad54de52055910539118a34355f818e57c4a (diff) | |
[MERGE] forward port branch saas-14 up to 6956ad54de5
45 files changed, 267 insertions, 71 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 872859134bd..7b1a467a0e7 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -675,7 +675,7 @@ class AccountInvoice(models.Model): self.ensure_one() credit_aml = self.env['account.move.line'].browse(credit_aml_id) if not credit_aml.currency_id and self.currency_id != self.company_id.currency_id: - credit_aml.with_context(allow_amount_currency=True).write({ + credit_aml.with_context(allow_amount_currency=True, check_move_validity=False).write({ 'amount_currency': self.company_id.currency_id.with_context(date=credit_aml.date).compute(credit_aml.balance, self.currency_id), 'currency_id': self.currency_id.id}) if credit_aml.payment_id: diff --git a/addons/account/models/res_config.py b/addons/account/models/res_config.py index 0b7284bcc54..0073e8049db 100644 --- a/addons/account/models/res_config.py +++ b/addons/account/models/res_config.py @@ -76,10 +76,8 @@ class AccountConfigSettings(models.TransientModel): def set_default_product_taxes(self): """ Set the product taxes if they have changed """ ir_values_obj = self.env['ir.values'] - if self.default_sale_tax_id: - ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id], for_all_users=True, company_id=self.company_id.id) - if self.default_purchase_tax_id: - ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id], for_all_users=True, company_id=self.company_id.id) + ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id] if self.default_sale_tax_id else False, for_all_users=True, company_id=self.company_id.id) + ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id] if self.default_purchase_tax_id else False, for_all_users=True, company_id=self.company_id.id) def set_chart_of_accounts(self): """ install a chart of accounts for the given company (if required) """ diff --git a/addons/account/wizard/account_invoice_refund.py b/addons/account/wizard/account_invoice_refund.py index c449bbfcb6b..cddfd61c776 100644 --- a/addons/account/wizard/account_invoice_refund.py +++ b/addons/account/wizard/account_invoice_refund.py @@ -74,7 +74,7 @@ class AccountInvoiceRefund(models.TransientModel): for tmpline in refund.move_id.line_ids: if tmpline.account_id.id == inv.account_id.id: to_reconcile_lines += tmpline - to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile() + to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile() if mode == 'modify': invoice = inv.read(inv_obj._get_refund_modify_read_fields()) invoice = invoice[0] diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 0f120e000d8..48b38c90487 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -393,7 +393,7 @@ class AccountVoucherLine(models.Model): if product.description_purchase: values['name'] += '\n' + product.description_purchase else: - values['price_unit'] = product.lst_price + values['price_unit'] = price_unit or product.lst_price taxes = product.taxes_id or account.tax_ids if product.description_sale: values['name'] += '\n' + product.description_sale @@ -403,7 +403,7 @@ class AccountVoucherLine(models.Model): if company and currency: if company.currency_id != currency: if type == 'purchase': - values['price_unit'] = product.standard_price + values['price_unit'] = price_unit or product.standard_price values['price_unit'] = values['price_unit'] * currency.rate return {'value': values, 'domain': {}} diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py index b9e22ad9925..e970d1ca3ad 100644 --- a/addons/base_import/models/base_import.py +++ b/addons/base_import/models/base_import.py @@ -664,7 +664,13 @@ class Import(models.TransientModel): }] _logger.info('importing %d rows...', len(data)) - import_result = self.env[self.res_model].with_context(import_file=True).load(import_fields, data) + + model = self.env[self.res_model].with_context(import_file=True) + defer_parent_store = self.env.context.get('defer_parent_store_computation', True) + if defer_parent_store and model._parent_store: + model = model.with_context(defer_parent_store_computation=True) + + import_result = model.load(import_fields, data) _logger.info('done') # If transaction aborted, RELEASE SAVEPOINT is going to raise diff --git a/addons/google_account/models/google_service.py b/addons/google_account/models/google_service.py index cd046ec95a7..c8287500813 100644 --- a/addons/google_account/models/google_service.py +++ b/addons/google_account/models/google_service.py @@ -8,6 +8,7 @@ import urllib2 import werkzeug.urls from odoo import api, fields, models, registry, _ +from odoo.exceptions import UserError from odoo.http import request @@ -124,6 +125,9 @@ class GoogleService(models.TransientModel): client_id = get_param('google_%s_client_id' % (service,), default=False) client_secret = get_param('google_%s_client_secret' % (service,), default=False) + if not client_id or not client_secret: + raise UserError(_("The account for the Google service '%s' is not configured") % service) + headers = {"content-type": "application/x-www-form-urlencoded"} data = werkzeug.url_encode({ 'refresh_token': refresh_token, diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 91aee8708e8..cd90ffd16ed 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -4,6 +4,7 @@ import commands import logging import time +import subprocess from threading import Lock @@ -22,7 +23,7 @@ BANNED_DEVICES = set([ ]) -# drivers modules must add to drivers an object with a get_status() method +# drivers modules must add to drivers an object with a get_status() method # so that 'status' can return the status of all active drivers drivers = {} @@ -107,7 +108,7 @@ class Proxy(http.Controller): if not (device_id in BANNED_DEVICES): resp+= "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n" count += 1 - + if count == 0: resp += "<div class='device'>No USB Device Found</div>" @@ -125,7 +126,7 @@ class Proxy(http.Controller): """ % subprocess.check_output('lsusb -v', shell=True) return request.make_response(resp,{ - 'Cache-Control': 'no-cache', + 'Cache-Control': 'no-cache', 'Content-Type': 'text/html; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET', @@ -166,7 +167,7 @@ class Proxy(http.Controller): @http.route('/hw_proxy/payment_request', type='json', auth='none', cors='*') def payment_request(self, price): """ - The PoS will activate the method payment + The PoS will activate the method payment """ print "payment_request: price:"+str(price) return 'ok' @@ -174,7 +175,7 @@ class Proxy(http.Controller): @http.route('/hw_proxy/payment_status', type='json', auth='none', cors='*') def payment_status(self): print "payment_status" - return { 'status':'waiting' } + return { 'status':'waiting' } @http.route('/hw_proxy/payment_cancel', type='json', auth='none', cors='*') def payment_cancel(self): @@ -206,12 +207,12 @@ class Proxy(http.Controller): @http.route('/hw_proxy/is_scanner_connected', type='json', auth='none', cors='*') def is_scanner_connected(self, receipt): - print 'is_scanner_connected?' + print 'is_scanner_connected?' return False @http.route('/hw_proxy/scanner', type='json', auth='none', cors='*') def scanner(self, receipt): - print 'scanner' + print 'scanner' time.sleep(10) return '' diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index 766fb9d5078..c7b01e29fbd 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -117,7 +117,7 @@ class MailController(http.Controller): follower_id = None follower_recs = request.env['mail.followers'].sudo().browse(follower_ids) res_ids = follower_recs.mapped('res_id') - request.env[res_model].browse(res_ids).check_access_rule("write") + request.env[res_model].browse(res_ids).check_access_rule("read") for follower in follower_recs: is_uid = partner_id == follower.partner_id follower_id = follower.id if is_uid else follower_id diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 35114a2542b..b55cc79f922 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -294,8 +294,11 @@ class MailComposer(models.TransientModel): 'mail_server_id': self.mail_server_id.id, 'mail_activity_type_id': self.mail_activity_type_id.id, } + # mass mailing: rendering override wizard static values if mass_mail_mode and self.model: + if self.model in self.env and hasattr(self.env[self.model], 'message_get_email_values'): + mail_values.update(self.env[self.model].browse(res_id).message_get_email_values()) # keep a copy unless specifically requested, reset record name (avoid browsing records) mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False) # auto deletion of mail_mail diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index e43e0cbd71f..0fe3dc746d9 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -17,8 +17,8 @@ STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'} # The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal INT_CURRENCIES = [ - 'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV', - 'VND', 'XOF' + u'BIF', u'XAF', u'XPF', u'CLP', u'KMF', u'DJF', u'GNF', u'JPY', u'MGA', u'PYGí', u'RWF', u'KRW', + u'VUV', u'VND', u'XOF' ]; @@ -116,10 +116,13 @@ class PaymentTransactionStripe(models.Model): def _stripe_form_get_tx_from_data(self, data): """ Given a data dict coming from stripe, verify it and find the related transaction record. """ - reference = data['metadata']['reference'] + reference = data.get('metadata', {}).get('reference') if not reference: - error_msg = _('Stripe: invalid reply received from provider, missing reference') - _logger.error(error_msg, data['metadata']) + error_msg = _( + 'Stripe: invalid reply received from provider, missing reference. Additional message: %s' + % data.get('error', {}).get('message', '') + ) + _logger.error(error_msg) raise ValidationError(error_msg) tx = self.search([('reference', '=', reference)]) if not tx: diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index 77570e9b3ce..14e19775747 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -2,6 +2,11 @@ odoo.define('payment_stripe.stripe', function(require) { "use strict"; var ajax = require('web.ajax'); + var core = require('web.core'); + var _t = core._t; + var qweb = core.qweb; + ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb); + // The following currencies are integer only, see // https://stripe.com/docs/currencies#zero-decimal var int_currencies = [ @@ -33,6 +38,10 @@ odoo.define('payment_stripe.stripe', function(require) { }).done(function(data){ handler.isTokenGenerate = false; window.location.href = data; + }).fail(function(){ + var msg = arguments && arguments[1] && arguments[1].data && arguments[1].data.message; + var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')})); + wizard.appendTo($('body')).modal({'keyboard': true}); }); }, }); diff --git a/addons/payment_stripe/static/src/xml/stripe_templates.xml b/addons/payment_stripe/static/src/xml/stripe_templates.xml new file mode 100644 index 00000000000..c5966fe6274 --- /dev/null +++ b/addons/payment_stripe/static/src/xml/stripe_templates.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> +<t t-name="stripe.error"> + <div class="modal fade"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> + <h4 class="modal-title">Error</h4> + </div> + <div class="modal-body"> + <t t-esc="msg"></t> + </div> + <div class="modal-footer"> + <a href="#" class="btn btn-link btn-xs" data-dismiss="modal">Close</a> + </div> + </div> + </div> + </div> +</t> +</templates> diff --git a/addons/point_of_sale/static/src/js/tests.js b/addons/point_of_sale/static/src/js/tests.js index 0c578088b20..c12d5c5bb0e 100644 --- a/addons/point_of_sale/static/src/js/tests.js +++ b/addons/point_of_sale/static/src/js/tests.js @@ -110,7 +110,8 @@ odoo.define('point_of_sale.Tour', function (require) { trigger: ".header-button", }, { content: "confirm closing the frontend", - trigger: ".header-button", + trigger: ".header-button.confirm", + run: function() {}, //it's a check, }]); tour.register('pos_basic_order', { test: true, url: '/pos/web' }, steps); diff --git a/addons/pos_restaurant/static/src/js/multiprint.js b/addons/pos_restaurant/static/src/js/multiprint.js index 8dc63ea07fc..65956ffa66d 100644 --- a/addons/pos_restaurant/static/src/js/multiprint.js +++ b/addons/pos_restaurant/static/src/js/multiprint.js @@ -191,7 +191,12 @@ models.Order = models.Order.extend({ var product_id = line.get_product().id; if (typeof resume[line_hash] === 'undefined') { - resume[line_hash] = { qty: qty, note: note, product_id: product_id }; + resume[line_hash] = { + qty: qty, + note: note, + product_id: product_id, + product_name_wrapped: line.generate_wrapped_product_name(), + }; } else { resume[line_hash].qty += qty; } @@ -222,6 +227,7 @@ models.Order = models.Order.extend({ add.push({ 'id': curr.product_id, 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, 'note': curr.note, 'qty': curr.qty, }); @@ -229,6 +235,7 @@ models.Order = models.Order.extend({ add.push({ 'id': curr.product_id, 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, 'note': curr.note, 'qty': curr.qty - old.qty, }); @@ -236,6 +243,7 @@ models.Order = models.Order.extend({ rem.push({ 'id': curr.product_id, 'name': this.pos.db.get_product_by_id(curr.product_id).display_name, + 'name_wrapped': curr.product_name_wrapped, 'note': curr.note, 'qty': old.qty - curr.qty, }); @@ -248,6 +256,7 @@ models.Order = models.Order.extend({ rem.push({ 'id': old.product_id, 'name': this.pos.db.get_product_by_id(old.product_id).display_name, + 'name_wrapped': old.product_name_wrapped, 'note': old.note, 'qty': old.qty, }); diff --git a/addons/pos_restaurant/static/src/xml/multiprint.xml b/addons/pos_restaurant/static/src/xml/multiprint.xml index 4f3d0cf7379..282d07421c2 100644 --- a/addons/pos_restaurant/static/src/xml/multiprint.xml +++ b/addons/pos_restaurant/static/src/xml/multiprint.xml @@ -8,6 +8,15 @@ </span> </t> + <t t-name="NameWrapped"> + <t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line"> + <line> + <left></left> + <right><t t-esc="wrapped_line"/></right> + </line> + </t> + </t> + <t t-name="OrderChangeReceipt"> <receipt align='center' @@ -33,8 +42,9 @@ <t t-foreach="changes.cancelled" t-as="change"> <line> <left><value><t t-esc="change.qty" /></value></left> - <right><t t-esc="change.name" /></right> + <right><t t-esc="change.name_wrapped[0]" /></right> </line> + <t t-call="NameWrapped"/> <t t-if="change.note"> <line> <left>NOTE</left> @@ -55,8 +65,9 @@ <t t-foreach="changes.new" t-as="change"> <line> <left><value><t t-esc="change.qty" /></value></left> - <right><t t-esc="change.name" /></right> + <right><t t-esc="change.name_wrapped[0]" /></right> </line> + <t t-call="NameWrapped"/> <t t-if="change.note"> <line> <left>NOTE</left> diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index b0cc6e2d2db..492223df2fe 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -403,7 +403,7 @@ class ProductTemplate(models.Model): updated_products.write({'attribute_value_ids': [(4, value_id.id)]}) # list of values combination - existing_variants = [set(variant.attribute_value_ids.ids) for variant in tmpl_id.product_variant_ids] + existing_variants = [set(variant.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant).ids) for variant in tmpl_id.product_variant_ids] variant_matrix = itertools.product(*(line.value_ids for line in tmpl_id.attribute_line_ids if line.value_ids and line.value_ids[0].attribute_id.create_variant)) variant_matrix = map(lambda record_list: reduce(lambda x, y: x+y, record_list, self.env['product.attribute.value']), variant_matrix) to_create_variants = filter(lambda rec_set: set(rec_set.ids) not in existing_variants, variant_matrix) @@ -412,9 +412,9 @@ class ProductTemplate(models.Model): variants_to_activate = self.env['product.product'] variants_to_unlink = self.env['product.product'] for product_id in tmpl_id.product_variant_ids: - if not product_id.active and product_id.attribute_value_ids in variant_matrix: + if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix: variants_to_activate |= product_id - elif product_id.attribute_value_ids not in variant_matrix: + elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix: variants_to_unlink |= product_id if variants_to_activate: variants_to_activate.write({'active': True}) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index ed029b1a4f3..5f1b2996ec4 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -407,7 +407,7 @@ class Task(models.Model): @api.onchange('project_id') def _onchange_project(self): default_partner_id = self.env.context.get('default_partner_id') - default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else None + default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else self.env['res.partner'] if self.project_id: self.partner_id = self.project_id.partner_id or default_partner self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)]) diff --git a/addons/project_issue/views/project_project_views.xml b/addons/project_issue/views/project_project_views.xml index b5169400bb3..c5bdc5f75a9 100644 --- a/addons/project_issue/views/project_project_views.xml +++ b/addons/project_issue/views/project_project_views.xml @@ -24,7 +24,7 @@ <field name="arch" type="xml"> <field name="use_tasks" position="after"> <field name="use_issues"/> - <field name="issue_ids" invisible="1"/> + <field name="issue_count" invisible="1"/> <field name="issue_needaction_count" invisible="1"/> </field> <xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]/div[last()]" position="after"> @@ -35,7 +35,7 @@ <xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="before"> <div attrs="{'invisible': [('use_issues', '=', False)]}" class="o_project_kanban_box"> <a name="%(act_project_project_2_project_issue_all)d" type="action"> - <span class="o_value"><t t-raw="record.issue_ids.raw_value.length"/></span> + <span class="o_value"><t t-raw="record.issue_count.raw_value"/></span> <span class="o_label"><field name="label_issues"/></span> <a t-if="record.issue_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_issue_all)d" type="action" context="{'search_default_message_needaction': 1}"> <t t-esc="record.issue_needaction_count.raw_value"/> diff --git a/addons/report/views/report_paperformat_views.xml b/addons/report/views/report_paperformat_views.xml index 433ed8cc304..39a1b9bdaff 100644 --- a/addons/report/views/report_paperformat_views.xml +++ b/addons/report/views/report_paperformat_views.xml @@ -29,7 +29,7 @@ <field name="header_line" /> <field name="header_spacing" /> <field name="dpi" /> - <field name="report_ids" widget="many2many_tags"/> + <field name="report_ids" widget="many2many_tags" options="{'not_delete': True}"/> </group> </form> </field> diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index 7148d2c137e..d30dcebd686 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -244,9 +244,9 @@ <div> <field name="customer_lead" class="oe_inline"/> days </div> - <label for="analytic_tag_ids"/> + <label for="analytic_tag_ids" groups="analytic.group_analytic_accounting"/> <div> - <field name="analytic_tag_ids" widget="many2many_tags"/> + <field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_accounting"/> </div> </group> </group> diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 5f601549761..fb1eb0543c5 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -63,7 +63,7 @@ class SaleOrder(models.Model): @api.multi def action_cancel(self): - self.order_line.mapped('procurement_ids').cancel() + self.mapped('order_line').mapped('procurement_ids').cancel() return super(SaleOrder, self).action_cancel() @api.multi diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py index 5c5822a83d7..1a53fc743a0 100644 --- a/addons/stock/models/stock_inventory.py +++ b/addons/stock/models/stock_inventory.py @@ -202,6 +202,19 @@ class Inventory(models.Model): prepare_inventory = action_start @api.multi + def action_inventory_line_tree(self): + action = self.env.ref('stock.action_inventory_line_tree').read()[0] + action['context'] = { + 'default_location_id': self.location_id.id, + 'default_product_id': self.product_id.id, + 'default_prod_lot_id': self.lot_id.id, + 'default_package_id': self.package_id.id, + 'default_partner_id': self.partner_id.id, + 'default_inventory_id': self.id, + } + return action + + @api.multi def _get_inventory_lines_values(self): # TDE CLEANME: is sql really necessary ? I don't think so locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])]) @@ -332,6 +345,8 @@ class InventoryLine(models.Model): theoretical_qty = fields.Float( 'Theoretical Quantity', compute='_compute_theoretical_qty', digits=dp.get_precision('Product Unit of Measure'), readonly=True, store=True) + inventory_location_id = fields.Many2one( + 'stock.location', 'Location', related='inventory_id.location_id', related_sudo=False) @api.one @api.depends('location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id') diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 791607f6bd9..32157878f8c 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -156,7 +156,8 @@ class StockMove(models.Model): @api.depends('product_id', 'product_uom', 'product_uom_qty') def _compute_product_qty(self): if self.product_uom: - self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id) + rounding_method = self._context.get('rounding_method', 'UP') + self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id, rounding_method=rounding_method) def _set_product_qty(self): """ The meaning of product_qty field changed lately and is now a functional field computing the quantity @@ -950,11 +951,11 @@ class StockMove(models.Model): # TDE CLEANME: remove context key + add as parameter if self.env.context.get('source_location_id'): defaults['location_id'] = self.env.context['source_location_id'] - new_move = self.copy(defaults) + new_move = self.with_context(rounding_method='HALF-UP').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}) + self.with_context(do_not_propagate=True, rounding_method='HALF-UP').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) diff --git a/addons/stock/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml index 1e3c4c05933..fc1e5760aeb 100644 --- a/addons/stock/views/stock_inventory_views.xml +++ b/addons/stock/views/stock_inventory_views.xml @@ -14,6 +14,40 @@ </field> </record> + <record id="stock_inventory_line_tree2" model="ir.ui.view"> + <field name="name">stock.inventory.line.tree2</field> + <field name="model">stock.inventory.line</field> + <field name="arch" type="xml"> + <tree editable="top" string="Inventory Details" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty < 0"> + <field name="product_id" domain="[('type','=','product')]"/> + <field name="product_uom_id" string="UoM" groups="product.group_uom"/> + <field name="location_id" domain="[('id', 'child_of', inventory_location_id)]" groups="stock.group_stock_multi_locations"/> + <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/> + <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/> + <field name="partner_id" groups="stock.group_tracking_owner"/> + <field name="theoretical_qty" readonly="1"/> + <field name="product_qty" string="Real Quantity"/> + <field name="state" invisible="1"/> + <field name="inventory_id" invisible="1"/> + <field name="inventory_location_id" invisible="1"/> + </tree> + </field> + </record> + + <record id="stock_inventory_line_search" model="ir.ui.view"> + <field name="name">stock.inventory.line.search</field> + <field name="model">stock.inventory.line</field> + <field name="arch" type="xml"> + <search string="Search Inventory Lines"> + <field name="product_id"/> + <field name="location_id" groups="stock.group_stock_multi_locations"/> + <field name="prod_lot_id" groups="stock.group_production_lot"/> + <field name="package_id" groups="stock.group_tracking_lot"/> + <field name="partner_id" groups="stock.group_tracking_owner"/> + </search> + </field> + </record> + <record id="view_inventory_filter" model="ir.ui.view"> <field name="name">stock.inventory.filter</field> <field name="model">stock.inventory</field> @@ -56,6 +90,19 @@ <field name="state" widget="statusbar" statusbar_visible="draft,confirm,done"/> </header> <sheet> + <div class="oe_button_box" name="button_box"> + <button name="action_inventory_line_tree" + class="oe_stat_button" + icon="fa-building-o" + type="object" + help="List view of lines" + groups="base.group_no_one" + states="confirm"> + <div class="o_form_field o_stat_info"> + <span class="o_stat_text">Details</span> + </div> + </button> + </div> <div class="oe_title"> <label for="name" class="oe_edit_only"/> <h1><field name="name" placeholder="e.g. Annual inventory"/></h1> @@ -181,5 +228,16 @@ </p> </field> </record> + + <record id="action_inventory_line_tree" model="ir.actions.act_window"> + <field name="name">Inventory Lines</field> + <field name="type">ir.actions.act_window</field> + <field name="res_model">stock.inventory.line</field> + <field name="view_type">form</field> + <field name="view_mode">tree</field> + <field name="view_id" ref="stock_inventory_line_tree2"/> + <field name="domain">[('inventory_id', '=', active_id)]</field> + </record> + <menuitem action="action_inventory_form" id="menu_action_inventory_form" parent="menu_stock_inventory_control" sequence="30"/> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/web_editor/static/src/js/ace.js b/addons/web_editor/static/src/js/ace.js index 4a5e4a6d824..fed52c06f1d 100644 --- a/addons/web_editor/static/src/js/ace.js +++ b/addons/web_editor/static/src/js/ace.js @@ -204,7 +204,15 @@ var ViewEditor = Widget.extend({ initType = "less"; } if (!this.options.doNotLoadViews) { - initResID = (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id); + if (typeof this.viewKey === "number") { + initResID = this.viewKey; + } else { + var view = _.findWhere(this.views, {xml_id: this.viewKey}); + if (!view) { + view = _.findWhere(this.views, {key: this.viewKey}); + } + initResID = view.id; + } initType = "xml"; } } diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 1159a7948a8..89c4365376a 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -313,9 +313,14 @@ class Website(Home): def get_view_ids(self, xml_ids): ids = [] + View = request.env["ir.ui.view"].with_context(active_test=False) for xml_id in xml_ids: if "." in xml_id: - record_id = request.env.ref(xml_id).id + # Get website-specific view if possible + record_id = View.search([ + ("website_id", "=", request.website.id), + ("key", "=", xml_id), + ]).id or request.env.ref(xml_id).id else: record_id = int(xml_id) ids.append(record_id) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index a7d7d6bf934..0574c29dd8c 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -70,6 +70,11 @@ class View(models.Model): return view_id @api.model + def _get_inheriting_views_arch_domain(self, view_id, model): + domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model) + return ['|', ('website_id', '=', False), ('website_id', '=', self.env.context.get('website_id'))] + domain + + @api.model @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',)) def get_view_id(self, xml_id): if 'website_id' in self._context and not isinstance(xml_id, (int, long)): diff --git a/addons/website_blog/models/website_blog.py b/addons/website_blog/models/website_blog.py index eee9acc3c32..b70b61b578f 100644 --- a/addons/website_blog/models/website_blog.py +++ b/addons/website_blog/models/website_blog.py @@ -134,7 +134,7 @@ class BlogPost(models.Model): default='{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}') blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade') tag_ids = fields.Many2many('blog.tag', string='Tags') - content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize_attributes=False) + content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False) teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser') teaser_manual = fields.Text(string='Teaser Content') diff --git a/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml b/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml index 61fddcd5b3c..43ecfc36521 100644 --- a/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml +++ b/addons/website_hr_recruitment/security/website_hr_recruitment_security.xml @@ -34,7 +34,7 @@ <record id="hr_department_public" model="ir.rule"> <field name="name">Job department: Public</field> <field name="model_id" ref="hr.model_hr_department"/> - <field name="domain_force">[('jobs_ids.website_published', '=', True)]</field> + <field name="domain_force">['|', ('jobs_ids.website_published', '=', True), ('child_ids', 'not in', [])]</field> <field name="groups" eval="[(4, ref('base.group_public'))]"/> <field name="perm_read" eval="True"/> <field name="perm_write" eval="False"/> diff --git a/addons/website_portal/views/website_portal_templates.xml b/addons/website_portal/views/website_portal_templates.xml index 27ffc586ea7..fb2c20e031a 100644 --- a/addons/website_portal/views/website_portal_templates.xml +++ b/addons/website_portal/views/website_portal_templates.xml @@ -159,7 +159,7 @@ </template> <template id="contact" name="Contact"> - <h3 class="page-header">Saleman</h3> + <h3 class="page-header">Salesman</h3> <h5 t-esc="sales_rep.name"/> <p><a t-att-href="'mailto:'+sales_rep.email" t-esc="sales_rep.email"/></p> <p t-esc="sales_rep.phone"/> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 771ed8f2bd6..b669bdfc3ba 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -500,7 +500,7 @@ class WebsiteSale(http.Controller): def _checkout_form_save(self, mode, checkout, all_values): Partner = request.env['res.partner'] if mode[0] == 'new': - partner_id = Partner.sudo().create(checkout) + partner_id = Partner.sudo().create(checkout).id elif mode[0] == 'edit': partner_id = int(all_values.get('partner_id', 0)) if partner_id: diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py index d3e1c6b893a..5d372588088 100644 --- a/addons/website_sale/models/product.py +++ b/addons/website_sale/models/product.py @@ -190,7 +190,9 @@ class Product(models.Model): def _website_price(self): qty = self._context.get('quantity', 1.0) partner = self.env.user.partner_id - pricelist = self.env['website'].get_current_website().get_current_pricelist() + current_website = self.env['website'].get_current_website() + pricelist = current_website.get_current_pricelist() + company_id = current_website.company_id context = dict(self._context, pricelist=pricelist.id, partner=partner) self2 = self.with_context(context) if self._context != context else self @@ -198,7 +200,7 @@ class Product(models.Model): ret = self.env.user.has_group('sale.group_show_price_subtotal') and 'total_excluded' or 'total_included' for p, p2 in zip(self, self2): - taxes = partner.property_account_position_id.map_tax(p.taxes_id) + taxes = partner.property_account_position_id.map_tax(p.taxes_id.filtered(lambda x: x.company_id == company_id)) p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret] p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret] diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 880c3a62171..3cbeccdc513 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -459,7 +459,7 @@ class Website(models.Model): else: request.session['sale_order_id'] = None - return None + return self.env['sale.order'] return sale_order diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index b633596a0bf..7977b44bac8 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -101,6 +101,6 @@ class WebsiteSaleDigital(website_account): return request.not_found() elif attachment["datas"]: data = StringIO(base64.standard_b64decode(attachment["datas"])) - return http.send_file(data, filename=attachment['name'], as_attachment=True) + return http.send_file(data, filename=attachment['name'].encode('utf-8'), as_attachment=True) else: return request.not_found() diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md index 036e1a63620..096c835ef55 100644 --- a/doc/cla/corporate/vauxoo.md +++ b/doc/cla/corporate/vauxoo.md @@ -36,4 +36,5 @@ Luis González lgonzalez@vauxoo.com https://github.com/luisg123v Oriana Maita oriana@vauxoo.com https://github.com/maitaoriana Gabriela Mogollon gmogollon@vauxoo.com https://github.com/GavyMG Jesus Zapata jesus@vauxoo.com https://github.com/JesusZapata -Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
\ No newline at end of file +Germana Oliveira germana@vauxoo.com https://github.com/goliveirab +Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto diff --git a/doc/cla/individual/NothingCtrl.md b/doc/cla/individual/NothingCtrl.md new file mode 100644 index 00000000000..bf58b0034d3 --- /dev/null +++ b/doc/cla/individual/NothingCtrl.md @@ -0,0 +1,9 @@ +Vietnam, 2017-08-15 + +I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Duong Bao Thang nothingctrl@gmail.com https://github.com/NothingCtrl diff --git a/doc/cla/individual/repodevs.md b/doc/cla/individual/repodevs.md new file mode 100644 index 00000000000..06f8ec2f166 --- /dev/null +++ b/doc/cla/individual/repodevs.md @@ -0,0 +1,10 @@ +Indonesia, 2017-08-12 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, +Edi Santoso repodevs@gmail.com https://github.com/repodevs diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index c92a8b034a9..b8881e9a935 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -159,7 +159,11 @@ database - ``%h`` is replaced by the whole hostname the request is made on. - ``%d`` is replaced by the subdomain the request is made on, with the exception of ``www`` (so domain ``odoo.com`` and ``www.odoo.com`` both - match the database ``odoo``) + match the database ``odoo``). + + These operations are case sensitive. Add option ``(?i)`` to match all + databases (so domain ``odoo.com`` using ``(?i)%d`` matches the database + ``Odoo``). .. option:: --db-template <template> diff --git a/doc/reference/views.rst b/doc/reference/views.rst index 386989b75c6..5d760b611be 100644 --- a/doc/reference/views.rst +++ b/doc/reference/views.rst @@ -840,17 +840,13 @@ attributes: Possible children of the view element are: ``field`` - declares fields to aggregate or to use in kanban *logic*. If the field is - simply displayed in the kanban view, it does not need to be pre-declared. + declares fields to use in kanban *logic*. If the field is simply displayed in + the kanban view, it does not need to be pre-declared. Possible attributes are: ``name`` (required) the name of the field to fetch - ``sum``, ``avg``, ``min``, ``max``, ``count`` - displays the corresponding aggregation at the top of a kanban column, the - field's value is the label of the aggregation (a string). Only one - aggregate operation per field is supported. ``templates`` defines a list of :ref:`reference/qweb` templates. Cards definition may be diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index f3d65226877..411395b300b 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -512,7 +512,8 @@ class IrModelFields(models.Model): if field.state == 'manual' and field.ttype == 'many2many': rel_name = field.relation_table or model._fields[field.name].relation tables_to_drop.add(rel_name) - model._pop_field(field.name) + if field.state == 'manual': + model._pop_field(field.name) if tables_to_drop: # drop the relation tables that are not used by other fields diff --git a/odoo/addons/base/ir/ir_qweb/ir_qweb.py b/odoo/addons/base/ir/ir_qweb/ir_qweb.py index ddb1c64d73d..a3e63df6bea 100644 --- a/odoo/addons/base/ir/ir_qweb/ir_qweb.py +++ b/odoo/addons/base/ir/ir_qweb/ir_qweb.py @@ -62,7 +62,7 @@ class IrQWeb(models.AbstractModel, QWeb): # assume cache will be invalidated by third party on write to ir.ui.view def _get_template_cache_keys(self): """ Return the list of context keys to use for caching ``_get_template``. """ - return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations'] + return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id'] # apply ormcache_context decorator unless in dev mode... @tools.conditional( @@ -186,14 +186,14 @@ class IrQWeb(models.AbstractModel, QWeb): # in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear # by restarting the server after updating the source code (or using the "Clear server cache" in debug tools) 'xml' not in tools.config['dev_mode'], - tools.ormcache('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async'), + tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)), ) def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None): files, remains = self._get_asset_content(xmlid, options) asset = AssetsBundle(xmlid, files, remains, env=self.env) return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url)) - @tools.ormcache('xmlid', 'options.get("lang", "en_US")') + @tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",)) def _get_asset_content(self, xmlid, options): options = dict(options, inherit_branding=False, inherit_branding_auto=False, diff --git a/odoo/addons/base/ir/ir_ui_view.py b/odoo/addons/base/ir/ir_ui_view.py index 407a3385950..7f8578c2b7b 100644 --- a/odoo/addons/base/ir/ir_ui_view.py +++ b/odoo/addons/base/ir/ir_ui_view.py @@ -430,6 +430,15 @@ actual arch. # Inheritance mecanism #------------------------------------------------------ @api.model + def _get_inheriting_views_arch_domain(self, view_id, model): + return [ + ['inherit_id', '=', view_id], + ['model', '=', model], + ['mode', '=', 'extension'], + ['active', '=', True], + ] + + @api.model def get_inheriting_views_arch(self, view_id, model): """Retrieves the architecture of views that inherit from the given view, from the sets of views that should currently be used in the system. During the module upgrade phase it @@ -444,13 +453,8 @@ actual arch. :return: [(view_arch,view_id), ...] """ user_groups = self.env.user.groups_id + conditions = self._get_inheriting_views_arch_domain(view_id, model) - conditions = [ - ['inherit_id', '=', view_id], - ['model', '=', model], - ['mode', '=', 'extension'], - ['active', '=', True], - ] if self.pool._init and not self._context.get('load_all_views'): # Module init currently in progress, only consider views from # modules whose code is already loaded @@ -917,7 +921,7 @@ actual arch. def _read_template_keys(self): """ Return the list of context keys to use for caching ``_read_template``. """ - return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations'] + return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id'] # apply ormcache_context decorator unless in dev mode... @api.model diff --git a/odoo/addons/base/res/res_config.py b/odoo/addons/base/res/res_config.py index 73eb71580f1..f1bb5ffff74 100644 --- a/odoo/addons/base/res/res_config.py +++ b/odoo/addons/base/res/res_config.py @@ -522,7 +522,10 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) IrValues = self.env['ir.values'].sudo() for name, model, field in classified['default']: if isinstance(self[name], models.BaseModel): - value = self[name].ids + if self._fields[name].type == 'many2one': + value = self[name].id + else: + value = self[name].ids else: value = self[name] IrValues.set_default(model, field, value) diff --git a/odoo/api.py b/odoo/api.py index 1adbd08f6e5..413a6b8a2a5 100644 --- a/odoo/api.py +++ b/odoo/api.py @@ -155,6 +155,14 @@ def constrains(*args): ``@constrains`` only supports simple field names, dotted names (fields of relational fields e.g. ``partner_id.customer``) are not supported and will be ignored + + ``@constrains`` will be triggered only if the declared fields in the + decorated method are included in the ``create`` or ``write`` call. + It implies that fields not present in a view will not trigger a call + during a record creation. A override of ``create`` is necessary to make + sure a constraint will always be triggered (e.g. to test the absence of + value). + """ return attrsetter('_constrains', args) diff --git a/odoo/models.py b/odoo/models.py index d6417b0b7cd..e6862d70e15 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -781,7 +781,7 @@ class BaseModel(object): # avoid broken transaction) and keep going cr.execute('ROLLBACK TO SAVEPOINT model_load_save') except Exception as e: - message = (_('Unknown error during import:') + ' %s: %s' % (type(e), unicode(e))) + message = (_('Unknown error during import:') + ' %s: %s' % (type(e), unicode(e.message or e.name))) moreinfo = _('Resolve other errors first') messages.append(dict(info, type='error', message=message, moreinfo=moreinfo)) # Failed for some reason, perhaps due to invalid data supplied, |
