diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-01-23 14:33:53 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-01-23 14:33:53 +0100 |
| commit | 6c4389df41fb29f9707b205a8dcca13b3bf8a322 (patch) | |
| tree | b06358fdcff3268574c9ccda8cb36d9f94eb09dd /addons | |
| parent | 255de893300d73670a15a102e85162ae48084d7c (diff) | |
| parent | 6804085b8ceb1411a23db65477f84a959f8c5bf7 (diff) | |
[MERGE] forward port branch 10.0 up to 6804085b8ce
Diffstat (limited to 'addons')
94 files changed, 609 insertions, 248 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 6cee000e51c..d8828794475 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -570,7 +570,7 @@ class AccountInvoice(models.Model): # lots of duplicate calls to action_invoice_paid, so we remove those already paid to_pay_invoices = self.filtered(lambda inv: inv.state != 'paid') if to_pay_invoices.filtered(lambda inv: inv.state != 'open'): - raise UserError(_('Invoice must be validated in order to set it to register payemnt.')) + raise UserError(_('Invoice must be validated in order to set it to register payment.')) if to_pay_invoices.filtered(lambda inv: not inv.reconciled): raise UserError(_('You cannot pay an invoice which is partially paid. You need to reconcile payment entries first.')) return to_pay_invoices.write({'state': 'paid'}) @@ -578,13 +578,13 @@ class AccountInvoice(models.Model): @api.multi def action_invoice_re_open(self): if self.filtered(lambda inv: inv.state != 'paid'): - raise UserError(_('Invoice must be paid in order to set it to register payemnt.')) + raise UserError(_('Invoice must be paid in order to set it to register payment.')) return self.write({'state': 'open'}) @api.multi def action_invoice_cancel(self): if self.filtered(lambda inv: inv.state not in ['proforma2', 'draft', 'open']): - raise UserError(_("Invoice must be in draft,Pro-forma or open state in order to be cancelled.")) + raise UserError(_("Invoice must be in draft, Pro-forma or open state in order to be cancelled.")) return self.action_cancel() @api.multi @@ -1151,7 +1151,8 @@ class AccountInvoiceLine(models.Model): @api.one @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', - 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id') + 'product_id', 'invoice_id.partner_id', 'invoice_id.currency_id', 'invoice_id.company_id', + 'invoice_id.date_invoice') def _compute_price(self): currency = self.invoice_id and self.invoice_id.currency_id or None price = self.price_unit * (1 - (self.discount or 0.0) / 100.0) @@ -1160,7 +1161,7 @@ class AccountInvoiceLine(models.Model): taxes = self.invoice_line_tax_ids.compute_all(price, currency, self.quantity, product=self.product_id, partner=self.invoice_id.partner_id) self.price_subtotal = price_subtotal_signed = taxes['total_excluded'] if taxes else self.quantity * price if self.invoice_id.currency_id and self.invoice_id.company_id and self.invoice_id.currency_id != self.invoice_id.company_id.currency_id: - price_subtotal_signed = self.invoice_id.currency_id.compute(price_subtotal_signed, self.invoice_id.company_id.currency_id) + price_subtotal_signed = self.invoice_id.currency_id.with_context(date=self.invoice_id.date_invoice).compute(price_subtotal_signed, self.invoice_id.company_id.currency_id) sign = self.invoice_id.type in ['in_refund', 'out_refund'] and -1 or 1 self.price_subtotal_signed = price_subtotal_signed * sign diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 683dff63037..1af8d118375 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -394,7 +394,7 @@ class AccountMoveLine(models.Model): statement_id = fields.Many2one('account.bank.statement', related='statement_line_id.statement_id', string='Statement', store=True, help="The bank statement used for bank reconciliation", index=True, copy=False) reconciled = fields.Boolean(compute='_amount_residual', store=True) - full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching Number") + full_reconcile_id = fields.Many2one('account.full.reconcile', string="Matching Number", copy=False) matched_debit_ids = fields.One2many('account.partial.reconcile', 'credit_move_id', String='Matched Debits', help='Debit journal items that are matched with this journal item.') matched_credit_ids = fields.One2many('account.partial.reconcile', 'debit_move_id', String='Matched Credits', @@ -1020,7 +1020,7 @@ class AccountMoveLine(models.Model): rec_move_ids = self.env['account.partial.reconcile'] for account_move_line in self: for invoice in account_move_line.payment_id.invoice_ids: - if account_move_line in invoice.payment_move_line_ids: + if invoice.id == self.env.context.get('invoice_id') and account_move_line in invoice.payment_move_line_ids: account_move_line.payment_id.write({'invoice_ids': [(3, invoice.id, None)]}) rec_move_ids += account_move_line.matched_debit_ids rec_move_ids += account_move_line.matched_credit_ids @@ -1388,7 +1388,7 @@ class AccountPartialReconcile(models.Model): company_currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, help='Utility field to express amount currency') company_id = fields.Many2one('res.company', related='debit_move_id.company_id', store=True, string='Currency') - full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile") + full_reconcile_id = fields.Many2one('account.full.reconcile', string="Full Reconcile", copy=False) def create_exchange_rate_entry(self, aml_to_fix, amount_diff, diff_in_currency, currency, move_date): """ Automatically create a journal entry to book the exchange rate difference. diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index bfeb6f727f6..34c7e28cac1 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -400,9 +400,12 @@ class account_payment(models.Model): # the writeoff debit and credit must be computed from the invoice residual in company currency # minus the payment amount in company currency, and not from the payment difference in the payment currency # to avoid loss of precision during the currency rate computations. See revision 20935462a0cabeb45480ce70114ff2f4e91eaf79 for a detailed example. - total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids) + total_residual_company_signed = self._compute_total_invoices_amount() total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id) - amount_wo = total_residual_company_signed - total_payment_company_signed + if self.invoice_ids[0].type in ['in_invoice', 'out_refund']: + amount_wo = total_payment_company_signed - total_residual_company_signed + else: + amount_wo = total_residual_company_signed - total_payment_company_signed debit_wo = amount_wo > 0 and amount_wo or 0.0 credit_wo = amount_wo < 0 and -amount_wo or 0.0 writeoff_line['name'] = _('Counterpart') diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 24e2b1579f4..65f33c4d748 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -394,22 +394,11 @@ class AccountChartTemplate(models.Model): acc_template_ref[account_template.id] = new_account return acc_template_ref - @api.multi - def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company): - """ This method for generating accounts from templates. - - :param tax_template_ref: Taxes templates reference for write taxes_id in account_account. - :param acc_template_ref: dictionary with the mappping between the account templates and the real accounts. - :param company_id: company_id selected from wizard.multi.charts.accounts. - :returns: return new_account_reconcile_model for reference purpose. - :rtype: dict + def _prepare_reconcile_model_vals(self, company, account_reconcile_model, acc_template_ref, tax_template_ref): + """ This method generates a dictionnary of all the values for the account.reconcile.model that will be created. """ self.ensure_one() - account_reconcile_models = self.env['account.reconcile.model.template'].search([ - ('account_id.chart_template_id', '=', self.id) - ]) - for account_reconcile_model in account_reconcile_models: - vals = { + return { 'name': account_reconcile_model.name, 'sequence': account_reconcile_model.sequence, 'has_second_line': account_reconcile_model.has_second_line, @@ -425,6 +414,23 @@ class AccountChartTemplate(models.Model): 'second_amount': account_reconcile_model.second_amount, 'second_tax_id': account_reconcile_model.second_tax_id and tax_template_ref[account_reconcile_model.second_tax_id.id] or False, } + + @api.multi + def generate_account_reconcile_model(self, tax_template_ref, acc_template_ref, company): + """ This method for generating accounts from templates. + + :param tax_template_ref: Taxes templates reference for write taxes_id in account_account. + :param acc_template_ref: dictionary with the mappping between the account templates and the real accounts. + :param company_id: company_id selected from wizard.multi.charts.accounts. + :returns: return new_account_reconcile_model for reference purpose. + :rtype: dict + """ + self.ensure_one() + account_reconcile_models = self.env['account.reconcile.model.template'].search([ + ('account_id.chart_template_id', '=', self.id) + ]) + for account_reconcile_model in account_reconcile_models: + vals = self._prepare_reconcile_model_vals(company, account_reconcile_model, acc_template_ref, tax_template_ref) self.create_record_with_xmlid(company, account_reconcile_model, 'account.reconcile.model', vals) return True diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index fdfcb1caaa5..846a86724b2 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -5,10 +5,14 @@ <record id="group_account_invoice" model="res.groups"> <field name="name">Billing</field> <field name="category_id" ref="base.module_category_accounting_and_finance"/> - <field name="users" eval="[(4, ref('base.user_root'))]"/> <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/> </record> + <!-- 2 steps to avoid making reference to xml id not defined yet --> + <record id="group_account_invoice" model="res.groups"> + <field name="users" eval="[(4, ref('base.user_root'))]"/> + </record> + <record id="group_account_user" model="res.groups"> <field name="name">Accountant</field> <field name="category_id" ref="base.module_category_accounting_and_finance"/> diff --git a/addons/account/static/src/js/account_payment_widget.js b/addons/account/static/src/js/account_payment_widget.js index 932de22eef5..a7f2b64c413 100644 --- a/addons/account/static/src/js/account_payment_widget.js +++ b/addons/account/static/src/js/account_payment_widget.js @@ -59,7 +59,7 @@ var ShowPaymentLineWidget = form_common.AbstractField.extend({ var payment_id = parseInt($(this).attr('payment-id')) if (payment_id !== undefined && payment_id !== NaN){ new Model("account.move.line") - .call("remove_move_reconcile", [payment_id]) + .call("remove_move_reconcile", [payment_id, {'invoice_id': self.view.datarecord.id}]) .then(function (result) { self.view.reload(); }); diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index a3fcb77d389..9a485aed1c2 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -46,7 +46,7 @@ <field name="quantity"/> <field name="uom_id" groups="product.group_uom"/> <field name="price_unit"/> - <field name="discount" groups="sale.group_discount_per_so_line"/> + <field name="discount" groups="base.group_no_one"/> <field name="price_subtotal"/> <field name="currency_id" invisible="1"/> </tree> @@ -68,7 +68,7 @@ <field name="uom_id" class="oe_inline" groups="product.group_uom"/> </div> <field name="price_unit"/> - <field name="discount" groups="sale.group_discount_per_so_line"/> + <field name="discount" groups="base.group_no_one"/> <field name="currency_id" invisible="1"/> </group> <group> @@ -272,7 +272,7 @@ <field name="quantity"/> <field name="uom_id" groups="product.group_uom"/> <field name="price_unit"/> - <field name="discount" groups="sale.group_discount_per_so_line"/> + <field name="discount" groups="base.group_no_one"/> <field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}" domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/> <field name="price_subtotal"/> @@ -411,7 +411,7 @@ <field name="quantity"/> <field name="uom_id" groups="product.group_uom"/> <field name="price_unit"/> - <field name="discount" groups="sale.group_discount_per_so_line"/> + <field name="discount" groups="base.group_no_one"/> <field name="invoice_line_tax_ids" widget="many2many_tags" context="{'type':parent.type}" domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]" options="{'no_create': True}"/> <field name="price_subtotal"/> diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml index f700f57a940..67f99a43fe6 100644 --- a/addons/account/views/report_invoice.xml +++ b/addons/account/views/report_invoice.xml @@ -61,7 +61,7 @@ <th class="hidden">Source Document</th> <th class="text-right">Quantity</th> <th class="text-right">Unit Price</th> - <th t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line">Disc.(%)</th> + <th t-if="display_discount" class="text-right">Disc.(%)</th> <th class="text-right">Taxes</th> <th class="text-right">Subtotal</th> </tr> @@ -77,7 +77,7 @@ <td class="text-right"> <span t-field="l.price_unit"/> </td> - <td t-if="display_discount" class="text-right" groups="sale.group_discount_per_so_line"> + <td t-if="display_discount" class="text-right"> <span t-field="l.discount"/> </td> <td class="text-right"> diff --git a/addons/account_asset/models/account_asset.py b/addons/account_asset/models/account_asset.py index c2b57cd7d2c..6f1251ccaca 100644 --- a/addons/account_asset/models/account_asset.py +++ b/addons/account_asset/models/account_asset.py @@ -42,7 +42,10 @@ class AccountAssetCategory(models.Model): @api.onchange('account_asset_id') def onchange_account_asset(self): - self.account_depreciation_id = self.account_asset_id + if self.type == "purchase": + self.account_depreciation_id = self.account_asset_id + elif self.type == "sale": + self.account_depreciation_expense_id = self.account_asset_id @api.onchange('type') def onchange_type(self): diff --git a/addons/account_asset/models/account_invoice.py b/addons/account_asset/models/account_invoice.py index 3e45b722976..d6fb00358c9 100644 --- a/addons/account_asset/models/account_invoice.py +++ b/addons/account_asset/models/account_invoice.py @@ -13,13 +13,15 @@ class AccountInvoice(models.Model): _inherit = 'account.invoice' @api.multi + def action_cancel(self): + res = super(AccountInvoice, self).action_cancel() + self.env['account.asset.asset'].sudo().search([('invoice_id', 'in', self.ids)]).write({'active': False}) + return res + + @api.multi def action_move_create(self): result = super(AccountInvoice, self).action_move_create() for inv in self: - if inv.number: - asset_ids = self.env['account.asset.asset'].sudo().search([('invoice_id', '=', inv.id), ('company_id', '=', inv.company_id.id)]) - if asset_ids: - asset_ids.write({'active': False}) context = dict(self.env.context) # Within the context of an invoice, # this default value is for the type of the invoice, not the type of the asset. @@ -83,7 +85,7 @@ class AccountInvoiceLine(models.Model): if self.invoice_id.type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.invoice_id.type == 'in_invoice' and self.asset_category_id: - self.account_id = self.asset_category_id.account_depreciation_id.id + self.account_id = self.asset_category_id.account_asset_id.id @api.onchange('uom_id') def _onchange_uom_id(self): diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py index fc3e200840e..8ee9a40a11f 100644 --- a/addons/account_check_printing/models/account_payment.py +++ b/addons/account_check_printing/models/account_payment.py @@ -88,7 +88,7 @@ class AccountPayment(models.Model): and vals.get('check_manual_sequencing'): sequence = self.env['account.journal'].browse(vals['journal_id']).check_sequence_id vals.update({'check_number': sequence.next_by_id()}) - return super(AccountPayment, self.sudo()).create(vals) + return super(AccountPayment, self).create(vals) @api.multi def print_checks(self): diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 233cd839e6b..aa3f4e69b5c 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -111,8 +111,6 @@ class OAuthLogin(Home): @http.route() def web_auth_signup(self, *args, **kw): providers = self.list_providers() - if len(providers) == 1: - werkzeug.exceptions.abort(werkzeug.utils.redirect(providers[0]['auth_link'], 303)) response = super(OAuthLogin, self).web_auth_signup(*args, **kw) response.qcontext.update(providers=providers) return response diff --git a/addons/board/controllers/main.py b/addons/board/controllers/main.py index 5f17afd8bb9..2ba236ea109 100644 --- a/addons/board/controllers/main.py +++ b/addons/board/controllers/main.py @@ -13,7 +13,7 @@ class Board(Controller): # Retrieve the 'My Dashboard' action from its xmlid action = request.env.ref('board.open_board_my_dash_action') - if action and action['res_model'] == 'board.board' and action['views'][0][1] == 'form': + if action and action['res_model'] == 'board.board' and action['views'][0][1] == 'form' and action_id: # Maybe should check the content instead of model board.board ? view_id = action['views'][0][0] board = request.env['board.board'].fields_view_get(view_id, 'form') diff --git a/addons/board/static/src/js/dashboard.js b/addons/board/static/src/js/dashboard.js index 7d881187fe0..eb458812a57 100644 --- a/addons/board/static/src/js/dashboard.js +++ b/addons/board/static/src/js/dashboard.js @@ -413,7 +413,7 @@ FavoriteMenu.include({ name = self.$add_dashboard_input.val(); return self.rpc('/board/add_to_dashboard', { - action_id: self.action_id, + action_id: self.action_id || false, context_to_save: c, domain: d, view_mode: self.view_manager.active_view.type, diff --git a/addons/bus/models/res_partner.py b/addons/bus/models/res_partner.py index 4e97437720d..b0b4c23d74f 100644 --- a/addons/bus/models/res_partner.py +++ b/addons/bus/models/res_partner.py @@ -34,24 +34,30 @@ class ResPartner(models.Model): :param name : the partner name to search :param limit : the limit of result to return """ - name = '%' + name + '%' - excluded_partner_ids = [self.env.user.partner_id.id] - self.env.cr.execute(""" - SELECT - U.id as user_id, - P.id as id, - P.name as name, - CASE WHEN B.last_poll IS NULL THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' - WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' - ELSE 'online' - END as im_status - FROM res_users U - JOIN res_partner P ON P.id = U.partner_id - LEFT JOIN bus_presence B ON B.user_id = U.id - WHERE P.name ILIKE %s - AND P.id NOT IN %s - AND U.active = 't' - LIMIT %s - """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) - return self.env.cr.dictfetchall() + # This method is supposed to be used only in the context of channel creation or + # extension via an invite. As both of these actions require the 'create' access + # right, we check this specific ACL. + if self.env['mail.channel'].check_access_rights('create', raise_exception=False): + name = '%' + name + '%' + excluded_partner_ids = [self.env.user.partner_id.id] + self.env.cr.execute(""" + SELECT + U.id as user_id, + P.id as id, + P.name as name, + CASE WHEN B.last_poll IS NULL THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_poll) > interval %s THEN 'offline' + WHEN age(now() AT TIME ZONE 'UTC', B.last_presence) > interval %s THEN 'away' + ELSE 'online' + END as im_status + FROM res_users U + JOIN res_partner P ON P.id = U.partner_id + LEFT JOIN bus_presence B ON B.user_id = U.id + WHERE P.name ILIKE %s + AND P.id NOT IN %s + AND U.active = 't' + LIMIT %s + """, ("%s seconds" % DISCONNECTION_TIMER, "%s seconds" % AWAY_TIMER, name, tuple(excluded_partner_ids), limit)) + return self.env.cr.dictfetchall() + else: + return {} diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py index dc398afed3c..d55b35ae36c 100644 --- a/addons/calendar/models/calendar.py +++ b/addons/calendar/models/calendar.py @@ -173,6 +173,7 @@ class Attendee(models.Model): 'datas_fname': 'invitation.ics', 'datas': str(ics_file).encode('base64')})] vals['model'] = None # We don't want to have the mail in the tchatter while in queue! + vals['res_id'] = False current_mail = self.env['mail.mail'].browse(mail_id) current_mail.mail_message_id.write(vals) mails_to_send |= current_mail @@ -1360,11 +1361,11 @@ class Meeting(models.Model): super(Meeting, real_meetings).write(values) # set end_date for calendar searching - if values.get('recurrency') and values.get('end_type', 'count') in ('count', unicode('count')) and \ - (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')): + if any(field in values for field in ['recurrency', 'end_type', 'count', 'rrule_type', 'start', 'stop']): for real_meeting in real_meetings: - final_date = real_meeting._get_recurrency_end_date() - super(Meeting, real_meeting).write({'final_date': final_date}) + if real_meeting.recurrency and real_meeting.end_type in ('count', unicode('count')): + final_date = real_meeting._get_recurrency_end_date() + super(Meeting, real_meeting).write({'final_date': final_date}) attendees_create = False if values.get('partner_ids', False): @@ -1464,7 +1465,8 @@ class Meeting(models.Model): for r in result: if r['user_id']: user_id = type(r['user_id']) in (tuple, list) and r['user_id'][0] or r['user_id'] - if user_id == self.env.user.id: + partner_id = self.env.user.partner_id.id + if user_id == self.env.user.id or partner_id in r.get("partner_ids", []): continue if r['privacy'] == 'private': for f in r.keys(): diff --git a/addons/calendar/security/calendar_security.xml b/addons/calendar/security/calendar_security.xml index e448e61a075..8b5385e3cdf 100644 --- a/addons/calendar/security/calendar_security.xml +++ b/addons/calendar/security/calendar_security.xml @@ -23,5 +23,15 @@ <field name="groups" eval="[(4, ref('base.group_portal'))]"/> </record> + <record id="calendar_event_rule_private" model="ir.rule"> + <field ref="model_calendar_event" name="model_id"/> + <field name="name">Private events</field> + <field name="domain_force">['|', ('privacy', '!=', 'private'), '&', ('privacy', '=', 'private'), ('partner_ids', 'in', user.partner_id.id)]</field> + <field name="perm_read" eval="False"/> + <field name="perm_write" eval="True"/> + <field name="perm_create" eval="True"/> + <field name="perm_unlink" eval="True"/> + </record> + </data> </odoo> diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py index dff59a6481b..7781e15184b 100644 --- a/addons/crm/__manifest__.py +++ b/addons/crm/__manifest__.py @@ -40,6 +40,9 @@ Dashboard for CRM will include: 'web_tour', ], 'data': [ + 'security/crm_security.xml', + 'security/ir.model.access.csv', + 'data/crm_data.xml', 'data/crm_stage_data.xml', 'data/sales_config_settings_data.xml', @@ -47,9 +50,6 @@ Dashboard for CRM will include: 'data/web_planner_data.xml', 'data/mail_template_data.xml', - 'security/crm_security.xml', - 'security/ir.model.access.csv', - 'wizard/base_partner_merge_views.xml', 'wizard/crm_lead_lost_views.xml', 'wizard/crm_lead_to_opportunity_views.xml', diff --git a/addons/crm/data/mail_template_data.xml b/addons/crm/data/mail_template_data.xml index ac84bea844c..7da44977f2a 100644 --- a/addons/crm/data/mail_template_data.xml +++ b/addons/crm/data/mail_template_data.xml @@ -91,11 +91,10 @@ <span style="font-style:italic;">You’re not alone</span> </p> <p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;"> - <span>We would be delighted to assist you along the way. Contact us at - <a href="mailto:help@odoo.com"><span style="text-decoration:underline;"> - help@odoo.com</span></a> if you have any question. You can also discover - how to get the best out of Odoo CRM with our </span> - <a href="https://www.odoo.com/documentation/user/9.0/crm.html"> + <span>We would be delighted to assist you along the way. Contact us through our + <a href="https://www.odoo.com/help">support form</a> if you have any question. + You can also discover how to get the best out of Odoo CRM with our </span> + <a href="https://www.odoo.com/documentation/user/10.0/crm.html"> <span style="text-decoration:underline;">User Documentation</span></a> </span><span> or with our </span> <a href="https://www.odoo.com/documentation"> diff --git a/addons/crm/wizard/base_partner_merge.py b/addons/crm/wizard/base_partner_merge.py index 44320807797..18fd4c6a3e6 100644 --- a/addons/crm/wizard/base_partner_merge.py +++ b/addons/crm/wizard/base_partner_merge.py @@ -406,7 +406,7 @@ class MergePartnerAutomatic(models.TransientModel): :param models : dict mapping a model name with its foreign key with res_partner table """ return any( - self.env[model].search_count([(field, 'in', aggr_ids)], limit=1) + self.env[model].search_count([(field, 'in', aggr_ids)]) for model, field in models.iteritems() ) diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index ebe6dc06b38..c39f1c4e721 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -51,8 +51,11 @@ class StockPicking(models.Model): _inherit = 'stock.picking' def _default_uom(self): - uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id - return self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1) + weight_uom_id = self.env.ref('product.product_uom_kgm', raise_if_not_found=False) + if not weight_uom_id: + uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id + weight_uom_id = self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1) + return weight_uom_id @api.one @api.depends('pack_operation_ids') diff --git a/addons/event_sale/models/event.py b/addons/event_sale/models/event.py index 65371c15047..5eba5703918 100644 --- a/addons/event_sale/models/event.py +++ b/addons/event_sale/models/event.py @@ -36,7 +36,7 @@ class EventTicket(models.Model): name = fields.Char(string='Name', required=True, translate=True) event_id = fields.Many2one('event.event', string="Event", required=True, ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', - required=True, domain=["|", ("event_type_id", "!=", False), ("event_ok", "=", True)], + required=True, domain=[("event_ok", "=", True)], default=_default_product_id) registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations') price = fields.Float(string='Price', digits=dp.get_precision('Product Price')) @@ -44,6 +44,7 @@ class EventTicket(models.Model): is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired') price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits=dp.get_precision('Product Price')) + price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc') # seats fields seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited") @@ -71,6 +72,13 @@ class EventTicket(models.Model): discount = product.lst_price and (product.lst_price - product.price) / product.lst_price or 0.0 record.price_reduce = (1.0 - discount) * record.price + def _get_price_reduce_tax(self): + for record in self: + # sudo necessary here since the field is most probably accessed through the website + tax_ids = record.sudo().product_id.taxes_id.filtered(lambda r: r.company_id == record.event_id.company_id) + taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id) + record.price_reduce_taxinc = taxes['total_included'] + @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py index 007bbffeaf9..fb2e2e4f611 100644 --- a/addons/google_calendar/models/google_calendar.py +++ b/addons/google_calendar/models/google_calendar.py @@ -795,6 +795,7 @@ class GoogleCalendar(models.AbstractModel): actToDo = event.OP actSrc = event.OP.src + # To avoid redefining 'self', all method below should use 'recs' instead of 'self' recs = self.with_context(curr_attendee=event.OE.attendee_id) if isinstance(actToDo, NothingToDo): @@ -828,12 +829,9 @@ class GoogleCalendar(models.AbstractModel): if event_to_synchronize[base_event][0][1].OE.event_id: parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) - res = self.update_from_google(parent_event, event.GG.event, "copy") + res = recs.update_from_google(parent_event, event.GG.event, "copy") else: - self.create_from_google(event, my_partner_id) - - parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) - res = recs.update_from_google(parent_event, event.GG.event, "copy") + recs.create_from_google(event, my_partner_id) else: parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id if parent_oe_id: diff --git a/addons/hr/models/res_partner.py b/addons/hr/models/res_partner.py index 47bfe7e0534..8772bc46040 100644 --- a/addons/hr/models/res_partner.py +++ b/addons/hr/models/res_partner.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, models +from openerp.exceptions import AccessError class Partner(models.Model): @@ -13,12 +14,10 @@ class Partner(models.Model): """ Extend the mail's static mention suggestions by adding the employees. """ suggestions = super(Partner, self).get_static_mention_suggestions() - employee_group_id = self.env['ir.model.data'].xmlid_to_res_id('base.group_user') - self._cr.execute(""" - SELECT P.id, P.name, P.email - FROM res_users U - INNER JOIN res_groups_users_rel R ON U.id = R.uid - INNER JOIN res_partner P ON P.id = U.partner_id - WHERE R.gid = %s AND U.active = 't'""", (employee_group_id,)) - suggestions.append(self._cr.dictfetchall()) - return suggestions + try: + employee_group = self.env.ref('base.group_user') + for user in employee_group.users: + suggestions.append((user.partner_id.id, user.name, user.email)) + return suggestions + except AccessError: + return suggestions diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py index 7fcd8ee8ff4..a4dac9883ea 100644 --- a/addons/hr_attendance/models/hr_employee.py +++ b/addons/hr_attendance/models/hr_employee.py @@ -74,7 +74,7 @@ class HrEmployee(models.Model): @api.multi def attendance_manual(self, next_action, entered_pin=None): self.ensure_one() - if self.env['res.users'].browse(SUPERUSER_ID).has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id): + if not (entered_pin is None) or self.env['res.users'].browse(SUPERUSER_ID).has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id.id != self._uid or not self.user_id): if entered_pin != self.pin: return {'warning': _('Wrong PIN')} return self.attendance_action(next_action) diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 03623c29fbf..773202963ef 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -562,7 +562,7 @@ <filter string="New Mail" name="message_unread" domain="[('message_unread', '=', True)]"/> <group expand="0" string="Group By"> <filter string="Employee" domain="[]" context="{'group_by': 'employee_id'}"/> - <filter string="Department" domain="[]" context="{'group_by': 'department_id'}" groups="hr.group_multi_departments"/> + <filter string="Department" domain="[]" context="{'group_by': 'department_id'}"/> <filter string="Company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/> <filter string="Expenses Month" domain="[]" context="{'group_by': 'accounting_date'}" help="Expenses by Month"/> <filter string="State" domain="[]" context="{'group_by': 'state'}" name="state"/> diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py index acaa865a296..750794fcf57 100644 --- a/addons/hr_holidays/models/hr_holidays.py +++ b/addons/hr_holidays/models/hr_holidays.py @@ -330,7 +330,7 @@ class Holidays(models.Model): def name_get(self): res = [] for leave in self: - res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) + res.append((leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) return res def _check_state_access_right(self, vals): diff --git a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py index 93033a44b6d..e4c05fbd2b1 100644 --- a/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py +++ b/addons/hr_timesheet_sheet/models/hr_timesheet_sheet.py @@ -27,7 +27,7 @@ class HrTimesheetSheet(models.Model): return (datetime.today() + relativedelta(weekday=0, days=-6)).strftime('%Y-%m-%d') elif r == 'year': return time.strftime('%Y-01-01') - return fields.date.context_today(self) + return fields.Date.context_today(self) def _default_date_to(self): user = self.env['res.users'].browse(self.env.uid) @@ -38,7 +38,7 @@ class HrTimesheetSheet(models.Model): return (datetime.today() + relativedelta(weekday=6)).strftime('%Y-%m-%d') elif r == 'year': return time.strftime('%Y-12-31') - return fields.date.context_today(self) + return fields.Date.context_today(self) def _default_employee(self): emp_ids = self.env['hr.employee'].search([('user_id', '=', self.env.uid)]) diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index f8670a70e23..766fb9d5078 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -75,12 +75,13 @@ class MailController(http.Controller): return cls._redirect_to_messaging() # the record has a window redirection: check access rights - if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False): - return cls._redirect_to_messaging() - try: - record_sudo.sudo(uid).check_access_rule('read') - except AccessError: - return cls._redirect_to_messaging() + if uid is not None: + if not RecordModel.sudo(uid).check_access_rights('read', raise_exception=False): + return cls._redirect_to_messaging() + try: + record_sudo.sudo(uid).check_access_rule('read') + except AccessError: + return cls._redirect_to_messaging() url_params = { 'view_type': record_action['view_type'], diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 825053f4e36..c10cb090ecb 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -118,23 +118,31 @@ class Followers(models.Model): # Modifying followers change access rights to individual documents. As the # cache may contain accessible/inaccessible data, one has to refresh it. # + @api.multi + def _invalidate_documents(self): + """ Invalidate the cache of the documents followed by ``self``. """ + for record in self: + if record.res_id: + self.env[record.res_model].invalidate_cache(ids=[record.res_id]) + @api.model def create(self, vals): res = super(Followers, self).create(vals) - self.invalidate_cache() + res._invalidate_documents() return res @api.multi def write(self, vals): + if 'res_model' in vals or 'res_id' in vals: + self._invalidate_documents() res = super(Followers, self).write(vals) - self.invalidate_cache() + self._invalidate_documents() return res @api.multi def unlink(self): - res = super(Followers, self).unlink() - self.invalidate_cache() - return res + self._invalidate_documents() + return super(Followers, self).unlink() _sql_constraints = [ ('mail_followers_res_partner_res_model_id_uniq', 'unique(res_model,res_id,partner_id)', 'Error, a partner cannot follow twice the same object.'), diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 38f7070a36d..0fdcabf5323 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -699,6 +699,13 @@ class Message(models.Model): message_id = tools.generate_tracking_message_id('private') return message_id + @api.multi + def _invalidate_documents(self): + """ Invalidate the cache of the documents followed by ``self``. """ + for record in self: + if record.res_id: + self.env[record.model].invalidate_cache(ids=[record.res_id]) + @api.model def create(self, values): # coming from mail.js that does not have pid in its values @@ -715,6 +722,7 @@ class Message(models.Model): values['record_name'] = self._get_record_name(values) message = super(Message, self).create(values) + message._invalidate_documents() if not self.env.context.get('message_create_from_mail_mail'): message._notify(force_send=self.env.context.get('mail_notify_force_send', True), @@ -729,6 +737,14 @@ class Message(models.Model): return super(Message, self).read(fields=fields, load=load) @api.multi + def write(self, vals): + if 'model' in vals or 'res_id' in vals: + self._invalidate_documents() + res = super(Message, self).write(vals) + self._invalidate_documents() + return res + + @api.multi def unlink(self): # cascade-delete attachments that are directly attached to the message (should only happen # for mail.messages that act as parent for a standalone mail.mail record). @@ -736,6 +752,7 @@ class Message(models.Model): self.mapped('attachment_ids').filtered( lambda attach: attach.res_model == self._name and (attach.res_id in self.ids or attach.res_id == 0) ).unlink() + self._invalidate_documents() return super(Message, self).unlink() #------------------------------------------------------ diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index b4e66e71f33..0576bd66db7 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import babel import base64 import copy import datetime @@ -18,6 +19,20 @@ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) +def format_date(env, date, pattern=False): + date = datetime.datetime.strptime(date[:10], tools.DEFAULT_SERVER_DATE_FORMAT) + lang_code = env.context.get('lang') or 'en_US' + if not pattern: + lang = env['res.lang']._lang_get(lang_code) + pattern = lang.date_format + try: + locale = babel.Locale.parse(lang_code) + pattern = tools.posix_to_ldml(pattern, locale=locale) + return babel.dates.format_date(date, format=pattern, locale=locale) + except babel.core.UnknownLocaleError: + return date.strftime(pattern) + + def format_tz(env, dt, tz=False, format=False): record_user_timestamp = env.user.sudo().with_context(tz=tz or env.user.sudo().tz or 'UTC') timestamp = datetime.datetime.strptime(dt, tools.DEFAULT_SERVER_DATETIME_FORMAT) @@ -350,6 +365,7 @@ class MailTemplate(models.Model): for record in records: res_to_rec[record.id] = record variables = { + 'format_date': lambda date, format=False, context=self._context: format_date(self.env, date, format), 'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format), 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 1e7196600cc..822f87dc0a2 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -127,8 +127,12 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.$(".o_mail_annoying_notification_bar").slideUp(); var def = window.Notification.requestPermission(); if (def) { - def.then(function () { - utils.send_notification('Permission granted', 'Odoo has now the permission to send you native notifications on this device.'); + def.then(function (value) { + if (value === 'denied') { + utils.send_notification(_t('Permission denied'), _t('Odoo will not have the permission to send native notifications on this device.')); + } else { + utils.send_notification(_t('Permission granted'), _t('Odoo has now the permission to send you native notifications on this device.')); + } }); } }, diff --git a/addons/mail/static/src/js/utils.js b/addons/mail/static/src/js/utils.js index db82b976945..2c30a82701e 100644 --- a/addons/mail/static/src/js/utils.js +++ b/addons/mail/static/src/js/utils.js @@ -130,7 +130,6 @@ var accented_letters_mapping = { 'oe': 'œ', 'u': '[ùúûűü]', 'y': '[ýÿ]', - ' ': '[()\\[\\]]', }; function unaccent (str) { _.each(accented_letters_mapping, function (value, key) { diff --git a/addons/mail/tests/test_mail_features.py b/addons/mail/tests/test_mail_features.py index 678ffb8c434..729f2df07b6 100644 --- a/addons/mail/tests/test_mail_features.py +++ b/addons/mail/tests/test_mail_features.py @@ -12,6 +12,17 @@ class TestMailFeatures(TestMail): alias = self.env['mail.alias'].with_context(alias_model_name='mail.channel').create({'alias_name': 'b4r+_#_R3wl$$'}) self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens') + def test_10_cache_invalidation(self): + """ Test that creating a mail-thread record does not invalidate the whole cache. """ + # make a new record in cache + record = self.env['res.partner'].new({'name': 'Brave New Partner'}) + self.assertTrue(record.name) + + # creating a mail-thread record should not invalidate the whole cache + self.env['res.partner'].create({'name': 'Actual Partner'}) + self.assertTrue(record.name) + + @mute_logger('odoo.addons.mail.models.mail_mail') def test_needaction(self): na_emp1_base = self.group_pigs.sudo(self.user_employee).message_needaction_counter diff --git a/addons/mail/tests/test_mail_message.py b/addons/mail/tests/test_mail_message.py index cc68a1dd8bc..21f6e1850a2 100644 --- a/addons/mail/tests/test_mail_message.py +++ b/addons/mail/tests/test_mail_message.py @@ -268,3 +268,8 @@ class TestMailMessage(TestMail): msg.toggle_message_starred() self.assertFalse(msg.starred) self.assertTrue(msg_emp.starred) + + def test_60_cache_invalidation(self): + msg_cnt = len(self.group_pigs.message_ids) + self.group_pigs.message_post(body='Hi!', subject='test') + self.assertEqual(len(self.group_pigs.message_ids), msg_cnt + 1) diff --git a/addons/marketing_campaign/models/marketing_campaign.py b/addons/marketing_campaign/models/marketing_campaign.py index fc841868673..b6f8ba64b48 100644 --- a/addons/marketing_campaign/models/marketing_campaign.py +++ b/addons/marketing_campaign/models/marketing_campaign.py @@ -341,6 +341,16 @@ class MarketingCampaignActivity(models.Model): return self.email_template_id.send_mail(workitem.res_id) @api.multi + def _process_wi_report(self, workitem): + self.ensure_one() + return self.report_id.render_report(workitem.res_id, self.report_id.report_name, None) + + @api.multi + def _process_wi_action(self, workitem): + self.ensure_one() + return self.server_action_id.run() + + @api.multi def process(self, workitem): self.ensure_one() method = '_process_wi_%s' % (self.action_type,) diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index bf145c9d59e..d81dd8d0a6c 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -94,7 +94,7 @@ class MrpProduction(models.Model): help="Bill of Materials allow you to define the list of required raw materials to make a finished product.") routing_id = fields.Many2one( 'mrp.routing', 'Routing', - readonly=True, related='bom_id.routing_id', store=True, + readonly=True, compute='_compute_routing', store=True, help="The list of operations (list of work centers) to produce the finished product. The routing " "is mainly used to compute work center costs during operations and to plan future loads on " "work centers based on production planning.") @@ -153,6 +153,15 @@ class MrpProduction(models.Model): readonly=True, states={'confirmed': [('readonly', False)]}, default='1') @api.multi + @api.depends('bom_id.routing_id', 'bom_id.routing_id.operation_ids') + def _compute_routing(self): + for production in self: + if production.bom_id.routing_id.operation_ids: + production.routing_id = production.bom_id.routing_id.id + else: + production.routing_id = False + + @api.multi @api.depends('workorder_ids') def _compute_workorder_count(self): data = self.env['mrp.workorder'].read_group([('production_id', 'in', self.ids)], ['production_id'], ['production_id']) @@ -243,10 +252,6 @@ class MrpProduction(models.Model): self.location_src_id = self.picking_type_id.default_location_src_id.id or location.id self.location_dest_id = self.picking_type_id.default_location_dest_id.id or location.id - @api.onchange('bom_id') - def onchange_bom_id(self): - self.routing_id = self.bom_id.routing_id.id - @api.model def create(self, values): if not values.get('name', False) or values['name'] == _('New'): @@ -345,7 +350,7 @@ class MrpProduction(models.Model): mto_route = False for move in self.move_raw_ids: product = move.product_id - routes = product.route_ids + product.categ_id.route_ids + routes = product.route_ids + product.route_from_categ_ids # TODO: optimize with read_group? pull = self.env['procurement.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id), ('location_id', '=', move.location_dest_id.id)], limit=1) @@ -481,11 +486,10 @@ class MrpProduction(models.Model): @api.multi def post_inventory(self): for order in self: - moves_to_do = order.move_raw_ids + moves_to_do = order.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves_to_do.action_done() - #order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel')).move_validate() order._cal_price(moves_to_do) - moves_to_finish = order.move_finished_ids + moves_to_finish = order.move_finished_ids.filtered(lambda x: x.state not in ('done','cancel')) moves_to_finish.action_done() for move in moves_to_finish: #Group quants by lots diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index dbc0ee221c2..fc0da1ecdab 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -377,8 +377,6 @@ class MrpWorkorder(models.Model): self.ensure_one() self.end_all() self.write({'state': 'done', 'date_finished': fields.Datetime.now()}) - if not self.production_id.workorder_ids.filtered(lambda x: x.state not in ('done','cancel')): - self.production_id.post_inventory() # User should put it to done manually @api.multi def end_previous(self, doall=False): diff --git a/addons/mrp/models/stock_picking.py b/addons/mrp/models/stock_picking.py index 90e7574185f..7955caf0722 100644 --- a/addons/mrp/models/stock_picking.py +++ b/addons/mrp/models/stock_picking.py @@ -16,12 +16,15 @@ class StockPickingType(models.Model): mrp_picking_types = self.filtered(lambda picking: picking.code == 'mrp_operation') if not mrp_picking_types: return - - MrpProduction = self.env['mrp.production'] - count_mo_waiting = MrpProduction.search_count([('availability', '=', 'waiting')]) - count_mo_todo = MrpProduction.search_count([('state', 'in', ('confirmed', 'planned', 'progress'))]) - count_mo_late = MrpProduction.search_count(['&', ('date_planned_start', '<', fields.Date.today()), ('state', '=', 'confirmed')]) - for picking in mrp_picking_types: - picking.count_mo_waiting = count_mo_waiting - picking.count_mo_todo = count_mo_todo - picking.count_mo_late = count_mo_late + domains = { + 'count_mo_waiting': [('availability', '=', 'waiting')], + 'count_mo_todo': [('state', 'in', ('confirmed', 'planned', 'progress'))], + 'count_mo_late': [('date_planned_start', '<', fields.Date.today()), ('state', '=', 'confirmed')], + } + for field in domains: + data = self.env['mrp.production'].read_group(domains[field] + + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], + ['picking_type_id'], ['picking_type_id']) + count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data)) + for record in mrp_picking_types: + record[field] = count.get(record.id, 0)
\ No newline at end of file diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 5e5535b9137..64a42fbb136 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -332,3 +332,98 @@ class TestMrpOrder(TestMrpCommon): production_2.action_assign() # check sub product availability state is assigned self.assertEqual(production_2.availability, 'assigned', 'Production order should be availability for assigned state') + + def test_empty_routing(self): + """ Check what happens when you work with an empty routing""" + routing = self.env['mrp.routing'].create({'name': 'Routing without operations', + 'location_id': self.warehouse_1.wh_input_stock_loc_id.id,}) + self.bom_3.routing_id = routing.id + production = self.env['mrp.production'].create({'name': 'MO test', + 'product_id': self.product_6.id, + 'product_qty': 3, + 'bom_id': self.bom_3.id, + 'product_uom_id': self.product_6.uom_id.id,}) + self.assertEqual(production.routing_id.id, False, 'The routing field should be empty on the mo') + self.assertEqual(production.move_raw_ids[0].location_id.id, self.warehouse_1.wh_input_stock_loc_id.id, 'Raw moves start location should have altered.') + + def test_multiple_post_inventory(self): + """ Check the consumed quants of the produced quants when intermediate calls to `post_inventory` during a MO.""" + + # create a bom for `custom_laptop` with components that aren't tracked + unit = self.ref("product.product_uom_unit") + custom_laptop = self.env.ref("product.product_product_27") + custom_laptop.tracking = 'none' + product_charger = self.env['product.product'].create({ + 'name': 'Charger', + 'type': 'product', + 'uom_id': unit, + 'uom_po_id': unit}) + product_keybord = self.env['product.product'].create({ + 'name': 'Usb Keybord', + 'type': 'product', + 'uom_id': unit, + 'uom_po_id': unit}) + bom_custom_laptop = self.env['mrp.bom'].create({ + 'product_tmpl_id': custom_laptop.product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': unit, + 'bom_line_ids': [(0, 0, { + 'product_id': product_charger.id, + 'product_qty': 1, + 'product_uom_id': unit + }), (0, 0, { + 'product_id': product_keybord.id, + 'product_qty': 1, + 'product_uom_id': unit + })] + }) + + # put the needed products in stock + source_location_id = self.ref('stock.stock_location_14') + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': product_charger.id, + 'product_uom_id': product_charger.uom_id.id, + 'product_qty': 2, + 'location_id': source_location_id + }), (0, 0, { + 'product_id': product_keybord.id, + 'product_uom_id': product_keybord.uom_id.id, + 'product_qty': 2, + 'location_id': source_location_id + })] + }) + inventory.action_done() + + # create a mo for this bom + mo_custom_laptop = self.env['mrp.production'].create({ + 'product_id': custom_laptop.id, + 'product_qty': 2, + 'product_uom_id': unit, + 'bom_id': bom_custom_laptop.id + }) + mo_custom_laptop.action_assign() + self.assertEqual(mo_custom_laptop.availability, 'assigned') + + # produce one item, call `post_inventory` + context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} + custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00}) + custom_laptop_produce.do_produce() + mo_custom_laptop.post_inventory() + + # check the consumed quants of the produced quant + first_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'done') + self.assertEquals(sum(first_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2) + + second_move = mo_custom_laptop.move_finished_ids.filtered(lambda mo: mo.state == 'confirmed') + + # produce the second item, call `post_inventory` + context = {"active_ids": [mo_custom_laptop.id], "active_id": mo_custom_laptop.id} + custom_laptop_produce = self.env['mrp.product.produce'].with_context(context).create({'product_qty': 1.00}) + custom_laptop_produce.do_produce() + mo_custom_laptop.post_inventory() + + # check the consumed quants of the newly produced quant + self.assertEquals(sum(second_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2) diff --git a/addons/mrp/tests/test_procurement.py b/addons/mrp/tests/test_procurement.py index bc212050db5..98074e5e004 100644 --- a/addons/mrp/tests/test_procurement.py +++ b/addons/mrp/tests/test_procurement.py @@ -105,3 +105,38 @@ class TestProcurement(TestMrpCommon): production_product_6.button_mark_done() self.assertEqual(production_product_6.state, 'done', 'Production order should be in state done') self.assertEqual(self.product_6.qty_available, 24, 'Wrong quantity available of finished product.') + + def test_procurement_2(self): + """Check that a manufacturing order create the right procurements when the route are set on + a parent category of a product""" + # find a child category id + all_categ_id = self.env['product.category'].search([('parent_id', '=', None)], limit=1) + child_categ_id = self.env['product.category'].search([('parent_id', '=', all_categ_id.id)], limit=1) + + # set the product of `self.bom_1` to this child category + for bom_line_id in self.bom_1.bom_line_ids: + # check that no routes are defined on the product + self.assertEquals(len(bom_line_id.product_id.route_ids), 0) + # set the category of the product to a child category + bom_line_id.product_id.categ_id = child_categ_id + + # set the MTO route to the parent category (all) + self.warehouse = self.env.ref('stock.warehouse0') + mto_route = self.warehouse.mto_pull_id.route_id + mto_route.product_categ_selectable = True + all_categ_id.write({'route_ids': [(6, 0, [mto_route.id])]}) + + # create MO + production_product_4 = self.env['mrp.production'].create({ + 'name': 'MO/Test-00002', + 'product_id': self.product_4.id, + 'product_qty': 1, + 'bom_id': self.bom_1.id, + 'product_uom_id': self.product_4.uom_id.id, + }) + + # check that procurement are correctly created + procurement = self.env['procurement.order'].search( + [('group_id', '=', production_product_4.procurement_group_id.id), + ('product_id', 'in', self.bom_1.bom_line_ids.mapped('product_id.id'))]) + self.assertEqual(len(procurement), 2) diff --git a/addons/mrp/tests/test_workorder_operation.py b/addons/mrp/tests/test_workorder_operation.py index 00541dfc8df..14eb7a65b31 100644 --- a/addons/mrp/tests/test_workorder_operation.py +++ b/addons/mrp/tests/test_workorder_operation.py @@ -123,9 +123,10 @@ class TestWorkOrderProcess(common.TransactionCase): # This behaviour was changed #self.assertEqual(production_table.state, 'done', "Production order should be in done state.") - # ---------------------------------------- - # Check consume quants and produce quants. - # ---------------------------------------- + # --------------------------------------------------------------- + # Check consume quants and produce quants after posting inventory + # --------------------------------------------------------------- + production_table.button_mark_done() self.assertEqual(sum(move_table_sheet.quant_ids.mapped('qty')), 1, "Wrong quantity of consumed product %s" % move_table_sheet.product_id.name) self.assertEqual(sum(move_leg.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_leg.product_id.name) self.assertEqual(sum(move_table_bolt.quant_ids.mapped('qty')), 4, "Wrong quantity of consumed product %s" % move_table_bolt.product_id.name) diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml index 7d03462504b..5203bebcb59 100644 --- a/addons/mrp/views/mrp_production_views.xml +++ b/addons/mrp/views/mrp_production_views.xml @@ -320,7 +320,8 @@ <field name="view_mode">tree,form</field> <field name="view_id" eval="False"/> <field name="search_view_id" ref="view_mrp_production_filter"/> - <field name="context">{}</field> + <field name="domain">[('picking_type_id', '=', active_id)]</field> + <field name="context">{'default_picking_type_id': active_id}</field> </record> diff --git a/addons/payment/models/account_payment.py b/addons/payment/models/account_payment.py index 52b1e9954e1..8609b6b96c9 100644 --- a/addons/payment/models/account_payment.py +++ b/addons/payment/models/account_payment.py @@ -17,7 +17,8 @@ class AccountPayment(models.Model): def _onchange_partner_id(self): res = {} if self.partner_id: - res['domain'] = {'payment_token_id': [('partner_id', '=', self.partner_id.id), ('acquirer_id.auto_confirm', '!=', 'authorize')]} + partners = self.partner_id | self.partner_id.commercial_partner_id | self.partner_id.commercial_partner_id.child_ids + res['domain'] = {'payment_token_id': [('partner_id', 'in', partners.ids), ('acquirer_id.auto_confirm', '!=', 'authorize')]} return res @@ -57,4 +58,4 @@ class AccountPayment(models.Model): if not s2s_result or tx.state != 'done': raise ValidationError(_("Payment transaction failed (%s)") % tx.state_message) - self.payment_transaction_id = tx
\ No newline at end of file + self.payment_transaction_id = tx diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index e8e3bbccd2f..864cde778fd 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -433,7 +433,7 @@ class PaymentTransaction(models.Model): # duplicate partner / transaction data to store the values at transaction time partner_id = fields.Many2one('res.partner', 'Partner', track_visibility='onchange') partner_name = fields.Char('Partner Name') - partner_lang = fields.Selection(_lang_get, 'Language', default='en_US') + partner_lang = fields.Selection(_lang_get, 'Language', default=lambda self: self.env.lang) partner_email = fields.Char('Email') partner_zip = fields.Char('Zip') partner_address = fields.Char('Address') @@ -634,7 +634,7 @@ class PaymentTransaction(models.Model): class PaymentToken(models.Model): _name = 'payment.token' - _order = 'partner_id' + _order = 'partner_id, id desc' name = fields.Char('Name', help='Name of the payment token') short_name = fields.Char('Short name', compute='_compute_short_name') diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py index ceebb17e500..b4627157a9e 100644 --- a/addons/payment_stripe/controllers/main.py +++ b/addons/payment_stripe/controllers/main.py @@ -32,7 +32,7 @@ class StripeController(http.Controller): tx = request.env['payment.transaction'].sudo().browse( int(request.session.get('sale_transaction_id') or request.session.get('website_payment_tx_id', False)) ) - response = tx._create_stripe_charge(tokenid=post['tokenid']) + response = tx._create_stripe_charge(tokenid=post['tokenid'], email=post['email']) _logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response)) if response: request.env['payment.transaction'].sudo().form_feedback(response, 'stripe') diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py index 2ef4e42061a..edec99e41ab 100644 --- a/addons/payment_stripe/models/payment.py +++ b/addons/payment_stripe/models/payment.py @@ -81,7 +81,7 @@ class PaymentAcquirerStripe(models.Model): class PaymentTransactionStripe(models.Model): _inherit = 'payment.transaction' - def _create_stripe_charge(self, acquirer_ref=None, tokenid=None): + def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None): api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url()) charge_params = { 'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support) @@ -92,6 +92,8 @@ class PaymentTransactionStripe(models.Model): charge_params['customer'] = acquirer_ref if tokenid: charge_params['card'] = str(tokenid) + if email: + charge_params['receipt_email'] = email r = requests.post(api_url_charge, auth=(self.acquirer_id.stripe_secret_key, ''), params=charge_params, diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index a0c1170ee38..201ed22e66c 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -511,6 +511,7 @@ class PosOrder(models.Model): if to_invoice: pos_order.action_pos_order_invoice() pos_order.invoice_id.sudo().action_invoice_open() + pos_order.account_move = pos_order.invoice_id.move_id return order_ids def test_paid(self): diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py index 8dee7d3efeb..4335f1d3f2b 100644 --- a/addons/point_of_sale/models/pos_session.py +++ b/addons/point_of_sale/models/pos_session.py @@ -22,10 +22,11 @@ class PosSession(models.Model): orders = session.order_ids.filtered(lambda order: order.state == 'paid') journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id) + move = self.env['pos.order'].with_context(force_company=company_id)._create_account_move(session.start_at, session.name, int(journal_id), company_id) orders.with_context(force_company=company_id)._create_account_move_line(session, move) - for order in session.order_ids.filtered(lambda o: o.state != 'done'): - if order.state not in ('paid', 'invoiced'): + for order in session.order_ids.filtered(lambda o: o.state not in ['done', 'invoiced']): + if order.state not in ('paid'): raise UserError(_("You cannot confirm all orders of this session, because they don't have the 'paid' status")) order.action_pos_order_done() diff --git a/addons/procurement/views/product_views.xml b/addons/procurement/views/product_views.xml index e11fc82092a..9e4e0567846 100644 --- a/addons/procurement/views/product_views.xml +++ b/addons/procurement/views/product_views.xml @@ -10,7 +10,7 @@ <xpath expr="//button[@name='toggle_active']" position="before"> <button class="oe_stat_button" name="%(procurement.procurement_order_action_product_template_stat)d" icon="fa-truck" string="Procurements" type="action" - groups="stock.group_adv_location" + groups="base.group_no_one" attrs="{'invisible':[('type', '=', 'service')]}"/> </xpath> </field> @@ -24,7 +24,7 @@ <xpath expr="//button[@name='toggle_active']" position="before"> <button class="oe_stat_button" name="%(procurement.procurement_order_action_product_product_stat)d" icon="fa-truck" string="Procurements" type="action" - groups="stock.group_adv_location" + groups="base.group_no_one" attrs="{'invisible':[('type', '=', 'service')]}"/> </xpath> </field> diff --git a/addons/product/models/product.py b/addons/product/models/product.py index 92ac868376b..58bcde63f0a 100644 --- a/addons/product/models/product.py +++ b/addons/product/models/product.py @@ -552,7 +552,7 @@ class ProductProduct(models.Model): PriceHistory.create({ 'product_id': product.id, 'cost': value, - 'company_id': self._context.get('force_compay', self.env.user.company_id.id), + 'company_id': self._context.get('force_company', self.env.user.company_id.id), }) @api.multi diff --git a/addons/product/models/res_partner.py b/addons/product/models/res_partner.py index 8dc745eea1a..d307bd6ab09 100644 --- a/addons/product/models/res_partner.py +++ b/addons/product/models/res_partner.py @@ -31,7 +31,8 @@ class Partner(models.Model): # update at each change country, and so erase old pricelist if self.property_product_pricelist or (actual and default_for_country and default_for_country.id != actual.id): - self.env['ir.property'].sudo().set_multi( + # keep the company of the current user before sudo + self.env['ir.property'].with_context(force_company=self.env.user.company_id.id).sudo().set_multi( 'property_product_pricelist', self._name, {self.id: self.property_product_pricelist or default_for_country.id}, diff --git a/addons/product_expiry/models/production_lot.py b/addons/product_expiry/models/production_lot.py index 95e37a31deb..cfaf7984d28 100644 --- a/addons/product_expiry/models/production_lot.py +++ b/addons/product_expiry/models/production_lot.py @@ -44,7 +44,10 @@ class StockProductionLot(models.Model): # Assign dates according to products data @api.model def create(self, vals): - vals.update(self._get_dates(vals.get('product_id'))) + dates = self._get_dates(vals.get('product_id')) + for d in dates.keys(): + if not vals.get(d): + vals[d] = dates[d] return super(StockProductionLot, self).create(vals) @api.onchange('product_id') diff --git a/addons/product_extended/models/product.py b/addons/product_extended/models/product.py index 97d6f34b637..11a7bdc07ef 100644 --- a/addons/product_extended/models/product.py +++ b/addons/product_extended/models/product.py @@ -36,11 +36,11 @@ class ProductProduct(models.Model): def _calc_price(self, bom): price = 0.0 workcenter_cost = 0.0 - for sbom in bom.bom_line_ids: - my_qty = sbom.product_qty + result, result2 = bom.explode(self, 1) + for sbom, sbom_data in result2: if not sbom.attribute_value_ids: # No attribute_value_ids means the bom line is not variant specific - price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * my_qty + price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty'] if bom.routing_id: total_cost = 0.0 for order in bom.routing_id.operation_ids: @@ -49,5 +49,5 @@ class ProductProduct(models.Model): price += bom.product_uom_id._compute_price(workcenter_cost, bom.product_id.uom_id) # Convert on product UoM quantities if price > 0: - price = bom.product_uom_id._compute_price(price / bom.product_qty, bom.product_id.uom_id) + price = bom.product_uom_id._compute_price(price / bom.product_qty, self.uom_id) return price diff --git a/addons/project/data/project_mail_template_data.xml b/addons/project/data/project_mail_template_data.xml index a1a326bd35f..1035d113dd6 100644 --- a/addons/project/data/project_mail_template_data.xml +++ b/addons/project/data/project_mail_template_data.xml @@ -93,11 +93,10 @@ <span style="font-style:italic;">You’re not alone</span> </p> <p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;"> - <span>We would be delighted to assist you along the way. Contact us at - <a href="mailto:help@odoo.com"><span style="text-decoration:underline;"> - help@odoo.com</span></a> if you have any question. You can also discover - how to get the best out of Odoo Project with our </span> - <a href="https://www.odoo.com/documentation/user/9.0/project.html"> + <span>We would be delighted to assist you along the way. Contact us through our + <a href="https://www.odoo.com/help">support form</a> if you have any question. + You can also discover how to get the best out of Odoo Project with our </span> + <a href="https://www.odoo.com/documentation/user/10.0/project.html"> <span style="text-decoration:underline;">User Documentation</span></a> </span><span> or with our </span> <a href="https://www.odoo.com/documentation"> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index 2569283bb45..08f3b80c8c7 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -238,12 +238,10 @@ <kanban class="oe_background_grey o_kanban_dashboard o_project_kanban" on_create="project.open_create_project"> <field name="name"/> <field name="use_tasks"/> - <field name="user_id"/> <field name="color"/> <field name="task_count"/> <field name="task_needaction_count"/> <field name="label_tasks"/> - <field name="task_ids"/> <field name="alias_id"/> <field name="alias_name"/> <field name="alias_domain"/> diff --git a/addons/project_issue/report/project_issue_report.py b/addons/project_issue/report/project_issue_report.py index c81f5ccc23c..70389f603e8 100644 --- a/addons/project_issue/report/project_issue_report.py +++ b/addons/project_issue/report/project_issue_report.py @@ -41,7 +41,7 @@ class ProjectIssueReport(models.Model): c.working_hours_open, c.working_hours_close, c.stage_id, - date(c.date_closed) as date_closed, + c.date_closed as date_closed, c.company_id as company_id, c.priority as priority, c.project_id as project_id, diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py index d8821da079b..a7971764734 100644 --- a/addons/purchase/models/account_invoice.py +++ b/addons/purchase/models/account_invoice.py @@ -38,12 +38,12 @@ class AccountInvoice(models.Model): if float_compare(qty, 0.0, precision_rounding=line.product_uom.rounding) <= 0: qty = 0.0 taxes = line.taxes_id - invoice_line_tax_ids = self.purchase_id.fiscal_position_id.map_tax(taxes) + invoice_line_tax_ids = line.order_id.fiscal_position_id.map_tax(taxes) invoice_line = self.env['account.invoice.line'] data = { 'purchase_line_id': line.id, - 'name': self.purchase_id.name+': '+line.name, - 'origin': self.purchase_id.origin, + 'name': line.order_id.name+': '+line.name, + 'origin': line.order_id.origin, 'uom_id': line.product_uom.id, 'product_id': line.product_id.id, 'account_id': invoice_line.with_context({'journal_id': self.journal_id.id, 'type': 'in_invoice'})._default_account(), @@ -54,7 +54,7 @@ class AccountInvoice(models.Model): 'analytic_tag_ids': line.analytic_tag_ids.ids, 'invoice_line_tax_ids': invoice_line_tax_ids.ids } - account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, self.purchase_id.fiscal_position_id, self.env.user.company_id) + account = invoice_line.get_invoice_line_account('in_invoice', line.product_id, line.order_id.fiscal_position_id, self.env.user.company_id) if account: data['account_id'] = account.id return data diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index 0b13e4e144e..c46557ee51f 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -640,7 +640,7 @@ class PurchaseOrderLine(models.Model): sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel') existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom) procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, line.product_uom) - existing_proc_qty - if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0: + if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0 and float_compare(diff_quantity, 0.0, precision_rounding=line.product_uom.rounding) > 0: tmp = template.copy() tmp.update({ 'product_uom_qty': min(procurement_qty, diff_quantity), @@ -865,7 +865,7 @@ class ProcurementOrder(models.Model): """Return the datetime value to use as Order Date (``date_order``) for the Purchase Order created to satisfy the given procurement. """ self.ensure_one() - seller_delay = int(self.product_id._select_seller().delay) + seller_delay = int(self.product_id._select_seller(quantity=self.product_qty, uom_id=self.product_uom).delay) return schedule_date - relativedelta(days=seller_delay) @api.multi diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 93a9cf3c809..e057578a0cc 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -99,7 +99,6 @@ class Report(models.Model): context = dict(self.env.context, inherit_branding=True) # Tell QWeb to brand the generated html - view_obj = self.env['ir.ui.view'] # Browse the user instead of using the sudo self.env.user user = self.env['res.users'].browse(self.env.uid) website = None @@ -108,6 +107,7 @@ class Report(models.Model): website = request.website context = dict(context, translatable=context.get('lang') != request.website.default_lang_code) + view_obj = self.env['ir.ui.view'].with_context(context) values.update( time=time, context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t), diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml index d539800b5f7..05062800258 100644 --- a/addons/sale/views/sale_views.xml +++ b/addons/sale/views/sale_views.xml @@ -683,6 +683,19 @@ </field> </record> + <record model="ir.ui.view" id="account_invoice_line_tree"> + <field name="name">account.invoice.line.tree</field> + <field name="model">account.invoice.line</field> + <field name="inherit_id" ref="account.view_invoice_line_tree"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='discount']" position="attributes"> + <attribute name="groups">sale.group_discount_per_so_line</attribute> + </xpath> + </data> + </field> + </record> + <record model="ir.ui.view" id="account_invoice_line_form"> <field name="name">account.invoice.line.form</field> <field name="model">account.invoice.line</field> @@ -692,6 +705,22 @@ <xpath expr="//group/group/field[@name='product_id']" position="after"> <field name="layout_category_id" groups="sale.group_sale_layout"/> </xpath> + <xpath expr="//field[@name='discount']" position="attributes"> + <attribute name="groups">sale.group_discount_per_so_line</attribute> + </xpath> + </data> + </field> + </record> + + <record model="ir.ui.view" id="account_invoice_supplier_form"> + <field name="name">account.invoice.supplier.form</field> + <field name="model">account.invoice</field> + <field name="inherit_id" ref="account.invoice_supplier_form"/> + <field name="arch" type="xml"> + <data> + <xpath expr="//field[@name='discount']" position="attributes"> + <attribute name="groups">sale.group_discount_per_so_line</attribute> + </xpath> </data> </field> </record> diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index f8117365fee..f3c05e4dcb1 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -165,8 +165,8 @@ </group> <group name="email_template_and_project" position="before"> <group name="sale_condition" string="Sale Conditions"> - <label for="warranty" groups="stock.group_stock_production_lot"/> - <div groups="stock.group_stock_production_lot"> + <label for="warranty" groups="stock.group_production_lot"/> + <div groups="stock.group_production_lot"> <field name="warranty" class="oe_inline"/> months </div> <label for="sale_delay"/> @@ -178,6 +178,29 @@ </field> </record> + <record id="product_template_view_form_procurement_button" model="ir.ui.view"> + <field name="name">product.template.stock.stat.button.group</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="procurement.product_template_view_form_procurement_button"/> + <field name="arch" type="xml"> + <xpath expr="//button[@name='%(procurement.procurement_order_action_product_template_stat)d']" position="attributes"> + <attribute name="groups">base.group_no_one,stock.group_adv_location</attribute> + </xpath> + </field> + </record> + + <record id="product_product_view_form_procurement_button" model="ir.ui.view"> + <field name="name">product.product.stock.stat.button.group</field> + <field name="model">product.product</field> + <field name="inherit_id" ref="procurement.product_product_view_form_procurement_button"/> + <field name="arch" type="xml"> + <xpath expr="//button[@name='%(procurement.procurement_order_action_product_product_stat)d']" position="attributes"> + <attribute name="groups">base.group_no_one,stock.group_adv_location</attribute> + </xpath> + </field> + </record> + + <record model="ir.ui.view" id="product_template_kanban_stock_view"> <field name="name">Product Template Kanban Stock</field> <field name="model">product.template</field> diff --git a/addons/stock/views/stock_inventory_views.xml b/addons/stock/views/stock_inventory_views.xml index 16d73f8a68c..1e3c4c05933 100644 --- a/addons/stock/views/stock_inventory_views.xml +++ b/addons/stock/views/stock_inventory_views.xml @@ -94,7 +94,7 @@ <kanban class="o_kanban_mobile"> <field name="product_id" domain="[('type','=','product')]"/> <field name="product_uom_id" groups="product.group_uom" string="UoM"/> - <field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_locations"/> + <field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations"/> <field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/> <field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]"/> <field name="partner_id"/> diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml index 750fc8a4f8d..a564116407f 100644 --- a/addons/stock/views/stock_picking_views.xml +++ b/addons/stock/views/stock_picking_views.xml @@ -279,8 +279,8 @@ <field name="owner_id" groups="stock.group_tracking_owner"/> <field name="location_id" domain="[('id', 'child_of', parent.location_id)]" invisible="1"/> <field name="location_dest_id" domain="[('id', 'child_of', parent.location_dest_id)]" invisible="1"/> - <field name="from_loc" groups="stock.group_locations,stock.group_tracking_lot"/> - <field name="to_loc" groups="stock.group_locations,stock.group_tracking_lot"/> + <field name="from_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/> + <field name="to_loc" groups="stock.group_stock_multi_locations,stock.group_tracking_lot"/> <field name="result_package_id" groups="stock.group_tracking_lot" context="{'default_location_id': location_dest_id}" invisible="1"/> <field name="state" invisible="1"/> <field name="product_qty" readonly="1" attrs="{'required': [('product_id', '!=', False)]}"/> diff --git a/addons/stock/views/stock_scrap_views.xml b/addons/stock/views/stock_scrap_views.xml index 8286537a5f7..3b522dc22a5 100644 --- a/addons/stock/views/stock_scrap_views.xml +++ b/addons/stock/views/stock_scrap_views.xml @@ -47,7 +47,7 @@ <label for="scrap_qty"/> <div class="o_row"> <field name="scrap_qty"/> - <field name="product_uom_id" groups="stock.product_group_uom"/> + <field name="product_uom_id" groups="product.group_uom"/> </div> </group> <group> @@ -76,7 +76,7 @@ <field name="create_date" string="Create Date"/> <field name="product_id"/> <field name="scrap_qty"/> - <field name="product_uom_id" groups="stock.product_group_uom"/> + <field name="product_uom_id" groups="product.group_uom"/> <field name="location_id" groups="stock.group_stock_multi_locations"/> <field name="scrap_location_id" groups="stock.group_stock_multi_locations"/> <field name="state"/> @@ -111,7 +111,7 @@ <label for="scrap_qty"/> <div class="o_row"> <field name="scrap_qty"/> - <field name="product_uom_id" groups="stock.product_group_uom"/> + <field name="product_uom_id" groups="product.group_uom"/> </div> </group> <group> diff --git a/addons/stock_account/report/stock_history_views.xml b/addons/stock_account/report/stock_history_views.xml index 888ac577e26..085a73fe319 100644 --- a/addons/stock_account/report/stock_history_views.xml +++ b/addons/stock_account/report/stock_history_views.xml @@ -58,4 +58,10 @@ </search> </field> </record> + <record id="action_stock_history" model="ir.actions.act_window"> + <field name="name">Stock History</field> + <field name="res_model">stock.history</field> + <field name="view_type">form</field> + <field name="view_mode">tree,graph,pivot</field> + </record> </odoo> diff --git a/addons/stock_account/wizard/wizard_valuation_history.py b/addons/stock_account/wizard/wizard_valuation_history.py index cda89d0a243..729b3d429db 100644 --- a/addons/stock_account/wizard/wizard_valuation_history.py +++ b/addons/stock_account/wizard/wizard_valuation_history.py @@ -23,15 +23,21 @@ class WizardValuationHistory(models.TransientModel): history_date=self.date, search_default_group_by_product=True, search_default_group_by_location=True) - return { - 'domain': "[('date', '<=', '" + self.date + "')]", - 'name': _('Stock Value At Date'), - 'view_type': 'form', - 'view_mode': 'tree', - 'res_model': 'stock.history', - 'type': 'ir.actions.act_window', - 'context': ctx, - } + + action = self.env['ir.model.data'].xmlid_to_object('stock_account.action_stock_history') + if not action: + action = { + 'view_type': 'form', + 'view_mode': 'tree,graph,pivot', + 'res_model': 'stock.history', + 'type': 'ir.actions.act_window', + } + else: + action = action[0].read()[0] + + action['domain'] = "[('date', '<=', '" + self.date + "')]" + action['name'] = _('Stock Value At Date') + action['context'] = ctx else: action = self.env.ref('stock.quantsact').read()[0] - return action + return action diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 930a8233eb5..866241fe4f4 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1214,7 +1214,7 @@ class Export(http.Controller): info = {} fields = self.fields_get(model) if ".id" in export_fields: - fields['.id'] = fields.pop('id', {'string': 'ID'}) + fields['.id'] = fields.get('id', {'string': 'ID'}) # To make fields retrieval more efficient, fetch all sub-fields of a # given field at the same time. Because the order in the export list is diff --git a/addons/web/static/src/js/framework/formats.js b/addons/web/static/src/js/framework/formats.js index d084b285f6c..479fa1082cd 100644 --- a/addons/web/static/src/js/framework/formats.js +++ b/addons/web/static/src/js/framework/formats.js @@ -161,26 +161,30 @@ function parse_value (value, descriptor, value_if_empty) { return parse_value(value, {type: "float"}); case 'datetime': var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero, moment.ISO_8601], true); - if (datetime.isValid()) + if (datetime.isValid() && datetime.year() >= 1900) return time.datetime_to_str(datetime.toDate()); datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero, moment.ISO_8601]); if (datetime.isValid()) { if (datetime.year() === 0) { datetime.year(moment.utc().year()); } - return time.datetime_to_str(datetime.toDate()); + if (datetime.year() >= 1900) { + return time.datetime_to_str(datetime.toDate()); + } } throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value)); case 'date': var date = moment(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601], true); - if (date.isValid()) + if (date.isValid() && date.year() >= 1900) return time.date_to_str(date.toDate()); date = moment(value, [date_pattern, date_pattern_wo_zero, moment.ISO_8601]); if (date.isValid()) { if (date.year() === 0) { date.year(moment.utc().year()); } - return time.date_to_str(date.toDate()); + if (date.year() >= 1900) { + return time.date_to_str(date.toDate()); + } } throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value)); case 'time': diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index a15b588a297..46136e21565 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -1062,8 +1062,10 @@ var One2ManyListView = X2ManyListView.extend({ this._dataset_changed = false; }); - this.on('warning', this, function(e) { // In case of a one2many, we do not want any warning which comes from the editor - e.stop_propagation(); + this.on('warning', this, function(e) { // In case of editable list view, we do not want any warning which comes from the editor + if (this.editable()) { + e.stop_propagation(); + } }); }, do_add_record: function () { diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 2468db8cbcf..572fcbff95f 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1502,20 +1502,16 @@ ListView.Groups = Class.extend({ }); }, setup_resequence_rows: function (list, dataset) { + var sequence_field = _(this.columns).findWhere({'widget': 'handle'}); + var seqname = sequence_field ? sequence_field.name : 'sequence'; + // drag and drop enabled if list is not sorted (unless it is sorted by - // sequence (ASC)), and there is a visible column with @widget=handle - // or "sequence" column in the view. - if ((dataset.sort && dataset.sort() && dataset.sort() !== 'sequence' - && dataset.sort() !== 'sequence ASC') - || !_(this.columns).any(function (column) { - return column.widget === 'handle' - || column.name === 'sequence'; })) { + // its sequence field (ASC)), and there is a visible column with + // @widget=handle or "sequence" column in the view. + if ((dataset.sort && [seqname, seqname + 'ASC', ''].indexOf(dataset.sort()) === -1) + || !_(this.columns).findWhere({'name': seqname})) { return; } - var sequence_field = _(this.columns).find(function (c) { - return c.widget === 'handle'; - }); - var seqname = sequence_field ? sequence_field.name : 'sequence'; // ondrop, move relevant record & fix sequences list.$current.sortable({ diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js index 30adbd64017..d3135f86ba6 100644 --- a/addons/web/static/src/js/widgets/data_export.js +++ b/addons/web/static/src/js/widgets/data_export.js @@ -408,6 +408,7 @@ var DataExport = Dialog.extend({ }, add_field: function(field_id, string) { var $field_list = this.$('.o_fields_list'); + field_id = this.records[field_id].value || field_id; if($field_list.find("option[value='" + field_id + "']").length === 0) { $field_list.append(new Option(string, field_id)); } diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index c0a9b2c372f..e1f49eca005 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -269,7 +269,7 @@ <div class="form-group field-password"> <label for="password" class="control-label">Password</label> - <input type="password" name="password" id="password" class="form-control" required="required" t-att-autofocus="'autofocus' if login else None"/> + <input type="password" name="password" id="password" class="form-control" required="required" t-att-autofocus="'autofocus' if login else None" maxlength="4096"/> </div> <p class="alert alert-danger" t-if="error"> diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 5c344fbbcd1..9d7304caf75 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -134,7 +134,7 @@ dom.hasProgrammaticStyle = function (node) { }; dom.mergeFilter = function (prev, cur, parent) { // merge text nodes - if (prev && (dom.isText(prev) || ("H1 H2 H3 H4 H5 H6 LI P".indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) { + if (prev && (dom.isText(prev) || (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P'].indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) { return true; } if (prev && prev.tagName === "P" && dom.isText(cur)) { diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index f5054202f61..012802649f1 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -371,7 +371,7 @@ class PageConverter(werkzeug.routing.PathConverter): query = query and query.startswith('website.') and query[8:] or query if query: domain += [('key', 'like', query)] - website_id = request.context.get('website_id') or request.registry['website'].search(cr, uid, [], limit=1)[0] + website_id = request.context.get('website_id') or request.env['website'].search([], limit=1).id domain += ['|', ('website_id', '=', website_id), ('website_id', '=', False)] views = View.search_read(domain, fields=['key', 'priority', 'write_date'], order='name') diff --git a/addons/website/models/website.py b/addons/website/models/website.py index a747b85726b..f630eef13a9 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -527,7 +527,7 @@ class Website(models.Model): of the same. :rtype: list({name: str, url: str}) """ - request.context = dict(request.context, **context) + request.context = dict(request.context, **self.env.context) router = request.httprequest.app.get_db_router(request.db) # Force enumeration to be performed as public user url_set = set() diff --git a/addons/website/static/src/less/website.snippets.less b/addons/website/static/src/less/website.snippets.less index 1a290e65767..4a9e304299f 100644 --- a/addons/website/static/src/less/website.snippets.less +++ b/addons/website/static/src/less/website.snippets.less @@ -112,7 +112,6 @@ table.table_desc tr td { height: 100%; .item { height: 100%; - background-size: cover; } .carousel-control { cursor: pointer; diff --git a/addons/website/static/src/less/website.wrapwrap.less b/addons/website/static/src/less/website.wrapwrap.less index 9151e26e7ba..e572a11a975 100644 --- a/addons/website/static/src/less/website.wrapwrap.less +++ b/addons/website/static/src/less/website.wrapwrap.less @@ -37,6 +37,16 @@ footer { display: none!important; } +.o_text_overflow { + .text-overflow() +} + +.o_div_text_overflow * > div { + // .text-overflow() we don't want wordwrap + overflow: hidden; + text-overflow: ellipsis; +} + // Odoo options classes .jumbotron { diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 6cce24534a4..c791d330435 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -77,7 +77,7 @@ <t t-call="web.layout"> <t t-set="html_data" t-value="{ 'lang': lang and lang.replace('_', '-'), - 'data-website-id': website.id if (editable or translatable) and website else None, + 'data-website-id': website.id if website else None, 'data-editable': '1' if editable else None, 'data-translatable': '1' if translatable else None, 'data-edit_translations': '1' if edit_translations else None, diff --git a/addons/website_customer/controllers/main.py b/addons/website_customer/controllers/main.py index c05fb904f55..b69ffaf3e29 100644 --- a/addons/website_customer/controllers/main.py +++ b/addons/website_customer/controllers/main.py @@ -42,7 +42,7 @@ class WebsiteCustomer(http.Controller): if tag_id: tag_id = unslug(tag_id)[1] or 0 - domain += [('tag_ids', 'in', tag_id)] + domain += [('website_tag_ids', 'in', tag_id)] # group by country, based on customers found with the search(domain) countries = Partner.sudo().read_group(domain, ["id", "country_id"], groupby="country_id", orderby="country_id") diff --git a/addons/website_event_sale/views/website_event_sale_templates.xml b/addons/website_event_sale/views/website_event_sale_templates.xml index edb2b1583d2..d0005892fd6 100644 --- a/addons/website_event_sale/views/website_event_sale_templates.xml +++ b/addons/website_event_sale/views/website_event_sale_templates.xml @@ -51,7 +51,11 @@ <span t-field="ticket.price_reduce" t-options='{ "widget": "monetary", "display_currency": website.pricelist_id.currency_id - }'/> + }' groups="sale.group_show_price_subtotal"/> + <span t-field="ticket.price_reduce_taxinc" t-options='{ + "widget": "monetary", + "display_currency": website.pricelist_id.currency_id + }' groups="sale.group_show_price_total"/> <span itemprop="price" style="display:none;" t-esc="ticket.price"/> <span itemprop="priceCurrency" style="display:none;" t-esc="website.pricelist_id.currency_id.name"/> </t> diff --git a/addons/website_hr/views/hr_employee_views.xml b/addons/website_hr/views/hr_employee_views.xml index cc38353ca06..702bad88009 100644 --- a/addons/website_hr/views/hr_employee_views.xml +++ b/addons/website_hr/views/hr_employee_views.xml @@ -8,7 +8,7 @@ <field name="arch" type="xml"> <button name="toggle_active" position="before"> <button class="oe_stat_button" name="website_publish_button" - type="object" icon="fa-globe" groups="base.group_hr_user"> + type="object" icon="fa-globe" groups="hr.group_hr_user"> <field name="website_published" widget="website_button"/> </button> </button> diff --git a/addons/website_mail/data/mail_channel_data.xml b/addons/website_mail/data/mail_channel_data.xml index 27f83e71beb..16081671efe 100644 --- a/addons/website_mail/data/mail_channel_data.xml +++ b/addons/website_mail/data/mail_channel_data.xml @@ -93,10 +93,9 @@ <span style="font-style:italic;">You’re not alone</span> </p> <p dir="ltr" style="font-size:13px;margin-top:0pt;margin-bottom:8pt;"> - <span>We would be delighted to assist you along the way. Contact us at - <a href="mailto:help@odoo.com"><span style="text-decoration:underline;"> - help@odoo.com</span></a> if you have any question. You can also discover - how to get the best out of Odoo with our </span> + <span>We would be delighted to assist you along the way. Contact us through our + <a href="https://www.odoo.com/help">support form</a> if you have any question. + You can also discover how to get the best out of Odoo with our </span> <a href="https://www.odoo.com/documentation/user"> <span style="text-decoration:underline;">User Documentation</span></a> </span><span> or with our </span> diff --git a/addons/website_portal/views/website_portal_templates.xml b/addons/website_portal/views/website_portal_templates.xml index 67a240a8343..3e5924938a7 100644 --- a/addons/website_portal/views/website_portal_templates.xml +++ b/addons/website_portal/views/website_portal_templates.xml @@ -133,7 +133,7 @@ </template> <template id="contact" name="Contact"> - <h3 class="page-header">Contact</h3> + <h3 class="page-header">Saleman</h3> <h5 t-esc="sales_rep.name"/> <p><a t-att-href="'mailto:'+sales_rep.email" t-esc="sales_rep.email"/></p> <p t-esc="sales_rep.phone"/> diff --git a/addons/website_portal_sale/models/payment.py b/addons/website_portal_sale/models/payment.py index ec601dd1d36..d94b429bf18 100644 --- a/addons/website_portal_sale/models/payment.py +++ b/addons/website_portal_sale/models/payment.py @@ -16,8 +16,13 @@ class PaymentTransaction(models.Model): def _generate_and_pay_invoice(self, tx, acquirer_name): tx.sale_order_id._force_lines_to_invoice_policy_order() - created_invoice = tx.sale_order_id.action_invoice_create() - created_invoice = self.env['account.invoice'].browse(created_invoice) + # force company to ensure journals/accounts etc. are correct + # company_id needed for default_get on account.journal + # force_company needed for company_dependent fields + ctx_company = {'company_id': tx.sale_order_id.company_id.id, + 'force_company': tx.sale_order_id.company_id.id} + created_invoice = tx.sale_order_id.with_context(**ctx_company).action_invoice_create() + created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company) if created_invoice: _logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)', diff --git a/addons/website_sale/__manifest__.py b/addons/website_sale/__manifest__.py index 45fa7f8f8ec..a02e4627ce1 100644 --- a/addons/website_sale/__manifest__.py +++ b/addons/website_sale/__manifest__.py @@ -12,6 +12,8 @@ Odoo E-Commerce """, 'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_mail', 'website_form', 'rating'], 'data': [ + 'security/ir.model.access.csv', + 'security/website_sale.xml', 'data/data.xml', 'data/web_planner_data.xml', 'views/views.xml', @@ -24,8 +26,6 @@ Odoo E-Commerce 'views/snippets.xml', 'views/report_shop_saleorder.xml', 'views/res_config_view.xml', - 'security/ir.model.access.csv', - 'security/website_sale.xml', ], 'demo': [ 'data/demo.xml', diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index d22932915a0..1d7c24abcb1 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -303,6 +303,27 @@ class Website(models.Model): return self.env.ref(DEFAULT_PAYMENT_TERM, False).id or partner.property_payment_term_id.id @api.multi + def _prepare_sale_order_values(self, partner, pricelist): + self.ensure_one() + affiliate_id = request.session.get('affiliate_id') + salesperson_id = affiliate_id if self.env['res.users'].sudo().browse(affiliate_id).exists() else request.website.salesperson_id.id + addr = partner.address_get(['delivery', 'invoice']) + values = { + 'partner_id': partner.id, + 'pricelist_id': pricelist.id, + 'payment_term_id': self.sale_get_payment_term(partner), + 'team_id': self.salesteam_id.id, + 'partner_invoice_id': addr['invoice'], + 'partner_shipping_id': addr['delivery'], + 'user_id': salesperson_id or self.salesperson_id.id, + } + company = self.company_id or pricelist.company_id + if company: + values['company_id'] = company.id + + return values + + @api.multi def sale_get_order(self, force_create=False, code=None, update_pricelist=False, force_pricelist=False): """ Return the current sales order after mofications specified by params. :param bool force_create: Create sales order if not already existing @@ -337,25 +358,8 @@ class Website(models.Model): # create so if needed if not sale_order and (force_create or code): # TODO cache partner_id session - affiliate_id = request.session.get('affiliate_id') - if self.env['res.users'].sudo().browse(affiliate_id).exists(): - salesperson_id = affiliate_id - else: - salesperson_id = request.website.salesperson_id.id - addr = partner.address_get(['delivery', 'invoice']) - so_data = { - 'partner_id': partner.id, - 'pricelist_id': pricelist_id, - 'payment_term_id': self.sale_get_payment_term(partner), - 'team_id': self.salesteam_id.id, - 'partner_invoice_id': addr['invoice'], - 'partner_shipping_id': addr['delivery'], - 'user_id': salesperson_id or self.salesperson_id.id, - } - company = self.company_id or self.env['product.pricelist'].browse(pricelist_id).sudo().company_id - if company: - so_data['company_id'] = company.id - + pricelist = self.env['product.pricelist'].browse(pricelist_id).sudo() + so_data = self._prepare_sale_order_values(partner, pricelist) sale_order = self.env['sale.order'].sudo().create(so_data) # set fiscal position diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 1205951b039..1bb49194f37 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -233,7 +233,7 @@ odoo.define('website_sale.website_sale', function (require) { var precision = 2; if ($(".decimal_precision").length) { - precision = parseInt($(".decimal_precision").first().data('precision')); + precision = parseInt($(".decimal_precision").last().data('precision')); } var formatted = _.str.sprintf('%.' + precision + 'f', price).split('.'); formatted[0] = utils.insert_thousand_seps(formatted[0]); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 71b110dfca0..bec827eac68 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1735,14 +1735,14 @@ <div class="panel panel-info break-word"> <div class="panel-heading">Bill To:</div> <div class="panel-body"> - <div t-field="order.partner_invoice_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone', 'email']}" /> + <div class='o_div_text_overflow' t-field="order.partner_invoice_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone', 'email']}" /> </div> </div> <t t-if="not order.only_services"> <div class="panel panel-info break-word"> <div class="panel-heading">Ship To:</div> <div class="panel-body"> - <div t-field="order.partner_shipping_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone']}" /> + <div class='o_div_text_overflow' t-field="order.partner_shipping_id" t-options="{'widget': 'contact','fields': ['address', 'name', 'phone']}" /> </div> </div> </t> diff --git a/addons/website_sale_stock/__init__.py b/addons/website_sale_stock/__init__.py index 67dee8c60db..5f9ef702708 100644 --- a/addons/website_sale_stock/__init__.py +++ b/addons/website_sale_stock/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import models
\ No newline at end of file diff --git a/addons/website_sale_stock/models/__init__.py b/addons/website_sale_stock/models/__init__.py new file mode 100644 index 00000000000..b8b239729b6 --- /dev/null +++ b/addons/website_sale_stock/models/__init__.py @@ -0,0 +1 @@ +import website
\ No newline at end of file diff --git a/addons/website_sale_stock/models/website.py b/addons/website_sale_stock/models/website.py new file mode 100644 index 00000000000..b5c866c27bb --- /dev/null +++ b/addons/website_sale_stock/models/website.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from odoo import api, models + +class Website(models.Model): + _inherit = 'website' + + @api.multi + def _prepare_sale_order_values(self, partner, pricelist): + self.ensure_one() + values = super(Website, self)._prepare_sale_order_values(partner, pricelist) + if values['company_id']: + warehouses = self.env['stock.warehouse'].sudo().search([('company_id', '=', values['company_id'])], limit=1) + if warehouses: + values['warehouse_id'] = warehouses.id + return values diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 247094d605f..16604d1f5fd 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -476,7 +476,7 @@ class Slide(models.Model): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id): publish_template = slide.channel_id.publish_template_id - html_body = publish_template.with_context({'base_url': base_url}).render_template(publish_template.body_html, 'slide.slide', slide.id) + html_body = publish_template.with_context(base_url=base_url).render_template(publish_template.body_html, 'slide.slide', slide.id) subject = publish_template.render_template(publish_template.subject, 'slide.slide', slide.id) slide.channel_id.message_post( subject=subject, @@ -487,7 +487,7 @@ class Slide(models.Model): @api.one def send_share_email(self, email): base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') - return self.channel_id.share_template_id.with_context({'email': email, 'base_url': base_url}).send_mail(self.id) + return self.channel_id.share_template_id.with_context(email=email, base_url=base_url).send_mail(self.id) # -------------------------------------------------- # Parsing methods |
