diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-10-01 16:01:54 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-10-01 16:01:54 +0200 |
| commit | 43b63a04656276926dfecd65d3058d6464ee8ac5 (patch) | |
| tree | d0f7c5b4a66540fa8422de2be928a30167e45eed | |
| parent | b2870ae1ebc4d198c69142a2399827848d7d30b4 (diff) | |
| parent | 57e387b645200756c29423af463afe001707f2ac (diff) | |
[MERGE] forward port branch saas-11.4 up to 57e387b6452
85 files changed, 1010 insertions, 201 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index 7ffafe75010..e73bfc10345 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -203,6 +203,8 @@ class AccountAccount(models.Model): @api.onchange('internal_type') def onchange_internal_type(self): self.reconcile = self.internal_type in ('receivable', 'payable') + if self.internal_type == 'liquidity': + self.reconcile = False @api.onchange('code') def onchange_code(self): diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index fa1dc45f459..f65487073fe 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -1575,7 +1575,7 @@ class AccountInvoiceLine(models.Model): ondelete='set null', index=True, oldname='uos_id') product_id = fields.Many2one('product.product', string='Product', ondelete='restrict', index=True) - product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=False) + product_image = fields.Binary('Product Image', related="product_id.image", store=False, readonly=True) account_id = fields.Many2one('account.account', string='Account', domain=[('deprecated', '=', False)], default=_default_account, help="The income or expense account related to the selected product.") diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 6e8b5e43769..b1df4fca6f4 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -396,7 +396,7 @@ class AccountMove(models.Model): reversed_move = self.copy(default={ 'date': date, 'journal_id': journal_id.id if journal_id else self.journal_id.id, - 'ref': _('%sreversal of: ') % (_('Automatic ') if auto else '') + self.name, + 'ref': (_('Automatic reversal of: %s') if auto else _('Reversal of: %s')) % (self.name), 'auto_reverse': False}) for acm_line in reversed_move.line_ids.with_context(check_move_validity=False): acm_line.write({ @@ -1268,8 +1268,9 @@ class AccountMoveLine(models.Model): """ self.ensure_one() amount = -self.balance * distribution.percentage / 100.0 + default_name = self.name or (self.ref or '/' + ' -- ' + (self.partner_id and self.partner_id.name or '/')) return { - 'name': self.name, + 'name': default_name, 'date': self.date, 'account_id': distribution.account_id.id, 'partner_id': self.partner_id.id, diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index a34a743db4a..f4e1d6863eb 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -22,8 +22,8 @@ <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/> <field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/> <field name="internal_type" invisible="1" readonly="1"/> - <label for="reconcile"/> - <div> + <label for="reconcile" attrs="{'invisible': [('internal_type','=','liquidity')]}"/> + <div attrs="{'invisible': [('internal_type','=','liquidity')]}"> <field name="reconcile"/> <button name="action_open_reconcile" class="oe_link" type="object" string=" -> Reconcile" attrs="{'invisible': [('reconcile', '=', False)]}"/> </div> diff --git a/addons/account_budget/models/account_budget.py b/addons/account_budget/models/account_budget.py index b2db529a260..e729b4bdba9 100644 --- a/addons/account_budget/models/account_budget.py +++ b/addons/account_budget/models/account_budget.py @@ -236,9 +236,10 @@ class CrossoveredBudgetLines(models.Model): @api.constrains('general_budget_id', 'analytic_account_id') def _must_have_analytical_or_budgetary_or_both(self): - if not self.analytic_account_id and not self.general_budget_id: - raise ValidationError( - _("You have to enter at least a budgetary position or analytic account on a budget line.")) + for record in self: + if not record.analytic_account_id and not record.general_budget_id: + raise ValidationError( + _("You have to enter at least a budgetary position or analytic account on a budget line.")) @api.multi def action_open_budget_entries(self): diff --git a/addons/account_payment/controllers/payment.py b/addons/account_payment/controllers/payment.py index c9ef4530f99..496a158563e 100644 --- a/addons/account_payment/controllers/payment.py +++ b/addons/account_payment/controllers/payment.py @@ -28,7 +28,7 @@ class PaymentPortal(http.Controller): except: return False - if request.env.user == request.env.ref('base.public_user'): + if request.env.user._is_public(): save_token = False # we avoid to create a token for the public user vals = { 'acquirer_id': acquirer_id, @@ -69,7 +69,7 @@ class PaymentPortal(http.Controller): token = request.env['payment.token'].sudo().browse(int(pm_id)) except (ValueError, TypeError): token = False - token_owner = invoice_sudo.partner_id if request.env.user == request.env.ref('base.public_user') else request.env.user.partner_id + token_owner = invoice_sudo.partner_id if request.env.user._is_public() else request.env.user.partner_id if not token or token.partner_id != token_owner: params['error'] = 'pay_invoice_invalid_token' return request.redirect(_build_url_w_params(error_url, params)) diff --git a/addons/account_payment/controllers/portal.py b/addons/account_payment/controllers/portal.py index 28bca2a12a8..c5b86c47250 100644 --- a/addons/account_payment/controllers/portal.py +++ b/addons/account_payment/controllers/portal.py @@ -11,7 +11,7 @@ class PortalAccount(PortalAccount): values = super(PortalAccount, self)._invoice_get_page_view_values(invoice, access_token, **kwargs) payment_inputs = request.env['payment.acquirer']._get_available_payment_input(company=invoice.company_id) # if not connected (using public user), the method _get_available_payment_input will return public user tokens - is_public_user = request.env.ref('base.public_user') == request.env.user + is_public_user = request.env.user._is_public() if is_public_user: # we should not display payment tokens owned by the public user payment_inputs.pop('pms', None) diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index c0d0afae304..45647cd49a5 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -1089,9 +1089,8 @@ class Meeting(models.Model): else: sort_fields[field] = self[field] if isinstance(self[field], models.BaseModel): - name_get = self[field].name_get() - if len(name_get) and len(name_get[0]) >= 2: - sort_fields[field] = name_get[0][1] + name_get = self[field].mapped('display_name') + sort_fields[field] = name_get and name_get[0] or '' if r_date: sort_fields['sort_start'] = r_date.strftime(VIRTUALID_DATETIME_FORMAT) else: diff --git a/addons/document/static/src/js/document.js b/addons/document/static/src/js/document.js index 98bfe4e5ef3..b8437d70417 100644 --- a/addons/document/static/src/js/document.js +++ b/addons/document/static/src/js/document.js @@ -50,10 +50,10 @@ Sidebar.include({ * @override */ updateEnv: function (env) { - if (this.hasAttachments) { - this.env = env; - this._updateAttachments().then(this._redraw.bind(this)); - } + this.env = env; + var _super = _.bind(this._super, this, env); + var def = this.hasAttachments ? this._updateAttachments() : $.when(); + def.then(_super); }, //-------------------------------------------------------------------------- diff --git a/addons/google_drive/static/src/js/gdrive.js b/addons/google_drive/static/src/js/gdrive.js index def3e0d76a4..5855e056161 100644 --- a/addons/google_drive/static/src/js/gdrive.js +++ b/addons/google_drive/static/src/js/gdrive.js @@ -59,7 +59,7 @@ Sidebar.include({ } if (!already_there) { self._addItems('other', [{ - callback: self._onGoogleDocItemClicked.bind(self, res.id, resID), + callback: self._onGoogleDocItemClicked.bind(self, res.id), classname: 'oe_share_gdoc', config_id: res.id, label: res.name, @@ -81,8 +81,9 @@ Sidebar.include({ * @param {integer} configID * @param {integer} resID */ - _onGoogleDocItemClicked: function (configID, resID) { + _onGoogleDocItemClicked: function (configID) { var self = this; + var resID = this.env.activeIds[0]; var domain = [['id', '=', configID]]; var fields = ['google_drive_resource_id', 'google_drive_client_id']; this._rpc({ diff --git a/addons/google_drive/static/tests/gdrive_test.js b/addons/google_drive/static/tests/gdrive_test.js index 78fac5f39b6..091510109a1 100644 --- a/addons/google_drive/static/tests/gdrive_test.js +++ b/addons/google_drive/static/tests/gdrive_test.js @@ -33,6 +33,9 @@ QUnit.module('gdrive_integration', { records: [{ id: 1, display_name: "Locomotive Breath", + }, { + id: 2, + display_name: "Hey Macarena", }], }, 'google.drive.config': { @@ -104,7 +107,57 @@ QUnit.module('gdrive_integration', { // Trigger opening of the dynamic link $googleAction.click(); + form.destroy(); + }); + + QUnit.test('click on the google drive attachments after switching records', function (assert) { + assert.expect(3); + var self = this; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="display_name"/>' + + '</form>', + res_id: 1, + viewOptions: { + hasSidebar: true, + ids: [1, 2], + index: 0, + }, + mockRPC: function (route, args) { + if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_config') { + assert.deepEqual(args.args, ['partner', 1], + 'The route to get google drive config should have been called'); + return $.when([{id: 27, name: 'Cyberdyne Systems'}]); + } + if (route === '/web/dataset/call_kw/google.drive.config/search_read'){ + return $.when([{google_drive_resource_id: "T1000", + google_drive_client_id: "cyberdyne.org", + id: 1}]); + } + if (route === '/web/dataset/call_kw/google.drive.config/get_google_drive_url') { + assert.deepEqual(args.args, [27, self.activeId, 'T1000'], + 'The route to get the Google url should have been called'); + // We don't return anything useful, otherwise it will open a new tab + return $.when(); + } + return this._super.apply(this, arguments); + }, + }); + var nameToId = { + 'Locomotive Breath': 1, + 'Hey Macarena': 2, + } + // Trigger opening of the dynamic link + self.activeId = nameToId[$("[name='display_name']").text()]; + form.sidebar.$('.oe_share_gdoc').find('a:first').click(); + form.pager.$('.o_pager_next').click(); + self.activeId = nameToId[$("[name='display_name']").text()]; + form.sidebar.$('.oe_share_gdoc').find('a:first').click(); form.destroy(); }); }); diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 742986ab227..b826f110b08 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -387,7 +387,7 @@ class HolidaysRequest(models.Model): for holiday in self: domain = [ ('date_from', '<=', holiday.date_to), - ('date_to', '>=', holiday.date_from), + ('date_to', '>', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '!=', holiday.id), ('state', 'not in', ['cancel', 'refuse']), diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py index 0dd205f663e..c5364ee4101 100644 --- a/addons/iap/models/iap.py +++ b/addons/iap/models/iap.py @@ -151,7 +151,8 @@ class IapAccount(models.Model): def get(self, service_name): account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])]) if not account: - account = self.create({'service_name': service_name}) + # TODO: remove this sudo in v12 as we change the user rights. + account = self.sudo().create({'service_name': service_name}).with_env(self.env) # Since the account did not exist yet, we will encounter a NoCreditError, # which is going to rollback the database and undo the account creation, # preventing the process to continue any further. diff --git a/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml b/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml index 9f73fbf2369..8ea3d6f7283 100644 --- a/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml +++ b/addons/l10n_cn_small_business/data/l10n_cn_small_business_chart_data.xml @@ -23,77 +23,77 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="spoken_languages" eval="'en_US'"/> </record> <record id="l10n_cn_1012" model="account.account.template"> - <field name="name">其他貨幣資金</field> + <field name="name">其他货币资金</field> <field name="code">1012</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1101" model="account.account.template"> - <field name="name">交易性金融資産</field> + <field name="name">交易性金融资产</field> <field name="code">1101</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1121" model="account.account.template"> - <field name="name">應收票據</field> + <field name="name">应收票据</field> <field name="code">1121</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_receivable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1122" model="account.account.template"> - <field name="name">應付賬款</field> + <field name="name">应付账款</field> <field name="code">1122</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_receivable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1123" model="account.account.template"> - <field name="name">預付賬款</field> + <field name="name">预付账款</field> <field name="code">1123</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_prepayments"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1131" model="account.account.template"> - <field name="name">應收股利</field> + <field name="name">应收股利</field> <field name="code">1131</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_receivable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1132" model="account.account.template"> - <field name="name">應收利息</field> + <field name="name">应收利息</field> <field name="code">1132</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_receivable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1221" model="account.account.template"> - <field name="name">其他應收款</field> + <field name="name">其他应收款</field> <field name="code">1221</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_receivable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1231" model="account.account.template"> - <field name="name">壞賬準備</field> + <field name="name">坏账准备</field> <field name="code">1231</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1401" model="account.account.template"> - <field name="name">材料采購</field> + <field name="name">材料采购</field> <field name="code">1401</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1402" model="account.account.template"> - <field name="name">在途物資</field> + <field name="name">在途物资</field> <field name="code">1402</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> @@ -114,105 +114,105 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1405" model="account.account.template"> - <field name="name">庫存商品</field> + <field name="name">库存商品</field> <field name="code">1405</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1406" model="account.account.template"> - <field name="name">發出商品</field> + <field name="name">发出商品</field> <field name="code">1406</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1471" model="account.account.template"> - <field name="name">存貨跌價準備</field> + <field name="name">存货跌价准备</field> <field name="code">1471</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1407" model="account.account.template"> - <field name="name">商品進銷差價</field> + <field name="name">商品进销差价</field> <field name="code">1407</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1408" model="account.account.template"> - <field name="name">委托加工物資</field> + <field name="name">委托加工物资</field> <field name="code">1408</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1501" model="account.account.template"> - <field name="name">持有至到期投資</field> + <field name="name">持有至到期投资</field> <field name="code">1501</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1502" model="account.account.template"> - <field name="name">持有至到期投資减值準備</field> + <field name="name">持有至到期投资减值准备</field> <field name="code">1502</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1503" model="account.account.template"> - <field name="name">可供出售金融資産</field> + <field name="name">可供出售金融资产</field> <field name="code">1503</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1511" model="account.account.template"> - <field name="name">長期股權投資</field> + <field name="name">长期股权投资</field> <field name="code">1511</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1512" model="account.account.template"> - <field name="name">長期股權投資减值準備</field> + <field name="name">长期股权投资减值准备</field> <field name="code">1512</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1521" model="account.account.template"> - <field name="name">投資性房地産</field> + <field name="name">投资性房地产</field> <field name="code">1521</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1531" model="account.account.template"> - <field name="name">長期應收款</field> + <field name="name">长期应收款</field> <field name="code">1531</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1601" model="account.account.template"> - <field name="name">固定資産</field> + <field name="name">固定资产</field> <field name="code">1601</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_fixed_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1602" model="account.account.template"> - <field name="name">累計折舊</field> + <field name="name">累计折旧</field> <field name="code">1602</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1603" model="account.account.template"> - <field name="name">固定資産减值準備</field> + <field name="name">固定资产减值准备</field> <field name="code">1603</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> @@ -226,49 +226,49 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1605" model="account.account.template"> - <field name="name">工程物資</field> + <field name="name">工程物资</field> <field name="code">1605</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1606" model="account.account.template"> - <field name="name">固定資産情况</field> + <field name="name">固定资产情况</field> <field name="code">1606</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1701" model="account.account.template"> - <field name="name">無形資産</field> + <field name="name">无形资产</field> <field name="code">1701</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1702" model="account.account.template"> - <field name="name">累計攤銷</field> + <field name="name">累计摊销</field> <field name="code">1702</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1703" model="account.account.template"> - <field name="name">無形資産减值準備</field> + <field name="name">无形资产减值准备</field> <field name="code">1703</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1711" model="account.account.template"> - <field name="name">商譽</field> + <field name="name">商誉</field> <field name="code">1711</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_non_current_assets"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_1801" model="account.account.template"> - <field name="name">長期待攤銷費用</field> + <field name="name">长期待摊销费用</field> <field name="code">1801</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_depreciation"/> @@ -282,266 +282,266 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2201" model="account.account.template"> - <field name="name">應付票據</field> + <field name="name">应付票据</field> <field name="code">2201</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2202" model="account.account.template"> - <field name="name">應付賬款</field> + <field name="name">应付账款</field> <field name="code">2202</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2203" model="account.account.template"> - <field name="name">預收賬款</field> + <field name="name">预收账款</field> <field name="code">2203</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2211" model="account.account.template"> - <field name="name">應付職工薪酬</field> + <field name="name">应付职工薪酬</field> <field name="code">2211</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2221" model="account.account.template"> - <field name="name">應交税費</field> + <field name="name">应交税费</field> <field name="code">2221</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2231" model="account.account.template"> - <field name="name">應付利息</field> + <field name="name">应付利息</field> <field name="code">2231</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2241" model="account.account.template"> - <field name="name">應付股利</field> + <field name="name">应付股利</field> <field name="code">2241</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2501" model="account.account.template"> - <field name="name">其他應付款</field> + <field name="name">其他应付款</field> <field name="code">2501</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2502" model="account.account.template"> - <field name="name">應付債券</field> + <field name="name">应付债券</field> <field name="code">2502</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2701" model="account.account.template"> - <field name="name">長期應付款</field> + <field name="name">长期应付款</field> <field name="code">2701</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2711" model="account.account.template"> - <field name="name">專項應付款</field> + <field name="name">专项应付款</field> <field name="code">2711</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2801" model="account.account.template"> - <field name="name">預計負債</field> + <field name="name">预计负债</field> <field name="code">2801</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_2901" model="account.account.template"> - <field name="name">遞延所得税負債</field> + <field name="name">递延所得税负债</field> <field name="code">2901</field> <field name="reconcile" eval="True"/> <field name="user_type_id" ref="account.data_account_type_payable"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4001" model="account.account.template"> - <field name="name">實收資本</field> + <field name="name">实收资本</field> <field name="code">4001</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4002" model="account.account.template"> - <field name="name">資本公積金</field> + <field name="name">资本公积金</field> <field name="code">4002</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4003" model="account.account.template"> - <field name="name">其他綜合收益</field> + <field name="name">其他综合收益</field> <field name="code">4003</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4101" model="account.account.template"> - <field name="name">盈餘公積</field> + <field name="name">盈余公积</field> <field name="code">4101</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4103" model="account.account.template"> - <field name="name">本年利潤</field> + <field name="name">本年利润</field> <field name="code">4103</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_4104" model="account.account.template"> - <field name="name">利潤分配</field> + <field name="name">利润分配</field> <field name="code">4104</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_equity"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_5001" model="account.account.template"> - <field name="name">生産成本</field> + <field name="name">生产成本</field> <field name="code">5001</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_direct_costs"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_5101" model="account.account.template"> - <field name="name">制造費用</field> + <field name="name">制造费用</field> <field name="code">5101</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_direct_costs"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_5201" model="account.account.template"> - <field name="name">勞務成本</field> + <field name="name">劳务成本</field> <field name="code">5201</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_direct_costs"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_5301" model="account.account.template"> - <field name="name">研發支出</field> + <field name="name">研发支出</field> <field name="code">5301</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_direct_costs"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6001" model="account.account.template"> - <field name="name">主營業務收入</field> + <field name="name">主营业务收入</field> <field name="code">6001</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_revenue"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6051" model="account.account.template"> - <field name="name">其他業務收入</field> + <field name="name">其他业务收入</field> <field name="code">6051</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_other_income"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6101" model="account.account.template"> - <field name="name">公允價值變動損益</field> + <field name="name">公允价值变动损益</field> <field name="code">6101</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_other_income"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6111" model="account.account.template"> - <field name="name">投資收益</field> + <field name="name">投资收益</field> <field name="code">6111</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_other_income"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6301" model="account.account.template"> - <field name="name">營業外收入</field> + <field name="name">营业外收入</field> <field name="code">6301</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_other_income"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6401" model="account.account.template"> - <field name="name">主營業務成本</field> + <field name="name">主营业务成本</field> <field name="code">6401</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6402" model="account.account.template"> - <field name="name">其他業務成本</field> + <field name="name">其他业务成本</field> <field name="code">6402</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6403" model="account.account.template"> - <field name="name">營業税及附加</field> + <field name="name">营业税及附加</field> <field name="code">6403</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6601" model="account.account.template"> - <field name="name">銷售費用</field> + <field name="name">销售费用</field> <field name="code">6601</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6602" model="account.account.template"> - <field name="name">管理費用</field> + <field name="name">管理费用</field> <field name="code">6602</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6603" model="account.account.template"> - <field name="name">財務費用</field> + <field name="name">财务费用</field> <field name="code">6603</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6701" model="account.account.template"> - <field name="name">資産减值損失</field> + <field name="name">资产减值损失</field> <field name="code">6701</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6711" model="account.account.template"> - <field name="name">營業外支出</field> + <field name="name">营业外支出</field> <field name="code">6711</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6801" model="account.account.template"> - <field name="name">所得税費用</field> + <field name="name">所得税费用</field> <field name="code">6801</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> <field name="chart_template_id" ref="l10n_chart_china_small_business"/> </record> <record id="l10n_cn_6901" model="account.account.template"> - <field name="name">以前年度損益調整</field> + <field name="name">以前年度损益调整</field> <field name="code">6901</field> <field name="reconcile" eval="False"/> <field name="user_type_id" ref="account.data_account_type_expenses"/> @@ -560,3 +560,5 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html </data> </odoo> + + diff --git a/addons/l10n_co/data/account.account.template.csv b/addons/l10n_co/data/account.account.template.csv index b68f925a074..6eb0e2372ac 100644 --- a/addons/l10n_co/data/account.account.template.csv +++ b/addons/l10n_co/data/account.account.template.csv @@ -1,5 +1,5 @@ id,code,name,reconcile,user_type_id:id,chart_template_id:id -co_puc_110505,110505,Caja General,TRUE,account.data_account_type_liquidity,l10n_co_chart_template_generic +co_puc_110505,110505,Caja General,FALSE,account.data_account_type_liquidity,l10n_co_chart_template_generic co_puc_120535,120535,Comercio al Por Mayor y al Por Menor,FALSE,account.data_account_type_current_assets,l10n_co_chart_template_generic co_puc_129595,129595,Diversas,FALSE,account.data_account_type_current_assets,l10n_co_chart_template_generic co_puc_130505,130505,Nacionales,TRUE,account.data_account_type_receivable,l10n_co_chart_template_generic diff --git a/addons/l10n_cr/data/account_template_data.xml b/addons/l10n_cr/data/account_template_data.xml index ba4ac297c0e..fcc4426b84a 100644 --- a/addons/l10n_cr/data/account_template_data.xml +++ b/addons/l10n_cr/data/account_template_data.xml @@ -8,7 +8,7 @@ <field name="code">0-111301</field> <field name="name">0-Cuenta PayPal 1</field> <field name="user_type_id" ref="account.data_account_type_liquidity"/> - <field name="reconcile" eval="True"/> + <field name="reconcile" eval="False"/> <field name="chart_template_id" ref="account_chart_template_0"/> </record> <record id="account_account_template_0_111401" model="account.account.template"> diff --git a/addons/l10n_es/data/account_group.xml b/addons/l10n_es/data/account_group.xml index e4a9303b164..4d457aeba6e 100644 --- a/addons/l10n_es/data/account_group.xml +++ b/addons/l10n_es/data/account_group.xml @@ -2687,7 +2687,7 @@ <field name="parent_id" ref="l10n_es.account_group_599"/> </record> <record id="account_group_6" model="account.group"> - <field name="code_prefix">2</field> + <field name="code_prefix">6</field> <field name="name">Compras y gastos</field> </record> <record id="account_group_60" model="account.group"> diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 39ed6641687..8bedba7ca56 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -178,15 +178,14 @@ class Message(models.Model): @api.model def _search_need_moderation(self, operator, operand): - if operator == '=' and operand: + if operator == '=' and operand is True: return ['&', '&', ('moderation_status', '=', 'pending_moderation'), ('model', '=', 'mail.channel'), ('res_id', 'in', self.env.user.moderation_channel_ids.ids)] - return ['|', '|', - ('moderation_status', '!=', 'pending_moderation'), - ('model', '!=', 'mail.channel'), - ('res_id', 'not in', self.env.user.moderation_channel_ids.ids)] + + # no support for other operators + return ValueError(_('Unsupported search filter on moderation status')) #------------------------------------------------------ # Notification API @@ -419,8 +418,19 @@ class Message(models.Model): return messages._format_mail_failures() @api.model - def message_fetch(self, domain, limit=20): - return self.search(domain, limit=limit).message_format() + def message_fetch(self, domain, limit=20, moderated_channel_ids=None): + messages = self.search(domain, limit=limit) + user_mod_channels = self.env.user.moderation_channel_ids.ids + if moderated_channel_ids and set(moderated_channel_ids).issubset(user_mod_channels): + # Split load moderated and regular messages, as the ORed domain can + # cause performance issues on large databases. + moderated_messages_dom = [['model', '=', 'mail.channel'], + ['res_id', 'in', moderated_channel_ids], + ['need_moderation', '=', True]] + messages |= self.search(moderated_messages_dom, limit=limit) + # Truncate the results to `limit` + messages = messages.sorted(key='id', reverse=True)[:limit] + return messages.message_format() @api.multi def message_format(self): diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 2916a60e0cb..5e1d6b52ede 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1323,6 +1323,7 @@ class MailThread(models.AbstractModel): # if a new thread is created, parent is irrelevant message_dict.pop('parent_id', None) thread = MessageModel.message_new(message_dict, custom_values) + thread_id = thread.id else: if thread_id: raise ValueError("Posting a message without model should be with a null res_id, to create a private message.") diff --git a/addons/mail/static/src/js/followers.js b/addons/mail/static/src/js/followers.js index d90b8cce261..9d5ed5424c2 100644 --- a/addons/mail/static/src/js/followers.js +++ b/addons/mail/static/src/js/followers.js @@ -204,7 +204,9 @@ var Followers = AbstractField.extend({ return $.when(def).then(function (results) { if (results) { self.followers = _.uniq(results.followers.concat(self.followers), 'id'); - self.subtypes = results.subtypes; + if (results.subtypes) { //read_followers will return False if current user is not in the list + self.subtypes = results.subtypes; + } } // filter out previously fetched followers that are no longer following self.followers = _.filter(self.followers, function (follower) { @@ -291,6 +293,15 @@ var Followers = AbstractField.extend({ }); } else { var kwargs = _.extend({}, ids); + if (follower_id === undefined || follower_id === this.partnerID) { + //this.subtypes will only be updated if the current user + //just added himself to the followers. We need to update + //the subtypes manually when editing subtypes + //for current user + _.each(this.subtypes, function (subtype) { + subtype.followed = checklist.indexOf(subtype.id) > -1; + }); + } kwargs.subtype_ids = checklist; kwargs.context = {}; // FIXME this._rpc({ diff --git a/addons/mail/static/src/js/models/threads/searchable_thread.js b/addons/mail/static/src/js/models/threads/searchable_thread.js index 5b7157fde5b..17fa119890a 100644 --- a/addons/mail/static/src/js/models/threads/searchable_thread.js +++ b/addons/mail/static/src/js/models/threads/searchable_thread.js @@ -165,6 +165,7 @@ var SearchableThread = Thread.extend({ var self = this; var domain = this._getThreadDomain(); + var moderatedChannelIds = typeof(this._id) === 'number' ? [this._id] : false; var cache = this._getCache(pDomain); if (pDomain) { @@ -179,6 +180,7 @@ var SearchableThread = Thread.extend({ method: 'message_fetch', args: [domain], kwargs: { + moderated_channel_ids: moderatedChannelIds, limit: this._FETCH_LIMIT, context: session.user_context }, diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index b69bc6825e5..c853ffb99e8 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -1720,11 +1720,6 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) _.each(subtypes, function (subtype) { subtype.followed = _.contains(args.kwargs.subtype_ids, subtype.id); }); - // hack: the server creates a new follower each time the subtypes are updated - // so we need here to mock that weird behavior here, as the followers widget - // relies on that behavior - this.data.partner.records[0].message_follower_ids = [2]; - followers[0].id = 2; } else { // follow this.data.partner.records[0].message_follower_ids = [1]; @@ -1743,6 +1738,7 @@ QUnit.test('followers widget: follow/unfollow, edit subtypes', function (assert) return $.when({ followers: followers, subtypes: subtypes, + // caution, subtype will only be returned if current user is in args follower list }); } if (route === '/web/dataset/call_kw/partner/message_unsubscribe') { diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index 2372c76631e..785a8358786 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -75,7 +75,17 @@ MockServer.include({ _mockMessageFetch: function (args) { var domain = args.args[0]; var model = args.model; + var mod_channel_ids = args.kwargs.moderated_channel_ids; var messages = this._getRecords(model, domain); + if (mod_channel_ids) { + var mod_messages = this._getRecords( + model, + [['model', '=', 'mail.channel'], + ['res_id', 'in', mod_channel_ids], + ['need_moderation', '=', true]] + ); + messages = _.union(messages, mod_messages); + } // sorted from highest ID to lowest ID (i.e. from youngest to oldest) messages.sort(function (m1, m2) { return m1.id < m2.id ? 1 : -1; diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index c26a9bab5a7..29f99cb4f00 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -287,7 +287,7 @@ class MassMailingCampaign(models.Model): mass_mailing_ids = fields.One2many( 'mail.mass_mailing', 'mass_mailing_campaign_id', string='Mass Mailings') - unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=True, + unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False, help='If checked, recipients will be mailed only once for the whole campaign. ' 'This lets you send different mailings to randomly selected recipients and test ' 'the effectiveness of the mailings, without causing duplicate messages.') diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py index 0364ebe54be..a009d9d49d6 100644 --- a/addons/mrp/report/mrp_report_bom_structure.py +++ b/addons/mrp/report/mrp_report_bom_structure.py @@ -134,7 +134,7 @@ class ReportBomStructure(models.AbstractModel): components = [] total = 0 for line in bom.bom_line_ids: - line_quantity = (bom_quantity) * line.product_qty + line_quantity = (bom_quantity / (bom.product_qty or 1.0)) * line.product_qty if line._skip_bom_line(product): continue price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py index c3a45c3c6bd..ba2a2551892 100644 --- a/addons/payment/controllers/portal.py +++ b/addons/payment/controllers/portal.py @@ -203,6 +203,7 @@ class WebsitePayment(http.Controller): values['sale_order_ids'] = [(6, 0, [order_id])] reference_values = order_id and {'sale_order_ids': [(4, order_id)]} or {} + reference_values.update(acquirer_id=int(acquirer_id)) values['reference'] = request.env['payment.transaction']._compute_reference(values=reference_values, prefix=reference) tx = request.env['payment.transaction'].sudo().with_context(lang=None).create(values) tx.return_url = '/website_payment/confirm?tx_id=%d' % tx.id diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 93efd9212aa..17e5f26f8fb 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -743,7 +743,7 @@ class PaymentTransaction(models.Model): payments = defaultdict(lambda: self.env['account.payment']) for trans in self: if trans.payment_id: - payments += trans.payment_id + payments[trans.acquirer_id.company_id.id] += trans.payment_id continue payment_vals = trans._prepare_account_payment_vals() diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py index 9e97854287c..f6439cb17d3 100644 --- a/addons/payment_ogone/models/payment.py +++ b/addons/payment_ogone/models/payment.py @@ -283,7 +283,7 @@ class PaymentTxOgone(models.Model): return invalid_parameters def _ogone_form_validate(self, data): - if self.state != 'draft': + if self.state not in ['draft', 'pending']: _logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference) return True @@ -431,7 +431,7 @@ class PaymentTxOgone(models.Model): return self._ogone_s2s_validate_tree(tree) def _ogone_s2s_validate_tree(self, tree, tries=2): - if self.state != 'draft': + if self.state not in ['draft', 'pending']: _logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference) return True @@ -469,7 +469,7 @@ class PaymentTxOgone(models.Model): if status == 46: # HTML 3DS vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text)) self.write(vals) - self._set_transaction_done() + self._set_transaction_pending() elif status in self._ogone_wait_tx_status and tries > 0: time.sleep(0.5) self.write({'acquirer_reference': tree.get('PAYID')}) diff --git a/addons/payment_sips/models/payment.py b/addons/payment_sips/models/payment.py index bdcc73f3a55..7d46d56da2b 100644 --- a/addons/payment_sips/models/payment.py +++ b/addons/payment_sips/models/payment.py @@ -5,6 +5,7 @@ import json import logging import re +import time from hashlib import sha256 from werkzeug import urls @@ -98,7 +99,7 @@ class AcquirerSips(models.Model): return_context = {} if sips_tx_values.get('return_url'): - return_context[u'return_url'] = u'%s' % sips_tx_values.pop('return_url') + return_context[u'return_url'] = u'%s' % urls.url_quote(sips_tx_values.pop('return_url')) return_context[u'reference'] = u'%s' % sips_tx_values['reference'] sips_tx_values['Data'] += u'|returnContext=%s' % (json.dumps(return_context)) @@ -124,13 +125,11 @@ class TxSips(models.Model): @api.model def _compute_reference(self, values=None, prefix=None): - acquirer = self.env['payment.acquirer'].browse(values['acquirer_id']) + res = super(TxSips, self)._compute_reference(values=values, prefix=prefix) + acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id')) if acquirer and acquirer.provider == 'sips': - if not prefix and values: - prefix = self._compute_reference_prefix(values) - prefix = re.sub(r'[^0-9a-zA-Z-]+', 'x', prefix) - - return super(TxSips, self)._compute_reference(values=values, prefix=prefix) + return re.sub(r'[^0-9a-zA-Z]+', 'x', res) + 'x' + str(int(time.time())) + return res # -------------------------------------------------- # FORM RELATED METHODS diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index fc52d816c49..ff98c6c944d 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -469,7 +469,7 @@ class PosOrder(models.Model): def _filtered_for_reconciliation(self): filter_states = ['invoiced', 'done'] - if self.env['ir.config_parameter'].get_param('point_of_sale.order_reconcile_mode', 'all') == 'partner_only': + if self.env['ir.config_parameter'].sudo().get_param('point_of_sale.order_reconcile_mode', 'all') == 'partner_only': return self.filtered(lambda order: order.state in filter_states and order.partner_id) return self.filtered(lambda order: order.state in filter_states) diff --git a/addons/point_of_sale/tests/test_anglo_saxon.py b/addons/point_of_sale/tests/test_anglo_saxon.py index 7fae0de8b83..8ddc0110063 100644 --- a/addons/point_of_sale/tests/test_anglo_saxon.py +++ b/addons/point_of_sale/tests/test_anglo_saxon.py @@ -36,9 +36,13 @@ class TestAngloSaxonCommon(common.TransactionCase): self.product = self.product.copy({'name': 'New product','standard_price': 100}) self.company.anglo_saxon_accounting = True self.product.categ_id = self.category + self.product.property_account_expense_id = account_expense + self.product.property_account_income_id = self.account sale_journal = self.env['account.journal'].create({'name': 'POS journal', 'type': 'sale', 'code': 'POS00'}) self.pos_config.journal_id = sale_journal self.cash_journal = self.env['account.journal'].create({'name': 'CASH journal', 'type': 'cash', 'code': 'CSH00'}) + self.pos_config.invoice_journal_id = False + self.pos_config.journal_ids = [self.cash_journal.id] class TestAngloSaxonFlow(TestAngloSaxonCommon): diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index 073e7153eeb..bfe59057391 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -42,7 +42,7 @@ def _message_post_helper(res_model='', res_id=None, message='', token='', nosubs access_as_sudo = _has_token_access(res_model, res_id, token=token) if access_as_sudo: record = record.sudo() - if request.env.user == request.env.ref('base.public_user'): + if request.env.user._is_public(): if kw.get('pid') and consteq(kw.get('hash'), record._sign_token(int(kw.get('pid')))): author_id = kw.get('pid') else: diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 0649bc10ced..5025b597cf6 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -126,6 +126,7 @@ class ProductProduct(models.Model): image_medium = fields.Binary( "Medium-sized image", compute='_compute_images', inverse='_set_image_medium', help="Image of the product variant (Medium-sized image of product template if false).") + is_product_variant = fields.Boolean(compute='_compute_is_product_variant') standard_price = fields.Float( 'Cost', company_dependent=True, @@ -153,6 +154,10 @@ class ProductProduct(models.Model): def _get_invoice_policy(self): return False + def _compute_is_product_variant(self): + for product in self: + product.is_product_variant = True + def _compute_product_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 5dfcec1b063..89b3a269d4c 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -241,10 +241,7 @@ class ProductTemplate(models.Model): def _compute_is_product_variant(self): for template in self: - if template._name == 'product.template': - template.is_product_variant = False - else: - template.is_product_variant = True + template.is_product_variant = False @api.model def _get_weight_uom_id_from_ir_config_parameter(self): diff --git a/addons/product/tests/common.py b/addons/product/tests/common.py index cc812db81a2..559b8b9dcfa 100644 --- a/addons/product/tests/common.py +++ b/addons/product/tests/common.py @@ -36,6 +36,7 @@ class TestProductCommon(common.SavepointCase): cls.product_1 = Product.create({ 'name': 'Courage', 'type': 'consu', + 'default_code': 'PROD-1', 'uom_id': cls.uom_dunit.id, 'uom_po_id': cls.uom_dunit.id}) diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py index 0819c7dfdf3..8fc890c31f1 100644 --- a/addons/product/tests/test_variants.py +++ b/addons/product/tests/test_variants.py @@ -60,6 +60,14 @@ class TestVariants(common.TestProductCommon): self.size_attr_value_l = self.env['product.attribute.value'].create({'name': 'L', 'attribute_id': self.size_attr.id}) return res + def test_variants_is_product_variant(self): + template = self.product_7_template + variants = template.product_variant_ids + self.assertFalse(template.is_product_variant, + 'Product template is not a variant') + self.assertEqual({True}, set(v.is_product_variant for v in variants), + 'Product variants are variants') + def test_variants_creation_mono(self): test_template = self.env['product.template'].create({ 'name': 'Sofa', diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 1cb29ca834d..b3286c2afe3 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -285,7 +285,9 @@ class Project(models.Model): def map_tasks(self, new_project_id): """ copy and map tasks from old to new project """ tasks = self.env['project.task'] - for task in self.tasks: + # We want to copy archived task, but do not propagate an active_test context key + task_ids = self.env['project.task'].with_context(active_test=False).search([('project_id', '=', self.id)]).ids + for task in self.env['project.task'].browse(task_ids): # preserve task name and stage, normally altered during copy defaults = self._map_tasks_default_valeus(task) tasks += task.copy(defaults) @@ -296,7 +298,6 @@ class Project(models.Model): def copy(self, default=None): if default is None: default = {} - self = self.with_context(active_test=False) if not default.get('name'): default['name'] = _("%s (copy)") % (self.name) project = super(Project, self).copy(default) diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 6a38243ef2f..4a5de69a657 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -716,7 +716,7 @@ <field name="name">Project's tasks</field> <field name="view_type">form</field> <field name="view_mode">tree,form,calendar,graph,kanban</field> - <field name="domain">[('project_id', 'child_of', [active_id])]</field> + <field name="domain">[('project_id', '=', active_id)]</field> <field name="context">{'project_id':active_id}</field> </record> diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml index 437d86a0c75..e167fc1f3ce 100644 --- a/addons/purchase/views/purchase_views.xml +++ b/addons/purchase/views/purchase_views.xml @@ -201,6 +201,7 @@ </tree> <form string="Purchase Order Line"> <sheet> + <field name="state" invisible="1"/> <group> <group> <field name="product_id" diff --git a/addons/purchase_stock/tests/test_create_picking.py b/addons/purchase_stock/tests/test_create_picking.py index df46b3c48bb..8a2e6c530c7 100644 --- a/addons/purchase_stock/tests/test_create_picking.py +++ b/addons/purchase_stock/tests/test_create_picking.py @@ -118,6 +118,7 @@ class TestCreatePicking(common.TestProductCommon): 'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))], 'seller_ids': [(6, 0, [seller.id])], 'categ_id': self.env.ref('product.product_category_all').id, + 'supplier_taxes_id': [(6, 0, [])], }) customer_move = self.env['stock.move'].create({ diff --git a/addons/sale_mrp/models/sale_mrp.py b/addons/sale_mrp/models/sale_mrp.py index c0d0753f139..82c2ca7ed3a 100644 --- a/addons/sale_mrp/models/sale_mrp.py +++ b/addons/sale_mrp/models/sale_mrp.py @@ -48,6 +48,17 @@ class SaleOrderLine(models.Model): components[product] = {'qty': qty, 'uom': to_uom.id} return components + def _get_qty_procurement(self): + self.ensure_one() + # Specific case when we change the qty on a SO for a kit product. + # We don't try to be too smart and keep a simple approach: we compare the quantity before + # and after update, and return the difference. We don't take into account what was already + # sent, or any other exceptional case. + bom = self.env['mrp.bom']._bom_find(product=self.product_id) + if bom and bom.type == 'phantom' and 'previous_product_uom_qty' in self.env.context: + return self.env.context['previous_product_uom_qty'].get(self.id, 0.0) + return super(SaleOrderLine, self)._get_qty_procurement() + class AccountInvoiceLine(models.Model): # TDE FIXME: what is this code ?? diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py index 6ac44761c11..8db9adc9916 100644 --- a/addons/sale_stock/models/sale_order.py +++ b/addons/sale_stock/models/sale_order.py @@ -201,14 +201,15 @@ class SaleOrderLine(models.Model): @api.multi def write(self, values): - lines = False + lines = self.env['sale.order.line'] if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') lines = self.filtered( lambda r: r.state == 'sale' and not r.is_expense and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + previous_product_uom_qty = {line.id: line.product_uom_qty for line in lines} res = super(SaleOrderLine, self).write(values) if lines: - lines._action_launch_stock_rule() + lines.with_context(previous_product_uom_qty=previous_product_uom_qty)._action_launch_stock_rule() return res @api.depends('order_id.state') @@ -257,7 +258,10 @@ class SaleOrderLine(models.Model): return {} if self.product_id.type == 'product': precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - product = self.product_id.with_context(warehouse=self.order_id.warehouse_id.id) + product = self.product_id.with_context( + warehouse=self.order_id.warehouse_id.id, + lang=self.order_id.partner_id.lang or self.env.user.lang or 'en_US' + ) product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id) if float_compare(product.virtual_available, product_qty, precision_digits=precision) == -1: is_available = self._check_routing() diff --git a/addons/stock/data/stock_demo.xml b/addons/stock/data/stock_demo.xml index 36cff37b2a4..5ba6b1a3dd6 100644 --- a/addons/stock/data/stock_demo.xml +++ b/addons/stock/data/stock_demo.xml @@ -14,26 +14,31 @@ <field name="location_id" ref="stock_location_suppliers"/> <field name="usage">supplier</field> <field name="barcode">2601985</field> + <field name="company_id"></field> </record> <record id="stock_location_4" model="stock.location"> <field name="name">Big Vendors</field> <field name="location_id" ref="stock_location_3"/> <field name="usage">supplier</field> + <field name="company_id"></field> </record> <record id="stock_location_5" model="stock.location"> <field name="name">Generic IT Vendors</field> <field name="location_id" ref="stock_location_3"/> <field name="usage">supplier</field> + <field name="company_id"></field> </record> <record id="stock_location_7" model="stock.location"> <field name="name">European Customers</field> <field name="location_id" ref="stock_location_customers"/> <field name="usage">customer</field> + <field name="company_id"></field> </record> <record id="stock_location_8" model="stock.location"> <field name="name">Non European Customers</field> <field name="location_id" ref="stock_location_customers"/> <field name="usage">customer</field> + <field name="company_id"></field> </record> diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index c6a65af4e76..2b3f9af2baa 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -161,7 +161,7 @@ class Product(models.Model): if self.env.context.get('company_owned', False): company_id = self.env.user.company_id.id return ( - [('location_id.company_id', '=', company_id)], + [('location_id.company_id', '=', company_id), ('location_id.usage', 'in', ['internal', 'transit'])], [('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id)], [('location_id.company_id', '=', company_id), ('location_dest_id.company_id', '=', False), ]) diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 6b8c9fe85c2..29433c13091 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1020,7 +1020,7 @@ class StockMove(models.Model): if move.propagate: # only cancel the next move if all my siblings are also cancelled if all(state == 'cancel' for state in siblings_states): - move.move_dest_ids._action_cancel() + move.move_dest_ids.filtered(lambda m: m.state != 'done')._action_cancel() else: if all(state in ('done', 'cancel') for state in siblings_states): move.move_dest_ids.write({'procure_method': 'make_to_stock'}) @@ -1030,6 +1030,7 @@ class StockMove(models.Model): def _prepare_extra_move_vals(self, qty): vals = { + 'origin_returned_move_id': self.origin_returned_move_id.id, 'product_uom_qty': qty, 'picking_id': self.picking_id.id, 'price_unit': self.price_unit, @@ -1085,8 +1086,7 @@ class StockMove(models.Model): def _action_done(self): self.filtered(lambda move: move.state == 'draft')._action_confirm() # MRP allows scrapping draft moves - - moves = self.filtered(lambda x: x.state not in ('done', 'cancel')) + moves = self.exists().filtered(lambda x: x.state not in ('done', 'cancel')) moves_todo = self.env['stock.move'] # Cancel moves where necessary ; we should do it before creating the extra moves because diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py index 7ad562792fb..de024209466 100644 --- a/addons/stock/models/stock_quant.py +++ b/addons/stock/models/stock_quant.py @@ -418,7 +418,7 @@ class QuantPackage(models.Model): return action def _get_contained_quants(self): - return self.env['stock.quant'].search([('package_id', 'child_of', self.ids)]) + return self.env['stock.quant'].search([('package_id', 'in', self.ids)]) def _get_all_products_quantities(self): '''This function computes the different product quantities for the given package diff --git a/addons/stock/tests/test_move2.py b/addons/stock/tests/test_move2.py index 9249fc5f0ac..71df404527e 100644 --- a/addons/stock/tests/test_move2.py +++ b/addons/stock/tests/test_move2.py @@ -185,6 +185,30 @@ class TestPickShip(TestStockCommon): # the client picking should not be assigned anymore, as we returned partially what we took self.assertEqual(picking_client.state, 'confirmed') + def test_mto_moves_return_extra(self): + picking_pick, picking_client = self.create_pick_ship() + stock_location = self.env['stock.location'].browse(self.stock_location) + self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0) + + picking_pick.action_assign() + picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0 + picking_pick.action_done() + self.assertEqual(picking_pick.state, 'done') + self.assertEqual(picking_client.state, 'assigned') + + # return more than we've done + stock_return_picking = self.env['stock.return.picking']\ + .with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\ + .create({}) + stock_return_picking.product_return_moves.quantity = 12.0 # Return 2 extra + stock_return_picking_action = stock_return_picking.create_returns() + return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) + + # Verify the extra move has been merged with the original move + self.assertAlmostEqual(return_pick.move_lines.product_uom_qty, 12.0) + self.assertAlmostEqual(return_pick.move_lines.quantity_done, 0.0) + self.assertAlmostEqual(return_pick.move_lines.reserved_availability, 10.0) + def test_mto_resupply_cancel_ship(self): """ This test simulates a pick pack ship with a resupply route set. Pick and pack are validated, ship is cancelled. This test diff --git a/addons/stock/tests/test_warehouse.py b/addons/stock/tests/test_warehouse.py index f0f260889d3..8c5d10cd3f0 100644 --- a/addons/stock/tests/test_warehouse.py +++ b/addons/stock/tests/test_warehouse.py @@ -62,7 +62,7 @@ class TestWarehouse(TestStockCommon): # Check inventory obj details (1 inventory with 1 line, because 1 product change) inventory = self.env['stock.inventory'].search([('id', 'not in', self.existing_inventories.ids)]) self.assertEqual(len(inventory), 1) - self.assertIn('INV: %s' % self.product_1.name, inventory.name) + self.assertIn('INV: %s' % self.product_1.display_name, inventory.name) self.assertEqual(len(inventory.line_ids), 1) self.assertEqual(inventory.line_ids.product_id, self.product_1) self.assertEqual(inventory.line_ids.product_qty, 50.0) diff --git a/addons/stock/views/stock_quant_views.xml b/addons/stock/views/stock_quant_views.xml index 08c44cae7bd..1a4a627449d 100644 --- a/addons/stock/views/stock_quant_views.xml +++ b/addons/stock/views/stock_quant_views.xml @@ -189,7 +189,7 @@ <field name="location_id"/> </group> </group> - <separator string="Content" /> + <separator string="Content"/> <field name="quant_ids"> <tree> <field name="product_id"/> diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index cae6b7b87ae..f13f8316977 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -384,7 +384,8 @@ class StockMove(models.Model): qty_done = 0.0 if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() - elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): + elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \ + float_is_zero(product_tot_qty_available + qty_done, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price @@ -507,9 +508,9 @@ class StockMove(models.Model): if not accounts_data.get('stock_journal', False): raise UserError(_('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts.')) if not acc_src: - raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name)) + raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.display_name)) if not acc_dest: - raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name)) + raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.display_name)) if not acc_valuation: raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.')) journal_id = accounts_data['stock_journal'].id @@ -533,7 +534,7 @@ class StockMove(models.Model): # check that all data is correct if self.company_id.currency_id.is_zero(debit_value): - raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.name,)) + raise UserError(_("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation.") % (self.product_id.display_name,)) credit_value = debit_value diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py index df22e8a382f..39ced676a1a 100644 --- a/addons/stock_account/tests/test_stockvaluation.py +++ b/addons/stock_account/tests/test_stockvaluation.py @@ -3138,7 +3138,7 @@ class TestStockValuation(TransactionCase): self.assertEqual(move1.remaining_value, 10) self.assertEqual(move1.remaining_qty, 1) self.assertAlmostEqual(self.product1.qty_available, 0.0) - self.assertAlmostEqual(self.product1.qty_at_date, 2.0) + self.assertAlmostEqual(self.product1.qty_at_date, 1.0) self.assertEqual(self.product1.stock_value, 10) self.assertTrue(len(move1.account_move_ids), 1) diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml index 21d41cdb067..237a3862444 100644 --- a/addons/stock_account/views/product_views.xml +++ b/addons/stock_account/views/product_views.xml @@ -73,8 +73,8 @@ <field name="cost_method" invisible="1"/> </field> <field name="standard_price" position="replace"> + <label for="standard_price"/> <div name="standard_price_div"> - <label for="standard_price"/> <field name="standard_price" attrs="{'readonly':[('valuation','=','real_time')]}" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}" class="float-left"/> diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index 0722183757c..07aa7be9a51 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -107,9 +107,9 @@ class LandedCost(models.Model): new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost line.move_id.write({ 'landed_cost_value': new_landed_cost_value, - 'value': line.move_id.value + cost_to_add, + 'value': line.move_id.value + line.additional_landed_cost, 'remaining_value': line.move_id.remaining_value + cost_to_add, - 'price_unit': (line.move_id.value + cost_to_add) / line.move_id.product_qty, + 'price_unit': (line.move_id.value + line.additional_landed_cost) / line.move_id.product_qty, }) # `remaining_qty` is negative if the move is out and delivered proudcts that were not # in stock. diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js index 2eacf6caa0d..d121ec929f0 100644 --- a/addons/web/static/src/js/chrome/action_manager_act_window.js +++ b/addons/web/static/src/js/chrome/action_manager_act_window.js @@ -340,7 +340,9 @@ ActionManager.include({ action.controllerID = controller.jsID; return self._executeAction(action, options).done(function () { if (lazyLoadedController) { - self.controllerStack.unshift(lazyLoadedController.jsID); + // controller should be placed just before the current one + var index = self.controllerStack.length - 1; + self.controllerStack.splice(index, 0, lazyLoadedController.jsID); self.controlPanel.update({ breadcrumbs: self._getBreadcrumbs(), }, {clear: false}); diff --git a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js index a8d95d57a79..dc65d2ea610 100644 --- a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js +++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js @@ -144,6 +144,13 @@ odoo.define('web.KeyboardNavigationMixin', function (require) { }); this._addAccessKeyOverlays(); } + // on mac, there are a number of keys that are only accessible though the usage of + // the ALT key (like the @ sign in most keyboards) + // for them we do not facilitate the access keys, so they will need to be activated classically + // though Control + Alt + key (case sensitive), see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey + if (this.BrowserDetection.isOsMac()) + return; + if (keyDownEvent.altKey && !keyDownEvent.ctrlKey && keyDownEvent.key.length === 1) { // we don't want to catch the Alt key down, only the characters A to Z and number keys var elementWithAccessKey = []; if (keyDownEvent.keyCode >= 65 && keyDownEvent.keyCode <= 90 || keyDownEvent.keyCode >= 97 && keyDownEvent.keyCode <= 122) { diff --git a/addons/web/static/src/js/tools/debug_manager.js b/addons/web/static/src/js/tools/debug_manager.js index 6f6022b021f..391d8613aac 100644 --- a/addons/web/static/src/js/tools/debug_manager.js +++ b/addons/web/static/src/js/tools/debug_manager.js @@ -504,7 +504,7 @@ DebugManager.include({ model: 'ir.default', method: 'set', args: [ - self._controller.fields_view.model, + self._action.res_model, fieldToSet, value, selfUser, diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js index 3b7477f4857..9233229a984 100644 --- a/addons/web/static/tests/chrome/action_manager_tests.js +++ b/addons/web/static/tests/chrome/action_manager_tests.js @@ -811,6 +811,43 @@ QUnit.module('ActionManager', { actionManager.destroy(); }); + QUnit.test('lazy load multi record view with previous action', function (assert) { + assert.expect(6); + + var actionManager = createActionManager({ + actions: this.actions, + archs: this.archs, + data: this.data, + debug: 1, + }); + actionManager.doAction(4); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 1, + "there should be one controller in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4', + "breadcrumbs should contain the display_name of the opened record"); + + actionManager.doAction(3, { + resID: 2, + viewType: 'form', + }); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 3, + "there should be three controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4PartnersSecond record', + "the breadcrumb elements should be correctly ordered"); + + // go back to List + $('.o_control_panel .breadcrumb a:last').click(); + + assert.strictEqual($('.o_control_panel .breadcrumb li').length, 2, + "there should be two controllers in the breadcrumbs"); + assert.strictEqual($('.o_control_panel .breadcrumb li').text(), 'Partners Action 4Partners', + "the breadcrumb elements should be correctly ordered"); + + actionManager.destroy(); + }); + QUnit.test('change the viewType of the current action', function (assert) { assert.expect(13); diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js index 44763bc42b9..6594b274be1 100644 --- a/addons/web/static/tests/helpers/test_utils.js +++ b/addons/web/static/tests/helpers/test_utils.js @@ -125,6 +125,7 @@ var createActionManager = function (params) { */ var createDebugManager = function (params) { params = params || {}; + var mockRPC = params.mockRPC; _.extend(params, { mockRPC: function (route, args) { if (args.method === 'check_access_rights') { @@ -133,6 +134,9 @@ var createDebugManager = function (params) { if (args.method === 'xmlid_to_res_id') { return $.when(true); } + if (mockRPC) { + return mockRPC.apply(this, arguments); + } return this._super.apply(this, arguments); }, session: { diff --git a/addons/web/static/tests/tools/debug_manager_tests.js b/addons/web/static/tests/tools/debug_manager_tests.js index 8e41481734f..a52ad9e9c65 100644 --- a/addons/web/static/tests/tools/debug_manager_tests.js +++ b/addons/web/static/tests/tools/debug_manager_tests.js @@ -84,5 +84,84 @@ QUnit.module('DebugManager', {}, function () { debugManager.destroy(); }); + + QUnit.test("Debug: Set defaults with right model", function (assert) { + assert.expect(2); + + /* Click on debug > set default, + * set some defaults, click on save + * model and some other data should be sent to server + */ + + // We'll need a full blown architecture with some data + var data = { + partner: { + fields: { + foo: {string: "Foo", type: "char", default: "My little Foo Value"}, + }, + records: [{ + id: 1, + foo: "yop", + }] + }, + 'ir.default': { // We just need this to be defined + fields: {}, + }, + } + + var form = testUtils.createView({ + View: FormView, + model: 'partner', + data: data, + arch: '<form string="Partners">' + + '<field name="foo" />' + + '</form>', + res_id: 1, + }); + + // Now the real tested component + var debugManager = createDebugManager({ + data: data, + mockRPC: function (route, args) { + if (route == "/web/dataset/call_kw/ir.default/set") { + assert.deepEqual(args.args, ["partner", "foo", "yop", true, true, false], + 'Model, field, value and booleans for current user/company should have been passed'); + return $.when(); + } + return this._super.apply(this, arguments); + } + }); + + debugManager.appendTo($('#qunit-fixture')); + + // Simulate update debug manager from web client + var action = { + views: [{ + fieldsView: { + view_id: 1, + model: 'partner', + type: 'form', + }, + type: "form", + }], + res_model: 'partner', + }; + + // We are all set + debugManager.update('action', action, form); + + // click on set_defaults dropdown + debugManager.$('a[data-action="set_defaults"]').click(); + var $modal = $('.modal-content'); + assert.strictEqual($modal.length, 1, 'One modal present'); + + $modal.find('select[id=formview_default_fields] option[value=foo]').prop('selected', true); + + // Save + $modal.find('.modal-footer button').eq(1).click(); + + form.destroy(); + debugManager.destroy(); + }); }); }); diff --git a/addons/website/data/website_demo.xml b/addons/website/data/website_demo.xml index cd7ab09f522..517399a758c 100644 --- a/addons/website/data/website_demo.xml +++ b/addons/website/data/website_demo.xml @@ -304,5 +304,293 @@ </t> </field> </record> + + <!-- BS Debug Page --> + <!-- Showcase all (most?) BS components and utilities --> + <record id="bs_debug_page" model="website.page"> + <field name="name">BS Debug</field> + <field name="url">/website/demo/bootstrap</field> + <field name="website_published">False</field> + <field name="type">qweb</field> + <field name="key">website.bs_debug_page_view</field> + <field name="arch" type="xml"> + <t name="Debug" t-name="website.bs_debug_page_view"> + <t t-call="website.layout"> + <t t-set="odoo_theme_colors" t-value="[['alpha', 'Alpha'], ['beta', 'Beta'], ['gamma', 'Gamma'], ['delta', 'Delta'], ['epsilon', 'Epsilon']]"/> + <t t-set="bs_basic_colors" t-value="[['primary', 'Primary'], ['default', 'Default']]"/> + <t t-set="bs_state_colors" t-value="[['success', 'Success'], ['info', 'Info'], ['warning', 'Warning'], ['danger', 'Danger']]"/> + <t t-set="bs_theme_colors" t-value="bs_basic_colors + bs_state_colors"/> + <t t-set="bs_gray_colors" t-value="[['white', 'White'], ['gray-lighter', 'Gray Lighter'], ['gray-light', 'Gray Light'], ['gray', 'Gray'], ['gray-dark', 'Gray Dark'], ['gray-darker', 'Gray Darker'], ['black', 'Black']]"/> + <div id="wrap" class="oe_structure"> + <section class="py-2"> + <div class="container"> + <h1>Components</h1> + <div class="row"> + <div class="col-md-9"> + <h2>Badge</h2> + <span class="badge mb4">This is a badge</span> + + <h2 class="mt8">Label</h2> + <t t-foreach="bs_theme_colors" t-as="color"> + <span t-attf-class="label mb4 label-#{color[0]}"><t t-esc="color[1]"/></span> + </t> + <h3 class="mt8 h5">Link</h3> + <t t-foreach="bs_theme_colors" t-as="color"> + <a href="#" t-attf-class="label mb4 label-#{color[0]}"><t t-esc="color[1]"/></a> + </t> + <h3 class="mt8 h5">Autosizing</h3> + <div class="mt0 h3"> + <t t-foreach="bs_theme_colors" t-as="color"> + <span t-attf-class="label mb4 label-#{color[0]}"><t t-esc="color[1]"/></span> + </t> + </div> + + <h2 class="mt16">Button</h2> + <t t-foreach="odoo_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <br/> + <t t-foreach="bs_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <h3 class="mt8 h6">Extra Small</h3> + <t t-foreach="odoo_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-xs btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <br/> + <t t-foreach="bs_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-xs btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <h3 class="mt8 h5">Small</h3> + <t t-foreach="odoo_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-sm btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <br/> + <t t-foreach="bs_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-sm btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <h3 class="mt8 h5">Large</h3> + <t t-foreach="odoo_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-lg btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + <br/> + <t t-foreach="bs_theme_colors" t-as="color"> + <button type="button" t-attf-class="btn mb4 btn-lg btn-#{color[0]}"><t t-esc="color[1]"/></button> + </t> + + <h2 class="mt16">Dropdown</h2> + <div class="dropdown"> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">Toggle <span class="caret"/></button> + <ul class="dropdown-menu"> + <li class="dropdown-header">Header</li> + <li><a href="#">Action</a></li> + <li><a href="#">Something else here</a></li> + <li class="divider"/> + <li><a href="#">Separated link</a></li> + </ul> + </div> + + <h2 class="mt16">Navbar</h2> + <nav class="navbar navbar-default"> + <div class="navbar-header"> + <a class="navbar-brand" href="#">Navbar</a> + <button class="navbar-toggle collapsed" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> + <span class="sr-only">Toggle navigation</span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + <span class="icon-bar"></span> + </button> + </div> + + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="nav navbar-nav"> + <li class="active"> + <a href="#">Home <span class="sr-only">(current)</span></a> + </li> + <li> + <a href="#">Link</a> + </li> + <li> + <a class="disabled" href="#">Disabled</a> + </li> + </ul> + <form class="navbar-form navbar-right"> + <input class="form-control" type="search" placeholder="Search" aria-label="Search"/> + <button class="btn btn-success" type="submit">Search</button> + </form> + </div> + </nav> + + <h2 class="mt16">Form</h2> + <form> + <div class="form-group"> + <label for="exampleInputEmail1">Email address</label> + <input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email"/> + <small id="emailHelp" class="help-block text-muted">We'll never share your email with anyone else.</small> + </div> + </form> + + <h2 class="mt16">Pagination</h2> + <nav> + <ul class="pagination"> + <li class="disabled"> + <a href="#" tabindex="-1">Previous</a> + </li> + <li> + <a href="#">1</a> + </li> + <li class="active"> + <a href="#">2 <span class="sr-only">(current)</span></a> + </li> + <li> + <a href="#">3</a> + </li> + <li> + <a href="#">Next</a> + </li> + </ul> + </nav> + </div> + <div class="col-md-3"> + <h2>Alert</h2> + <t t-foreach="bs_state_colors" t-as="color"> + <div t-attf-class="alert alert-#{color[0]}"> + This is a "<t t-esc="color[1]"/>" alert with a <a href="#" class="alert-link">link</a>. + </div> + </t> + + <h2 class="mt16">Breadcrumb</h2> + <nav aria-label="breadcrumb"> + <ol class="breadcrumb"> + <li><a href="#">Home</a></li> + <li><a href="#">Library</a></li> + <li class="active" aria-current="page">Data</li> + </ol> + </nav> + + <h2 class="mt16">Card</h2> + <div class="panel panel-default"> + <div class="panel-heading"> + Card Header + </div> + <div class="panel-body"> + Card Body + </div> + <ul class="list-group"> + <li class="list-group-item">Item 1</li> + <li class="list-group-item">Item 2</li> + </ul> + <div class="panel-footer"> + Card Footer + </div> + </div> + </div> + </div> + </div> + </section> + <section class="py-2"> + <div class="container"> + <h1>Utilities & Typography</h1> + <div class="row"> + <div class="col-md-9"> + <div> + <t t-foreach="odoo_theme_colors" t-as="color"> + <div t-attf-class="bg-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + <div class="mt8"> + <t t-foreach="bs_theme_colors" t-as="color"> + <div t-attf-class="bg-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + <div class="mt8"> + <t t-foreach="bs_gray_colors" t-as="color"> + <div t-attf-class="bg-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + <div class="mt16"> + <t t-foreach="odoo_theme_colors" t-as="color"> + <div t-attf-class="text-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + <div class="mt8"> + <t t-foreach="bs_theme_colors" t-as="color"> + <div t-attf-class="text-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + <div class="mt8"> + <t t-foreach="bs_gray_colors" t-as="color"> + <div t-attf-class="text-#{color[0]}" style="display: inline-block; padding: 4px 12px;"> + <t t-esc="color[1]"/> + </div> + </t> + </div> + </div> + <div class="col-md-3"> + <h1>Headings 1</h1> + <h2>Headings 2</h2> + <h3>Headings 3</h3> + <h4>Headings 4</h4> + <h5>Headings 5</h5> + <h6>Headings 6</h6> + <p>Paragraph with <strong>bold</strong>, <span class="text-muted">muted</span> and <em>italic</em> texts</p> + <p><a href="#">Link</a></p> + <p><button type="button" class="btn btn-link">Link button</button></p> + </div> + </div> + </div> + </section> + </div> + </t> + </t> + </field> + </record> + + <!-- Snippet Debug Page --> + <!-- Showcase all snippets --> + <record id="snippets_debug_page" model="website.page"> + <field name="name">Snippet Debug</field> + <field name="url">/website/demo/snippets</field> + <field name="website_published">False</field> + <field name="type">qweb</field> + <field name="key">website.snippets_debug_page_view</field> + <field name="arch" type="xml"> + <t name="Debug" t-name="website.snippets_debug_page_view"> + <t t-call="website.layout"> + <style> + #snippets_menu, #o_scroll > .o_panel > .o_panel_header { + display: none !important; + } + [data-oe-type="snippet"]:not([data-module-id])::before { + content: attr(name); + display: block; + padding: 16px; + background-color: lightgray; + color: black; + font-size: 24px; + } + [data-oe-type="snippet"]:not([data-module-id])::after { + content: ""; + display: table; + clear: both; + } + </style> + <div id="wrap" class="oe_structure"> + <t t-call="website.snippets"/> + </div> + </t> + </t> + </field> + </record> </data> </odoo> diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index a7b65d465b1..99678b34e93 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -699,6 +699,7 @@ registry.gallery = Animation.extend({ * @param {Event} ev */ _onClickImg: function (ev) { + var self = this; var $cur = $(ev.currentTarget); var urls = []; @@ -743,7 +744,12 @@ registry.gallery = Animation.extend({ $modal.find('.modal-content, .modal-body.o_slideshow').css('height', '100%'); $modal.appendTo(document.body); - this.carousel = new registry.gallery_slider($modal.find('.carousel').carousel()); + $modal.one('shown.bs.modal', function () { + self.trigger_up('animation_start_demand', { + editableMode: false, + $target: $modal.find('.modal-body.o_slideshow'), + }); + }); }, }); diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index a66de337ef7..a397cda6dce 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -788,28 +788,26 @@ table.table_desc tr td { border-width: 0; position: absolute; bottom: 0; + width: 100%; + margin-left: 0; + left: 0%; > * { list-style-image: none; display: inline-block; width: 40px; height: 40px; - margin: 0 0px 5px 5px; + margin: 2.5px 2.5px 2.5px 2.5px; padding: 0; border: 1px solid #aaa; text-indent: initial; background-size: cover; background-color: #fff; border-radius: 0; + vertical-align: bottom; flex: 0 0 40px; &:not(.active) { opacity: 0.8; - -webkit-filter: grayscale(100%); - filter: grayscale(100%); - filter: gray; - filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale"); - } - &:first-child { - margin-left: 0; + filter: grayscale(1); } } } diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 7fb50947993..960a241d948 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -53,6 +53,7 @@ class Forum(models.Model): 'build your professional profile and become a better marketer together.')) welcome_message = fields.Html( 'Welcome Message', + translate=True, default = """<section class="bg-info" style="height: 168px;"><div class="container"> <div class="row"> <div class="col-lg-12"> diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 2624a2f50ca..1d83cdb28ff 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -17,8 +17,9 @@ class SaleOrder(models.Model): _inherit = "sale.order" website_order_line = fields.One2many( - 'sale.order.line', 'order_id', - string='Order Lines displayed on Website', readonly=True, + 'sale.order.line', + compute='_compute_website_order_line', + string='Order Lines displayed on Website', help='Order Lines to be displayed on the website. They should not be used for computation purpose.', ) cart_quantity = fields.Integer(compute='_compute_cart_info', string='Cart Quantity') @@ -29,6 +30,10 @@ class SaleOrder(models.Model): help='Website through which this order was placed.', store=True, readonly=True) + @api.one + def _compute_website_order_line(self): + self.website_order_line = self.order_line + @api.multi @api.depends('website_order_line.product_uom_qty', 'website_order_line.product_id') def _compute_cart_info(self): diff --git a/addons/website_sale/models/website.py b/addons/website_sale/models/website.py index 570ef96c45f..4f8f7b15519 100644 --- a/addons/website_sale/models/website.py +++ b/addons/website_sale/models/website.py @@ -177,7 +177,7 @@ class Website(models.Model): 'partner_id': partner.id, 'pricelist_id': pricelist.id, 'payment_term_id': self.sale_get_payment_term(partner), - 'team_id': self.salesteam_id.id, + 'team_id': self.salesteam_id.id or partner.parent_id.team_id.id or partner.team_id.id, 'partner_invoice_id': partner.id, 'partner_shipping_id': addr['delivery'], 'user_id': salesperson_id or self.salesperson_id.id or default_user_id, diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py index a44c18fa07d..4cf21e3b587 100644 --- a/addons/website_sale_delivery/models/sale_order.py +++ b/addons/website_sale_delivery/models/sale_order.py @@ -17,11 +17,11 @@ class SaleOrder(models.Model): has_delivery = fields.Boolean( compute='_compute_has_delivery', string='Has delivery', help="Has an order line set for delivery", store=True) - website_order_line = fields.One2many( - 'sale.order.line', 'order_id', - string='Order Lines displayed on Website', readonly=True, - domain=[('is_delivery', '=', False)], - help='Order Lines to be displayed on the website. They should not be used for computation purpose.') + + @api.one + def _compute_website_order_line(self): + super(SaleOrder, self)._compute_website_order_line() + self.website_order_line = self.website_order_line.filtered(lambda l: not l.is_delivery) @api.depends('order_line.price_unit', 'order_line.tax_id', 'order_line.discount', 'order_line.product_uom_qty') def _compute_amount_delivery(self): diff --git a/doc/cla/corporate/dreambits.md b/doc/cla/corporate/dreambits.md new file mode 100644 index 00000000000..d1dd942436c --- /dev/null +++ b/doc/cla/corporate/dreambits.md @@ -0,0 +1,16 @@ +India, 2018-09-18 + +Dreambits Technologies Pvt. Ltd. agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Karan Shah admin@dreambits.in https://github.com/karan-dreambits + +List of contributors: + +Karan Shah admin@dreambits.in https://github.com/karan-dreambits +Devang Pipaliya devang@dreambits.in https://github.com/devang-dreambits diff --git a/doc/cla/corporate/inspur.md b/doc/cla/corporate/inspur.md new file mode 100644 index 00000000000..88999bdf10b --- /dev/null +++ b/doc/cla/corporate/inspur.md @@ -0,0 +1,17 @@ +China, 2018-09-28 + +Inspur Genersoft Co., Ltd agrees to the terms of the Odoo Corporate Contributor +License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +David Yu, yudw@inspur.com + +List of contributors: + +Brain Wang wangbuke@inspur.com +David Yu yudw@inspur.com +Alex Aisin-Gioro wubai@inspur.com diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index 18c1efb4656..83df644f150 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -56,7 +56,7 @@ Running the server Maximum allowed virtual memory per worker. If the limit is exceeded, the worker is killed and recycled at the end of the current request. - Defaults to 2048MB. + Defaults to 2048MiB. .. option:: --limit-memory-hard <limit> @@ -64,7 +64,7 @@ Running the server immediately killed without waiting for the end of the current request processing. - Defaults to 2560MB. + Defaults to 2560MiB. .. option:: --limit-time-cpu <limit> diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml index 2eaa0d0abf3..3e0db583e3e 100644 --- a/odoo/addons/base/data/res_country_data.xml +++ b/odoo/addons/base/data/res_country_data.xml @@ -653,6 +653,7 @@ <field file="base/static/img/country_flags/gt.png" name="image" type="base64" /> <field name="currency_id" ref="GTQ" /> <field eval="502" name="phone_code" /> + <field name="vat_label">NIT</field> </record> <record id="gu" model="res.country"> <field name="name">Guam</field> @@ -1127,6 +1128,7 @@ <field name="currency_id" ref="MXN" /> <field eval="52" name="phone_code" /> <field name="vat_label">RFC</field> + <field eval="'%(street)s %(street2)s\n%(zip)s %(city)s, %(state_code)s\n%(country_name)s'" name="address_format" /> </record> <record id="my" model="res.country"> <field name="name">Malaysia</field> @@ -1732,6 +1734,7 @@ <field file="base/static/img/country_flags/vn.png" name="image" type="base64" /> <field name="currency_id" ref="VND" /> <field eval="84" name="phone_code" /> + <field eval="'%(street)s\n%(street2)s\n%(city)s\n%(name)s %(zip)s %(country_name)s'" name="address_format" /> </record> <record id="vu" model="res.country"> <field name="name">Vanuatu</field> diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py index 8d820da5d9f..18951749f61 100644 --- a/odoo/addons/base/models/ir_actions.py +++ b/odoo/addons/base/models/ir_actions.py @@ -568,6 +568,9 @@ class IrActionsServer(models.Model): active_id = self._context.get('active_id') if not active_id and self._context.get('onchange_self'): active_id = self._context['onchange_self']._origin.id + if not active_id: # onchange on new record + func = getattr(self, 'run_action_%s' % action.state) + res = func(action, eval_context=eval_context) active_ids = self._context.get('active_ids', [active_id] if active_id else []) for active_id in active_ids: # run context dedicated to a particular active_id diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py index 23bd0af4df1..84b29147f40 100644 --- a/odoo/addons/base/models/ir_http.py +++ b/odoo/addons/base/models/ir_http.py @@ -298,16 +298,18 @@ class IrHttp(models.AbstractModel): obj = cls._xmlid_to_obj(env, xmlid) if access_mode: obj = cls.check_access_mode(env, id, access_mode, model, access_token=access_token, related_id=related_id) - elif id and model == 'ir.attachment' and access_token: - obj = env[model].sudo().browse(int(id)) - if not consteq(obj.access_token or '', access_token): - return (403, [], None) elif id and model in env.registry: obj = env[model].browse(int(id)) # obj exists if not obj or not obj.exists() or field not in obj: return (404, [], None) + # access token grant access + if model == 'ir.attachment' and access_token: + obj = obj.sudo() + if not consteq(obj.access_token or '', access_token): + return (403, [], None) + # check read access try: last_update = obj['__last_update'] diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index b1aee2245f0..247fb367334 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -1038,7 +1038,7 @@ class GroupsView(models.Model): def linearize(app, gs): # 'User Type' is an exception if app.xml_id == 'base.module_category_user_type': - return (app, 'selection', gs) + return (app, 'selection', gs.sorted('id')) # determine sequence order: a group appears after its implied groups order = {g: len(g.trans_implied_ids & gs) for g in gs} # check whether order is total, i.e., sequence orders are distinct diff --git a/odoo/addons/base/tests/test_ir_http.py b/odoo/addons/base/tests/test_ir_http.py index 313a86f308d..2cdcaf74dcf 100644 --- a/odoo/addons/base/tests/test_ir_http.py +++ b/odoo/addons/base/tests/test_ir_http.py @@ -80,3 +80,53 @@ class test_ir_http_mimetype(common.TransactionCase): mimetype = dict(headers).get('Content-Type') self.assertEqual(mimetype, 'image/gif') + def test_ir_http_attachment_access(self): + """ Test attachment access with and without access token """ + public_user = self.env.ref('base.public_user') + attachment = self.env['ir.attachment'].create({ + 'datas': GIF, + 'name': 'Test valid access token with image', + 'datas_fname': 'image.gif' + }) + + defaults = { + 'id': attachment.id, + 'default_mimetype': 'image/gif', + 'env': public_user.sudo(public_user.id).env, + } + + def test_access(**kwargs): + status, _, _ = self.env['ir.http'].binary_content( + **defaults, **kwargs + ) + return status + + status = test_access() + self.assertEqual(status, 403, "no access") + + status = test_access(access_token='Secret') + self.assertEqual(status, 403, + "no access if access token for attachment without access token") + + attachment.access_token = 'Secret' + status = test_access(access_token='Secret') + self.assertEqual(status, 200, "access for correct access token") + + status = test_access(access_token='Wrong') + self.assertEqual(status, 403, "no access for wrong access token") + + attachment.public = True + status = test_access() + self.assertEqual(status, 200, "access for attachment with access") + + status = test_access(access_token='Wrong') + self.assertEqual(status, 403, + "no access for wrong access token for attachment with access") + + attachment.unlink() + status = test_access() + self.assertEqual(status, 404, "no access for deleted attachment") + + status = test_access(access_token='Secret') + self.assertEqual(status, 404, + "no access with access token for deleted attachment") diff --git a/odoo/addons/test_new_api/demo_data.xml b/odoo/addons/test_new_api/demo_data.xml index 9edab779fcc..ff048467183 100644 --- a/odoo/addons/test_new_api/demo_data.xml +++ b/odoo/addons/test_new_api/demo_data.xml @@ -16,6 +16,7 @@ <record id="message_0_0" model="test_new_api.message"> <field name="discussion" ref="discussion_0"/> <field name="body">Hey dude!</field> + <field name="label">hello</field> </record> <record id="message_0_1" model="test_new_api.message"> <field name="discussion" ref="discussion_0"/> @@ -27,6 +28,11 @@ <field name="body">This is a much longer message</field> </record> + <record id="emailmessage_0_0" model="test_new_api.emailmessage"> + <field name="message" ref="message_0_0"/> + <field name="email_to">first@example.localhost</field> + </record> + <record id="bool_1" model="domain.bool"> <field name="bool_true" eval="True"/> </record> diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index a932197afc9..bc0a3e45876 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -74,7 +74,7 @@ class Discussion(models.Model): categories = fields.Many2many('test_new_api.category', 'test_new_api_discussion_category', 'discussion', 'category') participants = fields.Many2many('res.users', context={'active_test': False}) - messages = fields.One2many('test_new_api.message', 'discussion') + messages = fields.One2many('test_new_api.message', 'discussion', copy=True) message_concat = fields.Text(string='Message concatenate') important_messages = fields.One2many('test_new_api.message', 'discussion', domain=[('important', '=', True)]) @@ -128,6 +128,7 @@ class Message(models.Model): 'res.partner', compute='_compute_author_partner', search='_search_author_partner') important = fields.Boolean() + label = fields.Char(translate=True) @api.one @api.constrains('author', 'discussion') diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py index 79dab154941..8284f4652c6 100644 --- a/odoo/addons/test_new_api/tests/test_new_fields.py +++ b/odoo/addons/test_new_api/tests/test_new_fields.py @@ -964,6 +964,40 @@ class TestFields(common.TransactionCase): self.assertEqual(len(discussion.important_messages), 2) self.assertEqual(len(discussion.very_important_messages), 2) + def test_80_copy(self): + Translations = self.env['ir.translation'] + discussion = self.env.ref('test_new_api.discussion_0') + message = self.env.ref('test_new_api.message_0_0') + message1 = self.env.ref('test_new_api.message_0_1') + + email = self.env.ref('test_new_api.emailmessage_0_0') + self.assertEqual(email.message, message) + + french = self.env['res.lang']._lang_get('fr_FR') + french.active = True + + def count(msg): + # return the number of translations of msg.label + return Translations.search_count([ + ('name', '=', 'test_new_api.message,label'), + ('res_id', '=', msg.id), + ]) + + # set a translation for message.label + email.with_context(lang='fr_FR').label = "bonjour" + self.assertEqual(count(message), 1) + self.assertEqual(count(message1), 0) + + # setting the parent record should not copy its translations + email.copy({'message': message1.id}) + self.assertEqual(count(message), 1) + self.assertEqual(count(message1), 0) + + # setting a one2many should not copy translations on the lines + discussion.copy({'messages': [(6, 0, message1.ids)]}) + self.assertEqual(count(message), 1) + self.assertEqual(count(message1), 0) + class TestX2many(common.TransactionCase): def test_search_many2many(self): diff --git a/odoo/addons/test_translation_import/i18n/dot.csv b/odoo/addons/test_translation_import/i18n/dot.csv new file mode 100644 index 00000000000..5c4fc6ae34d --- /dev/null +++ b/odoo/addons/test_translation_import/i18n/dot.csv @@ -0,0 +1,2 @@ +module,type,name,res_id,src,value,comments +test_translation_import,code,addons/test_translation_import/models.py,20,Accounting,samva, diff --git a/odoo/addons/test_translation_import/i18n/tlh.po b/odoo/addons/test_translation_import/i18n/tlh.po new file mode 100644 index 00000000000..758a27ffa08 --- /dev/null +++ b/odoo/addons/test_translation_import/i18n/tlh.po @@ -0,0 +1,20 @@ +# This is a test PO file, not a complete one. It is manually maintained +# to test the import translation behavior of Odoo. +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server saas~11.4\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-09-18 13:20+0000\n" +"PO-Revision-Date: 2018-09-18 13:20+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: test_translation_import +#: code:addons/test_translation_import/models.py:18 +#, python-format +msgid "Klingon" +msgstr "tlhIngan" diff --git a/odoo/addons/test_translation_import/models.py b/odoo/addons/test_translation_import/models.py index 58d17c8c162..9e803190eea 100644 --- a/odoo/addons/test_translation_import/models.py +++ b/odoo/addons/test_translation_import/models.py @@ -15,3 +15,7 @@ class m(models.TransientModel): # With the name label above, this source string should be generated twice. _('1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB') + + _('Klingon') + + _('Accounting') diff --git a/odoo/addons/test_translation_import/tests/test_term_count.py b/odoo/addons/test_translation_import/tests/test_term_count.py index c89bf44ed25..7e224bfdd7b 100644 --- a/odoo/addons/test_translation_import/tests/test_term_count.py +++ b/odoo/addons/test_translation_import/tests/test_term_count.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- from contextlib import closing +import base64 import io import odoo from odoo.tests import common +from odoo.tools.misc import file_open, mute_logger +from odoo.tools.translate import _ class TestTermCount(common.TransactionCase): @@ -79,3 +82,49 @@ class TestTermCount(common.TransactionCase): menu.with_context(lang='fr_FR').name = "Nouveau nom" update_translations() self.assertEqual(menu.with_context(lang='fr_FR').name, "Nouveau nom", 'The translation of "New Name" should be "Nouveau nom"') + + def test_import_from_po_file(self): + """Test the import from a single po file works""" + with file_open('test_translation_import/i18n/tlh.po', 'rb') as f: + po_file = base64.encodestring(f.read()) + + import_tlh = self.env["base.language.import"].create({ + 'name': 'Klingon', + 'code': 'tlh', + 'data': po_file, + 'filename': 'tlh.po', + }) + with mute_logger('odoo.addons.base.models.res_lang'): + import_tlh.import_lang() + + lang_count = self.env['res.lang'].search_count([('code', '=', 'tlh')]) + self.assertEqual(lang_count, 1, "The imported language was not creates") + + trans_count = self.env['ir.translation'].search_count([('lang', '=', 'tlh')]) + self.assertEqual(trans_count, 1, "The imported translations were not created") + + self.env.context = dict(self.env.context, lang="tlh") + self.assertEqual(_("Klingon"), "tlhIngan", "The code translation was not applied") + + def test_import_from_csv_file(self): + """Test the import from a single CSV file works""" + with file_open('test_translation_import/i18n/dot.csv', 'rb') as f: + po_file = base64.encodestring(f.read()) + + import_tlh = self.env["base.language.import"].create({ + 'name': 'Dothraki', + 'code': 'dot', + 'data': po_file, + 'filename': 'dot.csv', + }) + with mute_logger('odoo.addons.base.models.res_lang'): + import_tlh.import_lang() + + lang_count = self.env['res.lang'].search_count([('code', '=', 'dot')]) + self.assertEqual(lang_count, 1, "The imported language was not creates") + + trans_count = self.env['ir.translation'].search_count([('lang', '=', 'dot')]) + self.assertEqual(trans_count, 1, "The imported translations were not created") + + self.env.context = dict(self.env.context, lang="dot") + self.assertEqual(_("Accounting"), "samva", "The code translation was not applied") diff --git a/odoo/fields.py b/odoo/fields.py index deee1492315..8d2f939fd88 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1754,7 +1754,10 @@ class Binary(Field): return None if isinstance(value, bytes): return psycopg2.Binary(value) - return psycopg2.Binary(pycompat.text_type(value).encode('ascii')) + try: + return psycopg2.Binary(pycompat.text_type(value).encode('ascii')) + except UnicodeEncodeError: + raise UserError(_("ASCII characters are required for %s in %s") % (value, self.name)) def convert_to_cache(self, value, record, validate=True): if isinstance(value, _BINARY): diff --git a/odoo/models.py b/odoo/models.py index 48e17c487e3..c0e5aa8033a 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -4133,7 +4133,13 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): return [default] @api.multi - def copy_translations(old, new): + def copy_translations(old, new, excluded=()): + """ Recursively copy the translations from original to new record + + :param old: the original record + :param new: the new record (copy of the original one) + :param excluded: a container of user-provided field names + """ # avoid recursion through already copied records in case of circular relationship if '__copy_translations_seen' not in old._context: old = old.with_context(__copy_translations_seen=defaultdict(set)) @@ -4159,13 +4165,20 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): if not field.copy: continue - if field.type == 'one2many': + if field.inherited and field.related[0] in excluded: + # inherited fields that come from a user-provided parent record + # must not copy translations, as the parent record is not a copy + # of the old parent record + continue + + if field.type == 'one2many' and field.name not in excluded: # we must recursively copy the translations for o2m; here we # rely on the order of the ids to match the translations as # foreseen in copy_data() old_lines = old[name].sorted(key='id') new_lines = new[name].sorted(key='id') for (old_line, new_line) in pycompat.izip(old_lines, new_lines): + # don't pass excluded as it is not about those lines old_line.copy_translations(new_line) elif field.translate: @@ -4207,7 +4220,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): vals = self.copy_data(default)[0] # To avoid to create a translation in the lang of the user, copy_translation will do it new = self.with_context(lang=None).create(vals) - self.with_context(from_copy_translation=True).copy_translations(new) + self.with_context(from_copy_translation=True).copy_translations(new, excluded=default or ()) return new @api.multi diff --git a/odoo/tools/config.py b/odoo/tools/config.py index 778fcc1def1..0f4c0d0761d 100644 --- a/odoo/tools/config.py +++ b/odoo/tools/config.py @@ -286,10 +286,12 @@ class configmanager(object): help="Specify the number of workers, 0 disable prefork mode.", type="int") group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024, - help="Maximum allowed virtual memory per worker, when reached the worker be reset after the current request (default 671088640 aka 640MB).", + help="Maximum allowed virtual memory per worker, when reached the worker be " + "reset after the current request (default 2048MiB).", type="int") group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024, - help="Maximum allowed virtual memory per worker, when reached, any memory allocation will fail (default 805306368 aka 768MB).", + help="Maximum allowed virtual memory per worker, when reached, any memory " + "allocation will fail (default 2560MiB).", type="int") group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60, help="Maximum allowed CPU time per request (default 60).", diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index 4ff7f559cf6..cb672b9d3b4 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -1038,6 +1038,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, # (Because the POT comments are correct on Launchpad but not the # PO comments due to a Launchpad limitation. See LP bug 933496.) pot_reader = [] + use_pot_reference = False # now, the serious things: we read the language file fileobj.seek(0) @@ -1066,6 +1067,7 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, pot_handle = file_open(os.path.join( addons, module, i18n_dir, module + '.pot'), mode='rb') pot_reader = PoFile(pot_handle) + use_pot_reference = True except: pass @@ -1100,17 +1102,18 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, dic['lang'] = lang dic.update(pycompat.izip(fields, row)) - # discard the target from the POT targets. - src = dic['src'] - target_key = (dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0) - target = pot_targets.get(src) - if not target or target_key not in target.targets: - _logger.info("Translation '%s' (%s, %s, %s) not found in reference pot, skipping", - src[:60], dic['type'], dic['name'], dic['res_id']) - return + if use_pot_reference: + # discard the target from the POT targets. + src = dic['src'] + target_key = (dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0) + target = pot_targets.get(src) + if not target or target_key not in target.targets: + _logger.info("Translation '%s' (%s, %s, %s) not found in reference pot, skipping", + src[:60], dic['type'], dic['name'], dic['res_id']) + return - target.value = dic['value'] - target.targets.discard(target_key) + target.value = dic['value'] + target.targets.discard(target_key) # This would skip terms that fail to specify a res_id res_id = dic['res_id'] @@ -1138,15 +1141,16 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True, for row in reader: process_row(row) - # Then process the entries implied by the POT file (which is more - # correct w.r.t. the targets) if some of them remain. - pot_rows = [] - for src, target in pot_targets.items(): - if target.value: - for type, name, res_id in target.targets: - pot_rows.append((type, name, res_id, src, target.value, target.comments)) - for row in pot_rows: - process_row(row) + if use_pot_reference: + # Then process the entries implied by the POT file (which is more + # correct w.r.t. the targets) if some of them remain. + pot_rows = [] + for src, target in pot_targets.items(): + if target.value: + for type, name, res_id in target.targets: + pot_rows.append((type, name, res_id, src, target.value, target.comments)) + for row in pot_rows: + process_row(row) irt_cursor.finish() Translation.clear_caches() |
