diff options
| author | Christophe Simonis <chs@odoo.com> | 2017-02-10 18:41:06 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2017-02-10 18:41:06 +0100 |
| commit | a282d4ae7f27869a0b828e8562f8efd901e02799 (patch) | |
| tree | 1fa369ee7526a0a7ccee075f0d8232d2f4fac870 | |
| parent | b229e9cb8c1fdc83a1aa5abd51141e32aea59bcd (diff) | |
| parent | be9dfb331f90bd0352728c78e115bc3647f472e9 (diff) | |
[MERGE] forward port branch saas-14 up to be9dfb331f9
150 files changed, 1435 insertions, 536 deletions
diff --git a/addons/account/data/account_data.xml b/addons/account/data/account_data.xml index 84b208c3430..09daa42bbeb 100644 --- a/addons/account/data/account_data.xml +++ b/addons/account/data/account_data.xml @@ -51,6 +51,7 @@ <field name="prefix">A</field> <field eval="1" name="number_next"/> <field eval="1" name="number_increment"/> + <field eval="False" name="company_id"/> </record> <!-- Invoice requests (deprecated) diff --git a/addons/account/demo/account_invoice_demo.yml b/addons/account/demo/account_invoice_demo.yml index a925aefa6dd..e131aec2292 100644 --- a/addons/account/demo/account_invoice_demo.yml +++ b/addons/account/demo/account_invoice_demo.yml @@ -52,6 +52,27 @@ !python {model: account.invoice, id: demo_invoice_3}: self.action_invoice_open() - + Create an invoice that will generate a followup +- + !record {model: account.invoice, id: demo_invoice_followup}: + company_id: base.main_company + partner_id: base.res_partner_2 + user_id: base.user_demo + reference_type: none + payment_term_id: account.account_payment_term_immediate + type: 'out_invoice' + date_invoice: !eval (datetime.today() + timedelta(days=-15)).strftime('%Y-%m-%d') + invoice_line_ids: + - product_id: product.consu_delivery_02 + price_unit: 642.0 + quantity: 5 + - product_id: product.consu_delivery_03 + price_unit: 280.0 + quantity: 5.0 +- + !python {model: account.invoice, id: demo_invoice_followup}: + self.action_invoice_open() +- Set the context for supplier bills because the 'type' field is readonly in invoice form views, so it value won't be considered by the yaml import - !context diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 58c2b72ceb1..d5d5b7fb776 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -484,7 +484,6 @@ class AccountInvoice(models.Model): addr = self.partner_id.address_get(['delivery']) fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery']) - bank_id = p.bank_ids and p.bank_ids.ids[0] or False # If partner has no warning, check its company if p.invoice_warn == 'no-message' and p.parent_id: @@ -505,8 +504,12 @@ class AccountInvoice(models.Model): self.payment_term_id = payment_term_id self.fiscal_position_id = fiscal_position - if type in ('in_invoice', 'in_refund'): + if type in ('in_invoice', 'out_refund'): + bank_ids = p.commercial_partner_id.bank_ids + bank_id = bank_ids[0].id if bank_ids else False self.partner_bank_id = bank_id + return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}} + return {} @api.onchange('journal_id') @@ -524,7 +527,7 @@ class AccountInvoice(models.Model): self.date_due = self.date_due or self.date_invoice else: pterm = self.payment_term_id - pterm_list = pterm.with_context(currency_id=self.currency_id.id).compute(value=1, date_ref=date_invoice)[0] + pterm_list = pterm.with_context(currency_id=self.company_id.currency_id.id).compute(value=1, date_ref=date_invoice)[0] self.date_due = max(line[0] for line in pterm_list) @api.multi @@ -761,8 +764,9 @@ class AccountInvoice(models.Model): will be grouped together if the journal has the 'group line' option. Of course a module can add fields to invoice lines that would need to be tested too before merging lines or not.""" - return "%s-%s-%s-%s-%s-%s" % ( + return "%s-%s-%s-%s-%s-%s-%s" % ( invoice_line['account_id'], + invoice_line.get('tax_ids', 'False'), invoice_line.get('tax_line_id', 'False'), invoice_line.get('product_id', 'False'), invoice_line.get('analytic_account_id', 'False'), @@ -822,7 +826,7 @@ class AccountInvoice(models.Model): name = inv.name or '/' if inv.payment_term_id: - totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=inv.currency_id.id).compute(total, date_invoice)[0] + totlines = inv.with_context(ctx).payment_term_id.with_context(currency_id=company_currency.id).compute(total, date_invoice)[0] res_amount_currency = total_currency ctx['date'] = date_invoice for i, t in enumerate(totlines): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 0d2569593f1..1252aa647bf 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -709,22 +709,37 @@ class AccountMoveLine(models.Model): line_currency = (line.currency_id and line.amount_currency) and line.currency_id or company_currency amount_currency_str = "" total_amount_currency_str = "" - if line_currency != company_currency and target_currency != company_currency: + if line_currency != company_currency and target_currency == line_currency: + # The payment currency is the invoice currency, but they are different than the company currency + # We use the `amount_currency` computed during the invoice validation, at the invoice date + # to avoid exchange gain/loss + # e.g. an invoice of 100€ must be paid with 100€, whatever the company currency and the exchange rates total_amount = line.amount_currency actual_debit = debit > 0 and amount_currency or 0.0 actual_credit = credit > 0 and -amount_currency or 0.0 + currency = line_currency else: + # Either: + # - the invoice, payment, company currencies are all the same, + # - the payment currency is the company currency, but the invoice currency is different, + # - the invoice currency is the company currency, but the payment currency is different, + # - the invoice, payment and company currencies are all different. + # For the two first cases, we can simply use the debit/credit of the invoice move line, which are always in the company currency, + # and this is what the target need. + # For the two last cases, we can use the debit/credit which are in the company currency, and then change them to the target currency total_amount = abs(debit - credit) actual_debit = debit > 0 and amount or 0.0 actual_credit = credit > 0 and -amount or 0.0 - if line_currency != target_currency and target_currency != company_currency: + currency = company_currency + if line_currency != target_currency: amount_currency_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=line_currency) total_amount_currency_str = formatLang(self.env, total_amount, currency_obj=line_currency) + if currency != target_currency: ctx = context.copy() ctx.update({'date': target_date or line.date}) - total_amount = line_currency.with_context(ctx).compute(total_amount, target_currency) - actual_debit = line_currency.with_context(ctx).compute(actual_debit, target_currency) - actual_credit = line_currency.with_context(ctx).compute(actual_credit, target_currency) + total_amount = currency.with_context(ctx).compute(total_amount, target_currency) + actual_debit = currency.with_context(ctx).compute(actual_debit, target_currency) + actual_credit = currency.with_context(ctx).compute(actual_credit, target_currency) amount_str = formatLang(self.env, abs(actual_debit or actual_credit), currency_obj=target_currency) total_amount_str = formatLang(self.env, total_amount, currency_obj=target_currency) diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py index dbdd7c16335..937f689400f 100644 --- a/addons/account/models/account_payment.py +++ b/addons/account/models/account_payment.py @@ -21,7 +21,7 @@ class account_payment_method(models.Model): _name = "account.payment.method" _description = "Payment Methods" - name = fields.Char(required=True) + name = fields.Char(required=True, translate=True) code = fields.Char(required=True) # For internal identification payment_type = fields.Selection([('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True) @@ -400,7 +400,7 @@ 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 = self._compute_total_invoices_amount() + total_residual_company_signed = sum(invoice.residual_company_signed for invoice in self.invoice_ids) total_payment_company_signed = self.currency_id.with_context(date=self.payment_date).compute(self.amount, self.company_id.currency_id) if self.invoice_ids[0].type in ['in_invoice', 'out_refund']: amount_wo = total_payment_company_signed - total_residual_company_signed diff --git a/addons/account/security/account_security.xml b/addons/account/security/account_security.xml index d80c2797177..f9a4257df66 100644 --- a/addons/account/security/account_security.xml +++ b/addons/account/security/account_security.xml @@ -79,6 +79,13 @@ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> </record> + <record id="invoice_analysis_comp_rule" model="ir.rule"> + <field name="name">Invoice Analysis multi-company</field> + <field name="model_id" ref="model_account_invoice_report"/> + <field name="global" eval="True"/> + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + </record> + <record id="account_fiscal_position_comp_rule" model="ir.rule"> <field name="name">Account fiscal Mapping company rule</field> <field name="model_id" ref="model_account_fiscal_position"/> diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index ce0079f032e..fe1d6f21152 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -61,7 +61,7 @@ <group> <group> <field name="product_id" - context="{'partner_id': parent.partner_id}"/> + context="parent and {'partner_id': parent.partner_id}"/> <label for="quantity"/> <div> <field name="quantity" class="oe_inline"/> diff --git a/addons/account/views/account_report.xml b/addons/account/views/account_report.xml index 56f2a8adaa3..0ab423feea4 100644 --- a/addons/account/views/account_report.xml +++ b/addons/account/views/account_report.xml @@ -9,6 +9,7 @@ report_type="qweb-pdf" name="account.report_invoice" file="account.report_invoice" + attachment_use="True" attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/','')+'.pdf')" /> diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index 3161a48c032..e705f39e1af 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -1106,7 +1106,6 @@ <field name="move_id" required="False"/> <newline/> <field name="date_maturity"/> - <field name="date"/> <field name="blocked"/> <newline/> <field name="tax_ids" readonly="1"/> @@ -1195,7 +1194,7 @@ <filter string="Unposted" domain="[('move_id.state','=','draft')]" help="Unposted Journal Items"/> <filter name="posted" string="Posted" domain="[('move_id.state','=','posted')]" help="Posted Journal Items"/> <separator/> - <filter string="Unreconciled" domain="[('full_reconcile_id','=',False), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/> + <filter string="Unreconciled" domain="[('full_reconcile_id', '=', False), ('balance','!=', 0), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/> <separator/> <filter string="Payable" domain="[('account_id.internal_type', '=', 'payable')]" help="From Payable accounts" name="payable"/> <filter string="Receivable" domain="[('account_id.internal_type', '=', 'receivable')]" help="From Receivable accounts" name="receivable"/> diff --git a/addons/account/views/res_config_view.xml b/addons/account/views/res_config_view.xml index 278908c57f8..429de71fff7 100644 --- a/addons/account/views/res_config_view.xml +++ b/addons/account/views/res_config_view.xml @@ -86,7 +86,7 @@ <div class="o_setting_left_pane"> <field name="module_l10n_eu_service"/> </div> - <div class="o_setting_right_pane" > + <div class="o_setting_right_pane" name="l10n_eu_service_right_pane"> <label for="module_l10n_eu_service"/> <div class="text-muted"> Apply right VAT rates for digital products sold in EU diff --git a/addons/account/wizard/account_reconcile.py b/addons/account/wizard/account_reconcile.py index 3ca9d5cc47f..c0d1b29d210 100644 --- a/addons/account/wizard/account_reconcile.py +++ b/addons/account/wizard/account_reconcile.py @@ -36,7 +36,7 @@ class AccountMoveLineReconcile(models.TransientModel): credit = debit = 0 lines = self.env['account.move.line'].browse(context.get('active_ids', [])) for line in lines: - if not line.reconciled: + if not line.full_reconcile_id: credit += line.credit debit += line.debit precision = self.env.user.company_id.currency_id.decimal_places @@ -64,9 +64,13 @@ class AccountMoveLineReconcile(models.TransientModel): if aml.currency_id.id == currency: continue raise UserError(_('Operation not allowed. You can only reconcile entries that share the same secondary currency or that don\'t have one. Edit your journal items or make another selection before proceeding any further.')) + #Don't consider entrires that are already reconciled + move_lines_filtered = move_lines.filtered(lambda aml: not aml.reconciled) #Because we are making a full reconcilition in batch, we need to consider use cases as defined in the test test_manual_reconcile_wizard_opw678153 - #So we force the reconciliation in company currency only at first, then in second pass the amounts in secondary currency. - move_lines.with_context(skip_full_reconcile_check='amount_currency_excluded', manual_full_reconcile_currency=currency).reconcile() + #So we force the reconciliation in company currency only at first + move_lines_filtered.with_context(skip_full_reconcile_check='amount_currency_excluded', manual_full_reconcile_currency=currency).reconcile() + + #then in second pass the amounts in secondary currency, only if some lines are still not fully reconciled move_lines_filtered = move_lines.filtered(lambda aml: not aml.reconciled) if move_lines_filtered: move_lines_filtered.with_context(skip_full_reconcile_check='amount_currency_only', manual_full_reconcile_currency=currency).reconcile() @@ -123,13 +127,18 @@ class AccountMoveLineReconcileWriteoff(models.TransientModel): if aml.currency_id.id == currency: continue raise UserError(_('Operation not allowed. You can only reconcile entries that share the same secondary currency or that don\'t have one. Edit your journal items or make another selection before proceeding any further.')) + #Don't consider entrires that are already reconciled + move_lines_filtered = move_lines.filtered(lambda aml: not aml.reconciled) #Because we are making a full reconcilition in batch, we need to consider use cases as defined in the test test_manual_reconcile_wizard_opw678153 - #So we force the reconciliation in company currency only at first, then in second pass the amounts in secondary currency. + #So we force the reconciliation in company currency only at first, context['skip_full_reconcile_check'] = 'amount_currency_excluded' context['manual_full_reconcile_currency'] = currency - writeoff = move_lines.with_context(context).reconcile(self.writeoff_acc_id, self.journal_id) + writeoff = move_lines_filtered.with_context(context).reconcile(self.writeoff_acc_id, self.journal_id) + #then in second pass the amounts in secondary currency, only if some lines are still not fully reconciled move_lines_filtered = move_lines.filtered(lambda aml: not aml.reconciled) if move_lines_filtered: move_lines_filtered.with_context(skip_full_reconcile_check='amount_currency_only', manual_full_reconcile_currency=currency).reconcile() - (move_lines + writeoff).compute_full_after_batch_reconcile() + if not isinstance(writeoff, bool): + move_lines += writeoff + move_lines.compute_full_after_batch_reconcile() return {'type': 'ir.actions.act_window_close'} diff --git a/addons/account_accountant/views/res_config_view.xml b/addons/account_accountant/views/res_config_view.xml index e8db2e6bb6d..1375e72501e 100644 --- a/addons/account_accountant/views/res_config_view.xml +++ b/addons/account_accountant/views/res_config_view.xml @@ -21,6 +21,19 @@ <label string="Last Day" class="col-md-4 o_light_label"/> <field name="fiscalyear_last_month" class="col-md-4 oe_inline"/> <field name="fiscalyear_last_day" class="col-md-4 oe_inline text-center"/> + </div> + </div> + </div> + </div> + <div class="col-xs-12 col-md-6 o_setting_box" id="fiscalyear"> + <div class="o_setting_left_pane"/> + <div class="o_setting_right_pane"> + <label string="Entry Locking"/> + <div class="text-muted"> + Lock dates for period closing + </div> + <div class="content-group"> + <div class="row mt16"> <label for="fiscalyear_lock_date" class="col-md-6 o_light_label"/> <field name="fiscalyear_lock_date" class="col-md-6 oe_inline"/> <label for="period_lock_date" class="col-md-6 o_light_label"/> diff --git a/addons/crm/data/sales_config_settings_data.xml b/addons/crm/data/sales_config_settings_data.xml index 61bda872c46..be269cafec9 100644 --- a/addons/crm/data/sales_config_settings_data.xml +++ b/addons/crm/data/sales_config_settings_data.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - + <data noupdate='1'> <function id="default_generate_sales_team_alias" model="ir.values" name="set_default" - eval="('sales.config.settings', 'generate_sales_team_alias', True)"/> - + eval="('sale.config.settings', 'generate_sales_team_alias', True)"/> + </data> </odoo> diff --git a/addons/crm/models/crm_team.py b/addons/crm/models/crm_team.py index a8a215f906e..e0dfc08f25e 100644 --- a/addons/crm/models/crm_team.py +++ b/addons/crm/models/crm_team.py @@ -84,7 +84,7 @@ class Team(models.Model): @api.model def create(self, vals): - generate_alias_name = self.env['ir.values'].get_default('sales.config.settings', 'generate_sales_team_alias') + generate_alias_name = self.env['ir.values'].get_default('sale.config.settings', 'generate_sales_team_alias') if generate_alias_name and not vals.get('alias_name'): vals['alias_name'] = vals.get('name') return super(Team, self).create(vals) diff --git a/addons/crm/models/res_config.py b/addons/crm/models/res_config.py index 3282015b273..4d22ef0ebd8 100644 --- a/addons/crm/models/res_config.py +++ b/addons/crm/models/res_config.py @@ -30,7 +30,7 @@ class CRMSettings(models.TransientModel): @api.model def get_default_generate_sales_team_alias(self, fields): return { - 'generate_sales_team_alias': self.env['ir.values'].get_default('sales.config.settings', 'generate_sales_team_alias') + 'generate_sales_team_alias': self.env['ir.values'].get_default('sale.config.settings', 'generate_sales_team_alias') } @api.multi @@ -38,7 +38,7 @@ class CRMSettings(models.TransientModel): IrValues = self.env['ir.values'] if self.env['res.users'].has_group('base.group_erp_manager'): IrValues = IrValues.sudo() - IrValues.set_default('sales.config.settings', 'generate_sales_team_alias', self.generate_sales_team_alias) + IrValues.set_default('sale.config.settings', 'generate_sales_team_alias', self.generate_sales_team_alias) @api.model def get_default_alias_prefix(self, fields): diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 47f11c77f35..b0113aa0f31 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -583,6 +583,9 @@ <filter string="Overdue Activities" name="activities_overdue" domain="[('activity_ids.date_deadline', '<', context_today().strftime('%Y-%m-%d'))]" help="Show all opportunities for which the next action date is before today"/> + <filter string="Overdue Opportunities" name="overdue_opp" + domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]" + help="Show all opportunities with a deadline for which the next action date is before today"/> <separator/> <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction','=',True)]"/> <filter string="Lost" domain="[('active', '=', False)]"/> diff --git a/addons/crm/views/res_config_views.xml b/addons/crm/views/res_config_views.xml index f0f072a177a..69df0c5a712 100644 --- a/addons/crm/views/res_config_views.xml +++ b/addons/crm/views/res_config_views.xml @@ -39,7 +39,7 @@ <div attrs="{'invisible': [('alias_domain', 'not in', ['localhost', '', False])]}"> <button type="action" name="base_setup.action_general_configuration" - string="Setup your domain alias" icon="fa-arrow-right" class="oe_link"/> + string="Use an External Email Server" icon="fa-arrow-right" class="oe_link"/> </div> </div> </div> diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index d0d5ff5af4b..f572056dded 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -193,6 +193,10 @@ class Employee(models.Model): @api.multi def write(self, vals): + if 'address_home_id' in vals: + account_id = vals.get('bank_account_id') or self.bank_account_id.id + if account_id: + self.env['res.partner.bank'].browse(account_id).partner_id = vals['address_home_id'] tools.image_resize_images(vals) return super(Employee, self).write(vals) diff --git a/addons/hr/models/res_partner.py b/addons/hr/models/res_partner.py index 8772bc46040..f0b24ba42b5 100644 --- a/addons/hr/models/res_partner.py +++ b/addons/hr/models/res_partner.py @@ -16,8 +16,9 @@ class Partner(models.Model): 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)) + hr_suggestions = [{'id': user.partner_id.id, 'name': user.name, 'email': user.email} + for user in employee_group.users] + suggestions.append(hr_suggestions) return suggestions except AccessError: return suggestions diff --git a/addons/hr/views/hr_views.xml b/addons/hr/views/hr_views.xml index 9e4060e0eb2..45eb7e7fe2d 100644 --- a/addons/hr/views/hr_views.xml +++ b/addons/hr/views/hr_views.xml @@ -80,7 +80,7 @@ <field name="country_id" options='{"no_open": True, "no_create": True}'/> <field name="identification_id" groups="hr.group_hr_user"/> <field name="passport_id" groups="hr.group_hr_user"/> - <field name="bank_account_id"/> + <field name="bank_account_id" context="{'default_partner_id': address_home_id}"/> </group> <group string="Contact Information"> <field name="address_home_id" diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 6c830977301..fa2eadc2572 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -233,7 +233,7 @@ class HrExpense(models.Model): #convert eml into an osv-valid format lines = map(lambda x: (0, 0, expense._prepare_move_line(x)), move_lines) - move.write({'line_ids': lines}) + move.with_context(dont_create_taxes=True).write({'line_ids': lines}) expense.sheet_id.write({'account_move_id': move.id}) move.post() if expense.payment_mode == 'company_account': @@ -532,7 +532,6 @@ class HrExpenseSheet(models.Model): @api.multi def action_open_journal_entries(self): res = self.env['ir.actions.act_window'].for_xml_id('account', 'action_move_journal_line') - refs = [x.name for x in self] - res['domain'] = [('ref', 'in', refs)] + res['domain'] = [('id', 'in', self.mapped('account_move_id').ids)] res['context'] = {} return res diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 773202963ef..0fc37dd778f 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -500,7 +500,7 @@ </div> <div class="row"> <div class="col-xs-6 text-muted"> - <span><t t-esc="record.employee_id.value"/> <t t-esc="record.date.value"/></span> + <span><t t-esc="record.employee_id.value"/> <t t-esc="record.accounting_date.value"/></span> </div> <div class="col-xs-6"> <span class="pull-right text-right"> diff --git a/addons/hr_holidays/models/hr_holidays.py b/addons/hr_holidays/models/hr_holidays.py index 6532199e19b..b074517ddb6 100644 --- a/addons/hr_holidays/models/hr_holidays.py +++ b/addons/hr_holidays/models/hr_holidays.py @@ -120,7 +120,10 @@ class HolidaysType(models.Model): for record in self: name = record.name if not record.limit: - name = name + (' (%g remaining out of %g)' % (record.virtual_remaining_leaves or 0.0, record.max_leaves or 0.0)) + name = "%(name)s (%(count)s)" % { + 'name': name, + 'count': _('%g remaining out of %g') % (record.virtual_remaining_leaves or 0.0, record.max_leaves or 0.0) + } res.append((record.id, name)) return res diff --git a/addons/hw_escpos/escpos/printer.py b/addons/hw_escpos/escpos/printer.py index f994d946a02..e857fa2e365 100644 --- a/addons/hw_escpos/escpos/printer.py +++ b/addons/hw_escpos/escpos/printer.py @@ -82,7 +82,7 @@ class Usb(Escpos): def _raw(self, msg): """ Print any command sent in raw format """ - if len(msg) != self.device.write(self.out_ep, msg, self.interface): + if len(msg) != self.device.write(self.out_ep, msg, self.interface, timeout=5000): self.device.write(self.out_ep, self.errorText, self.interface) raise TicketNotPrinted() diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 4d0f1da89a4..10cc5610214 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -53,6 +53,7 @@ var LivechatButton = Widget.extend({ this.channel = null; this.chat_window = null; this.messages = []; + this.server_url = server_url; }, willStart: function () { @@ -115,16 +116,15 @@ var LivechatButton = Widget.extend({ } }, load_qweb_template: function(){ - return $.when( - $.get('/mail/static/src/xml/chat_window.xml'), - $.get('/mail/static/src/xml/thread.xml'), - $.get('/im_livechat/static/src/xml/im_livechat.xml') - ).then(function (chat_window, mail_thread, livechat) { - // results are triplets of [dom: XMLDocument, status: String, xhr: jqXHR] - QWeb.add_template(chat_window[0]); - QWeb.add_template(mail_thread[0]); - QWeb.add_template(livechat[0]); + var xml_files = ['/mail/static/src/xml/chat_window.xml', + '/mail/static/src/xml/thread.xml', + '/im_livechat/static/src/xml/im_livechat.xml']; + var defs = _.map(xml_files, function (tmpl) { + return session.rpc('/web/proxy/load', {path: tmpl}).then(function (xml) { + QWeb.add_template(xml); + }); }); + return $.when.apply($, defs); }, open_chat: _.debounce(function () { @@ -237,10 +237,11 @@ var LivechatButton = Widget.extend({ this.options.default_username; // Compute the avatar_url + msg.avatar_src = this.server_url; if (msg.author_id && msg.author_id[0]) { - msg.avatar_src = "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; + msg.avatar_src += "/web/image/res.partner/" + msg.author_id[0] + "/image_small"; } else { - msg.avatar_src = "/mail/static/src/img/smiley/avatar.jpg"; + msg.avatar_src += "/mail/static/src/img/smiley/avatar.jpg"; } if (options && options.prepend) { diff --git a/addons/im_livechat/static/src/less/im_livechat.less b/addons/im_livechat/static/src/less/im_livechat.less index c57bfad230a..c52fc7d2b9e 100644 --- a/addons/im_livechat/static/src/less/im_livechat.less +++ b/addons/im_livechat/static/src/less/im_livechat.less @@ -16,6 +16,7 @@ border-bottom: 0px; border-top-left-radius: 5px; border-top-right-radius: 5px; + z-index: 5; } .o_chat_window { diff --git a/addons/l10n_be_invoice_bba/models/account_invoice.py b/addons/l10n_be_invoice_bba/models/account_invoice.py index 428a18451a1..b477cde7d10 100644 --- a/addons/l10n_be_invoice_bba/models/account_invoice.py +++ b/addons/l10n_be_invoice_bba/models/account_invoice.py @@ -23,7 +23,7 @@ class AccountInvoice(models.Model): def _get_reference_type(self):
"""Add BBA Structured Communication Type and change labels from 'reference' into 'communication' """
res = super(AccountInvoice, self)._get_reference_type()
- res[[i for i, x in enumerate(res) if x[0] == 'none'][0]] = ('none', 'Free Communication')
+ res[[i for i, x in enumerate(res) if x[0] == 'none'][0]] = ('none', _('Free Communication'))
res.append(('bba', 'BBA Structured Communication'))
return res
diff --git a/addons/l10n_eu_service/__manifest__.py b/addons/l10n_eu_service/__manifest__.py index c363e6f2b23..cfa7f25f97c 100644 --- a/addons/l10n_eu_service/__manifest__.py +++ b/addons/l10n_eu_service/__manifest__.py @@ -56,6 +56,7 @@ References 'data': [ 'security/ir.model.access.csv', 'wizard/wizard.xml', - 'data/l10n_eu_service.service_tax_rate.csv' + 'data/l10n_eu_service.service_tax_rate.csv', + 'views/account_settings_config_views.xml' ], } diff --git a/addons/l10n_eu_service/views/account_settings_config_views.xml b/addons/l10n_eu_service/views/account_settings_config_views.xml new file mode 100644 index 00000000000..071cf2fd397 --- /dev/null +++ b/addons/l10n_eu_service/views/account_settings_config_views.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + + <record id="account_config_settings_view_form" model="ir.ui.view"> + <field name="name">account.config.settings.form</field> + <field name="model">account.config.settings</field> + <field name="inherit_id" ref="account.account_config_settings_view_form"/> + <field name="arch" type="xml"> + <div name="l10n_eu_service_right_pane" position="inside"> + <div class="content-group"> + <div> + <button name="%(l10n_eu_service.action_eu_service)d" icon="fa-arrow-right" type="action" string="Add new countries" class="btn-link"/> + </div> + </div> + </div> + </field> + </record> +</odoo>
\ No newline at end of file diff --git a/addons/l10n_gt/data/l10n_gt_chart_data.xml b/addons/l10n_gt/data/l10n_gt_chart_data.xml index 1d116ec8191..3feecab930b 100644 --- a/addons/l10n_gt/data/l10n_gt_chart_data.xml +++ b/addons/l10n_gt/data/l10n_gt_chart_data.xml @@ -12,7 +12,7 @@ <field name="name">Plantilla de cuentas de Guatemala (sencilla)</field> <field name="bank_account_code_prefix">1.0.01.0</field> <field name="cash_account_code_prefix">1.0.02.0</field> - <field name="code_digits">6</field> + <field name="code_digits">9</field> <field name="currency_id" ref="base.QTQ"/> <field name="transfer_account_id" ref="transfer_account_id"/> </record> diff --git a/addons/l10n_in/data/account_tax_template_data.xml b/addons/l10n_in/data/account_tax_template_data.xml index 48d06ad2f3f..92cb1e45eb0 100644 --- a/addons/l10n_in/data/account_tax_template_data.xml +++ b/addons/l10n_in/data/account_tax_template_data.xml @@ -484,7 +484,7 @@ <field name="account_id" ref="p11232"/> <field name="refund_account_id" ref="p20011"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_33'), ref('st_tag_in_74')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_26'), ref('st_tag_in_58')])]"/> </record> <!--output purchase tax--> @@ -498,7 +498,7 @@ <field name="account_id" ref="p10050"/> <field name="refund_account_id" ref="p2107"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_22'), ref('st_tag_in_67')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_51'), ref('st_tag_in_15')])]"/> </record> <record id="output_tax_12" model="account.tax.template"> <field name="name">(standard)Output Purchase Tax @ 12%</field> @@ -510,7 +510,7 @@ <field name="account_id" ref="p10050"/> <field name="refund_account_id" ref="p2107"/> <field name="chart_template_id" ref="indian_chart_template_standard"/> - <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_19'), ref('st_tag_in_64')])]"/> + <field name="tag_ids" eval="[(6, 0, [ref('st_tag_in_12'), ref('st_tag_in_48')])]"/> </record> <record id="output_tax_4" model="account.tax.template"> <field name="name">(standard)Output Purchase Tax @ 4%</field> diff --git a/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv b/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv index 043ba8f0522..caeb3e9ec60 100644 --- a/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv +++ b/addons/l10n_in_hr_payroll/data/hr.salary.rule.csv @@ -1,5 +1,5 @@ "id","amount_select","condition_range_min","condition_range_max","amount_percentage","amount_fix","name","category_id/id","sequence","code","parent_rule_id/id","condition_select","condition_range","amount_percentage_base" 1,"fix",1,1,,100,"Education Allowance For One Child",hr_payroll.ALW,23,"CHEAONE","hr_payroll_rule_child1","range","employee.children", -2,"fix",2,10,,200,"Education Allowance For Two Child",hr_payroll.ALW,24,"CHEATWO","hr_payroll_rule_child1","range","employee.children", +2,"fix",2,10,,200,"Education Allowance For Two Children",hr_payroll.ALW,24,"CHEATWO","hr_payroll_rule_child1","range","employee.children", 3,"fix",1,1,,300,"Child Hostel Allowance For One Child",hr_payroll.ALW,26,"CHOONE","hr_payroll_rule_child2","range","employee.children", -4,"fix",2,10,,600,"Child Hostel Allowance For Two Child",hr_payroll.ALW,27,"CHOTWO","hr_payroll_rule_child2","range","employee.children", +4,"fix",2,10,,600,"Child Hostel Allowance For Two Children",hr_payroll.ALW,27,"CHOTWO","hr_payroll_rule_child2","range","employee.children", diff --git a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml index 6664d080bff..17ec10ad01d 100644 --- a/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml +++ b/addons/l10n_in_hr_payroll/data/l10n_in_hr_payroll_data.xml @@ -70,7 +70,7 @@ <field name="code">HRAMN</field> <field name="amount_select">code</field> <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and -((contract.wage + DA) * contract.house_rent_allowance_metro_nonmetro / 100) +((contract.wage + categories.DA) * contract.house_rent_allowance_metro_nonmetro / 100) or (contract.wage * contract.house_rent_allowance_metro_nonmetro / 100))</field> <field name="category_id" ref="hr_payroll.ALW"/> <field name="name">House Rent Allowance</field> @@ -130,7 +130,7 @@ or (contract.wage * contract.house_rent_allowance_metro_nonmetro / 100))</field> <field name="condition_select">none</field> <field name="sequence" eval="21"/> <field name="amount_select">code</field> - <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and ((contract.wage + DA) * 0.10) + <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and ((contract.wage + categories.DA) * 0.10) or (contract.wage * 0.10))</field> <field name="note">This allowance is paid to Employees who are posted in big cities. The purpose is to compensate the high cost of living in cities like Mumbai, Delhi, etc. However it is Fully Taxable.</field> </record> @@ -365,7 +365,7 @@ else -0)</field> <field name="name">Employee's PF Contribution</field> <field name="category_id" ref="hr_payroll.DED"/> <field name="amount_select">code</field> - <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.12) + <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.12) or - (contract.wage * 0.12))</field> <field name= "note">Employer contribution does not become part of employee’s income and hence income tax is not payable on this part.</field> <field name="register_id" ref="hr_register_provident_fund"/> @@ -377,7 +377,7 @@ or - (contract.wage * 0.12))</field> <field name="name">Employee's NPS Contribution</field> <field name="category_id" ref="hr_payroll.DED"/> <field name="amount_select">code</field> - <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.10) + <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.10) or - (contract.wage * 0.10))</field> <field name="sequence" eval="155"/> <field name="register_id" ref="hr_nps_contribution_register"/> @@ -392,7 +392,7 @@ or - (contract.wage * 0.10))</field> <field name="condition_python">result = bool(contract.voluntary_provident_fund)</field> <field name="amount_select">code</field> <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and -- ((contract.wage + DA) * contract.voluntary_provident_fund / 100) +- ((contract.wage + categories.DA) * contract.voluntary_provident_fund / 100) or - (contract.wage * contract.voluntary_provident_fund / 100))</field> <field name="sequence" eval="160"/> <field name="register_id" ref="hr_vpf_contribution_register"/> @@ -498,7 +498,7 @@ or - (contract.wage * contract.voluntary_provident_fund / 100))</field> <field name="name">Employer's NPS Contribution</field> <field name="category_id" ref="hr_payroll.DED"/> <field name="amount_select">code</field> - <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.10) + <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.10) or - (contract.wage * 0.10))</field> <field name="sequence" eval="190"/> <field name="register_id" ref="hr_nps_contribution_register"/> @@ -511,7 +511,7 @@ or - (contract.wage * 0.10))</field> <field name="name">Employer's PF Contribution</field> <field name="category_id" ref="hr_payroll.DED"/> <field name="amount_select">code</field> - <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + DA) * 0.12) + <field name="amount_python_compute">result = (payslip.company_id.dearness_allowance and - ((contract.wage + categories.DA) * 0.12) or - (contract.wage * 0.12))</field> <field name="sequence" eval="195"/> <field name="register_id" ref="hr_register_provident_fund"/> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index c5e300e1275..8bd16cf1dc7 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -2,174 +2,138 @@ <odoo> <data noupdate="1"> <!-- Account Tags --> - <record id="tax_tag_01" model="account.account.tag"> - <field name="name">IVA(0%) VENTAS</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_03" model="account.account.tag"> - <field name="name">IVA(16%) VENTAS</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_04" model="account.account.tag"> - <field name="name">RET IVA FLETES 4%</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_05" model="account.account.tag"> - <field name="name">RET IVA ARRENDAMIENTO 10%</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_06" model="account.account.tag"> - <field name="name">RET ISR ARRENDAMIENTO 10%</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_07" model="account.account.tag"> - <field name="name">RET ISR HONORARIOS 10%</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_08" model="account.account.tag"> - <field name="name">RETENCION IVA ARRENDAMIENTO 10.67%</field> - <field name="applicability">taxes</field> - </record> - <record id="tax_tag_09" model="account.account.tag"> - <field name="name">RETENCION IVA HONORARIOS 10.67%</field> + <record id="tag_iva" model="account.account.tag"> + <field name="name">IVA</field> <field name="applicability">taxes</field> </record> - <record id="tax_tag_10" model="account.account.tag"> - <field name="name">IVA(0%) COMPRAS</field> + <record id="tag_isr" model="account.account.tag"> + <field name="name">ISR</field> <field name="applicability">taxes</field> </record> - <record id="tax_tag_12" model="account.account.tag"> - <field name="name">IVA(16%) COMPRAS</field> + <record id="tag_ieps" model="account.account.tag"> + <field name="name">IEPS</field> <field name="applicability">taxes</field> </record> <record id="tax9" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">IVA(0%) VENTAS</field> - <field name="description">IVA</field> + <field name="description">IVA(0%)</field> <field name="amount">0</field> <field name="amount_type">percent</field> <field name="type_tax_use">sale</field> <field name="account_id" ref="cuenta209_01"/> <field name="refund_account_id" ref="cuenta209_01"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_01')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> </record> <record id="tax12" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">IVA(16%) VENTAS</field> - <field name="description">IVA</field> + <field name="description">IVA(16%)</field> <field name="amount">16</field> <field name="amount_type">percent</field> <field name="type_tax_use">sale</field> <field name="account_id" ref="cuenta209_01"/> <field name="refund_account_id" ref="cuenta209_01"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_03')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tag_iva')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> </record> <record id="tax1" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RET IVA FLETES 4%</field> - <field name="description">ITAXR_04-OUT</field> + <field name="description">Retención IVA(-4%)</field> <field name="amount">-4</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_04')])]"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> </record> <record id="tax2" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RET IVA ARRENDAMIENTO 10%</field> - <field name="description">ITAXR_10-OUT</field> + <field name="description">Retención IVA(-10%)</field> <field name="amount">-10</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_05')])]"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> </record> <record id="tax3" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RET ISR ARRENDAMIENTO 10%</field> - <field name="description">ITAXA_10-OUT</field> + <field name="description">Retención ISR(-10%)</field> <field name="amount">-10</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_03"/> <field name="refund_account_id" ref="cuenta216_03"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_06')])]"/> <field name="tax_group_id" ref="tax_group_isr_ret"/> </record> <record id="tax5" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RET ISR HONORARIOS 10%</field> - <field name="description">ITAXH_10-OUT</field> + <field name="description">Retención ISR(-10%)</field> <field name="amount">-10</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_04"/> <field name="refund_account_id" ref="cuenta216_04"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_07')])]"/> <field name="tax_group_id" ref="tax_group_isr_ret"/> </record> <record id="tax7" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RETENCION IVA ARRENDAMIENTO 10.67%</field> - <field name="description">ITAX_1067-OUT</field> + <field name="description">Retención IVA(-10.67%)</field> <field name="amount">-10.67</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_08')])]"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> </record> <record id="tax8" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">RETENCION IVA HONORARIOS 10.67%</field> - <field name="description">ITAX_167-OUT</field> + <field name="description">Retención IVA(-10.67%)</field> <field name="amount">-10.67</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta216_10"/> <field name="refund_account_id" ref="cuenta216_10"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_09')])]"/> <field name="tax_group_id" ref="tax_group_iva_ret"/> </record> <record id="tax13" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">IVA(0%) COMPRAS</field> - <field name="description">ITAX_010-OUT</field> + <field name="description">IVA(0%)</field> <field name="amount">0</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta119_01"/> <field name="refund_account_id" ref="cuenta119_01"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_10')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> </record> <record id="tax14" model="account.tax.template"> <field name="chart_template_id" ref="mx_coa"/> <field name="name">IVA(16%) COMPRAS</field> - <field name="description">ITAX_160-OUT</field> + <field name="description">IVA(16%)</field> <field name="amount">16</field> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="account_id" ref="cuenta119_01"/> <field name="refund_account_id" ref="cuenta119_01"/> - <field name="tag_ids" eval="[(6,0,[ref('tax_tag_12')])]"/> <field name="tax_group_id" ref="tax_group_iva"/> </record> </data> diff --git a/addons/l10n_mx/data/fiscal_position_data.xml b/addons/l10n_mx/data/fiscal_position_data.xml index d2ad94d8613..32589454dad 100644 --- a/addons/l10n_mx/data/fiscal_position_data.xml +++ b/addons/l10n_mx/data/fiscal_position_data.xml @@ -1,39 +1,39 @@ <?xml version="1.0" ?> <odoo> <data noupdate="1"> - <record id="account_fiscal_position_personasmoralesdelrgimengeneral0" model="account.fiscal.position"> + <record id="account_fiscal_position_personasmoralesdelrgimengeneral0" model="account.fiscal.position.template"> <field name="name">Personas morales del régimen general</field> </record> - <record id="account_fiscal_position_personasmoralesconfinesnolucrativos0" model="account.fiscal.position"> + <record id="account_fiscal_position_personasmoralesconfinesnolucrativos0" model="account.fiscal.position.template"> <field name="name">Personas morales con fines no lucrativos</field> </record> - <record id="account_fiscal_position_asociacionesreligiosas0" model="account.fiscal.position"> + <record id="account_fiscal_position_asociacionesreligiosas0" model="account.fiscal.position.template"> <field name="name">Asociaciones Religiosas</field> </record> - <record id="account_fiscal_position_personasmoralesdelrgimensimplificado0" model="account.fiscal.position"> + <record id="account_fiscal_position_personasmoralesdelrgimensimplificado0" model="account.fiscal.position.template"> <field name="name">Personas morales del régimen simplificado</field> </record> - <record id="account_fiscal_position_personafisicaconactividadempresarial0" model="account.fiscal.position"> + <record id="account_fiscal_position_personafisicaconactividadempresarial0" model="account.fiscal.position.template"> <field name="name">Persona fÃsica con actividad empresarial</field> </record> - <record id="account_fiscal_position_foreign" model="account.fiscal.position"> + <record id="account_fiscal_position_foreign" model="account.fiscal.position.template"> <field name="name">Foreign Customer</field> </record> - <record id="account_fiscal_position0_purchase" model="account.fiscal.position.tax"> - <field name="tax_src_id" ref="l10n_mx.1_tax14"/> - <field name="tax_dest_id" ref="l10n_mx.1_tax13"/> + <record id="account_fiscal_position0_purchase" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="l10n_mx.tax14"/> + <field name="tax_dest_id" ref="l10n_mx.tax13"/> <field name="position_id" ref="account_fiscal_position_foreign"/> </record> - <record id="account_fiscal_position0_sale" model="account.fiscal.position.tax"> - <field name="tax_src_id" ref="l10n_mx.1_tax12"/> - <field name="tax_dest_id" ref="l10n_mx.1_tax9"/> + <record id="account_fiscal_position0_sale" model="account.fiscal.position.tax.template"> + <field name="tax_src_id" ref="l10n_mx.tax12"/> + <field name="tax_dest_id" ref="l10n_mx.tax9"/> <field name="position_id" ref="account_fiscal_position_foreign"/> </record> diff --git a/addons/l10n_nl/__manifest__.py b/addons/l10n_nl/__manifest__.py index cecd5c9ca72..0016847414f 100644 --- a/addons/l10n_nl/__manifest__.py +++ b/addons/l10n_nl/__manifest__.py @@ -6,7 +6,7 @@ { 'name': 'Netherlands - Accounting', 'version': '3.0', - 'category': 'Localization/Account Charts', + 'category': 'Localization', 'author': 'ONESTEiN BV', 'website': 'http://www.onestein.eu', 'depends': [ diff --git a/addons/l10n_ro/data/account_fiscal_position_data.xml b/addons/l10n_ro/data/account_fiscal_position_data.xml index 8649103fa8f..9ff61aa3f0e 100644 --- a/addons/l10n_ro/data/account_fiscal_position_data.xml +++ b/addons/l10n_ro/data/account_fiscal_position_data.xml @@ -88,6 +88,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvatip09"/> </record> + <record id="afptt_inverse_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_2"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvatip19"/> + </record> <record id="afptt_inverse_9" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_2"/> <field name="tax_src_id" ref="tvad_20"/> @@ -147,6 +152,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvati_intrap9"/> </record> + <record id="afptt_intracom_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_3"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvati_intrap19"/> + </record> <record id="afptt_intracom_13" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_3"/> <field name="tax_src_id" ref="tvad_20"/> @@ -207,6 +217,11 @@ <field name="tax_src_id" ref="tvad_09"/> <field name="tax_dest_id" ref="tvati_intrap9"/> </record> + <record id="afptt_intracoms_11" model="account.fiscal.position.tax.template"> + <field name="position_id" ref="fiscal_position_template_4"/> + <field name="tax_src_id" ref="tvad_19"/> + <field name="tax_dest_id" ref="tvati_intrap19"/> + </record> <record id="afptt_intracoms_13" model="account.fiscal.position.tax.template"> <field name="position_id" ref="fiscal_position_template_4"/> <field name="tax_src_id" ref="tvad_20"/> diff --git a/addons/l10n_ro/data/account_tax_data.xml b/addons/l10n_ro/data/account_tax_data.xml index 8bba8727f27..373fea4c16c 100644 --- a/addons/l10n_ro/data/account_tax_data.xml +++ b/addons/l10n_ro/data/account_tax_data.xml @@ -135,6 +135,21 @@ <field name="applicability">taxes</field> </record> + <record id="tax_tag_tva_taxare_inversa_colectat_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa Colectat 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_taxare_inversa_deductibil_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa Deductibil 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_taxare_inversa_19" model="account.account.tag"> + <field name="name">TVA Taxare Inversa 19%</field> + <field name="applicability">taxes</field> + </record> + <record id="tax_tag_tva_taxare_inversa_colectat_20" model="account.account.tag"> <field name="name">TVA Taxare Inversa Colectat 20%</field> <field name="applicability">taxes</field> @@ -210,6 +225,21 @@ <field name="applicability">taxes</field> </record> + <record id="tax_tag_tva_intracomunitara_achizitii_colectat_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii Colectat 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_intracomunitara_achizitii_deductibil_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field> + <field name="applicability">taxes</field> + </record> + + <record id="tax_tag_tva_intracomunitara_achizitii_19" model="account.account.tag"> + <field name="name">TVA Intracomunitara Achizitii 19%</field> + <field name="applicability">taxes</field> + </record> + <record id="tax_tag_tva_intracomunitara_achizitii_colectat_20" model="account.account.tag"> <field name="name">TVA Intracomunitara Achizitii Colectat 20%</field> <field name="applicability">taxes</field> @@ -281,7 +311,7 @@ <record id="tvac_19" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">11</field> + <field name="sequence">9</field> <field name="name">TVA colectat 19%</field> <field name="description">TVA colectat 19%</field> <field name="amount">19</field> @@ -307,7 +337,7 @@ <record id="tvac_24" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">9</field> + <field name="sequence">11</field> <field name="name">TVA colectat 24%</field> <field name="description">TVA colectat 24%</field> <field name="amount">24</field> @@ -359,7 +389,7 @@ <record id="tvad_19" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">23</field> + <field name="sequence">19</field> <field name="name">TVA deductibil 19%</field> <field name="description">TVA deductibil 19%</field> <field name="amount">19</field> @@ -385,7 +415,7 @@ <record id="tvad_24" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> - <field name="sequence">19</field> + <field name="sequence">23</field> <field name="name">TVA deductibil 24%</field> <field name="description">TVA deductibil 24%</field> <field name="amount">24</field> @@ -506,6 +536,38 @@ <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_9')])]"/> </record> + <record id="tvatip19-c" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Taxare Inversa Colectat 19%</field> + <field name="amount">-19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4427"/> + <field name="account_id" ref="pcg_4427"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_colectat_19')])]"/> + </record> + <record id="tvatip19-d" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Taxare Inversa Deductibil 19%</field> + <field name="amount">19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4426"/> + <field name="account_id" ref="pcg_4426"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_deductibil_19')])]"/> + </record> + <record id="tvatip19" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="sequence">32</field> + <field name="name">TVA Taxare Inversa 19%</field> + <field name="description">TVA Taxare Inversa 19%</field> + <field name="amount">1</field> + <field name="amount_type">group</field> + <field name="type_tax_use">purchase</field> + <field name="children_tax_ids" eval="[(6, 0, [ref('tvatip19-d'), ref('tvatip19-c')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_taxare_inversa_19')])]"/> + </record> + <record id="tvatip20-c" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> <field name="name">TVA Taxare Inversa Colectat 20%</field> @@ -730,6 +792,38 @@ <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_9')])]"/> </record> + <record id="tvati_intrap19c" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Intracomunitara Achizitii Colectat 19%</field> + <field name="amount">-19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4427"/> + <field name="account_id" ref="pcg_4427"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_colectat_19')])]"/> + </record> + <record id="tvati_intrap19d" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="name">TVA Intracomunitara Achizitii Deductibil 19%</field> + <field name="price_include" eval="0"/> + <field name="amount">19</field> + <field name="amount_type">percent</field> + <field name="type_tax_use">none</field> + <field name="refund_account_id" ref="pcg_4426"/> + <field name="account_id" ref="pcg_4426"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_deductibil_19')])]"/> + </record> + <record id="tvati_intrap19" model="account.tax.template"> + <field name="chart_template_id" ref="ro_chart_template"/> + <field name="sequence">62</field> + <field name="name">TVA Intracomunitara Achizitii 19%</field> + <field name="amount">1</field> + <field name="amount_type">group</field> + <field name="type_tax_use">purchase</field> + <field name="children_tax_ids" eval="[(6, 0, [ref('tvati_intrap19d'), ref('tvati_intrap19c')])]"/> + <field name="tag_ids" eval="[(6,0,[ref('tax_tag_tva_intracomunitara_achizitii_19')])]"/> + </record> + <record id="tvati_intrap20c" model="account.tax.template"> <field name="chart_template_id" ref="ro_chart_template"/> <field name="name">TVA Intracomunitara Achizitii Colectat 20%</field> diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index e1c7a1b458e..63bd82a5f87 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -209,8 +209,9 @@ class Channel(models.Model): @api.multi def message_receive_bounce(self, email, partner, mail_id=None): """ Override bounce management to unsubscribe bouncing addresses """ - if partner.message_bounce >= self.MAX_BOUNCE_LIMIT: - self._action_unfollow(partner) + for p in partner: + if p.message_bounce >= self.MAX_BOUNCE_LIMIT: + self._action_unfollow(p) return super(Channel, self).message_receive_bounce(email, partner, mail_id=mail_id) @api.multi diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 7bbf35809a4..d08158750dd 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -1049,19 +1049,20 @@ class MailThread(models.AbstractModel): bounce_match = bounce_re.search(email_to) if bounce_match: - bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), int(bounce_match.group(3)) + bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), bounce_match.group(3) email_part = next((part for part in message.walk() if part.get_content_type() == 'message/rfc822'), None) dsn_part = next((part for part in message.walk() if part.get_content_type() == 'message/delivery-status'), None) - partner, partner_address = self.env['res.partner'], False + partners, partner_address = self.env['res.partner'], False if dsn_part and len(dsn_part.get_payload()) > 1: dsn = dsn_part.get_payload()[1] final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient') partner_address = final_recipient_data.split(';', 1)[1].strip() if partner_address: - partner = partner.sudo().search([('email', 'like', partner_address)]) - partner.message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) + partners = partners.sudo().search([('email', 'like', partner_address)]) + for partner in partners: + partner.message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) mail_message = self.env['mail.message'] if email_part: @@ -1069,19 +1070,19 @@ class MailThread(models.AbstractModel): bounced_message_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email, 'Message-Id')) mail_message = MailMessage.sudo().search([('message_id', 'in', bounced_message_id)]) - if partner and mail_message: + if partners and mail_message: notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', '=', mail_message.id), - ('res_partner_id', '=', partner.id)]) + ('res_partner_id', 'in', partners.ids)]) notifications.write({ 'email_status': 'bounce' }) if bounced_model in self.env and hasattr(self.env[bounced_model], 'message_receive_bounce') and bounced_thread_id: - self.env[bounced_model].browse(bounced_thread_id).message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id) + self.env[bounced_model].browse(int(bounced_thread_id)).message_receive_bounce(partner_address, partners, mail_id=bounced_mail_id) _logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s: dest %s (partner %s)', - email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partner.id) + email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partners) return [] # 0. First check if this is a bounce message or not. diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index aa929445488..dd1b46c8d4f 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -66,6 +66,7 @@ var Activity = form_common.AbstractField.extend({ this._super.apply(this, arguments); this.context = this.options.context || {}; + this.model = this.view.dataset.model; this.activities = []; this.Activity = new Model('mail.activity'); }, @@ -98,7 +99,7 @@ var Activity = form_common.AbstractField.extend({ fetch_and_render_value: function () { var self = this; - return new Model(this.view.dataset.model) + return new Model(this.model) .call("read", [[this.get_res_id()], ['activity_ids']]) .then(function (results) { self.value = results[0]['activity_ids']; @@ -133,7 +134,7 @@ var Activity = form_common.AbstractField.extend({ target: 'new', context: _.extend({ default_res_id: this.get_res_id(), - default_res_model: this.view.dataset.model, + default_res_model: this.model, }, {'mark_done': true}, this.context), res_id: false, }; @@ -141,7 +142,7 @@ var Activity = form_common.AbstractField.extend({ on_close: function() { self.fetch_and_render_value(); self.chatter.refresh_followers(); - ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + ChatManager.get_messages({model: self.model, res_id: self.get_res_id()}); }, }); }, @@ -169,7 +170,7 @@ var Activity = form_common.AbstractField.extend({ on_close: function () { self.fetch_and_render_value(); self.chatter.refresh_followers(); - ChatManager.get_messages({model: self.res_model, res_id: self.get_res_id()}); + ChatManager.get_messages({model: self.model, res_id: self.get_res_id()}); }, }); }, diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index 146196af6c9..93c1cf07075 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -1045,7 +1045,9 @@ function init () { bus.on('notification', null, on_notification); - return session.rpc('/mail/client_action').then(function (result) { + return session.is_bound.then(function(){ + return session.rpc('/mail/client_action'); + }).then(function (result) { _.each(result.channel_slots, function (channels) { _.each(channels, add_channel); }); diff --git a/addons/maintenance/__manifest__.py b/addons/maintenance/__manifest__.py index b30f3bd2d73..9bdde95f507 100644 --- a/addons/maintenance/__manifest__.py +++ b/addons/maintenance/__manifest__.py @@ -15,6 +15,7 @@ 'data/maintenance_data.xml', 'views/maintenance_views.xml', 'views/maintenance_templates.xml', + 'data/maintenance_cron.xml', ], 'demo': ['data/maintenance_demo.xml'], 'installable': True, diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 14661e62322..a055b24f82f 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -144,17 +144,48 @@ class MaintenanceEquipment(models.Model): maintenance_team_id = fields.Many2one('maintenance.team', string='Maintenance Team') maintenance_duration = fields.Float(help="Maintenance Duration in minutes and seconds.") - @api.depends('period', 'maintenance_open_count', 'maintenance_ids.request_date') + @api.depends('period', 'maintenance_ids.request_date', 'maintenance_ids.close_date') def _compute_next_maintenance(self): - for equipment in self: - create_date = equipment.create_date and datetime.strptime(equipment.create_date, DEFAULT_SERVER_DATETIME_FORMAT) - if equipment.period: - next_date = create_date - if equipment.maintenance_ids: - maintenance = equipment.maintenance_ids.sorted(lambda x: x.request_date)[0] - next_date = maintenance.request_date and datetime.strptime(maintenance.request_date, DEFAULT_SERVER_DATE_FORMAT) or create_date - equipment.next_action_date = next_date and (next_date + timedelta(days=equipment.period)).strftime(DEFAULT_SERVER_DATE_FORMAT) + date_now = fields.Date.context_today(self) + for equipment in self.filtered(lambda x: x.period > 0): + next_maintenance_todo = self.env['maintenance.request'].search([ + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('stage_id.done', '!=', True), + ('close_date', '=', False)], order="request_date asc", limit=1) + last_maintenance_done = self.env['maintenance.request'].search([ + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('stage_id.done', '=', True), + ('close_date', '!=', False)], order="close_date desc", limit=1) + if next_maintenance_todo and last_maintenance_done: + next_date = next_maintenance_todo.request_date + date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(last_maintenance_done.close_date) + # If the gap between the last_maintenance_done and the next_maintenance_todo one is bigger than 2 times the period and next request is in the future + # We use 2 times the period to avoid creation too closed request from a manually one created + if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2 and fields.Date.from_string(next_maintenance_todo.request_date) > fields.Date.from_string(date_now): + # If the new date still in the past, we set it for today + if fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period) < fields.Date.from_string(date_now): + next_date = date_now + else: + next_date = fields.Date.to_string(fields.Date.from_string(last_maintenance_done.close_date) + timedelta(days=equipment.period)) + elif next_maintenance_todo: + next_date = next_maintenance_todo.request_date + date_gap = fields.Date.from_string(next_maintenance_todo.request_date) - fields.Date.from_string(date_now) + # If next maintenance to do is in the future, and in more than 2 times the period, we insert an new request + # We use 2 times the period to avoid creation too closed request from a manually one created + if date_gap > timedelta(0) and date_gap > timedelta(days=equipment.period) * 2: + next_date = fields.Date.to_string(fields.Date.from_string(date_now)+timedelta(days=equipment.period)) + elif last_maintenance_done: + next_date = fields.Date.from_string(last_maintenance_done.close_date)+timedelta(days=equipment.period) + # If when we add the period to the last maintenance done and we still in past, we plan it for today + if next_date < fields.Date.from_string(date_now): + next_date = date_now + else: + next_date = fields.Date.to_string(fields.Date.from_string(date_now) + timedelta(days=equipment.period)) + + equipment.next_action_date = next_date @api.one @api.depends('maintenance_ids.stage_id.done') def _compute_maintenance_count(self): @@ -190,18 +221,31 @@ class MaintenanceEquipment(models.Model): category_ids = categories._search([], order=order, access_rights_uid=SUPERUSER_ID) return categories.browse(category_ids) + def _create_new_request(self, date): + self.ensure_one() + self.env['maintenance.request'].create({ + 'name': _('Preventive Maintenance - %s') % self.name, + 'request_date': date, + 'schedule_date': date, + 'category_id': self.category_id.id, + 'equipment_id': self.id, + 'maintenance_type': 'preventive', + 'owner_user_id': self.owner_user_id.id, + 'technician_user_id': self.technician_user_id.id, + }) + @api.model def _cron_generate_requests(self): - for equipment in self.search([]): - if equipment.period and equipment.next_action_date == date.today().strftime(DEFAULT_SERVER_DATE_FORMAT): - self.env['maintenance.request'].create({ - 'name': _('Preventive Maintenance - %s' % equipment.next_action_date), - 'request_date': equipment.next_action_date, - 'category_id': equipment.category_id.id, - 'equipment_id': equipment.id, - 'maintenance_type': 'preventive', - }) - + """ + Generates maintenance request on the next_action_date or today if none exists + """ + for equipment in self.search([('period', '>', 0)]): + next_requests = self.env['maintenance.request'].search([('stage_id.done', '=', False), + ('equipment_id', '=', equipment.id), + ('maintenance_type', '=', 'preventive'), + ('request_date', '=', equipment.next_action_date)]) + if not next_requests: + equipment._create_new_request(equipment.next_action_date) class MaintenanceRequest(models.Model): _name = 'maintenance.request' @@ -227,8 +271,8 @@ class MaintenanceRequest(models.Model): name = fields.Char('Subjects', required=True) description = fields.Text('Description') - request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today) - + request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today, + help="Date requested for the maintenance to happen") owner_user_id = fields.Many2one('res.users', string='Created by', default=lambda s: s.env.uid) category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True) equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True) @@ -237,13 +281,13 @@ class MaintenanceRequest(models.Model): group_expand='_read_group_stage_ids', default=_default_stage) priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') color = fields.Integer('Color Index') - close_date = fields.Date('Close Date') + close_date = fields.Date('Close Date', help="Date the maintenance was finished. ") kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')], string='Kanban State', required=True, default='normal', track_visibility='onchange') # active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.") archive = fields.Boolean(default=False, help="Set archive to true to hide the maintenance request without deleting it.") maintenance_type = fields.Selection([('corrective', 'Corrective'), ('preventive', 'Preventive')], string='Maintenance Type', default="corrective") - schedule_date = fields.Datetime('Scheduled Date') + schedule_date = fields.Datetime('Scheduled Date', help="Date the maintenance team plans the maintenance. It should not differ much from the Request Date. ") maintenance_team_id = fields.Many2one('maintenance.team', string='Team', required=True, default=_get_default_team_id) duration = fields.Float(help="Duration in minutes and seconds.") diff --git a/addons/maintenance/security/maintenance.xml b/addons/maintenance/security/maintenance.xml index cf564b0c859..a567eb8ddab 100644 --- a/addons/maintenance/security/maintenance.xml +++ b/addons/maintenance/security/maintenance.xml @@ -12,14 +12,14 @@ <record id="equipment_request_rule_user" model="ir.rule"> <field name="name">Users are allowed to access their own maintenance requests</field> <field name="model_id" ref="model_maintenance_request"/> - <field name="domain_force">['|', ('owner_user_id.id', '=', user.id), ('technician_user_id.id', '=', user.id)]</field> + <field name="domain_force">['|', ('message_partner_ids', 'in', [user.partner_id.id]), ('technician_user_id.id', '=', user.id)]</field> <field name="groups" eval="[(4, ref('base.group_user'))]"/> </record> <record id="equipment_rule_user" model="ir.rule"> - <field name="name">Users are allowed to access their own equipments</field> + <field name="name">Users are allowed to access equipments they follow</field> <field name="model_id" ref="model_maintenance_equipment"/> - <field name="domain_force">[('owner_user_id.id', '=', user.id)]</field> + <field name="domain_force">[('message_partner_ids', 'in', [user.partner_id.id])]</field> <field name="groups" eval="[(4, ref('base.group_user'))]"/> </record> diff --git a/addons/maintenance/tests/test_maintenance.py b/addons/maintenance/tests/test_maintenance.py index a9c55d95ffc..d73301d7ec2 100644 --- a/addons/maintenance/tests/test_maintenance.py +++ b/addons/maintenance/tests/test_maintenance.py @@ -4,7 +4,8 @@ import time from odoo.tests.common import TransactionCase - +from dateutil import relativedelta +import datetime class TestEquipment(TransactionCase): """ Test used to check that when doing equipment/maintenance_request/equipment_category creation.""" @@ -36,7 +37,7 @@ class TestEquipment(TransactionCase): groups_id=[(6, 0, [res_manager.id])] )) - def test_equipment_request_category(self): + def test_10_equipment_request_category(self): # Create a new equipment equipment_01 = self.equipment.sudo(self.manager).create({ @@ -75,3 +76,32 @@ class TestEquipment(TransactionCase): # I check that maintenance request is in the "In Progress" stage self.assertEquals(maintenance_request_01.stage_id.id, self.ref('maintenance.stage_1')) + + def test_20_cron(self): + """ Check the cron creates the necessary preventive maintenance requests""" + equipment_cron = self.equipment.create({ + 'name': 'High Maintenance Monitor because of Color Calibration', + 'category_id': self.ref('maintenance.equipment_monitor'), + 'technician_user_id': self.ref('base.user_root'), + 'owner_user_id': self.user.id, + 'assign_date': time.strftime('%Y-%m-%d'), + 'period': 7, + 'color': 3, + }) + + maintenance_request_cron = self.maintenance_request.create({ + 'name': 'Need a special calibration', + 'technician_user_id': self.user.id, + 'request_date': (datetime.datetime.now() + relativedelta.relativedelta(days=7)).strftime('%Y-%m-%d'), + 'maintenance_type': 'preventive', + 'owner_user_id': self.user.id, + 'equipment_id': equipment_cron.id, + 'color': 7, + 'stage_id': self.ref('maintenance.stage_0'), + 'maintenance_team_id': self.ref('maintenance.equipment_team_maintenance') + }) + + self.env['maintenance.equipment']._cron_generate_requests() + # As it is generating the requests for one month in advance, we should have 4 requests in total + tot_requests = self.maintenance_request.search([('equipment_id', '=', equipment_cron.id)]) + self.assertEqual(len(tot_requests), 1, 'The cron should have generated just 1 request for the High Maintenance Monitor.') diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml index 99858bff69f..6be82359009 100644 --- a/addons/maintenance/views/maintenance_views.xml +++ b/addons/maintenance/views/maintenance_views.xml @@ -78,7 +78,7 @@ <field name="owner_user_id" string="Requested By"/> <field name="equipment_id" context="{'default_category_id':category_id}"/> <field name="category_id" groups="maintenance.group_equipment_manager"/> - <field name="request_date" groups="base.group_no_one"/> + <field name="request_date"/> <field name="close_date" groups="base.group_no_one"/> <field name="archive" invisible="1"/> <field name="maintenance_type" widget="radio"/> @@ -416,7 +416,7 @@ <div class="oe_kanban_footer_left mt4 label label-danger" t-if="record.maintenance_open_count.raw_value" > <t t-raw="record.maintenance_open_count.raw_value"/> Request </div> - <div class="oe_kanban_footer_left mt4 label label-default" t-if="record.maintenance_open_count.raw_value == 0 and record.next_action_date.raw_value" > + <div class="oe_kanban_footer_left mt4 label label-default" t-if="record.next_action_date.raw_value" > <t t-raw="moment(record.next_action_date.raw_value).format('MMMM Do')"/> </div> </div> diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py index 17473464126..130827e1fe4 100644 --- a/addons/mass_mailing/wizard/test_mailing.py +++ b/addons/mass_mailing/wizard/test_mailing.py @@ -10,7 +10,7 @@ class TestMassMailing(models.TransientModel): email_to = fields.Char(string='Recipients', required=True, help='Comma-separated list of email addresses.', default=lambda self: self.env['mail.message']._get_default_from()) - mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mailing', required=True) + mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mailing', required=True, ondelete='cascade') @api.multi def send_mail_test(self): diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py index 3feb7b498be..6ca6f8bbf4c 100644 --- a/addons/mrp/models/mrp_bom.py +++ b/addons/mrp/models/mrp_bom.py @@ -4,6 +4,7 @@ from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_round class MrpBom(models.Model): @@ -138,6 +139,10 @@ class MrpBom(models.Model): templates_done |= current_line.product_id.product_tmpl_id boms_done.append((bom, {'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line})) else: + # We round up here because the user expects that if he has to consume a little more, the whole UOM unit + # should be consumed. + rounding = current_line.product_uom_id.rounding + line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, {'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line})) return boms_done, lines_done diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index c57c079be7a..1f772e445f4 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -323,6 +323,7 @@ class MrpProduction(models.Model): data = { 'name': self.name, 'date': self.date_planned_start, + 'date_expected': self.date_planned_start, 'bom_line_id': bom_line.id, 'product_id': bom_line.product_id.id, 'product_uom_qty': quantity, diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py index fc0da1ecdab..23cd851b780 100644 --- a/addons/mrp/models/mrp_workorder.py +++ b/addons/mrp/models/mrp_workorder.py @@ -6,7 +6,7 @@ from dateutil.relativedelta import relativedelta from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -275,7 +275,8 @@ class MrpWorkorder(models.Model): raw_moves = self.move_raw_ids.filtered(lambda x: (x.has_tracking == 'none') and (x.state not in ('done', 'cancel')) and x.bom_line_id) for move in raw_moves: if move.unit_factor: - move.quantity_done += self.qty_producing * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding) # Transfer quantities from temporary to final move lots or make them final for move_lot in self.active_move_lot_ids: diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py index 9daa3069593..42df10850ca 100644 --- a/addons/mrp/models/stock_move.py +++ b/addons/mrp/models/stock_move.py @@ -3,7 +3,7 @@ from odoo import api, exceptions, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round from odoo.addons import decimal_precision as dp @@ -81,14 +81,14 @@ class StockMove(models.Model): store=True, help='Technical Field to order moves') # TDE: what ? - @api.multi + @api.depends('state', 'product_uom_qty', 'reserved_availability') def _qty_available(self): for move in self: # For consumables, state is available so availability = qty to do if move.state == 'assigned': move.quantity_available = move.product_uom_qty - else: - move.quantity_available = move.reserved_availability + elif move.product_id.uom_id and move.product_uom: + move.quantity_available = move.product_id.uom_id._compute_quantity(move.reserved_availability, move.product_uom) @api.multi @api.depends('move_lot_ids', 'move_lot_ids.quantity_done', 'quantity_done_store') @@ -225,8 +225,14 @@ class StockMove(models.Model): moves_to_unreserve = self.env['stock.move'] # Create extra moves where necessary for move in moves: + # Here, the `quantity_done` was already rounded to the product UOM by the `do_produce` wizard. However, + # it is possible that the user changed the value before posting the inventory by a value that should be + # rounded according to the move's UOM. In this specific case, we chose to round up the value, because it + # is what is expected by the user (if i consumed/produced a little more, the whole UOM unit should be + # consumed/produced and the moves are split correctly). rounding = move.product_uom.rounding - if float_compare(move.quantity_done, 0.0, precision_rounding=rounding) <= 0: + move.quantity_done = float_round(move.quantity_done, precision_rounding=rounding, rounding_method ='UP') + if move.quantity_done <= 0: continue moves_todo |= move moves_todo |= move._create_extra_move() diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index 64a42fbb136..0623a892e52 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -427,3 +427,35 @@ class TestMrpOrder(TestMrpCommon): # check the consumed quants of the newly produced quant self.assertEquals(sum(second_move.quant_ids.mapped('consumed_quant_ids').mapped('qty')), 2) + + def test_rounding(self): + """ In previous versions we had rounding and efficiency fields. We check if we can still do the same, but with only the rounding on the UoM""" + self.product_6.uom_id.rounding = 1.0 + bom_eff = self.env['mrp.bom'].create({'product_id': self.product_6.id, + 'product_tmpl_id': self.product_6.product_tmpl_id.id, + 'product_qty': 1, + 'product_uom_id': self.product_6.uom_id.id, + 'type': 'normal', + 'bom_line_ids': [ + (0, 0, {'product_id': self.product_2.id, 'product_qty': 2.03}), + (0, 0, {'product_id': self.product_8.id, 'product_qty': 4.16}) + ]}) + production = self.env['mrp.production'].create({'name': 'MO efficiency test', + 'product_id': self.product_6.id, + 'product_qty': 20, + 'bom_id': bom_eff.id, + 'product_uom_id': self.product_6.uom_id.id,}) + #Check the production order has the right quantities + self.assertEqual(production.move_raw_ids[0].product_qty, 41, 'The quantity should be rounded up') + self.assertEqual(production.move_raw_ids[1].product_qty, 84, 'The quantity should be rounded up') + + # produce product + produce_wizard = self.env['mrp.product.produce'].with_context({ + 'active_id': production.id, + 'active_ids': [production.id], + }).create({ + 'product_qty': 8, + }) + produce_wizard.do_produce() + self.assertEqual(production.move_raw_ids[0].quantity_done, 16, 'Should use half-up rounding when producing') + self.assertEqual(production.move_raw_ids[1].quantity_done, 34, 'Should use half-up rounding when producing') diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py index 3ac2b1bf7c3..b636fc15ec7 100644 --- a/addons/mrp/wizard/mrp_product_produce.py +++ b/addons/mrp/wizard/mrp_product_produce.py @@ -6,7 +6,7 @@ from datetime import datetime from odoo import api, fields, models, _ from odoo.addons import decimal_precision as dp from odoo.exceptions import UserError -from odoo.tools import float_compare +from odoo.tools import float_compare, float_round class MrpProductProduce(models.TransientModel): _name = "mrp.product.produce" @@ -80,13 +80,16 @@ class MrpProductProduce(models.TransientModel): raise UserError(_('You should at least produce some quantity')) for move in moves.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')): if move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + rounding = move.product_uom.rounding + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) moves = self.production_id.move_finished_ids.filtered(lambda x: x.product_id.tracking == 'none' and x.state not in ('done', 'cancel')) for move in moves: + rounding = move.product_uom.rounding if move.product_id.id == self.production_id.product_id.id: - move.quantity_done_store += quantity + move.quantity_done_store += float_round(quantity, precision_rounding=rounding) elif move.unit_factor: - move.quantity_done_store += quantity * move.unit_factor + # byproducts handling + move.quantity_done_store += float_round(quantity * move.unit_factor, precision_rounding=rounding) self.check_finished_move_lots() if self.production_id.state == 'confirmed': self.production_id.write({ diff --git a/addons/mrp/wizard/mrp_product_produce_views.xml b/addons/mrp/wizard/mrp_product_produce_views.xml index 9a22d279766..6650449dd16 100644 --- a/addons/mrp/wizard/mrp_product_produce_views.xml +++ b/addons/mrp/wizard/mrp_product_produce_views.xml @@ -17,7 +17,7 @@ <field name="product_uom_id" readonly="1"/> </div> <field name="product_tracking" invisible="1"/> - <field name="lot_id" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/> + <field name="lot_id" required="1" attrs="{'invisible': [('product_tracking', '=', 'none')]}" domain="[('product_id','=',product_id)]" context="{'default_product_id': product_id}"/> </group> </group> <group> diff --git a/addons/payment/models/account_payment.py b/addons/payment/models/account_payment.py index 8609b6b96c9..d507666f35f 100644 --- a/addons/payment/models/account_payment.py +++ b/addons/payment/models/account_payment.py @@ -41,7 +41,7 @@ class AccountPayment(models.Model): if self.payment_token_id.acquirer_id.auto_confirm == 'authorize': raise ValidationError('This feature is not available for payment acquirers set to the "Authorize" mode.\n' 'Please use a token from another provider than %s.' % self.payment_token_id.acquirer_id.name) - reference = "PAYMENT-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) + reference = "P-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S')) tx = self.env['payment.transaction'].create({ 'amount': self.amount, 'acquirer_id': self.payment_token_id.acquirer_id.id, diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 8de18021bcd..2f2037dc95a 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -148,6 +148,17 @@ class TxAuthorize(models.Model): # -------------------------------------------------- @api.model + def create(self, vals): + # The reference is used in the Authorize form to fill a field (invoiceNumber) which is + # limited to 20 characters. We truncate the reference now, since it will be reused at + # payment validation to find back the transaction. + if 'reference' in vals and 'acquirer_id' in vals: + acquier = self.env['payment.acquirer'].browse(vals['acquirer_id']) + if acquier.provider == 'authorize': + vals['reference'] = vals.get('reference', '')[:20] + return super(TxAuthorize, self).create(vals) + + @api.model def _authorize_form_get_tx_from_data(self, data): """ Given a data dict coming from authorize, verify it and find the related transaction record. """ diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index 7831bf2eb2c..ec96a147252 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -198,6 +198,7 @@ class PosOrder(models.Model): grouped_data = {} have_to_group_by = session and session.config_id.group_by or False + rounding_method = session and session.config_id.company_id.tax_calculation_rounding_method for order in self.filtered(lambda o: not o.account_move or order.state == 'paid'): current_company = order.sale_journal.company_id @@ -296,6 +297,14 @@ class PosOrder(models.Model): 'partner_id': partner_id }) + # round tax lines per order + if rounding_method == 'round_globally': + for group_key, group_value in grouped_data.iteritems(): + if group_key[0] == 'tax': + for line in group_value: + line['credit'] = cur.round(line['credit']) + line['debit'] = cur.round(line['debit']) + # counterpart insert_data('counter_part', { 'name': _("Trade Receivables"), # order.name, diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index f7bf1f78010..02d1123bcc3 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -278,13 +278,19 @@ var DebugWidget = PosBaseWidget.extend({ }); this.$('.button.export_unpaid_orders').click(function(){ - self.gui.download_file(self.pos.export_unpaid_orders(), - "unpaid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json'); + self.gui.prepare_download_link( + self.pos.export_unpaid_orders(), + _t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', + ".export_unpaid_orders", ".download_unpaid_orders" + ); }); this.$('.button.export_paid_orders').click(function() { - self.gui.download_file(self.pos.export_paid_orders(), - "paid_orders_" + (new Date()).toUTCString().replace(/\ /g,'_') + '.json'); + self.gui.prepare_download_link( + self.pos.export_paid_orders(), + _t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json', + ".export_paid_orders", ".download_paid_orders" + ); }); this.$('.button.import_orders input').on('change', function(event) { diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index f0755fa7573..87cb30caf78 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -362,7 +362,34 @@ var Gui = core.Class.extend({ // if 'contents' is not a string, it is converted into // a JSON representation of the contents. + + // TODO: remove me in master: deprecated in favor of prepare_download_link + // this method is kept for backward compatibility but is likely not going + // to work as many browsers do to not accept fake click events on links download_file: function(contents, name) { + href_params = this.prepare_file_blob(contents,name); + var evt = document.createEvent("HTMLEvents"); + evt.initEvent("click"); + + $("<a>",href_params).get(0).dispatchEvent(evt); + + }, + + prepare_download_link: function(contents, filename, src, target) { + var href_params = this.prepare_file_blob(contents, filename); + + $(target).parent().attr(href_params); + $(src).addClass('oe_hidden'); + $(target).removeClass('oe_hidden'); + + // hide again after click + $(target).click(function() { + $(src).removeClass('oe_hidden'); + $(this).addClass('oe_hidden'); + }); + }, + + prepare_file_blob: function(contents, name) { var URL = window.URL || window.webkitURL; if (typeof contents !== 'string') { @@ -371,13 +398,8 @@ var Gui = core.Class.extend({ var blob = new Blob([contents]); - var evt = document.createEvent("HTMLEvents"); - evt.initEvent("click"); - - $("<a>",{ - download: name || 'document.txt', - href: URL.createObjectURL(blob), - }).get(0).dispatchEvent(evt); + return {download: name || 'document.txt', + href: URL.createObjectURL(blob),} }, /* ---- Gui: EMAILS ---- */ diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index bc4f2a2fd2e..80ea86b9be9 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -675,6 +675,8 @@ exports.PosModel = Backbone.Model.extend({ flushed.always(function(ids){ pushed.resolve(); }); + + return flushed; }); return pushed; }, @@ -775,6 +777,11 @@ exports.PosModel = Backbone.Model.extend({ var self = this; var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length; + // Keep the order ids that are about to be sent to the + // backend. In between create_from_ui and the success callback + // new orders may have been added to it. + var order_ids_to_sync = _.pluck(orders, 'id'); + // we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice, // then we want to notify the user that we are waiting on something ) var posOrderModel = new Model('pos.order'); @@ -789,8 +796,8 @@ exports.PosModel = Backbone.Model.extend({ timeout: timeout } ).then(function (server_ids) { - _.each(orders, function (order) { - self.db.remove_order(order.id); + _.each(order_ids_to_sync, function (order_id) { + self.db.remove_order(order_id); }); self.set('failed',false); return server_ids; diff --git a/addons/point_of_sale/static/src/js/popups.js b/addons/point_of_sale/static/src/js/popups.js index f4429840237..eaeae4d6f83 100644 --- a/addons/point_of_sale/static/src/js/popups.js +++ b/addons/point_of_sale/static/src/js/popups.js @@ -105,7 +105,9 @@ var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({ this._super(opts); this.$('.download').off('click').click(function(){ - self.gui.download_file(self.options.body,'traceback.txt'); + self.gui.prepare_download_link(self.options.body, + _t('error') + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.txt', + '.download', '.download_error_file'); }); this.$('.email').off('click').click(function(){ diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 3245c9a409b..3821fd212dc 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -336,7 +336,7 @@ </div> <div class='client-detail'> <span class='label'>Country</span> - <select class='detail client-address-country' name='country_id'> + <select class='detail client-address-country needsclick' name='country_id'> <option value=''>None</option> <t t-foreach='widget.pos.countries' t-as='country'> <option t-att-value='country.id' t-att-selected="partner.country_id ? ((country.id === partner.country_id[0]) ? true : undefined) : undefined"> @@ -975,6 +975,9 @@ <div class="button cancel"> Ok </div> + <a><div class="button icon download_error_file oe_hidden"> + <i class="fa fa-arrow-down"></i> + </div></a> <div class="button icon download"> <i class="fa fa-download"></i> </div> @@ -1308,7 +1311,9 @@ <li class="button delete_orders">Delete Paid Orders</li> <li class="button delete_unpaid_orders">Delete Unpaid Orders</li> <li class="button export_paid_orders">Export Paid Orders</li> + <a><li class="button download_paid_orders oe_hidden">Download Paid Orders</li></a> <li class="button export_unpaid_orders">Export Unpaid Orders</li> + <a><li class="button download_unpaid_orders oe_hidden">Download Unpaid Orders</li></a> <li class="button import_orders" style="position:relative"> Import Orders <input type="file" style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer"/> diff --git a/addons/point_of_sale/views/pos_order_report_view.xml b/addons/point_of_sale/views/pos_order_report_view.xml index 9559833a92b..43700f170c2 100644 --- a/addons/point_of_sale/views/pos_order_report_view.xml +++ b/addons/point_of_sale/views/pos_order_report_view.xml @@ -4,7 +4,7 @@ <field name="name">report.pos.order.pivot</field> <field name="model">report.pos.order</field> <field name="arch" type="xml"> - <pivot string="Point of Sale Analysis"> + <pivot string="Point of Sale Analysis" disable_linking="True"> <field name="product_categ_id" type="row"/> <field name="date" interval="month" type="col"/> <field name="product_qty" type="measure"/> diff --git a/addons/product/models/product_uom.py b/addons/product/models/product_uom.py index 9c3baa4d6db..23acd5a5294 100644 --- a/addons/product/models/product_uom.py +++ b/addons/product/models/product_uom.py @@ -84,7 +84,7 @@ class ProductUoM(models.Model): if misc_category: values['category_id'] = misc_category.id else: - values['category_id'] = EnglishUoMCateg.name_create('Unsorted/Imported Units').id + values['category_id'] = EnglishUoMCateg.name_create('Unsorted/Imported Units')[0] new_uom = self.create(values) return new_uom.name_get()[0] diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 3bbc078c6cf..7c245ff55dd 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -44,6 +44,7 @@ <group> <group name="group_general"> <field name="type"/> + <field name="categ_id" string="Internal Category"/> </group> <group name="group_standard_price"> <field name="list_price" widget='monetary' options="{'currency_field': 'currency_id'}"/> @@ -310,6 +311,7 @@ <field name="item_ids" position="replace"> <field name="pricelist_item_ids" widget="many2many"> <tree string="Pricelist Items"> + <field name="pricelist_id"/> <field name="name" string="Applicable On"/> <field name="min_quantity"/> <field name="date_start"/> diff --git a/addons/project/data/project_data.xml b/addons/project/data/project_data.xml index 4c83590bdd6..fe047401c22 100644 --- a/addons/project/data/project_data.xml +++ b/addons/project/data/project_data.xml @@ -97,7 +97,7 @@ <data noupdate="1"> - <record id="project_project_data" model="project.project"> + <record forcecreate="False" id="project_project_data" model="project.project"> <field name="name">Start here to discover Odoo</field> <field name="privacy_visibility">followers</field> <field name="user_id" ref="base.user_root"/> @@ -111,25 +111,25 @@ <field name="alias_parent_model_id" ref="model_project_project"/> </record> - <record id="project_stage_data_0" model="project.task.type"> + <record forcecreate="False" id="project_stage_data_0" model="project.task.type"> <field name="sequence">1</field> <field name="name">New</field> <field name="project_ids" eval="[(4, ref('project_project_data'))]"/> </record> - <record id="project_stage_data_1" model="project.task.type"> + <record forcecreate="False" id="project_stage_data_1" model="project.task.type"> <field name="sequence">2</field> <field name="name">Basic</field> <field name="project_ids" eval="[(4, ref('project_project_data'))]"/> </record> - <record id="project_stage_data_2" model="project.task.type"> + <record forcecreate="False" id="project_stage_data_2" model="project.task.type"> <field name="sequence">3</field> <field name="name">Advanced</field> <field name="project_ids" eval="[(4, ref('project_project_data'))]"/> </record> - <record id="project_task_data_0" model="project.task"> + <record forcecreate="False" id="project_task_data_0" model="project.task"> <field name="sequence">1</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -141,7 +141,7 @@ <field name="stage_id" ref="project_stage_data_0"/> </record> - <record id="project_task_data_1" model="project.task"> + <record forcecreate="False" id="project_task_data_1" model="project.task"> <field name="sequence">2</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -152,7 +152,7 @@ <field name="stage_id" ref="project_stage_data_0"/> </record> - <record id="project_task_data_5" model="project.task"> + <record forcecreate="False" id="project_task_data_5" model="project.task"> <field name="sequence">3</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -163,7 +163,7 @@ <field name="stage_id" ref="project_stage_data_0"/> </record> - <record id="project_task_data_2" model="project.task"> + <record forcecreate="False" id="project_task_data_2" model="project.task"> <field name="sequence">4</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -174,7 +174,7 @@ <field name="stage_id" ref="project_stage_data_0"/> </record> - <record id="project_task_data_4" model="project.task"> + <record forcecreate="False" id="project_task_data_4" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -184,7 +184,7 @@ <field name="stage_id" ref="project_stage_data_2"/> </record> - <record id="msg_task_4" model="mail.message"> + <record forcecreate="False" id="msg_task_4" model="mail.message"> <field name="subject">Converse with your customers and colleagues</field> <field name="model">project.task</field> <field name="author_id" ref="base.partner_root"/> @@ -194,12 +194,12 @@ <field name="subtype_id" ref="mail.mt_comment"/> </record> - <record id="project_tag_data" model="project.tags"> + <record forcecreate="False" id="project_tag_data" model="project.tags"> <field name="name">NeedAssistance</field> <field name="color" eval="5"/> </record> - <record id="project_task_data_6" model="project.task"> + <record forcecreate="False" id="project_task_data_6" model="project.task"> <field name="sequence">3</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -212,7 +212,7 @@ <field name="tag_ids" eval="[(6,0,[ref('project.project_tag_data')])]"/> </record> - <record id="project_task_data_12" model="project.task"> + <record forcecreate="False" id="project_task_data_12" model="project.task"> <field name="sequence">4</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -224,7 +224,7 @@ <field name="stage_id" ref="project_stage_data_1"/> </record> - <record id="project_task_data_13" model="project.task"> + <record forcecreate="False" id="project_task_data_13" model="project.task"> <field name="sequence">5</field> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> @@ -235,7 +235,7 @@ <field name="stage_id" ref="project_stage_data_1"/> </record> - <record id="project_task_data_8" model="project.task"> + <record forcecreate="False" id="project_task_data_8" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -245,7 +245,7 @@ <field name="description">Click on the bar in the upper right corner</field> </record> - <record id="msg_task_data_8_attach" model="ir.attachment"> + <record forcecreate="False" id="msg_task_data_8_attach" model="ir.attachment"> <field name="name">planner_icon.png</field> <field name="datas_fname">planner_icon.png</field> <field name="datas" type="base64" file="project/static/src/img/planner_icon.png"></field> @@ -253,7 +253,7 @@ <field name="res_id" ref="project_task_data_8"/> </record> - <record id="msg_task_data_8" model="mail.message"> + <record forcecreate="False" id="msg_task_data_8" model="mail.message"> <field name="subject">How to open the implementation guide ?</field> <field name="model">project.task</field> <field name="res_id" ref="project_task_data_8"/> @@ -264,11 +264,11 @@ <field name="attachment_ids" eval="[(6, 0, [ref('msg_task_data_8_attach')])]"/> </record> - <record id="project_task_data_8" model="project.task"> + <record forcecreate="False" id="project_task_data_8" model="project.task"> <field name="displayed_image_id" ref="msg_task_data_8_attach"/> </record> - <record id="project_task_data_7" model="project.task"> + <record forcecreate="False" id="project_task_data_7" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -278,7 +278,7 @@ <field name="description">Click on the gear icon on the column, to archive the stage, with all the tasks in it. You can also archive one card only, by clicking on the button in the task form view.</field> </record> - <record id="project_task_data_9" model="project.task"> + <record forcecreate="False" id="project_task_data_9" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -288,7 +288,7 @@ <field name="description">Click on the last column to create a stage. The name depends on the process. For example, in a customer service process, a stage name may be 'Backlog', 'Waiting Customer Feedback' or 'Done'.</field> </record> - <record id="project_task_data_11" model="project.task"> + <record forcecreate="False" id="project_task_data_11" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -298,7 +298,7 @@ <field name="date_deadline" eval="datetime.now()+timedelta(days=30)"/> </record> - <record id="project_task_data_14" model="project.task"> + <record forcecreate="False" id="project_task_data_14" model="project.task"> <field name="user_id" ref="base.user_root"/> <field name="priority">0</field> <field name="project_id" ref="project.project_project_data"/> @@ -308,7 +308,7 @@ <field name="description">In the chatter, sending an email with an attachment will display the picture on the card. When there are several image attachments, you can choose which one you want to display.</field> </record> - <record id="msg_task_data_14_attach" model="ir.attachment"> + <record forcecreate="False" id="msg_task_data_14_attach" model="ir.attachment"> <field name="name">bird.jpg</field> <field name="datas_fname">bird.jpg</field> <field name="datas" type="base64" file="project/static/src/img/bird.jpg"></field> @@ -317,7 +317,7 @@ </record> - <record id="msg_task_data_14" model="mail.message"> + <record forcecreate="False" id="msg_task_data_14" model="mail.message"> <field name="subject">How to open the planner?</field> <field name="model">project.task</field> <field name="res_id" ref="project_task_data_14"/> @@ -327,7 +327,7 @@ <field name="attachment_ids" eval="[(6, 0, [ref('msg_task_data_14_attach')])]"/> </record> - <record id="project_task_data_14" model="project.task"> + <record forcecreate="False" id="project_task_data_14" model="project.task"> <field name="displayed_image_id" ref="msg_task_data_14_attach"/> </record> diff --git a/addons/project/data/web_planner_data.xml b/addons/project/data/web_planner_data.xml index 91bfecfb30d..8e8bf631798 100644 --- a/addons/project/data/web_planner_data.xml +++ b/addons/project/data/web_planner_data.xml @@ -124,7 +124,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-check-square-o"/> + <span class="fa fa-check-square-o"></span> <strong> Tasks</strong> </span> </div> @@ -142,7 +142,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-exclamation-circle"/> + <span class="fa fa-exclamation-circle"></span> <strong> Issues</strong> </span> </div> @@ -159,7 +159,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-clock-o"/> + <span class="fa fa-clock-o"></span> <strong> Timesheets</strong> </span> </div> @@ -225,8 +225,8 @@ </div> <table class="table"> <thead> - <th><h4><span class="fa fa-thumbs-o-up"/> The <strong>Right Way</strong> to use projects:</h4></th> - <th><h4><span class="fa fa-thumbs-o-down"/> The <strong> Wrong Way</strong> to use projects:</h4></th> + <th><h4><span class="fa fa-thumbs-o-up"></span> The <strong>Right Way</strong> to use projects:</h4></th> + <th><h4><span class="fa fa-thumbs-o-down"></span> The <strong> Wrong Way</strong> to use projects:</h4></th> </thead> <tbody> <tr> @@ -246,8 +246,8 @@ <div class="well well-lg"> <strong>Need help with defining your Projects? Contact us now.</strong><br/> - <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"/> Website Live Chat</a> - <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"/> Email Our Project Expert</a> + <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"></span> Website Live Chat</a> + <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"></span> Email Our Project Expert</a> </div> </t> @@ -360,8 +360,8 @@ <div class="col-md-6 col-xs-12"> <div class="well well-lg"> <strong>Need help to structure your projects?</strong><br/> - <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"/> Website Live Chat on</a><br/> - <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"/> Email Our Project Expert</a> + <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"></span> Website Live Chat on</a><br/> + <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"></span> Email Our Project Expert</a> </div> </div> </div> @@ -379,7 +379,7 @@ <section> <div class="o_planner_paragraphs"> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-10 col-md-10"> <p><strong>Define a Naming Convention</strong><br/> Add keywords in the 'Task title' field, for example [Customer name] or [Website]. This will help you navigate and search through the dozens of tasks in your project. @@ -387,7 +387,7 @@ </div> </div> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-11 col-md-10"> <p><strong>Add a Deadline</strong><br/> The deadline will help you determine if a task or issue is progressing as expected and to anticipate its next update. @@ -395,7 +395,7 @@ </div> </div> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-11 col-md-10"> <p><strong>Use Tags</strong><br/> Tags are complementary to your project stages: they can work as a second level categorization which is very useful if you have a lot of Tasks or Issues to manage. Also, it becomes very easy to find Tasks or Issues by typing the Tag into the main Search bar. @@ -403,7 +403,7 @@ </div> </div> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-11 col-md-10"> <p><strong>Use Status Indicator</strong><br/> The Status indicator helps you manage Tasks and Issues by giving them one of 3 different colour: Grey, Green or Red. The meaning of these Statuses can be freely configured, for example: @@ -414,7 +414,7 @@ </div> </div> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-11 col-md-10"> <p><strong>Set Priorities</strong><br/> The <i class="fa fa-star"/> is used to indicate priority: in Kanban views, high priority Tasks or Issues will be displayed on top. This is particulary useful if you use a Scrum methodology to indicate the tasks for the week. Similarly to Status, you can change the meaning of the Star indicator from the Project Stages tab. @@ -422,7 +422,7 @@ </div> </div> <div class="row"> - <div class="col-sm-1 col-md-2 icon"><span data-icon="" /></div> + <div class="col-sm-1 col-md-2 icon"><span data-icon=""></span></div> <div class="col-sm-11 col-md-10"> <p><strong>Have Clear Responsibilities</strong><br/> The person assigned to a task is responsible for its progress and their avatar is displayed in Kanban view for quick reference. Of course, the responsibility for a task can change depending on its stage. @@ -470,7 +470,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-tasks"/> + <span class="fa fa-tasks"></span> <strong> Directly in Odoo</strong> </span> </div> @@ -488,7 +488,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-puzzle-piece"/> + <span class="fa fa-puzzle-piece"></span> <strong> Via Chrome extension</strong> </span> </div> @@ -509,7 +509,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-mobile"/> + <span class="fa fa-mobile"></span> <strong> From your Mobile phone</strong> </span> </div> @@ -570,9 +570,9 @@ <div class="well well-lg"> <strong>Need help with Timesheets management? Contact us now:</strong><br/> <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"> - <span class="fa fa-comment-o"/> Website Live Chat on + <span class="fa fa-comment-o"></span> Website Live Chat on </a> - <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"/> Email Our Project Expert</a> + <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"></span> Email Our Project Expert</a> </div> </section> </t> @@ -588,7 +588,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-laptop"/> + <span class="fa fa-laptop"></span> <strong> Screen Customization</strong> </span> </div> @@ -607,7 +607,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-sitemap"/> + <span class="fa fa-sitemap"></span> <strong> Workflow Customization</strong> </span> </div> @@ -624,7 +624,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-pencil-square-o"/> + <span class="fa fa-pencil-square-o"></span> <strong> Create Custom Reports</strong> </span> </div> @@ -650,8 +650,8 @@ <div class="col-md-4 col-xs-12"> <div class="well well-lg"> <strong>Contact us now:</strong><br/> - <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"/> Website Live Chat on</a><br/> - <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"/> Email Our Project Expert</a> + <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"></span> Website Live Chat on</a><br/> + <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"></span> Email Our Project Expert</a> </div> </div> </div> @@ -671,7 +671,7 @@ <p>Create bills automatically based on Time & Material.</p> <a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'account')" class="btn odoo_purple"> - <span class="fa fa-arrow-circle-o-down"/> Install now + <span class="fa fa-arrow-circle-o-down"></span> Install now </a> </div> <div class="text-center col-md-4 col-xs-6"> @@ -680,7 +680,7 @@ <p>Organize your company, from personal tasks to collaborative meeting minutes.</p> <a t-att-href="prepare_backend_url('base.open_module_tree', 'form', 'note')" class="btn odoo_purple"> - <span class="fa fa-arrow-circle-o-down"/> Install now + <span class="fa fa-arrow-circle-o-down"></span> Install now </a> </div> </div> @@ -822,7 +822,7 @@ </div> </div> <div class="alert alert-info info_icon" role="alert"> - <span class="fa fa-lightbulb-o fa-lg"/> + <span class="fa fa-lightbulb-o fa-lg"></span> <h4><strong>There are two ways of managing your Kanban stages:</strong></h4> <li><strong>In 'Push' mode</strong>, tasks are pushed into the next stage (once they satisfy all requirements) by the person responsible for the current stage. This is a simple way to work but only functions well if you work alone.</li> <li><strong>In 'Pull' mode</strong>, tasks ready to progress to the next stage are just marked as 'Ready for next stage (using the status icon) by the person responsible for the current stage. Then, the person responsible for the next stage takes the task and moves it to the next stage. This is the best way to work if you have diluted responsibilities for your Kanban stages.</li> @@ -834,7 +834,7 @@ <p class="text-muted">Managing notifications is essential: too few and you risk missing critical information, too many and you will be overloaded with unnecessary information. The trick is to find the right balance between the projects, stages and tasks you want to be informed about. Fortunately, Odoo Project has many levels of notifications and messages you can choose from.</p> <div class="o_planner_paragraphs"> <div class="row"> - <div class="col-md-2 icon"><span data-icon="" /></div> + <div class="col-md-2 icon"><span data-icon=""></span></div> <div class="col-md-7"> <p> <strong>Follow only what you need</strong><br/> @@ -850,7 +850,7 @@ </div> </div> <div class="row"> - <div class="col-md-2 icon"><span data-icon="" /></div> + <div class="col-md-2 icon"><span data-icon=""></span></div> <div class="col-md-10"> <p> <strong>Rely on the chatter</strong><br/> @@ -861,7 +861,7 @@ </div> </div> <div class="row"> - <div class="col-md-2 icon"><span data-icon="" /></div> + <div class="col-md-2 icon"><span data-icon=""></span></div> <div class="col-md-10"> <p> <strong>Be careful about the messages you send</strong><br/> @@ -876,7 +876,7 @@ </div> <div class="row"> - <div class="col-md-2 icon"><span data-icon="" /></div> + <div class="col-md-2 icon"><span data-icon=""></span></div> <div class="col-md-10"> <p> <strong>Create tasks and issues by email</strong><br/> @@ -890,7 +890,7 @@ </p> </div> <div class="alert alert-info info_icon" role="alert"> - <span class="fa fa-lightbulb-o fa-lg"/> + <span class="fa fa-lightbulb-o fa-lg"></span> <li>Internal notes are messages that will appear in the Chatter but will not be notified in Odoo's Inbox.</li> <li>You can reply directly to a message from you email software; the message and its attachments will be added to the Chatter.</li> <li>If you don't want to receive email notifications, you can uncheck the option in your <a t-att-href="prepare_backend_url('base.action_res_users', 'form')">Account Preferences</a>.</li> @@ -911,7 +911,7 @@ </p> <div class="o_planner_paragraphs"> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <p> <strong>Ask participants to prepare</strong><br/> @@ -922,7 +922,7 @@ </div> </div> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <p> <strong>Prepare yourself as well</strong><br/> @@ -933,7 +933,7 @@ </div> </div> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <p> <strong>Start with an answer.</strong><br/> @@ -945,7 +945,7 @@ </div> </div> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <strong> Prepare an agenda (and keep to it)</strong> <p>Be aware of the team’s productivity and time: try to keep meetings to one hour or less.</p> @@ -967,7 +967,7 @@ </div> </div> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <p> <strong>Follow every meeting with a recap email</strong> @@ -976,7 +976,7 @@ </div> </div> <div class="row"> - <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""/></div> + <div class="hidden-xs hidden-sm col-md-2 icon"><span data-icon=""></span></div> <div class="col-xs-12 col-md-10"> <p> <strong>Choose the right day</strong><br/> @@ -996,7 +996,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-thumb-tack"/> + <span class="fa fa-thumb-tack"></span> <strong> Exercise 1</strong><br/> <span class="small">Check Workload</span> </span> @@ -1015,7 +1015,7 @@ <div class="panel panel-success"> <div class="panel-heading"> <span class="panel-title"> - <span class="fa fa-thumb-tack"/> + <span class="fa fa-thumb-tack"></span> <strong> Exercise 2</strong><br/> <span class="small">Delay to close an Issue</span> </span> @@ -1032,7 +1032,7 @@ </div> </div> <div class="alert alert-info info_icon" role="alert"> - <span class="fa fa-lightbulb-o fa-lg"/> + <span class="fa fa-lightbulb-o fa-lg"></span> <li>You can save your reports to easily reuse it later</li> <li>You can even include any report in your dashboard for permanent access!</li> </div> @@ -1046,8 +1046,8 @@ <div class="col-md-4 col-xs-12"> <div class="well well-lg"> <strong>Contact us now:</strong><br/> - <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"/> Website Live Chat on</a><br/> - <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"/> Email Our Project Expert</a> + <a href="https://www.odoo.com/page/live-chat" target="_blank" style="margin-right:10px"><span class="fa fa-comment-o"></span> Website Live Chat on</a><br/> + <a href="https://www.odoo.com/help"><span class="fa fa-envelope-o"></span> Email Our Project Expert</a> </div> </div> </div> diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py index b5e95d28efc..7120d7daa64 100644 --- a/addons/purchase/models/purchase.py +++ b/addons/purchase/models/purchase.py @@ -450,6 +450,7 @@ class PurchaseOrder(models.Model): result = action.read()[0] #override the context to get rid of the default filtering on picking type + result.pop('id', None) result['context'] = {} pick_ids = sum([order.picking_ids.ids for order in self], []) #choose the view_mode accordingly diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 3219d14754b..265e48bea35 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -51,7 +51,7 @@ class ReportController(Controller): #------------------------------------------------------ # Misc. route utils #------------------------------------------------------ - @route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="user") + @route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="public") def report_barcode(self, type, value, width=600, height=100, humanreadable=0): """Contoller able to render barcode images thanks to reportlab. Samples: diff --git a/addons/report/models/ir_actions_report_xml.py b/addons/report/models/ir_actions_report_xml.py index 667a2b43349..8461815e389 100644 --- a/addons/report/models/ir_actions_report_xml.py +++ b/addons/report/models/ir_actions_report_xml.py @@ -17,7 +17,7 @@ class ir_actions_report(models.Model): """ self.ensure_one() action_ref = self.env.ref('base.action_ui_view') - if not action_ref: + if not action_ref or len(self.report_name.split('.')) < 2: return False action_data = action_ref.read()[0] action_data['domain'] = [('name', 'ilike', self.report_name.split('.')[1]), ('type', '=', 'qweb')] diff --git a/addons/report/views/base_config_settings_views.xml b/addons/report/views/base_config_settings_views.xml index 0253fde66b6..326a83575e4 100644 --- a/addons/report/views/base_config_settings_views.xml +++ b/addons/report/views/base_config_settings_views.xml @@ -36,7 +36,7 @@ <field name="external_report_layout" class="oe_inline"/> </div> <div class="mt16"> - <button name="change_report_template" string="Change Report Template" type="object" class="oe_link"/> + <button name="change_report_template" string="Change Report Template" type="object" class="oe_link" icon="fa-arrow-right"/> <button name="edit_external_header" string="Edit Header" type="object" class="oe_link" groups="base.group_no_one"/> <button name="%(action_report_externalpreview)d" string="Preview Report" type="action" class="oe_link" groups="base.group_no_one"/> </div> diff --git a/addons/report_intrastat/views/report_intrastat_views.xml b/addons/report_intrastat/views/report_intrastat_views.xml index ca1c5c67c10..f91c5db3745 100644 --- a/addons/report_intrastat/views/report_intrastat_views.xml +++ b/addons/report_intrastat/views/report_intrastat_views.xml @@ -29,9 +29,11 @@ <field name="model">product.template</field> <field name="inherit_id" ref="stock.view_template_property_form"/> <field name="arch" type="xml"> - <field name="categ_id" position="after"> - <field name="intrastat_id"/> - </field> + <group name="group_lots_and_weight" position="after"> + <group> + <field name="intrastat_id"/> + </group> + </group> </field> </record> diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index a994d5218dd..fa2dd448c08 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -506,10 +506,12 @@ class SaleOrder(models.Model): for tax in line.tax_id: group = tax.tax_group_id res.setdefault(group, 0.0) - amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty)['taxes'][0]['amount'] + amount = tax.compute_all(line.price_reduce + base_tax, quantity=line.product_uom_qty, + product=line.product_id, partner=line.order_partner_id)['taxes'][0]['amount'] res[group] += amount if tax.include_base_amount: - base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1)['taxes'][0]['amount'] + base_tax += tax.compute_all(line.price_reduce + base_tax, quantity=1, product=line.product_id, + partner=line.order_partner_id)['taxes'][0]['amount'] res = sorted(res.items(), key=lambda l: l[0].sequence) res = map(lambda l: (l[0].name, l[1]), res) return res diff --git a/addons/sale/models/sale_config_settings.py b/addons/sale/models/sale_config_settings.py index 4bd9099c464..9dbe42b1f50 100644 --- a/addons/sale/models/sale_config_settings.py +++ b/addons/sale/models/sale_config_settings.py @@ -87,7 +87,7 @@ class SaleConfiguration(models.TransientModel): @api.model def get_default_sale_pricelist_setting(self, fields): - sale_pricelist_setting = self.env['ir.values'].get_default('sales.config.settings', 'sale_pricelist_setting') + sale_pricelist_setting = self.env['ir.values'].get_default('sale.config.settings', 'sale_pricelist_setting') multi_sales_price = sale_pricelist_setting in ['percentage', 'formula'] return { 'sale_pricelist_setting': sale_pricelist_setting, @@ -118,7 +118,7 @@ class SaleConfiguration(models.TransientModel): }) def set_default_sale_pricelist_setting(self): - return self.env['ir.values'].sudo().set_default('sales.config.settings', 'sale_pricelist_setting', self.sale_pricelist_setting) + return self.env['ir.values'].sudo().set_default('sale.config.settings', 'sale_pricelist_setting', self.sale_pricelist_setting) def set_deposit_product_id_defaults(self): return self.env['ir.values'].sudo().set_default('sale.config.settings', 'deposit_product_id_setting', self.deposit_product_id_setting.id) diff --git a/addons/sale/views/sale_config_settings_views.xml b/addons/sale/views/sale_config_settings_views.xml index 565b8d4dffe..923a8928c40 100644 --- a/addons/sale/views/sale_config_settings_views.xml +++ b/addons/sale/views/sale_config_settings_views.xml @@ -72,9 +72,6 @@ <div class="mt16"> <button name="%(base.action_currency_form)d" icon="fa-arrow-right" type="action" string="Currencies" class="btn-link"/> </div> - <div> - <button name="%(account.action_account_config)d" icon="fa-arrow-right" type="action" string="Apply Automatic Rates Update" class="btn-link"/> - </div> </div> </div> </div> @@ -188,7 +185,7 @@ </div> <div class="content-group" attrs="{'invisible': [('default_use_sale_note','=',False)]}"> <div class="mt16"> - <field name="sale_note"/> + <field name="sale_note" placeholder="Insert your terms & conditions here..."/> </div> </div> </div> diff --git a/addons/sales_team/static/src/less/sales_team_dashboard.less b/addons/sales_team/static/src/less/sales_team_dashboard.less index 2665f6b9960..25094260291 100644 --- a/addons/sales_team/static/src/less/sales_team_dashboard.less +++ b/addons/sales_team/static/src/less/sales_team_dashboard.less @@ -4,6 +4,9 @@ @sale-table-spacing: 10px; .o_kanban_record { + span { + display: inline-block; + } min-height: 250px; @media (min-width: @screen-sm-min) { min-width: 450px; diff --git a/addons/sales_team/static/src/xml/sales_team_dashboard.xml b/addons/sales_team/static/src/xml/sales_team_dashboard.xml index 93f2f3ca9a9..b1bb4a6ca65 100644 --- a/addons/sales_team/static/src/xml/sales_team_dashboard.xml +++ b/addons/sales_team/static/src/xml/sales_team_dashboard.xml @@ -84,7 +84,7 @@ </a> </td> <td t-if="show_demo or (values and values['closing']['overdue'] != 0)" class="o_warning" title="To Opportunities"> - <a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_activities_overdue':'1'}"> + <a href="#" class="o_dashboard_action" name="crm.action_your_pipeline" data-context="{'search_default_overdue_opp':'1'}"> <t t-if="!show_demo"> <t t-esc="values['closing']['overdue']"/> exp. closing </t> diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 3aafae6aade..a661169296f 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -13,7 +13,7 @@ OPERATORS = { '>': py_operator.gt, '<=': py_operator.le, '>=': py_operator.ge, - '==': py_operator.eq, + '=': py_operator.eq, '!=': py_operator.ne } @@ -260,9 +260,6 @@ class Product(models.Model): if not isinstance(value, (float, int)): raise UserError('Invalid domain right operand') - if operator == '=': - operator = '==' - # TODO: Still optimization possible when searching virtual quantities ids = [] for product in self.search([]): @@ -272,7 +269,7 @@ class Product(models.Model): def _search_qty_available(self, operator, value): # TDE FIXME: should probably clean the search methods - if value == 0.0 and operator in ('==', '>=', '<='): + if value == 0.0 and operator in ('=', '>=', '<='): return self._search_product_quantity(operator, value, 'qty_available') product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id')) return [('id', 'in', product_ids)] diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 744aa1ff6df..d88ad3229f2 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -575,7 +575,9 @@ class StockMove(models.Model): # in case the move is returned, we want to try to find quants before forcing the assignment if not move.origin_returned_move_id: continue - if move.product_id.type == 'consu': + # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move + ancestors = move.find_move_ancestors() + if move.product_id.type == 'consu' and not ancestors: moves_to_assign |= move continue else: @@ -584,8 +586,6 @@ class StockMove(models.Model): # we always search for yet unassigned quants main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)] - # if the move is preceeded, restrict the choice of quants in the ones moved previously in original move - ancestors = move.find_move_ancestors() if move.state == 'waiting' and not ancestors: # if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock main_domain[move.id] += [('id', '=', False)] @@ -797,7 +797,11 @@ class StockMove(models.Model): # compute quantities for each lot + check quantities match lot_quantities = dict((pack_lot.lot_id.id, operation.product_uom_id._compute_quantity(pack_lot.qty, operation.product_id.uom_id) ) for pack_lot in operation.pack_lot_ids) - if operation.pack_lot_ids and float_compare(sum(lot_quantities.values()), operation.product_qty, precision_rounding=operation.product_uom_id.rounding) != 0.0: + + qty = operation.product_qty + if operation.product_uom_id and operation.product_uom_id != operation.product_id.uom_id: + qty = operation.product_uom_id._compute_quantity(qty, operation.product_id.uom_id) + if operation.pack_lot_ids and float_compare(sum(lot_quantities.values()), qty, precision_rounding=operation.product_id.uom_id.rounding) != 0.0: raise UserError(_('You have a difference between the quantity on the operation and the quantities specified for the lots. ')) quants_taken = [] @@ -972,4 +976,4 @@ class StockMove(models.Model): def quants_unreserve(self): self.filtered(lambda x: x.partially_available).write({'partially_available': False}) - self.mapped('reserved_quant_ids').sudo().write({'reservation_id': False})
\ No newline at end of file + self.mapped('reserved_quant_ids').sudo().write({'reservation_id': False}) diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py index b6c46310b94..6e51a573546 100644 --- a/addons/stock/models/stock_pack_operation.py +++ b/addons/stock/models/stock_pack_operation.py @@ -34,8 +34,7 @@ class PackOperation(models.Model): product_qty = fields.Float('To Do', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True) ordered_qty = fields.Float('Ordered Quantity', digits=dp.get_precision('Product Unit of Measure')) qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure')) - # TDE FIXME: what what what what ?? - is_done = fields.Boolean(compute='_compute_is_done', inverse='_set_is_done', string='Done', oldname='processed_boolean') + is_done = fields.Boolean(compute='_compute_is_done', string='Done', readonly=False, oldname='processed_boolean') package_id = fields.Many2one('stock.quant.package', 'Source Package') pack_lot_ids = fields.One2many('stock.pack.operation.lot', 'operation_id', 'Lots/Serial Numbers Used') result_package_id = fields.Many2one( @@ -73,9 +72,8 @@ class PackOperation(models.Model): def _compute_is_done(self): self.is_done = self.qty_done > 0.0 - @api.one - def _set_is_done(self): - # TDE FIXME: whuuuut ??? + @api.onchange('is_done') + def on_change_is_done(self): if not self.product_id: if self.is_done and self.qty_done == 0: self.qty_done = 1.0 diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 2a875453509..635e3b3ad53 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -316,22 +316,18 @@ class Picking(models.Model): self.state = 'cancel' elif all(move.state in ['cancel', 'done'] for move in self.move_lines): self.state = 'done' - elif self.move_type == 'one': - ordered_moves = self.move_lines.filtered( - lambda move: move.state not in ['cancel', 'done'] - ).sorted( - key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=False - ) - self.state = ordered_moves[0].state else: - filtered_moves = self.move_lines.filtered(lambda move: move.state not in ['cancel', 'done']) - if not all(move.state == 'assigned' for move in filtered_moves) and any(move.state == 'assigned' for move in filtered_moves): - self.state = 'partially_available' - elif any(move.partially_available for move in filtered_moves): + # We sort our moves by importance of state: "confirmed" should be first, then we'll have + # "waiting" and finally "assigned" at the end. + moves_todo = self.move_lines\ + .filtered(lambda move: move.state not in ['cancel', 'done'])\ + .sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0) + if self.move_type == 'one': + self.state = moves_todo[0].state + elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo): self.state = 'partially_available' else: - ordered_moves = filtered_moves.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0, reverse=True) - self.state = ordered_moves[0].state + self.state = moves_todo[-1].state @api.one @api.depends('move_lines.priority') @@ -597,7 +593,10 @@ class Picking(models.Model): continue move_quants = move.reserved_quant_ids picking_quants += move_quants - forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0 + forced_qty = 0.0 + if move.state == 'assigned': + qty = move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, round=False) + forced_qty = qty - sum([x.qty for x in move_quants]) # if we used force_assign() on the move, or if the move is incoming, forced_qty > 0 if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0: if forced_qties.get(move.product_id): diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index 65d2a862866..3ee20467a88 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1210,7 +1210,6 @@ class TestStockFlow(TestStockCommon): inventory5.action_done() self.assertEqual(inventory5_lines_count, inventory4_lines_count + 1, "The new product is not taken into account in the inventory valuation.") self.assertTrue(product.id in inventory5.line_ids.mapped('product_id').ids, "The new product is not take into account in the inventory valuation.") - def test_30_check_with_no_incoming_lot(self): """ Picking in without lots and picking out with""" @@ -1278,7 +1277,6 @@ class TestStockFlow(TestStockCommon): self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot2.id]), 1.0, 'Wrong sum of quants with lot 2') self.assertEqual(sum([x.qty for x in quants if x.lot_id.id == lot3.id]), 2.0, 'Wrong sum of quants with lot 3') - def test_40_pack_in_pack(self): """ Put a pack in pack""" picking_out = self.PickingObj.create({ @@ -1368,7 +1366,8 @@ class TestStockFlow(TestStockCommon): # set the pack in pack operation to 'done' for pack in picking_pack.pack_operation_pack_ids: - pack.qty_done = 1.0 + pack.is_done = True + pack.on_change_is_done() # Put in a pack picking_pack.put_in_pack() @@ -1386,7 +1385,8 @@ class TestStockFlow(TestStockCommon): # set the pack in pack operation to 'done' for pack in picking_out.pack_operation_pack_ids: - pack.qty_done = 1.0 + pack.is_done = True + pack.on_change_is_done() # Validate picking picking_out.do_new_transfer() @@ -1536,3 +1536,194 @@ class TestStockFlow(TestStockCommon): # We should also make sure that when matching stock moves with pack operations, it takes the correct self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations') self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + def test_70_picking_state_all_at_once_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'one' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_71_picking_state_all_at_once_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "all at once" and "force assign" scenario + # ----------------------------------------------------------- + # create a "all at once" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_72_picking_state_partial_reserve(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "reserve" scenario + # ----------------------------------------------------------- + # get one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 1, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # create a "partial" delivery order for two products + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in partially available + picking_out.action_assign() + self.assertEquals(picking_out.state, "partially_available") + + # receive one product in stock + inventory = self.env['stock.inventory'].create({ + 'name': 'Inventory Product Table', + 'filter': 'partial', + 'line_ids': [(0, 0, { + 'product_id': self.productA.id, + 'product_uom_id': self.productA.uom_id.id, + 'product_qty': 2, + 'location_id': self.stock_location + })] + }) + inventory.action_done() + + # recheck availability of the delivery order, it should be assigned + picking_out.action_assign() + self.assertEquals(picking_out.state, "assigned") + + def test_73_picking_state_partial_force_assign(self): + """ This test will check that the state of the picking is correctly computed according + to the state of its move lines and its move type. + """ + # move_type: direct == partial, one == all at once + # picking: confirmed == waiting availability, partially_available = partially available + + # ----------------------------------------------------------- + # "partial" and "force assign" scenario + # ----------------------------------------------------------- + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.move_type = 'direct' + + self.MoveObj.create({ + 'name': self.productA.name, + 'product_id': self.productA.id, + 'product_uom_qty': 2, + 'product_uom': self.productA.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + + # validate this delivery order, it should be in the waiting state + picking_out.action_assign() + self.assertEquals(picking_out.state, "confirmed") + + # force assign on the delivery order, it should be assigned + picking_out.force_assign() + self.assertEquals(picking_out.state, "assigned")
\ No newline at end of file diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index f3c05e4dcb1..6fe6c489083 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -140,9 +140,6 @@ <field name="volume" string="Volume"/> </div> </group> - <group> - <field name="categ_id" string="Internal Category"/> - </group> <group name="stock_property" groups="base.group_no_one"> <field name="property_stock_procurement" domain="[('usage','=','procurement')]"/> <field name="property_stock_production" domain="[('usage','=','production')]"/> diff --git a/addons/stock/views/stock_production_lot_views.xml b/addons/stock/views/stock_production_lot_views.xml index 31a2c532a13..cfa5d3929ce 100644 --- a/addons/stock/views/stock_production_lot_views.xml +++ b/addons/stock/views/stock_production_lot_views.xml @@ -15,7 +15,6 @@ <div class="oe_button_box" name="button_box"> <button name="%(lot_open_quants)d" icon="fa-arrows" class="oe_stat_button" string="Locate" type="action" attrs="{'invisible': [('quant_ids','=',[])]}"/> <button name="action_traceability" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="object" attrs="{'invisible': [('quant_ids','=',[])]}"/> - <field name="quant_ids" invisible="True"/> </div> <div class="oe_title"> <label for="name" class="oe_edit_only"/> diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py index a51ea171128..39caa0ff069 100644 --- a/addons/stock/wizard/stock_change_product_qty.py +++ b/addons/stock/wizard/stock_change_product_qty.py @@ -36,7 +36,7 @@ class ProductChangeQuantity(models.TransientModel): def onchange_location_id(self): # TDE FIXME: should'nt we use context / location ? if self.location_id and self.product_id: - availability = self.product_id._product_available() + availability = self.product_id.with_context(compute_child=False)._product_available() self.new_quantity = availability[self.product_id.id]['qty_available'] @api.onchange('product_id') diff --git a/addons/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py index 3e67dd1db8d..ccf150954dc 100644 --- a/addons/stock_account/report/stock_history.py +++ b/addons/stock_account/report/stock_history.py @@ -79,7 +79,7 @@ class StockHistory(models.Model): product_template_id, SUM(quantity) as quantity, date, - SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant, + COALESCE(SUM(price_unit_on_quant * quantity) / NULLIF(SUM(quantity), 0), 0) as price_unit_on_quant, source, string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number FROM diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 99ce35e02a3..3d367b0364b 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -638,7 +638,7 @@ class SurveyQuestion(models.Model): if self.comments_allowed: comment_answer = answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() # Preventing answers with blank value - if all([True if not answer.strip() else False for answer in answer_candidates.values()]): + if all([True if not answer.strip() else False for answer in answer_candidates.values()]) and answer_candidates: errors.update({answer_tag: self.constr_error_msg}) # There is no answer neither comments (if comments count as answer) if not answer_candidates and self.comment_count_as_answer and (not comment_flag or not comment_answer): diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index e8ff09721bd..ab3f892a424 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -587,6 +587,22 @@ class WebClient(http.Controller): class Proxy(http.Controller): + @http.route('/web/proxy/load', type='json', auth="none") + def load(self, path): + """ Proxies an HTTP request through a JSON request. + + It is strongly recommended to not request binary files through this, + as the result will be a binary data blob as well. + + :param path: actual request path + :return: file content + """ + from werkzeug.test import Client + from werkzeug.wrappers import BaseResponse + + base_url = request.httprequest.base_url + return Client(request.httprequest.app, BaseResponse).get(path, base_url=base_url).data + @http.route('/web/proxy/post/<path:path>', type='http', auth='user', methods=['GET']) def post(self, path): """Effectively execute a POST request that was hooked through user login""" diff --git a/addons/web/static/src/js/framework/session.js b/addons/web/static/src/js/framework/session.js index 6af97721926..516436af93c 100644 --- a/addons/web/static/src/js/framework/session.js +++ b/addons/web/static/src/js/framework/session.js @@ -86,8 +86,11 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, { * Init a session, reloads from cookie, if it exists */ session_init: function () { + var def = this.session_reload(); + if (this.is_frontend) return def; + var self = this; - return this.session_reload().then(function() { + return def.then(function() { var modules = self.module_list.join(','); var deferred = self.load_qweb(modules); if(self.session_is_valid()) { 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 fc3db6fd1ce..8261329b90d 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -26,7 +26,7 @@ var M2ODialog = Dialog.extend({ title: _.str.sprintf(_t("Create a %s"), parent.string), size: 'medium', buttons: [ - {text: _t('Create'), classes: 'btn-primary', click: function() { + {text: _t('Create'), classes: 'btn-primary', click: function(e) { if (this.$("input").val() !== ''){ this.getParent()._quick_create(this.$("input").val()); this.close(); @@ -997,7 +997,9 @@ var X2ManyListView = ListView.extend({ */ var X2ManyList = ListView.List.extend({ pad_table_to: function (count) { - if (!this.view.is_action_enabled('create') || this.view.x2m.get('effective_readonly')) { + var ftype = this.view.x2m.field.type; + var is_readonly = this.view.x2m.get('effective_readonly'); + if (is_readonly || (ftype === 'one2many' && !this.view.is_action_enabled('create'))) { this._super(count); return; } @@ -1304,7 +1306,7 @@ var Many2ManyListView = X2ManyListView.extend({ context: this.x2m.build_context(), title: _t("Add: ") + this.x2m.string, alternative_form_view: this.x2m.field.views ? this.x2m.field.views.form : undefined, - no_create: this.x2m.options.no_create, + no_create: this.x2m.options.no_create || !this.is_action_enabled('create'), on_selected: function(element_ids) { return self.x2m.data_link_multi(element_ids).then(function() { self.x2m.reload_current_view(); diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index 31e83d24777..c715d7c3812 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -697,6 +697,12 @@ var FieldBooleanButton = common.AbstractField.extend({ this.string_false = _t("Archived"); this.hover_false = _t("Restore"); break; + case "close": + this.string_true = _t("Active"); + this.string_false = _t("Closed"); + this.hover_true = _t("Close"); + this.hover_false = _t("Open"); + break; default: var terms = typeof this.options["terminology"] === 'string' ? {} : this.options["terminology"]; this.string_true = _t(terms.string_true || "On"); diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 572fcbff95f..b02c91ed147 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -1508,7 +1508,7 @@ ListView.Groups = Class.extend({ // drag and drop enabled if list is not sorted (unless it is sorted by // 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) + if ((dataset.sort && [seqname, seqname + ' ASC', ''].indexOf(dataset.sort()) === -1) || !_(this.columns).findWhere({'name': seqname})) { return; } diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index fc0380c61b9..95e8d13e5ff 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -57,6 +57,7 @@ var PivotView = View.extend({ this.headers = {}; this.cells = {}; this.has_data = false; + this.$buttons = $(); this.last_header_selected = null; this.sorted_column = {}; @@ -199,7 +200,7 @@ var PivotView = View.extend({ this.initial_row_groupby = context.pivot_row_groupby || this.initial_row_groupby; this.initial_col_groupby = context.pivot_col_groupby || this.initial_col_groupby; } - this.main_row.groupbys = group_by.length ? group_by : this.initial_row_groupby.slice(0); + this.main_row.groupbys = group_by.length ? group_by : (context.pivot_row_groupby || this.initial_row_groupby.slice(0)); this.main_col.groupbys = context.pivot_column_groupby || this.initial_col_groupby.slice(0); this.active_measures = context.pivot_measures || this.active_measures; @@ -219,6 +220,10 @@ var PivotView = View.extend({ this.do_push_state({}); return this.data_loaded.done(function () { self.display_table(); + self.$buttons.find('.o_pivot_measures_list li').removeClass('selected'); + self.active_measures.forEach(function (measure) { + self.$buttons.find('li[data-field="' + measure + '"]').addClass('selected'); + }); _super(); }); }, diff --git a/addons/web/static/src/js/widgets/model_field_selector.js b/addons/web/static/src/js/widgets/model_field_selector.js index 6d65108b7dc..99528e3e74e 100644 --- a/addons/web/static/src/js/widgets/model_field_selector.js +++ b/addons/web/static/src/js/widgets/model_field_selector.js @@ -12,32 +12,14 @@ var ModelFieldSelector = Widget.extend({ template: "FieldSelector", events: { // Handle popover opening and closing - "focusin input, .o_field_selector_popover": function (e) { - if (this._hidePopoverTimeout !== undefined) { - clearTimeout(this._hidePopoverTimeout); - this._hidePopoverTimeout = undefined; - return; - } - if (this.$input.is(e.currentTarget)) { - this.showPopover(); - } - }, - "focusout input, .o_field_selector_popover": function () { - if (this._hidePopoverTimeout !== undefined) { - return; - } - this._hidePopoverTimeout = setTimeout((function () { - this.hidePopover(); - this._hidePopoverTimeout = undefined; - }).bind(this), 100); + "focusin": function () { + clearTimeout(this._hidePopoverTimeout); + this.showPopover(); }, - "click .o_field_selector_close": function () { - if (this._hidePopoverTimeout !== undefined) { - clearTimeout(this._hidePopoverTimeout); - this._hidePopoverTimeout = undefined; - } - this.hidePopover(); + "focusout": function () { + this._hidePopoverTimeout = _.defer(this.hidePopover.bind(this)); }, + "click .o_field_selector_close": "hidePopover", // Handle popover field navigation "click .o_field_selector_prev_page": "goToPrevPage", @@ -190,6 +172,7 @@ var ModelFieldSelector = Widget.extend({ }, /// The removeChainNode method removes the last field name at the end of the current field chain. removeChainNode: function () { + this.dirty = true; this.setChain(this.chain.substring(0, this.chain.lastIndexOf("."))); }, /// The validate method toggles the valid status of the widget and display the error message if it @@ -200,16 +183,20 @@ var ModelFieldSelector = Widget.extend({ this.valid = valid; }, /// The showPopover method shows the popover to select the field chain. It prepares the popover pages - /// before actually showing it. + /// before actually showing it. (if already open, does nothing) showPopover: function () { + if (this._isOpen) return; + this._isOpen = true; this._prefill().then((function () { this.displayPage(); this.$popover.removeClass("hidden"); }).bind(this)); }, /// The hidePopover method closes the popover and mark the field as selected. If the field chain changed, - /// it notifies its parents. + /// it notifies its parents. (if not open, does nothing) hidePopover: function () { + if (!this._isOpen) return; + this._isOpen = false; this.$popover.addClass("hidden"); this.isSelected = true; if (this.dirty) { diff --git a/addons/web/static/src/less/list_view.less b/addons/web/static/src/less/list_view.less index 545f6fcafc7..267ee7aa78a 100644 --- a/addons/web/static/src/less/list_view.less +++ b/addons/web/static/src/less/list_view.less @@ -138,6 +138,10 @@ &.o_readonly { visibility: visible; color: transparent; + + > .o_checkbox { + visibility: hidden; + } } } } @@ -158,7 +162,7 @@ max-width: 100%; // Fix a chrome bug where badge would not break on multiple lines } - &.o_readonly { + &.o_readonly:not(.o_checkbox) { background-color: transparent; padding: 5px; } diff --git a/addons/web/static/src/less/model_field_selector.less b/addons/web/static/src/less/model_field_selector.less index 1dc953bf970..539f6fb4507 100644 --- a/addons/web/static/src/less/model_field_selector.less +++ b/addons/web/static/src/less/model_field_selector.less @@ -6,10 +6,10 @@ .o-position-absolute(0, 0, 1px); .o-flex-display(); .o-align-items(center); + cursor: pointer; &::after { .o-caret-down(); - cursor: pointer; } } .o_field_selector_popover { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 05548db32ad..34d24f925ff 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -49,15 +49,15 @@ <table class="o_group o_inner_group o_label_nowrap"> <tr> <td class="o_td_label"><label for="old_pwd" class="o_form_label">Old Password</label></td> - <td width="100%"><input type="password" class="o_form_field o_form_input" name="old_pwd" minlength="1" autofocus="autofocus"/></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="old_pwd" minlength="1" autofocus="autofocus" autocomplete="current-password"/></td> </tr> <tr> <td class="o_td_label"><label for="new_password" class="o_form_label">New Password</label></td> - <td width="100%"><input type="password" class="o_form_field o_form_input" name="new_password" minlength="1"/></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="new_password" minlength="1" autocomplete="new-password"/></td> </tr> <tr> <td class="o_td_label"><label for="confirm_pwd" class="o_form_label">Confirm New Password</label></td> - <td width="100%"><input type="password" class="o_form_field o_form_input" name="confirm_pwd" minlength="1"/></td> + <td width="100%"><input type="password" class="o_form_field o_form_input" name="confirm_pwd" minlength="1" autocomplete="new-password"/></td> </tr> </table> @@ -628,6 +628,7 @@ t-att-tabindex="widget.node.attrs.tabindex" t-att-autofocus="widget.node.attrs.autofocus" t-att-placeholder="widget.node.attrs.placeholder" + t-att-autocomplete="widget.password ? 'new-password' : widget.node.attrs.autocomplete" t-att-maxlength="widget.field.size"/> </t> <t t-name="FormSelection"> @@ -803,7 +804,7 @@ <div t-name="FieldSelector" class="o_field_selector"> <input type="text" t-att-value="widget.chain" t-att-readonly="!(widget.options.debugMode) and 1 or None"/> - <div class="o_field_selector_controls"> + <div class="o_field_selector_controls" tabindex="0"> <i class="fa fa-exclamation-triangle o_field_selector_warning hidden" title="Invalid field chain"/> </div> <div class="o_field_selector_popover hidden" tabindex="0"> diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 9cb88403ac9..9c679393f34 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -268,7 +268,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" maxlength="4096"/> + <input type="password" name="password" id="password" class="form-control" required="required" autocomplete="current-password" 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/translator.js b/addons/web_editor/static/src/js/translator.js index 4fd9825567f..bc431f3ba55 100644 --- a/addons/web_editor/static/src/js/translator.js +++ b/addons/web_editor/static/src/js/translator.js @@ -177,6 +177,10 @@ var Translate = Widget.extend({ }, rte_changed: function (node) { var $node = $(node); + $node.find("p").each(function () { // remove <p/> element which might have been inserted because of copy-paste + var $p = $(this); + $p.after($p.html()).remove(); + }); var trans = this.getTranlationObject($node[0]); $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' ')); }, diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index ef81fa10f83..ca519654ad1 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -24,6 +24,10 @@ from odoo.addons.website.models.website import slug, url_for, _UNSLUG_RE logger = logging.getLogger(__name__) +# global resolver (GeoIP API is thread-safe, for multithreaded workers) +# This avoids blowing up open files limit +odoo._geoip_resolver = None + class RequestUID(object): def __init__(self, **kw): @@ -34,7 +38,7 @@ class Http(models.AbstractModel): _inherit = 'ir.http' rerouting_limit = 10 - _geoip_resolver = None + _geoip_resolver = None # backwards-compatibility @classmethod def _get_converters(cls): @@ -90,26 +94,31 @@ class Http(models.AbstractModel): @classmethod def _geoip_setup_resolver(cls): - if cls._geoip_resolver is None: - try: - import GeoIP - # updated database can be downloaded on MaxMind website - # http://dev.maxmind.com/geoip/legacy/install/city/ - geofile = config.get('geoip_database') - if os.path.exists(geofile): - cls._geoip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD) - else: - cls._geoip_resolver = False - logger.warning('GeoIP database file %r does not exists, apt-get install geoip-database-contrib or download it from http://dev.maxmind.com/geoip/legacy/install/city/', geofile) - except ImportError: - cls._geoip_resolver = False + # Lazy init of GeoIP resolver + if cls._geoip_resolver is not None: + return + if odoo._geoip_resolver is not None: + cls._geoip_resolver = odoo._geoip_resolver + return + try: + import GeoIP + # updated database can be downloaded on MaxMind website + # http://dev.maxmind.com/geoip/legacy/install/city/ + geofile = config.get('geoip_database') + if os.path.exists(geofile): + odoo._geoip_resolver = GeoIP.open(geofile, GeoIP.GEOIP_STANDARD) + else: + odoo._geoip_resolver = False + logger.warning('GeoIP database file %r does not exists, apt-get install geoip-database-contrib or download it from http://dev.maxmind.com/geoip/legacy/install/city/', geofile) + except ImportError: + odoo._geoip_resolver = False @classmethod def _geoip_resolve(cls): if 'geoip' not in request.session: record = {} - if cls._geoip_resolver and request.httprequest.remote_addr: - record = cls._geoip_resolver.record_by_addr(request.httprequest.remote_addr) or {} + if odoo._geoip_resolver and request.httprequest.remote_addr: + record = odoo._geoip_resolver.record_by_addr(request.httprequest.remote_addr) or {} request.session['geoip'] = record @classmethod diff --git a/addons/website_crm_partner_assign/controllers/main.py b/addons/website_crm_partner_assign/controllers/main.py index 46267ee55c4..9edc425e69a 100644 --- a/addons/website_crm_partner_assign/controllers/main.py +++ b/addons/website_crm_partner_assign/controllers/main.py @@ -5,6 +5,7 @@ import datetime import werkzeug from collections import OrderedDict +from werkzeug.exceptions import NotFound from odoo import fields from odoo import http @@ -93,13 +94,13 @@ class WebsiteAccount(website_account): this_week_end_date = fields.Date.to_string(fields.Date.from_string(today) + datetime.timedelta(days=7)) searchbar_filters = { - 'all': {'label': _('All'), 'domain': []}, + 'all': {'label': _('Active'), 'domain': []}, 'today': {'label': _('Today Activities'), 'domain': [('activity_date_deadline', '=', today)]}, 'week': {'label': _('This Week Activities'), 'domain': [('activity_date_deadline', '>=', today), ('activity_date_deadline', '<=', this_week_end_date)]}, 'overdue': {'label': _('Overdue Activities'), 'domain': [('activity_date_deadline', '<', today)]}, - 'won': {'label': _('Won'), 'domain': [('stage_id.probability', '=', 100), ('stage_id.fold', '=', True)]}, - 'lost': {'label': _('Lost'), 'domain': [('active', '=', False)]}, + 'won': {'label': _('Won'), 'domain': [('stage_id.probability', '=', 100), ('stage_id.on_change', '=', True)]}, + 'lost': {'label': _('Lost'), 'domain': [('active', '=', False), ('probability', '=', 0)]}, } searchbar_sortings = { 'date': {'label': _('Newest'), 'order': 'create_date desc'}, @@ -118,6 +119,8 @@ class WebsiteAccount(website_account): if not filterby: filterby = 'all' domain += searchbar_filters[filterby]['domain'] + if filterby == 'lost': + CrmLead = CrmLead.with_context(active_test=False) # archive groups - Default Group By 'create_date' archive_groups = self._get_archive_groups('crm.lead', domain) @@ -149,16 +152,21 @@ class WebsiteAccount(website_account): }) return request.render("website_crm_partner_assign.portal_my_opportunities", values) - @http.route(['/my/lead/<model("crm.lead"):lead>'], type='http', auth="user", website=True) - def portal_my_lead(self, lead=None, **kw): + @http.route(['''/my/lead/<model('crm.lead', "[('type','=', 'lead')]"):lead>'''], type='http', auth="user", website=True) + def portal_my_lead(self, lead, **kw): + if lead.type != 'lead': + raise NotFound() return request.render("website_crm_partner_assign.portal_my_lead", {'lead': lead}) - @http.route(['/my/opportunity/<model("crm.lead"):lead>'], type='http', auth="user", website=True) - def portal_my_opportunity(self, lead=None, **kw): + @http.route(['''/my/opportunity/<model('crm.lead', "[('type','=', 'opportunity')]"):opp>'''], type='http', auth="user", website=True) + def portal_my_opportunity(self, opp, **kw): + if opp.type != 'opportunity': + raise NotFound() + return request.render( "website_crm_partner_assign.portal_my_opportunity", { - 'opportunity': lead, - 'user_activity': lead.activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1], + 'opportunity': opp, + 'user_activity': opp.activity_ids.filtered(lambda activity: activity.user_id == request.env.user)[:1], 'stages': request.env['crm.stage'].search([('probability', '!=', '100')], order='sequence desc'), 'activity_types': request.env['mail.activity.type'].sudo().search([]), 'states': request.env['res.country.state'].sudo().search([]), diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py index 95cf731c869..6556deeec70 100644 --- a/addons/website_crm_partner_assign/models/crm_lead.py +++ b/addons/website_crm_partner_assign/models/crm_lead.py @@ -232,26 +232,29 @@ class CrmLead(models.Model): 'planned_revenue': values['planned_revenue'], 'probability': values['probability'], 'priority': values['priority'], + 'date_deadline': values['date_deadline'] or False, } # As activities may belong to several users, only the current portal user activity # will be modified by the portal form. If no activity exist we create a new one instead # that we assign to the portal user. + user_activity = lead.activity_ids.filtered(lambda activity: activity.user_id == self.env.user)[:1] - if user_activity: - user_activity.sudo().write({ - 'activity_type_id': values['activity_type_id'], - 'summary': values['activity_summary'], - 'date_deadline': values['activity_date_deadline'], - }) - else: - self.env['mail.activity'].sudo().create({ - 'res_model_id': self.env.ref('crm.model_crm_lead').id, - 'res_id': lead.id, - 'user_id': self.env.user.id, - 'activity_type_id': values['activity_type_id'], - 'summary': values['activity_summary'], - 'date_deadline': values['activity_date_deadline'], - }) + if values['activity_date_deadline']: + if user_activity: + user_activity.sudo().write({ + 'activity_type_id': values['activity_type_id'], + 'summary': values['activity_summary'], + 'date_deadline': values['activity_date_deadline'], + }) + else: + self.env['mail.activity'].sudo().create({ + 'res_model_id': self.env.ref('crm.model_crm_lead').id, + 'res_id': lead.id, + 'user_id': self.env.user.id, + 'activity_type_id': values['activity_type_id'], + 'summary': values['activity_summary'], + 'date_deadline': values['activity_date_deadline'], + }) lead.write(lead_values) @api.model diff --git a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js index 3357c9d9384..b5920cb31ee 100644 --- a/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js +++ b/addons/website_crm_partner_assign/static/src/js/crm_partner_assign.js @@ -101,6 +101,8 @@ $('.edit_contact_confirm').on('click',function(){ }); $('.new_opp_confirm').on('click',function(e){ + var $btn = $(this); + $btn.prop('disabled', true); new Model('crm.lead') .call("create_opp_portal", [{ contact_name: $('.new_opp_form .contact_name').val(), @@ -111,10 +113,15 @@ $('.new_opp_confirm').on('click',function(e){ if (response.errors) { $('#new-opp-dialog .alert').remove(); $('#new-opp-dialog div:first').prepend("<div class='alert alert-danger'>" + response.errors + "</div>"); + $btn.prop('disabled', false); + } else { window.location = '/my/opportunity/' + response.id; } + }) + .fail(function() { + $btn.prop('disabled', false); }); return false; }); @@ -124,6 +131,7 @@ $('.edit_opp_confirm').on('click',function(){ $btn.prop('disabled', true); new Model('crm.lead') .call("update_lead_portal", [[parseInt($('.edit_opp_form .opportunity_id').val())],{ + date_deadline: $('.edit_opp_form .date_deadline').val(), planned_revenue: parseFloat($('.edit_opp_form .planned_revenue').val()), probability: parseFloat($('.edit_opp_form .probability').val()), activity_type_id: parseInt($('.edit_opp_form .next_activity').find(":selected").attr('data')), diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index df60fa3eac8..668848908ab 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -215,7 +215,7 @@ <span class='badge'><t t-esc="lead_count"/></span> </t> <t t-if="not lead_count"> - There are currently no leads for your account. + There are currently no leads. </t> </small> </a> @@ -227,7 +227,7 @@ <span class='badge'><t t-esc="opp_count"/></span> </t> <t t-if="not opp_count"> - There are currently no opportunities for your account. + There are currently no opportunities. </t> </small> </a> @@ -307,13 +307,13 @@ </form> </div> </div> - <div class="panel panel-default"> - <t t-if="not opportunities"> - <div class="alert alert-warning mt8" role="alert"> - There are no opportunities. - </div> - </t> - <table t-if="opportunities" class="table table-hover"> + <t t-if="not opportunities"> + <div class="alert alert-warning mt8" role="alert"> + There are no opportunities. + </div> + </t> + <div class="panel panel-default" t-if="opportunities"> + <table class="table table-hover"> <thead> <tr class="active"> <th class="col-xs-2">Date</th> diff --git a/addons/website_forum/static/src/js/website_forum.share.js b/addons/website_forum/static/src/js/website_forum.share.js index 60173903a45..5033a0ea624 100644 --- a/addons/website_forum/static/src/js/website_forum.share.js +++ b/addons/website_forum/static/src/js/website_forum.share.js @@ -7,7 +7,7 @@ var base = require('web_editor.base'); var SocialShare = require('website.share'); var website = require('website.website'); var qweb = core.qweb; -ajax.loadXML('/website_forum/static/src/xml/website_forum_share_templates.xml', qweb); +var tmpl = ajax.loadXML('/website_forum/static/src/xml/website_forum_share_templates.xml', qweb); if(!$('.website_forum').length) { @@ -44,7 +44,7 @@ var ForumShare = SocialShare.extend({ } }); -base.ready().done(function() { +$.when(base.ready(), tmpl).done(function() { // Store social share data to display modal on next page $(document.body).on('click', ':not(.karma_required).oe_social_share_call', function() { diff --git a/addons/website_payment/controllers/main.py b/addons/website_payment/controllers/main.py index dd4ef720e16..31244b87cda 100644 --- a/addons/website_payment/controllers/main.py +++ b/addons/website_payment/controllers/main.py @@ -78,8 +78,15 @@ class WebsitePayment(http.Controller): tx_id = request.session.pop('website_payment_tx_id', False) if tx_id: tx = request.env['payment.transaction'].browse(tx_id) - status = (tx.state == 'done' and 'success') or 'danger' - message = (tx.state == 'done' and 'Your payment was successful! It may take some time to be validated on our end.') or 'OOps! There was a problem with your payment.' + if tx.state == 'done': + status = 'success' + message = tx.acquirer_id.done_msg + elif tx.state == 'pending': + status = 'warning' + message = tx.acquirer_id.pending_msg + else: + status = 'danger' + message = tx.acquirer_id.error_msg return request.render('website_payment.confirm', {'tx': tx, 'status': status, 'message': message}) else: return request.redirect('/my/home') diff --git a/addons/website_payment/views/website_payment_templates.xml b/addons/website_payment/views/website_payment_templates.xml index bf01e712999..a7ec331cc59 100644 --- a/addons/website_payment/views/website_payment_templates.xml +++ b/addons/website_payment/views/website_payment_templates.xml @@ -136,7 +136,7 @@ <div class="row"> <div class="col-md-6"> <div class="form-horizontal"> - <div t-attf-class="alert alert-#{status}" t-esc="message"/> + <div t-attf-class="alert alert-#{status}" t-raw="message"/> <div class="form-group"> <label for="form_partner_name" class="col-sm-3 control-label">From</label> <div class="col-sm-9"> diff --git a/addons/website_portal/static/src/js/website_portal_tour.js b/addons/website_portal/static/src/js/website_portal_tour.js new file mode 100644 index 00000000000..9b63d711a29 --- /dev/null +++ b/addons/website_portal/static/src/js/website_portal_tour.js @@ -0,0 +1,24 @@ +odoo.define('website_portal.tour', function (require) { +'use strict'; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); + +tour.register('portal_load_homepage', { + test: true, + url: '/my', + wait_for: base.ready() +}, + [ + { + content: "Check portal is loaded", + trigger: 'a[href*="/my/account"]:contains("Change"):first', + }, + { + content: "Load my account details", + trigger: 'body:contains("Contact Details")', + } + ] +); + +}); diff --git a/addons/website_portal/static/src/less/website_portal.less b/addons/website_portal/static/src/less/website_portal.less index 11f4219abbd..ef62ecbc2f5 100644 --- a/addons/website_portal/static/src/less/website_portal.less +++ b/addons/website_portal/static/src/less/website_portal.less @@ -9,3 +9,7 @@ .o_my_home_content > .page-header > a:hover { text-decoration: none; } + +.portal-wrap table.table tr { + word-wrap: break-word; +}
\ No newline at end of file diff --git a/addons/website_portal/tests/__init__.py b/addons/website_portal/tests/__init__.py new file mode 100644 index 00000000000..a2e745ce861 --- /dev/null +++ b/addons/website_portal/tests/__init__.py @@ -0,0 +1 @@ +import test_load_process
\ No newline at end of file diff --git a/addons/website_portal/tests/test_load_process.py b/addons/website_portal/tests/test_load_process.py new file mode 100644 index 00000000000..25b139e6dfc --- /dev/null +++ b/addons/website_portal/tests/test_load_process.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import odoo.tests + + +@odoo.tests.common.at_install(False) +@odoo.tests.common.post_install(True) +class TestUi(odoo.tests.HttpCase): + def test_01_portal_load_tour(self): + self.phantom_js( + "/", + "odoo.__DEBUG__.services['web_tour.tour'].run('portal_load_homepage')", + "", + login="portal" + ) + diff --git a/addons/website_portal/views/website_portal_templates.xml b/addons/website_portal/views/website_portal_templates.xml index 3e5924938a7..cc6005e5640 100644 --- a/addons/website_portal/views/website_portal_templates.xml +++ b/addons/website_portal/views/website_portal_templates.xml @@ -5,6 +5,7 @@ <xpath expr="." position="inside"> <link rel='stylesheet' href='/website_portal/static/src/less/website_portal.less'/> <script type="text/javascript" src="/website_portal/static/src/js/website_portal.js"></script> + <script type="text/javascript" src="/website_portal/static/src/js/website_portal_tour.js"></script> </xpath> </template> @@ -90,7 +91,7 @@ </div> </div> </div> - <div id="wrap"> + <div id="wrap" class='portal-wrap'> <div class="container mb64"> <div class="row"> <div class="col-md-9 col-lg-8"> diff --git a/addons/website_portal_sale/views/website_portal_sale_templates.xml b/addons/website_portal_sale/views/website_portal_sale_templates.xml index 329cb242118..b32da6df056 100644 --- a/addons/website_portal_sale/views/website_portal_sale_templates.xml +++ b/addons/website_portal_sale/views/website_portal_sale_templates.xml @@ -9,7 +9,7 @@ <template id="portal_my_home_menu_sale" name="Portal layout : sales menu entries" inherit_id="website_portal.portal_layout" priority="25"> <xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside"> <li t-att-class="page_name == 'quote' and 'active'"> - <a href="/my/quotes">Quotes</a> + <a href="/my/quotes">Quotations</a> </li> <li t-att-class="page_name == 'order' and 'active'"> <a href="/my/orders">Orders</a> @@ -23,13 +23,13 @@ <template id="portal_my_home_sale" name="Portal My Home : sales entries" inherit_id="website_portal.portal_my_home" priority="25"> <xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside"> <h3 class="page-header"> - <a href="/my/quotes">Your Quotes + <a href="/my/quotes">Your Quotations <small class="ml8"> <t t-if="quotation_count"> <span class='badge'><t t-esc="quotation_count"/></span> </t> <t t-if="not quotation_count"> - There are currently no quotes for your account. + There are currently no quotations. </t> </small> </a> @@ -41,7 +41,7 @@ <span class='badge'><t t-esc="order_count"/></span> </t> <t t-if="not order_count"> - There are currently no orders for your account. + There are currently no orders. </t> </small> </a> @@ -53,7 +53,7 @@ <span class='badge'><t t-esc="invoice_count"/></span> </t> <t t-if="not invoice_count"> - There are currently no invoices for your account. + There are currently no invoices and payments. </t> </small> </a> @@ -64,11 +64,11 @@ <template id="portal_my_quotations" name="My Quotations"> <t t-call="website_portal.portal_layout"> <div class="page-header"> - <span class="h3">Your Quotes</span> + <span class="h3">Your Quotations</span> <t t-call="website_portal.portal_searchbar"/> </div> <t t-if="not quotations"> - <p>There are currently no quotes for your account.</p> + <p>There are currently no quotations for your account.</p> </t> <t t-if="quotations"> <div class="table-responsive"><table class="table table-hover o_my_status_table"> @@ -162,7 +162,7 @@ <t t-call="website_portal.portal_searchbar"/> </div> <t t-if="not invoices"> - <p>There are currently no invoices for your account.</p> + <p>There are currently no invoices and payments for your account.</p> </t> <t t-if="invoices"> <div class="table-responsive"><table class="table table-hover o_my_status_table"> @@ -220,7 +220,7 @@ <li> <a t-attf-href="/my/#{'quotes' if order.state == 'sent' else 'orders'}?{{ keep_query()}}"> <t t-if="order.state == 'sent'"> - My Quotes + My Quotations </t> <t t-if="order.state != 'sent'"> My Orders @@ -311,28 +311,28 @@ <div class="col-md-6"> <strong>Product</strong> </div> - <div class="col-md-2 text-right"> + <div class="hidden-sm hidden-xs col-md-2 text-right"> <strong>Unit Price</strong> </div> - <div class="col-md-2 text-right"> + <div class="hidden-sm hidden-xs col-md-2 text-right"> <strong>Quantity</strong> </div> - <div class="col-md-2 text-right"> + <div class="hidden-sm hidden-xs col-md-2 text-right"> <strong>Subtotal</strong> </div> </div> <t t-foreach="order.order_line" t-as="ol"> <div class="row orders_vertical_align"> - <div class="col-md-1 text-center"> - <img t-att-src="'/web/image/sale.order.line/%s/product_image/48x48' % ol.id"/> + <div class="visible-lg col-lg-1 text-center"> + <span t-field="ol.product_image" t-options="{'widget': 'image', 'class': 'img-rounded', 'style':'width: 48px;height:48px'}" /> </div> - <div id='product_name' class="col-md-5"> + <div id='product_name' class="col-lg-4 col-sm-6 col-xs-4"> <span t-esc="ol.product_id.name"/> </div> - <div class="col-md-2 text-right"> + <div class="col-xs-3 text-right"> <span t-field="ol.price_unit" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/> </div> - <div class="col-md-2 text-right"> + <div class="col-xs-2 text-right"> <t t-if="ol._name == 'sale.order.line'"> <span t-esc="ol.product_uom_qty"/> </t> @@ -340,7 +340,7 @@ <span t-esc="ol.quantity"/> </t> </div> - <div class="col-md-2 text-right"> + <div class="col-sm-2 col-xs-3 text-right"> <span t-field="ol.price_subtotal" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/> </div> </div> @@ -354,29 +354,30 @@ <strong>Contact</strong> </div> <div t-field="order.user_id.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone"]}'/> + <br/> </div> <div class="col-md-6"> <div class="row"> - <div class="col-md-10 text-right"> + <div class="col-md-10 col-xs-8 text-right"> Untaxed Amount: </div> - <div class="col-md-2 text-right"> + <div class="col-md-2 col-xs-4 text-right"> <span t-field="order.amount_untaxed" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/> </div> </div> <div class="row"> - <div class="col-md-10 text-right"> + <div class="col-md-10 col-xs-8 text-right"> Taxes: </div> - <div class="col-md-2 text-right"> + <div class="col-md-2 col-xs-4 text-right"> <span t-field="order.amount_tax" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/> </div> </div> <div class="row"> - <div class="col-md-10 text-right"> + <div class="col-md-10 col-xs-8 text-right"> <strong>Total:</strong> </div> - <div class="col-md-2 text-right"> + <div class="col-md-2 col-xs-4 text-right"> <strong><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></strong> </div> </div> diff --git a/addons/website_project/views/project_templates.xml b/addons/website_project/views/project_templates.xml index c40e30438f0..2052c80be50 100644 --- a/addons/website_project/views/project_templates.xml +++ b/addons/website_project/views/project_templates.xml @@ -20,7 +20,7 @@ <span class='badge'><t t-esc="project_count"/></span> </t> <t t-if="not project_count"> - There are currently no projects for your account. + There are currently no projects. </t> </small> </a> @@ -32,7 +32,7 @@ <span class='badge'><t t-esc="task_count"/></span> </t> <t t-if="not task_count"> - There are currently no quotes for your account. + There are currently no quotes. </t> </small> </a> diff --git a/addons/website_project_issue/views/project_issue_templates.xml b/addons/website_project_issue/views/project_issue_templates.xml index f8fc8ff7e89..1205709d967 100644 --- a/addons/website_project_issue/views/project_issue_templates.xml +++ b/addons/website_project_issue/views/project_issue_templates.xml @@ -18,7 +18,7 @@ <span class='badge'><t t-esc="issue_count"/></span> </t> <t t-if="not issue_count"> - There are currently no issues for your account. + There are currently no issues. </t> </small> </a> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 3ec5d7f591e..d5fa0a750e0 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -498,8 +498,7 @@ class WebsiteSale(http.Controller): shippings = Partner.sudo().search([("id", "child_of", order.partner_id.commercial_partner_id.ids)]) if partner_id not in shippings.mapped('id') and partner_id != order.partner_id.id: return Forbidden() - - Partner.browse(partner_id).sudo().update(checkout) + Partner.browse(partner_id).sudo().write(checkout) return partner_id def values_preprocess(self, order, mode, values): @@ -522,7 +521,8 @@ class WebsiteSale(http.Controller): lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None if lang: new_values['lang'] = lang - + if mode == ('edit', 'billing') and order.partner_id.type == 'contact': + new_values['type'] = 'other' if mode[1] == 'shipping': new_values['parent_id'] = order.partner_id.commercial_partner_id.id new_values['type'] = 'delivery' @@ -555,7 +555,7 @@ class WebsiteSale(http.Controller): else: if partner_id > 0: if partner_id == order.partner_id.id: - mode = ('edit', 'billing') + mode = ('edit', 'billing') else: shippings = Partner.search([('id', 'child_of', order.partner_id.commercial_partner_id.ids)]) if partner_id in shippings.mapped('id'): diff --git a/addons/website_sale/models/res_config.py b/addons/website_sale/models/res_config.py index 0f6dc38cb52..aa280ab7626 100644 --- a/addons/website_sale/models/res_config.py +++ b/addons/website_sale/models/res_config.py @@ -49,7 +49,7 @@ class WebsiteConfigSettings(models.TransientModel): group_sale_pricelist = fields.Boolean("Use pricelists to adapt your price per customers", implied_group='product.group_sale_pricelist') - group_product_variant = fields.Boolean("Attributes and Variants", implied_group='product.group_sale_pricelist') + group_product_variant = fields.Boolean("Attributes and Variants", implied_group='product.group_product_variant') group_pricelist_item = fields.Boolean("Show pricelists to customers", implied_group='product.group_pricelist_item') group_product_pricelist = fields.Boolean("Show pricelists On Products", diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 1bb49194f37..df755916fdc 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -201,11 +201,13 @@ odoo.define('website_sale.website_sale', function (require) { $('.oe_website_sale .a-submit, #comment .a-submit').off('click').on('click', function (event) { if (!event.isDefaultPrevented() && !$(this).is(".disabled")) { + event.preventDefault(); $(this).closest('form').submit(); } }); $('form.js_attributes input, form.js_attributes select', oe_website_sale).on('change', function (event) { if (!event.isDefaultPrevented()) { + event.preventDefault(); $(this).closest("form").submit(); } }); diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index bae7b1a147d..1da466ff0bc 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1151,6 +1151,7 @@ </template> <template id="address" name="Address Management"> + <t t-set="no_footer">1</t> <t t-call="website.layout"> <div id="wrap"> <div class="container oe_website_sale"> @@ -1163,17 +1164,17 @@ <div class="col-md-9 oe_cart"> <div class="row"> <t t-if="mode == ('new', 'billing')"> - <h2 class="page-header mt16">Your Address + <h2 class="page-header mt16 ml16">Your Address <small> or </small> <t t-set='connect' t-value="request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited') == 'True' and ('signup', 'Sign Up') or ('login', 'Log In')"/> <a t-attf-href='/web/{{connect[0]}}?redirect=/shop/checkout' class='btn btn-primary' style="margin-top: -11px"><t t-esc='connect[1]'/></a> </h2> </t> <t t-if="mode == ('edit', 'billing')"> - <h2 class="page-header mt16">Your Address</h2> + <h2 class="page-header mt16 ml16">Your Address</h2> </t> <t t-if="mode[1] == 'shipping'"> - <h2 class="page-header mt16">Shipping Address </h2> + <h2 class="page-header mt16 ml16">Shipping Address </h2> </t> <t t-if="error" t-foreach="error.get('error_message', [])" t-as="err"> <h4 class="text-danger" t-esc="err" /> diff --git a/addons/website_sale_digital/controllers/main.py b/addons/website_sale_digital/controllers/main.py index 312dcca81f2..089f2b97f35 100644 --- a/addons/website_sale_digital/controllers/main.py +++ b/addons/website_sale_digital/controllers/main.py @@ -34,20 +34,16 @@ class WebsiteSaleDigital(website_account): return response order = response.qcontext['order'] invoiced_lines = request.env['account.invoice.line'].sudo().search([('invoice_id', 'in', order.invoice_ids.ids), ('invoice_id.state', '=', 'paid')]) + products = invoiced_lines.mapped('product_id') | order.order_line.filtered(lambda r: not r.price_subtotal).mapped('product_id') purchased_products_attachments = {} - for il in invoiced_lines: - product = il.product_id - # Ignore products that do not have digital content - if not product.product_tmpl_id.type == 'digital': - continue - + for product in products: # Search for product attachments Attachment = request.env['ir.attachment'] product_id = product.id template = product.product_tmpl_id att = Attachment.search_read( - domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), ('res_id', '=', template.id)], + domain=['|', '&', ('res_model', '=', product._name), ('res_id', '=', product_id), '&', ('res_model', '=', template._name), '&', ('res_id', '=', template.id), ('product_downloadable', '=', True)], fields=['name', 'write_date'], order='write_date desc', ) diff --git a/addons/website_sale_digital/models/account_invoice.py b/addons/website_sale_digital/models/account_invoice.py index b6b28026dde..798faca55a0 100644 --- a/addons/website_sale_digital/models/account_invoice.py +++ b/addons/website_sale_digital/models/account_invoice.py @@ -13,7 +13,13 @@ class AccountInvoiceLine(models.Model): # Get paid invoices purchases = self.sudo().search_read( - domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id), ('product_id.product_tmpl_id.type', '=', 'digital')], + domain=[('invoice_id.state', '=', 'paid'), ('invoice_id.partner_id', '=', partner.id)], + fields=['product_id'], + ) + + # Get free products + purchases += self.env['sale.order.line'].sudo().search_read( + domain=[('price_subtotal', '=', 0.0), ('order_id.partner_id', '=', partner.id)], fields=['product_id'], ) diff --git a/addons/website_sale_options/static/src/js/website_sale.js b/addons/website_sale_options/static/src/js/website_sale.js index 49fa7f68858..e206184b52e 100644 --- a/addons/website_sale_options/static/src/js/website_sale.js +++ b/addons/website_sale_options/static/src/js/website_sale.js @@ -6,10 +6,6 @@ var website = require('website.website'); var base = require('web_editor.base'); require('website_sale.website_sale'); -if(!$('.js_add_cart_variants[data-attribute_value_ids]').length) { - return $.Deferred().reject("DOM doesn't contain '.js_add_cart_variants[data-attribute_value_ids]'"); -} - $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit') .off('click') .removeClass('a-submit') @@ -28,9 +24,14 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit') $modal.find('img:first').attr("src", "/web/image/product.product/" + product_id + "/image_medium"); + // disable opacity on the <form> if currently active (in case the product is + // not published), as it interferes with bs modals + $form.addClass('css_options'); + $modal.appendTo($form) .modal() .on('hidden.bs.modal', function () { + $form.removeClass('css_options'); // possibly reactivate opacity (see above) $(this).remove(); }); diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 16604d1f5fd..91211ea79f4 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -494,11 +494,12 @@ class Slide(models.Model): # -------------------------------------------------- @api.model - def _fetch_data(self, base_url, data, content_type=False): + def _fetch_data(self, base_url, data, content_type=False, extra_params=False): result = {'values': dict()} try: if data: - base_url = base_url + '?%s' % urlencode(data) + sep = '?' if not extra_params else '&' + base_url = base_url + '%s%s' % (sep, urlencode(data)) req = urllib2.Request(base_url) content = urllib2.urlopen(req).read() if content_type == 'json': @@ -566,14 +567,28 @@ class Slide(models.Model): def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? - image = Image.open(io.BytesIO(vals['image'].decode('base64'))) - width, height = image.size - if height > width: - return 'document' - else: - return 'presentation' - key = self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key') - fetch_res = self._fetch_data('https://www.googleapis.com/drive/v2/files/%s' % document_id, {'projection': 'BASIC', 'key': key}, "json") + slide_type = 'presentation' + if vals.get('image'): + image = Image.open(io.BytesIO(vals['image'].decode('base64'))) + width, height = image.size + if height > width: + return 'document' + return slide_type + + # Google drive doesn't use a simple API key to access the data, but requires an access + # token. However, this token is generated in module google_drive, which is not in the + # dependencies of website_slides. We still keep the 'key' parameter just in case, but that + # is probably useless. + params = {} + params['projection'] = 'BASIC' + if 'google.drive.config' in self.env: + access_token = self.env['google.drive.config'].get_access_token() + if access_token: + params['access_token'] = access_token + if not params.get('access_token'): + params['key'] = self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key') + + fetch_res = self._fetch_data('https://www.googleapis.com/drive/v2/files/%s' % document_id, params, "json") if fetch_res.get('error'): return fetch_res @@ -596,12 +611,14 @@ class Slide(models.Model): values['datas'] = values['image'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith('application/vnd.google-apps'): - values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) - if google_values['exportLinks'].get('text/plain'): - values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], {})['values'] - if google_values['exportLinks'].get('text/csv'): - values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], {})['values'] + if 'exportLinks' in google_values: + values['datas'] = self._fetch_data(google_values['exportLinks']['application/pdf'], params, 'pdf', extra_params=True)['values'] + # Content indexing + if google_values['exportLinks'].get('text/plain'): + values['index_content'] = self._fetch_data(google_values['exportLinks']['text/plain'], params, extra_params=True)['values'] + elif google_values['exportLinks'].get('text/csv'): + values['index_content'] = self._fetch_data(google_values['exportLinks']['text/csv'], params, extra_params=True)['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] diff --git a/addons/website_twitter/models/website_twitter_config.py b/addons/website_twitter/models/website_twitter_config.py index 99090d99d7b..d92c376a0d1 100644 --- a/addons/website_twitter/models/website_twitter_config.py +++ b/addons/website_twitter/models/website_twitter_config.py @@ -27,11 +27,11 @@ class WebsiteTwitterConfig(models.TransientModel): twitter_api_key = fields.Char( related='website_id.twitter_api_key', string='API Key', - help='Twitter API key you can get it from https://apps.twitter.com/app/new') + help='Twitter API key you can get it from https://apps.twitter.com/') twitter_api_secret = fields.Char( related='website_id.twitter_api_secret', string='API secret', - help='Twitter API secret you can get it from https://apps.twitter.com/app/new') + help='Twitter API secret you can get it from https://apps.twitter.com/') twitter_tutorial = fields.Boolean(string='Show me how to obtain the Twitter API Key and Secret') twitter_screen_name = fields.Char( related='website_id.twitter_screen_name', diff --git a/addons/website_twitter/views/website_twitter_settings_views.xml b/addons/website_twitter/views/website_twitter_settings_views.xml index 224e9f34c29..7e8e92a1f47 100644 --- a/addons/website_twitter/views/website_twitter_settings_views.xml +++ b/addons/website_twitter/views/website_twitter_settings_views.xml @@ -38,8 +38,8 @@ </div> <div class="modal-body"> <ol> - <li>Create a new Twitter application on <a href="https://apps.twitter.com/app/new" target="new"> - https://apps.twitter.com/app/new + <li>Create a new Twitter application on <a href="https://apps.twitter.com/" target="new"> + https://apps.twitter.com/ </a> with the following values: <ul> diff --git a/doc/cla/corporate/VanRoey.md b/doc/cla/corporate/VanRoey.md index 0e3122e27f4..1662b84f711 100644 --- a/doc/cla/corporate/VanRoey.md +++ b/doc/cla/corporate/VanRoey.md @@ -14,3 +14,4 @@ List of contributors: Yenthe Van Ginneken yenthe.vanginneken@vanroey.be https://github.com/Yenthe666 Vincent Adriaensen vincent.adriaensen@vanroey.be https://github.com/vincentadriaensen +Pol Van Dingenen pol.vandingenen@vanroey.be https://github.com/Cyhexy diff --git a/doc/cla/corporate/camptocamp.md b/doc/cla/corporate/camptocamp.md index 5d75bb46fa9..c934c831a83 100644 --- a/doc/cla/corporate/camptocamp.md +++ b/doc/cla/corporate/camptocamp.md @@ -31,3 +31,4 @@ Cyril Gaudin cyril.gaudin@camptocamp.com https://github.com/cyrilgdn Denis Leemann denis.leemann@camptocamp.com https://github.com/leemannd Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s +Simone Orsi simahawk@gmail.com https://github.com/simahawk diff --git a/doc/cla/corporate/nubark.md b/doc/cla/corporate/nubark.md new file mode 100644 index 00000000000..a570c6f3ddc --- /dev/null +++ b/doc/cla/corporate/nubark.md @@ -0,0 +1,15 @@ +Colombia, 2017-01-21 + +Nubark agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Esteban Echeverry eecheverry@nubark.com https://github.com/nubark + +List of contributors: + +Esteban Echeverry eecheverry@nubark.com https://github.com/tebanep diff --git a/doc/cla/corporate/somko.md b/doc/cla/corporate/somko.md new file mode 100644 index 00000000000..0c864ab8d24 --- /dev/null +++ b/doc/cla/corporate/somko.md @@ -0,0 +1,17 @@ +Belgium, 30/01/2017 + +Somko agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Frederik Santens info@somko.be https://github.com/somko + +List of contributors: + +Frederik Santens info@somko.be https://github.com/somko +Thomas Vanmellaerts thomas.vanmellaerts@somko.be https://github.com/thomassomko +Kasper Declercq kasper.declercq@somko.be https://github.com/kasperdeclercq
\ No newline at end of file diff --git a/doc/cla/corporate/teclib.md b/doc/cla/corporate/teclib.md index a35c0a3277d..ff9db6dd21c 100644 --- a/doc/cla/corporate/teclib.md +++ b/doc/cla/corporate/teclib.md @@ -11,3 +11,4 @@ Laurent Destailleur ldestailleur@teclib.com https://github.com/eldy List of contributors: Laurent Destailleur ldestailleur@teclib.com https://github.com/eldy +David Halgand dhalgand@teclib.com https://github.com/halgandd diff --git a/doc/cla/individual/ateijelo.md b/doc/cla/individual/ateijelo.md new file mode 100644 index 00000000000..098cdf4a02e --- /dev/null +++ b/doc/cla/individual/ateijelo.md @@ -0,0 +1,11 @@ +Cuba, January 26th, 2017 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Andy Teijelo <ateijelo@gmail.com> https://github.com/ateijelo diff --git a/doc/cla/individual/halgandd.md b/doc/cla/individual/halgandd.md new file mode 100644 index 00000000000..204d26c8ba7 --- /dev/null +++ b/doc/cla/individual/halgandd.md @@ -0,0 +1,11 @@ +Nantes, 2016-11-04 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +David Halgand david.halgand@gmail.com https://github.com/halgandd
\ No newline at end of file diff --git a/doc/cla/individual/hendrikw25.md b/doc/cla/individual/hendrikw25.md new file mode 100644 index 00000000000..27d6bf3fccb --- /dev/null +++ b/doc/cla/individual/hendrikw25.md @@ -0,0 +1,11 @@ +Indonesia, 2017-01-25 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Hendrik hendrikwidjaja25@gmail.com https://github.com/hendrikw25 diff --git a/doc/cla/individual/rajeeshknambiar.md b/doc/cla/individual/rajeeshknambiar.md new file mode 100644 index 00000000000..86c063f4275 --- /dev/null +++ b/doc/cla/individual/rajeeshknambiar.md @@ -0,0 +1,11 @@ +India, 2017-01-26 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Rajeesh KV rajeeshknambiar@gmail.com https://github.com/rajeeshknambiar diff --git a/doc/cla/individual/yueqin.md b/doc/cla/individual/yueqin.md new file mode 100644 index 00000000000..972acd4fa8f --- /dev/null +++ b/doc/cla/individual/yueqin.md @@ -0,0 +1,11 @@ + + +China, 2017-02-06 + +I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Yue Qin 602559817@qq.com https://github.com/yueqin diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst index 3a9a8ee9357..68e5e41e928 100644 --- a/doc/setup/deploy.rst +++ b/doc/setup/deploy.rst @@ -4,9 +4,15 @@ Deploying Odoo ============== -This document describes basic steps to set up Odoo in production. It follows -:ref:`installation <setup/install>`, but should not be used for development -systems. +This document describes basic steps to set up Odoo in production or on an +internet-facing server. It follows :ref:`installation <setup/install>`, and is +not generally necessary for a development systems that is not exposed on the +internet. + +.. warning:: If you are setting up a public server, be sure to check our :ref:`security` recommandations! + + +.. _db_filter: dbfilter ======== @@ -20,24 +26,25 @@ company user: the database can be selected when logging in, and customizations loaded afterwards. However it is an issue for non-logged users (portal, website) which aren't -bound to a database: Odoo need to know which database should be used for the -operations or to get the data. If multi-tenancy is not used that is not an +bound to a database: Odoo needs to know which database should be used to load +the website page or perform the operation. If multi-tenancy is not used that is not an issue, there's only one database to use, but if there are multiple databases accessible Odoo needs a rule to know which one it should use. That is one of the purposes of :option:`--db-filter <odoo-bin --db-filter>`: -it specifies the default database for the Odoo system. The value is a -`regular expression`_, possibly including the dynamically injected hostname -or subdomain through which the Odoo system is accessed. +it specifies how the database should be selected based on the hostname (domain) +that is being requested. The value is a `regular expression`_, possibly +including the dynamically injected hostname (`%h`) or the first subdomain (`%d`) +through which the system is being accessed. -If an Odoo hosts multiple databases in production, especially if ``website`` -is used, it **must** use a dbfilter or a number of features will not work -correctly or not use at all. +For servers hosting multiple databases in production, especially if ``website`` +is used, dbfilter **must** be set, otherwise a number of features will not work +correctly. -Configuration sample --------------------- +Configuration samples +--------------------- -* filtering only db with a name beginning with 'mycompany' +* show only databases with names beginning with 'mycompany' in ``/etc/odoo.conf`` set: @@ -45,16 +52,28 @@ in ``/etc/odoo.conf`` set: [options] dbfilter = ^mycompany.*$ - -* filtering only db with a name equal to hostname without domain + +* Show only databases matching the first subdomain after ``www``: for example + the database "mycompany" will be shown if the incoming request + was sent to ``www.mycompany.com`` or ``mycompany.co.uk``, but not + for ``www2.mycompany.com`` or ``helpdesk.mycompany.com``. in ``/etc/odoo.conf`` set: .. code-block:: apacheconf [options] - dbfilter = %d - + dbfilter = ^%d$ + +.. note:: + Setting a proper :option:`--db-filter <odoo-bin --db-filter>` is an important part + of securing your deployment. + Once it is correctly working and only matching a single database per hostname, it + is strongly recommended to block access to the database manager screens, + and to use the `--no-database-list` startup paramater to prevent listing + your databases. See also security_. + + PostgreSQL ========== @@ -145,6 +164,8 @@ in ``/etc/odoo.conf`` set: db_password = pwd dbfilter = ^mycompany.*$ +.. _builtin_server: + Builtin server ============== @@ -216,11 +237,12 @@ in ``/etc/odoo.conf``: max_cron_threads = 1 workers = 8 +.. _https_proxy: HTTPS ===== -Whether it's accessed via website/web client or the webservice, Odoo transmits +Whether it's accessed via website/web client or web service, Odoo transmits authentication information in cleartext. This means a secure deployment of Odoo must use HTTPS\ [#switching]_. SSL termination can be implemented via just about any SSL termination proxy, but requires the following setup: @@ -229,7 +251,7 @@ just about any SSL termination proxy, but requires the following setup: * set up the SSL termination proxy (`Nginx termination example`_) * set up the proxying itself (`Nginx proxying example`_) * your SSL termination proxy should also automatically redirect non-secure -connections to the secure port + connections to the secure port .. warning:: @@ -372,11 +394,75 @@ and looking up the right module (and file) in the various addons paths. via this, and how (e.g. possibility of mapping ir.attachment id to filestore hash in the database?) +.. _security: + Security ======== -"Super-admin" password ----------------------- +For starters, keep in mind that securing an information system is a continuous process, +not a one-shot operation. At any moment, you will only be as secure as the weakest link +in your environment. + +So please do not take this section as the ultimate list of measures that will prevent +all security problems. It's only intended as a summary of the first important things +you should be sure to include in your security action plan. The rest will come +from best security practices for your operating system and distribution, +best practices in terms of users, passwords, and access control management, etc. + +When deploying an internet-facing server, please be sure to consider the following +security-related topics: + +- Always set a strong super-admin admin password, and restrict access to the database + management pages as soon as the system is set up. See :ref:`db_manager_security`. + +- Choose unique logins and strong passwords for all administrator accounts on all databases. + Do not use 'admin' as the login. Do not use those logins for day-to-day operations, + only for controlling/managing the installation. + *Never* use any default passwords like admin/admin, even for test/staging databases. + +- Use appropriate database filters ( :option:`--db-filter <odoo-bin --db-filter>`) + to restrict the visibility of your databases according to the hostname. + See :ref:`db_filter`. + +- Once your ``db_filter`` is configured and only matches a single database per hostname, + you should set ``list_db`` configuration option to ``False``, to prevent listing databases + entirely (this is also exposed as the :option:`--no-database-list <odoo-bin --no-database-list>` + command-line option) + +- Make sure the PostgreSQL user (:option:`--db_user <odoo-bin --db_user>`) is *not* a super-user, + and that your databases are owned by a different user. For example they could be owned by + the ``postgres`` super-user if you are using a dedicated non-privileged ``db_user``. + See also :ref:`setup/deploy/odoo`. + +- Keep installations updated by regularly installing the latest builds, + either via GitHub or by downloading the latest version from + https://www.odoo.com/page/download or http://nightly.odoo.com + +- Configure your server in multi-process mode with proper limits matching your typical + usage (memory/CPU/timeouts). See also :ref:`builtin_server`. + +- Run Odoo behind a web server providing HTTPS termination with a valid SSL certificate, + in order to prevent eavesdropping on cleartext communications. SSL certificates are + cheap, and many free options exist. + Configure the web proxy to limit the size of requests, set appropriate timeouts, + and then enable the :option:`proxy mode <odoo-bin --proxy-mode>` option. + See also :ref:`https_proxy`. + +- Whenever possible, host your public-facing demo/test/staging instances on different + machines than the production ones. And apply the same security precautions as for + production. + +- If you are hosting multiple customers, isolate customer data and files from each other + using containers or appropriate "jail" techniques. + +- Setup daily backups of your databases and filestore data, and copy them to a remote + archiving server that is not accessible from the server itself. + + +.. _db_manager_security: + +Database Manager Security +------------------------- :ref:`setup/deploy/odoo` mentioned ``admin_passwd`` in passing. diff --git a/doc/setup/install.rst b/doc/setup/install.rst index 9d90fef01d7..3a8d45afa89 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -207,9 +207,9 @@ RPM .. warning:: - with RHEL-based distributions (RHEL, CenOS, Scientific Linux), EPEL_ must + with RHEL-based distributions (RHEL, CentOS, Scientific Linux), EPEL_ must be added to the distribution's repositories for all of Odoo's - dependencies to be available. For CenOS: + dependencies to be available. For CentOS: .. code-block:: console diff --git a/odoo/addons/base/ir/ir_actions.py b/odoo/addons/base/ir/ir_actions.py index 44df2bc4770..1b2cbad555d 100644 --- a/odoo/addons/base/ir/ir_actions.py +++ b/odoo/addons/base/ir/ir_actions.py @@ -611,10 +611,11 @@ class IrActionsServer(models.Model): :type action: browse record :returns: dict -- evaluation context given to (safe_)safe_eval """ def log(message, level="info"): - self._cr.execute(""" - INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) - VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) - """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) + with self.pool.cursor() as cr: + cr.execute(""" + INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) + VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) + """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) eval_context = super(IrActionsServer, self)._get_eval_context(action=action) model = self.env[action.model_id.model] diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py index 9bb7a554b14..e810be3ab7b 100644 --- a/odoo/addons/base/ir/ir_model.py +++ b/odoo/addons/base/ir/ir_model.py @@ -232,7 +232,7 @@ class IrModel(models.Model): query_insert(cr, self._table, params) record = self.browse(cr.fetchone()) - self._context['todo'].append((10, record.modified, [list(params)])) + self._context['todo'].append((10, record.modified, [['name', 'info', 'transient']])) if model._module == self._context.get('module'): # self._module is the name of the module that last extended self @@ -1451,7 +1451,7 @@ class IrModelData(models.Model): class WizardModelMenu(models.TransientModel): _name = 'wizard.ir.model.menu.create' - menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True) + menu_id = fields.Many2one('ir.ui.menu', string='Parent Menu', required=True, ondelete='cascade') name = fields.Char(string='Menu Name', required=True) @api.multi diff --git a/odoo/addons/base/ir/ir_model_view.xml b/odoo/addons/base/ir/ir_model_view.xml index 1fe5f0cf88a..f76cc4b416a 100644 --- a/odoo/addons/base/ir/ir_model_view.xml +++ b/odoo/addons/base/ir/ir_model_view.xml @@ -34,7 +34,7 @@ <field name="transient" attrs="{'readonly': [('id', '!=', False)]}" groups="base.group_no_one"/> </group> <group> - <field name="state" attrs="{'readonly': [('id', '!=', False)]}" groups="base.group_no_one"/> + <field name="state" readonly="1" groups="base.group_no_one"/> <field name="modules" groups="base.group_no_one"/> </group> </group> diff --git a/odoo/addons/base/ir/ir_ui_menu.py b/odoo/addons/base/ir/ir_ui_menu.py index 6d357866bcc..6a7aaa8a482 100644 --- a/odoo/addons/base/ir/ir_ui_menu.py +++ b/odoo/addons/base/ir/ir_ui_menu.py @@ -55,7 +55,7 @@ class IrUiMenu(models.Model): if level <= 0: return '...' if self.parent_id: - return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + self.name + return self.parent_id._get_full_name(level - 1) + MENU_ITEM_SEPARATOR + (self.name or "") else: return self.name @@ -102,7 +102,7 @@ class IrUiMenu(models.Model): 'ir.actions.report.xml': lambda action: action.model, 'ir.actions.server': lambda action: action.model_id.model, } - for menu in action_menus: + for menu in action_menus.sudo(): get_model = MODEL_GETTER.get(menu.action._name) if not get_model or not get_model(menu.action) or \ access.check(get_model(menu.action), 'read', False): diff --git a/odoo/addons/base/module/module.py b/odoo/addons/base/module/module.py index 1f370493d3f..873e012362e 100644 --- a/odoo/addons/base/module/module.py +++ b/odoo/addons/base/module/module.py @@ -643,6 +643,15 @@ class Module(models.Model): if not self.env.user.has_group('base.group_system'): raise AccessDenied() + # One-click install is opt-in - cfr Issue #15225 + ad_dir = tools.config.addons_data_dir + if not os.access(ad_dir, os.W_OK): + msg = (_("Automatic install of downloaded Apps is currently disabled.") + "\n\n" + + _("To enable it, make sure this directory exists and is writable on the server:") + + "\n%s" % ad_dir) + _logger.warning(msg) + raise UserError(msg) + apps_server = urlparse.urlparse(self.get_apps_server()) OPENERP = odoo.release.product_name.lower() diff --git a/odoo/addons/base/res/res_partner.py b/odoo/addons/base/res/res_partner.py index c8bcaa042ad..1601819ae94 100644 --- a/odoo/addons/base/res/res_partner.py +++ b/odoo/addons/base/res/res_partner.py @@ -430,6 +430,7 @@ class Partner(models.Model): sync_children = self.child_ids.filtered(lambda c: not c.is_company) for child in sync_children: child._commercial_sync_to_children() + sync_children._compute_commercial_partner() return sync_children.write(sync_vals) @api.multi @@ -453,6 +454,10 @@ class Partner(models.Model): commercial_fields = self._commercial_fields() if any(field in values for field in commercial_fields): self._commercial_sync_to_children() + for child in self.child_ids.filtered(lambda c: not c.is_company): + if child.commercial_partner_id != self.commercial_partner_id : + self._commercial_sync_to_children() + break # 2b. Address fields: sync if address changed address_fields = self._address_fields() if any(field in values for field in address_fields): diff --git a/odoo/addons/test_inherit/tests/test_inherit.py b/odoo/addons/test_inherit/tests/test_inherit.py index 4c93556a8c5..6b131d37c93 100644 --- a/odoo/addons/test_inherit/tests/test_inherit.py +++ b/odoo/addons/test_inherit/tests/test_inherit.py @@ -77,6 +77,12 @@ class test_inherits(common.TransactionCase): self.assertIn(daughter, partner_demo.daughter_ids) # search the partner from the daughter record + partners = self.env['res.partner'].search([('daughter_ids', 'like', 'not existing daugther')]) + self.assertFalse(partners) + partners = self.env['res.partner'].search([('daughter_ids', 'not like', 'not existing daugther')]) + self.assertIn(partner_demo, partners) + partners = self.env['res.partner'].search([('daughter_ids', '!=', False)]) + self.assertIn(partner_demo, partners) partners = self.env['res.partner'].search([('daughter_ids', 'in', daughter.ids)]) self.assertIn(partner_demo, partners) diff --git a/odoo/models.py b/odoo/models.py index 6327199c9a3..a2be00d2134 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -1916,7 +1916,7 @@ class BaseModel(object): prefix_term = lambda prefix, term: ('%s %s' % (prefix, term)) if term else '' query = """ - SELECT min(%(table)s.id) AS id, count(%(table)s.id) AS %(count_field)s %(extra_fields)s + SELECT min("%(table)s".id) AS id, count("%(table)s".id) AS "%(count_field)s" %(extra_fields)s FROM %(from)s %(where)s %(groupby)s diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 014f2d0ed9b..ecf95be088b 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -128,7 +128,7 @@ def initialize_sys_path(): global hooked dd = tools.config.addons_data_dir - if dd not in ad_paths: + if os.access(dd, os.R_OK) and dd not in ad_paths: ad_paths.append(dd) for ad in tools.config['addons_path'].split(','): diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py index b698ccfcb3a..da093dad323 100644 --- a/odoo/osv/expression.py +++ b/odoo/osv/expression.py @@ -957,7 +957,14 @@ class expression(object): if call_null: o2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in' - push(create_substitution_leaf(leaf, ('id', o2m_op, select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table)), model)) + # determine ids from field.inverse_name + if comodel._fields[field.inverse_name].store: + ids1 = select_distinct_from_where_not_null(cr, field.inverse_name, comodel._table) + else: + domain = [(field.inverse_name, '!=', False)] + recs = comodel.search(domain).sudo().with_context(prefetch_fields=False) + ids1 = recs.mapped(field.inverse_name).ids + push(create_substitution_leaf(leaf, ('id', o2m_op, ids1), model)) elif field.type == 'many2many': rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2 diff --git a/odoo/tools/config.py b/odoo/tools/config.py index 2473fe53304..5056ae77880 100644 --- a/odoo/tools/config.py +++ b/odoo/tools/config.py @@ -580,12 +580,17 @@ class configmanager(object): @property def addons_data_dir(self): - d = os.path.join(self['data_dir'], 'addons', release.series) + add_dir = os.path.join(self['data_dir'], 'addons') + d = os.path.join(add_dir, release.series) if not os.path.exists(d): - os.makedirs(d, 0700) - else: - assert os.access(d, os.W_OK), \ - "%s: directory is not writable" % d + try: + # bootstrap parent dir +rwx + if not os.path.exists(add_dir): + os.makedirs(add_dir, 0700) + # try to make +rx placeholder dir, will need manual +w to activate it + os.makedirs(d, 0500) + except OSError: + logging.getLogger(__name__).debug('Failed to create addons data dir %s', d) return d @property diff --git a/requirements.txt b/requirements.txt index 496b54c75af..77947255130 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ lxml==3.5.0 Mako==1.0.4 MarkupSafe==0.23 mock==2.0.0 +num2words==0.5.4 ofxparse==0.15 passlib==1.6.5 Pillow==3.4.1 |
