diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-03-20 11:45:11 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-03-20 11:45:11 +0100 |
| commit | e0345a4a3f4cfa8f4de8723c6b72b741b06483a1 (patch) | |
| tree | 5ad51dbebfd89cd7e56bf12df2da3ecf4d8e9509 /addons | |
| parent | e9db6b6d9de1e0589e8871ca2e5e8dde8ec8cab7 (diff) | |
| parent | 2835d299792ffa925d86ad3460057be713ac4fac (diff) | |
[MERGE] forward port branch 11.0 up to 2835d299792
Diffstat (limited to 'addons')
109 files changed, 1266 insertions, 336 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py index baa0aed666e..cde55f97bf4 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -412,7 +412,7 @@ class AccountJournal(models.Model): belongs_to_company = fields.Boolean('Belong to the user\'s current company', compute="_belong_to_company", search="_search_company_journals",) # Bank journals fields - bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False, domain="[('partner_id','=', company_id)]") + bank_account_id = fields.Many2one('res.partner.bank', string="Bank Account", ondelete='restrict', copy=False) bank_statements_source = fields.Selection([('undefined', 'Undefined Yet'),('manual', 'Record Manually')], string='Bank Feeds', default='undefined') bank_acc_number = fields.Char(related='bank_account_id.acc_number') bank_id = fields.Many2one('res.bank', related='bank_account_id.bank_id') @@ -522,11 +522,16 @@ class AccountJournal(models.Model): @api.multi def write(self, vals): for journal in self: + company = journal.company_id if ('company_id' in vals and journal.company_id.id != vals['company_id']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its company.')) - if self.bank_account_id: - self.bank_account_id.company_id = vals['company_id'] + company = self.env['res.company'].browse(vals['company_id']) + if self.bank_account_id.company_id and self.bank_account_id.company_id != company: + self.bank_account_id.write({ + 'company_id': company.id, + 'partner_id': company.partner_id.id, + }) if ('code' in vals and journal.code != vals['code']): if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1): raise UserError(_('This journal already contains items, therefore you cannot modify its short name.')) @@ -542,8 +547,14 @@ class AccountJournal(models.Model): self.default_credit_account_id.currency_id = vals['currency_id'] if self.bank_account_id: self.bank_account_id.currency_id = vals['currency_id'] - if 'bank_account_id' in vals and not vals.get('bank_account_id'): - raise UserError(_('You cannot remove the bank account from the journal once set.')) + if 'bank_account_id' in vals: + if not vals.get('bank_account_id'): + raise UserError(_('You cannot remove the bank account from the journal once set.')) + else: + bank_account = self.env['res.partner.bank'].browse(vals['bank_account_id']) + if bank_account.partner_id != company.partner_id: + raise UserError(_("The partners of the journal's company and the related bank account mismatch.")) + result = super(AccountJournal, self).write(vals) # Create the bank_account_id if necessary diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e05b02ae058..bca650dcc65 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -224,7 +224,7 @@ class AccountInvoice(models.Model): @api.depends('move_id.line_ids.amount_residual') def _compute_payments(self): payment_lines = set() - for line in self.move_id.line_ids: + for line in self.move_id.line_ids.filtered(lambda l: l.account_id.id == self.account_id.id): payment_lines.update(line.mapped('matched_credit_ids.credit_move_id.id')) payment_lines.update(line.mapped('matched_debit_ids.debit_move_id.id')) self.payment_move_line_ids = self.env['account.move.line'].browse(list(payment_lines)) @@ -267,7 +267,6 @@ class AccountInvoice(models.Model): ], string='Status', index=True, readonly=True, default='draft', track_visibility='onchange', copy=False, help=" * The 'Draft' status is used when a user is encoding a new and unconfirmed Invoice.\n" - " * The 'Pro-forma' status is used when the invoice does not have an invoice number.\n" " * The 'Open' status is used when user creates invoice, an invoice number is generated. It stays in the open status till the user pays the invoice.\n" " * The 'Paid' status is set automatically when the invoice is paid. Its related journal entries may or may not be reconciled.\n" " * The 'Cancelled' status is used when user cancel invoice.") diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index df18aa8fca9..5f9a2cf26ba 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -472,7 +472,7 @@ class AccountMoveLine(models.Model): tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict') analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') - company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True) + company_id = fields.Many2one('res.company', related='account_id.company_id', string='Company', store=True, readonly=True) counterpart = fields.Char("Counterpart", compute='_get_counterpart', help="Compute the counter part accounts of this journal item for this journal entry. This can be needed in reports.") # TODO: put the invoice link and partner_id on the account_move @@ -525,7 +525,7 @@ class AccountMoveLine(models.Model): raise ValidationError(_("You cannot create journal items with a secondary currency without filling both 'currency' and 'amount currency' field.")) @api.multi - @api.constrains('amount_currency') + @api.constrains('amount_currency', 'debit', 'credit') def _check_currency_amount(self): for line in self: if line.amount_currency: @@ -936,7 +936,9 @@ class AccountMoveLine(models.Model): def _get_pair_to_reconcile(self): #field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set) - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual' #reconciliation on bank accounts are special cases as we don't want to set them as reconcilable #but we still want to reconcile entries that are reversed together in order to clear those lines #in the bank reconciliation report. @@ -974,8 +976,9 @@ class AccountMoveLine(models.Model): #there is no more pair to reconcile so return what move_line are left if not sm_credit_move or not sm_debit_move: return self - - field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual' + company_currency_id = self[0].account_id.company_id.currency_id + account_curreny_id = self[0].account_id.currency_id + field = (account_curreny_id and company_currency_id != account_curreny_id) and 'amount_residual_currency' or 'amount_residual' if not sm_debit_move.debit and not sm_debit_move.credit: #both debit and credit field are 0, consider the amount_residual_currency field because it's an exchange difference entry field = 'amount_residual_currency' diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py index 93f17153fa8..d035fe594dc 100644 --- a/addons/account/models/chart_template.py +++ b/addons/account/models/chart_template.py @@ -738,6 +738,9 @@ class WizardMultiChartsAccounts(models.TransientModel): res.setdefault('domain', {}) res['domain']['sale_tax_id'] = repr(sale_tax_domain) res['domain']['purchase_tax_id'] = repr(purchase_tax_domain) + else: + self.sale_tax_id = False + self.purchase_tax_id = False if self.chart_template_id.transfer_account_id: self.transfer_account_id = self.chart_template_id.transfer_account_id.id if self.chart_template_id.code_digits: diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 68d8ef3e83b..d4593130d2c 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -36,12 +36,12 @@ class ResConfigSettings(models.TransientModel): module_account_budget = fields.Boolean(string='Budget Management') module_account_payment = fields.Boolean(string='Online Payment') module_account_reports = fields.Boolean("Dynamic Reports") - module_account_reports_followup = fields.Boolean("Enable payment followup management") + module_account_reports_followup = fields.Boolean("Follow-up Levels") module_account_check_printing = fields.Boolean("Allow check printing and deposits") module_account_batch_deposit = fields.Boolean(string='Use batch deposit', help='This allows you to group received checks before you deposit them to the bank.\n' '-This installs the module account_batch_deposit.') - module_account_sepa = fields.Boolean(string='Use SEPA payments') + module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)') module_account_sepa_direct_debit = fields.Boolean(string='Use SEPA Direct Debit') module_account_plaid = fields.Boolean(string="Plaid Connector") module_account_yodlee = fields.Boolean("Bank Interface - Sync your bank feeds automatically") @@ -49,7 +49,7 @@ class ResConfigSettings(models.TransientModel): module_account_bank_statement_import_ofx = fields.Boolean("Import in .ofx format") module_account_bank_statement_import_csv = fields.Boolean("Import in .csv format") module_account_bank_statement_import_camt = fields.Boolean("Import in CAMT.053 format") - module_currency_rate_live = fields.Boolean(string="Allow Currency Rate Live") + module_currency_rate_live = fields.Boolean(string="Automatic Currency Rates") module_print_docsaway = fields.Boolean(string="Docsaway") module_product_margin = fields.Boolean(string="Allow Product Margin") module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT") diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index d280b6cb850..4de3fedbbd4 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -13,6 +13,10 @@ class ReportAgedPartnerBalance(models.AbstractModel): _name = 'report.account.report_agedpartnerbalance' def _get_partner_move_lines(self, account_type, date_from, target_move, period_length): + # This method can receive the context key 'include_nullified_amount' {Boolean} + # Do an invoice and a payment and unreconcile. The amount will be nullified + # By default, the partner wouldn't appear in this report. + # The context key allow it to appear periods = {} start = datetime.strptime(date_from, "%Y-%m-%d") for i in range(5)[::-1]: @@ -190,7 +194,7 @@ class ReportAgedPartnerBalance(models.AbstractModel): values['name'] = _('Unknown Partner') values['trust'] = False - if at_least_one_amount: + if at_least_one_amount or self._context.get('include_nullified_amount'): res.append(values) return res, total, lines diff --git a/addons/account/report/account_overdue_report.py b/addons/account/report/account_overdue_report.py index dbfd933c99a..a41803f7062 100644 --- a/addons/account/report/account_overdue_report.py +++ b/addons/account/report/account_overdue_report.py @@ -25,7 +25,7 @@ class ReportOverdue(models.AbstractModel): "FROM account_move_line l " "JOIN account_account_type at ON (l.user_type_id = at.id) " "JOIN account_move m ON (l.move_id = m.id) " - "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND NOT l.reconciled GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) + "WHERE l.partner_id IN %s AND at.type IN ('receivable', 'payable') AND l.full_reconcile_id IS NULL GROUP BY l.date, l.name, l.ref, l.date_maturity, l.partner_id, at.type, l.blocked, l.amount_currency, l.currency_id, l.move_id, m.name", (((fields.date.today(), ) + (tuple(partner_ids),)))) for row in self.env.cr.dictfetchall(): res[row.pop('partner_id')].append(row) return res diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py index 2da6c42abe7..852b2a14e00 100644 --- a/addons/account/report/account_partner_ledger.py +++ b/addons/account/report/account_partner_ledger.py @@ -14,7 +14,7 @@ class ReportPartnerLedger(models.AbstractModel): full_account = [] currency = self.env['res.currency'] query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """ SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code @@ -51,7 +51,7 @@ class ReportPartnerLedger(models.AbstractModel): return result = 0.0 query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] query = """SELECT sum(""" + field + """) @@ -95,7 +95,7 @@ class ReportPartnerLedger(models.AbstractModel): AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),)) data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()] params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] - reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".full_reconcile_id IS NULL ' query = """ SELECT DISTINCT "account_move_line".partner_id FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml index 50124617526..6fdc285a97b 100644 --- a/addons/account/views/account_invoice_view.xml +++ b/addons/account/views/account_invoice_view.xml @@ -252,7 +252,7 @@ <group> <field name="origin" attrs="{'invisible': [('origin', '=', False)]}"/> <field name="date_invoice" string="Bill Date"/> - <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/> + <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/> <field name="move_name" invisible="1"/> <field name="currency_id" options="{'no_create': True, 'no_open': True}" groups="base.group_multi_currency"/> <field name="company_currency_id" invisible="1"/> @@ -299,7 +299,7 @@ <field name="amount"/> <field name="amount_rounding" invisible="1"/> <field name="amount_total" invisible="1"/> - <field name="currency_id" invisible="1"/> + <field name="currency_id" invisible="1" force_save="1"/> </tree> </field> </div> @@ -397,7 +397,7 @@ </group> <group> <field name="date_invoice"/> - <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state','=','paid')]}" force_save="1"/> + <field name="date_due" attrs="{'readonly': ['|',('payment_term_id','!=',False), ('state', 'in', ['open', 'paid'])]}" force_save="1"/> <field name="move_name" invisible="1"/> <field name="user_id" groups="base.group_user"/> <label for="currency_id" groups="base.group_multi_currency"/> @@ -500,7 +500,7 @@ <field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}"/> <field name="amount_rounding" invisible="1"/> <field name="amount_total"/> - <field name="currency_id" invisible="1"/> + <field name="currency_id" invisible="1" force_save="1"/> </tree> </field> </page> diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index 179699fa726..e02640721fe 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -176,7 +176,7 @@ <field name="module_currency_rate_live" widget="upgrade_boolean"/> </div> <div class="o_setting_right_pane"> - <label string="Automatic Currency Rates"/> + <label for="module_currency_rate_live"/> <div class="text-muted" id="update_currency_live"> Update exchange rates automatically </div> @@ -190,7 +190,7 @@ <field name="module_account_reports_followup" widget="upgrade_boolean"/> </div> <div class="o_setting_right_pane"> - <label string="Follow-up Levels"/> + <label for="module_account_reports_followup"/> <div class="text-muted"> Use follow-up levels and schedule actions </div> @@ -256,7 +256,7 @@ <field name="module_account_sepa" widget="upgrade_boolean"/> </div> <div class="o_setting_right_pane" name="sepa_right_pane"> - <label string="SEPA Credit Transfer (SCT)" for="module_account_sepa"/> + <label for="module_account_sepa"/> <span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/> <div class="text-muted"> Pay your bills in one-click using Euro SEPA service diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index c652267ad5d..ddc2d48ce76 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -154,6 +154,7 @@ class AccountVoucher(models.Model): @api.onchange('partner_id', 'pay_now') def onchange_partner_id(self): + pay_journal_domain = [('type', 'in', ['cash', 'bank'])] if self.pay_now != 'pay_now': if self.partner_id: self.account_id = self.partner_id.property_account_receivable_id \ @@ -163,6 +164,12 @@ class AccountVoucher(models.Model): domain = [('deprecated', '=', False), ('internal_type', '=', account_type)] self.account_id = self.env['account.account'].search(domain, limit=1) + else: + if self.voucher_type == 'purchase': + pay_journal_domain.append(('outbound_payment_method_ids', '!=', False)) + else: + pay_journal_domain.append(('inbound_payment_method_ids', '!=', False)) + return {'domain': {'payment_journal_id': pay_journal_domain}} @api.multi def proforma_voucher(self): diff --git a/addons/auth_crypt/models/res_users.py b/addons/auth_crypt/models/res_users.py index ad66d0c7025..ff7e983bc64 100644 --- a/addons/auth_crypt/models/res_users.py +++ b/addons/auth_crypt/models/res_users.py @@ -96,3 +96,6 @@ class ResUsers(models.Model): internally """ return default_crypt_context + + def _get_session_token_fields(self): + return super(ResUsers, self)._get_session_token_fields() | {'password_crypt'} diff --git a/addons/auth_oauth/models/res_users.py b/addons/auth_oauth/models/res_users.py index fa052e41a1b..3657885c3ac 100644 --- a/addons/auth_oauth/models/res_users.py +++ b/addons/auth_oauth/models/res_users.py @@ -117,3 +117,6 @@ class ResUsers(models.Model): res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)]) if not res: raise + + def _get_session_token_fields(self): + return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'} diff --git a/addons/base_address_city/models/res_partner.py b/addons/base_address_city/models/res_partner.py index dff5de888c7..c35430d1f92 100644 --- a/addons/base_address_city/models/res_partner.py +++ b/addons/base_address_city/models/res_partner.py @@ -4,6 +4,8 @@ from lxml import etree from odoo import api, models, fields +from odoo.tools.translate import _ + class Partner(models.Model): _inherit = 'res.partner' @@ -24,14 +26,15 @@ class Partner(models.Model): doc = etree.fromstring(arch) if doc.xpath("//field[@name='city_id']"): return arch + label = _('City') for city_node in doc.xpath("//field[@name='city']"): replacement_xml = """ <div> <field name="country_enforce_cities" invisible="1"/> - <field name='city' attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/> - <field name='city_id' attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]" string="City"/> + <field name='city' placeholder="%s" attrs="{'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}"/> + <field name='city_id' placeholder="%s" string="%s" attrs="{'invisible': [('country_enforce_cities', '=', False)], 'readonly': [('type', '=', 'contact'), ('parent_id', '!=', False)]}" context="{'default_country_id': country_id}" domain="[('country_id', '=', country_id)]"/> </div> - """ + """ % (label, label, label) city_id_node = etree.fromstring(replacement_xml) city_node.getparent().replace(city_node, city_id_node) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index 063ebf6a829..a781a314686 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -85,6 +85,8 @@ class ResConfigSettings(models.TransientModel): @api.multi def edit_external_header(self): + if not self.external_report_layout: + return False return self._prepare_report_view_action('web.external_layout_' + self.external_report_layout) @api.multi diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml index 24625320191..e798d5fd1eb 100644 --- a/addons/crm/report/crm_opportunity_report_views.xml +++ b/addons/crm/report/crm_opportunity_report_views.xml @@ -55,14 +55,14 @@ <field name="model_id">crm.opportunity.report</field> <field name="domain">[('probability', '=', 100)]</field> <field name="user_id" eval="False"/> - <field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count']}</field> + <field name="context">{'group_by': ['date_closed:month'],'col_group_by': ['create_date:month'], 'measures': ['__count__']}</field> </record> <record id="filter_opportunity_opportunities_won_per_team" model="ir.filters"> <field name="name">Won by Channel</field> <field name="model_id">crm.opportunity.report</field> <field name="domain">[('probability', '=', 100)]</field> <field name="user_id" eval="False"/> - <field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected revenue']}</field> + <field name="context">{'group_by': ['team_id'], 'col_group_by': ['date_last_stage_update:month'], 'measures': ['expected_revenue']}</field> </record> <record id="filter_opportunity_country" model="ir.filters"> <field name="name">Won by Country</field> @@ -75,7 +75,7 @@ <field name="name">Expected Revenue by Channel</field> <field name="model_id">crm.opportunity.report</field> <field name="user_id" eval="False"/> - <field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count']}</field> + <field name="context">{'group_by': ['create_date:month', 'team_id'], 'measures': ['expected_revenue', '__count__']}</field> </record> <!-- Opportunities by user and team Search View --> diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index 2710b693ccd..258e6ca8af8 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -251,8 +251,6 @@ <group> <field name="name" string="Opportunity Title" placeholder="e.g. Customer Deal"/> <field name="partner_id" domain="[('customer', '=', True)]" context="{'search_default_customer': 1}"/> - <field name="email_from" invisible="1"/> - <field name="phone" invisible="1"/> <field name="company_id" invisible="1"/> <field name="company_currency" invisible="1"/> <label for="planned_revenue"/> @@ -267,6 +265,20 @@ <button string="Create & Edit" name="edit_dialog" type="object" class="btn-primary"/> <button string="Discard" class="btn-default" special="cancel"/> </footer> + <field name="partner_name" invisible="1"/> + <field name="contact_name" invisible="1"/> + <field name="title" invisible="1"/> + <field name="street" invisible="1"/> + <field name="street2" invisible="1"/> + <field name="city" invisible="1"/> + <field name="state_id" invisible="1"/> + <field name="country_id" invisible="1"/> + <field name="email_from" invisible="1"/> + <field name="phone" invisible="1"/> + <field name="mobile" invisible="1"/> + <field name="zip" invisible="1"/> + <field name="function" invisible="1"/> + <field name="website" invisible="1"/> </sheet> </form> </field> diff --git a/addons/event_sale/wizard/event_edit_registration.xml b/addons/event_sale/wizard/event_edit_registration.xml index 47bb964b0f2..5adc5d50bae 100644 --- a/addons/event_sale/wizard/event_edit_registration.xml +++ b/addons/event_sale/wizard/event_edit_registration.xml @@ -11,7 +11,7 @@ <field name="event_registration_ids"> <tree string="Registration" editable="top" create="false" delete="false"> <field name="event_id" readonly='1' force_save="1"/> - <field name="registration_id" readonly='1'/> + <field name="registration_id" readonly='1' force_save="1"/> <field name="event_ticket_id" domain="[('event_id', '=', event_id)]" readonly='1'/> <field name="name"/> <field name="email"/> diff --git a/addons/hr/models/hr.py b/addons/hr/models/hr.py index 49d8a9e0859..21d302a63c5 100644 --- a/addons/hr/models/hr.py +++ b/addons/hr/models/hr.py @@ -126,7 +126,8 @@ class Employee(models.Model): ], groups="hr.group_hr_user", default="male") marital = fields.Selection([ ('single', 'Single'), - ('married', 'Married (or similar)'), + ('married', 'Married'), + ('cohabitant', 'Legal Cohabitant'), ('widower', 'Widower'), ('divorced', 'Divorced') ], string='Marital Status', groups="hr.group_hr_user", default='single') diff --git a/addons/hr_org_chart/static/src/js/hr_org_chart.js b/addons/hr_org_chart/static/src/js/hr_org_chart.js index c2c425d2612..69599fa39e3 100644 --- a/addons/hr_org_chart/static/src/js/hr_org_chart.js +++ b/addons/hr_org_chart/static/src/js/hr_org_chart.js @@ -13,6 +13,7 @@ var FieldOrgChart = AbstractField.extend({ events: { "click .o_employee_redirect": "_onEmployeeRedirect", + "click .o_employee_sub_redirect": "_onEmployeeSubRedirect", }, /** * @constructor diff --git a/addons/hr_payroll/models/hr_salary_rule.py b/addons/hr_payroll/models/hr_salary_rule.py index 5d6c49a0bf7..af9d6c342a5 100644 --- a/addons/hr_payroll/models/hr_salary_rule.py +++ b/addons/hr_payroll/models/hr_salary_rule.py @@ -76,7 +76,7 @@ class HrSalaryRuleCategory(models.Model): _name = 'hr.salary.rule.category' _description = 'Salary Rule Category' - name = fields.Char(required=True) + name = fields.Char(required=True, translate=True) code = fields.Char(required=True) parent_id = fields.Many2one('hr.salary.rule.category', string='Parent', help="Linking a salary category to its parent is used only for the reporting purpose.") @@ -89,7 +89,7 @@ class HrSalaryRuleCategory(models.Model): class HrSalaryRule(models.Model): _name = 'hr.salary.rule' - name = fields.Char(required=True) + name = fields.Char(required=True, translate=True) code = fields.Char(required=True, help="The code of salary rules can be used as reference in computation of other rules. " "In that case, it is case sensitive.") diff --git a/addons/hr_timesheet/models/hr_timesheet.py b/addons/hr_timesheet/models/hr_timesheet.py index ddfd68e0d56..e37f3139e57 100644 --- a/addons/hr_timesheet/models/hr_timesheet.py +++ b/addons/hr_timesheet/models/hr_timesheet.py @@ -18,7 +18,7 @@ class AccountAnalyticLine(models.Model): project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)]) employee_id = fields.Many2one('hr.employee', "Employee") - department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True) + department_id = fields.Many2one('hr.department', "Department", compute='_compute_department_id', store=True) @api.onchange('project_id') def onchange_project_id(self): @@ -28,6 +28,11 @@ class AccountAnalyticLine(models.Model): def _onchange_employee_id(self): self.user_id = self.employee_id.user_id + @api.depends('employee_id') + def _compute_department_id(self): + for line in self: + line.department_id = line.employee_id.department_id + @api.model def create(self, vals): vals = self._timesheet_preprocess(vals) diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py index 9cd8776eaf7..86a01497b46 100644 --- a/addons/hw_scanner/controllers/main.py +++ b/addons/hw_scanner/controllers/main.py @@ -4,7 +4,7 @@ import logging import time from os import listdir -from os.path import join +from os.path import join, isdir try: from queue import Queue, Empty except ImportError: @@ -124,7 +124,7 @@ class Scanner(Thread): if not evdev: return [] - if not os.path.isdir(self.input_dir): + if not isdir(self.input_dir): return [] new_devices = [device for device in listdir(self.input_dir) diff --git a/addons/im_livechat/views/im_livechat_channel_views.xml b/addons/im_livechat/views/im_livechat_channel_views.xml index 0c8d8ec72bb..cc4133b46ab 100644 --- a/addons/im_livechat/views/im_livechat_channel_views.xml +++ b/addons/im_livechat/views/im_livechat_channel_views.xml @@ -106,10 +106,11 @@ <templates> <t t-name="kanban-box"> <div> - <a t-if="! read_only_mode" type="delete" style="position: absolute; right: 0; padding: 4px; diplay: inline-block">X</a> - <div class="oe_group_details" style="min-height: 40px"> - <img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)" - class="oe_avatar oe_kanban_avatar_smallbox" style="float:left; margin-right: 10px;"/> + <a t-if="!read_only_mode" type="delete" class="fa fa-times pull-right"/> + <div class="o_kanban_image"> + <img t-att-src="kanban_image('res.users', 'image', record.id.raw_value)"/> + </div> + <div class="o_kanban_details"> <h4 class="o_kanban_record_title"><field name="name"/></h4> </div> </div> diff --git a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml index 0f6bb86479f..6920f154d9d 100644 --- a/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml +++ b/addons/l10n_be_hr_payroll/data/l10n_be_hr_payroll_data.xml @@ -160,7 +160,7 @@ elif salary <= 2510.47: <field name="sequence">165</field> <field name="condition_select">python</field> <field name="appears_on_payslip" eval="False"/> - <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field> + <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='without income')) and (employee.resident_bool!=True))</field> <field name="amount_select">code</field> <field name="amount_python_compute"> wage = categories.BASIC @@ -184,7 +184,7 @@ else: <field name="sequence">165</field> <field name="condition_select">python</field> <field name="appears_on_payslip" eval="False"/> - <field name="condition_python">result = (((employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field> + <field name="condition_python">result = (((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field> <field name="amount_select">code</field> <field name="amount_python_compute"> wage = categories.BASIC @@ -208,7 +208,7 @@ else: <field name="sequence">120</field> <field name="condition_select">python</field> <field name="appears_on_payslip" eval="False"/> - <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital=='married' and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field> + <field name="condition_python">result = (((employee.marital in ['divorced', 'single', 'widower']) or (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income')) and (employee.resident_bool!=True))</field> </record> <record id="hr_payroll_rules_baremeII" model="hr.salary.rule"> <field name="category_id" ref="hr_payroll_head_pp"/> @@ -218,7 +218,7 @@ else: <field name="sequence">120</field> <field name="condition_select">python</field> <field name="appears_on_payslip" eval="False"/> - <field name="condition_python">result = ((employee.marital=='married' and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field> + <field name="condition_python">result = ((employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='without income') and (employee.resident_bool!=True))</field> </record> <record id="hr_payroll_rules_baremeIII" model="hr.salary.rule"> <field name="category_id" ref="hr_payroll_head_pp"/> @@ -361,7 +361,7 @@ elif salary <= 2461.27: <field name="code">Red.Spouse.Net</field> <field name="sequence">148</field> <field name="condition_select">python</field> - <field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue <= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field> + <field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income' and employee.spouse_net_revenue <= 225.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field> <field name="amount_select">code</field> <field name="amount_python_compute">result = min(110.50, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field> </record> @@ -372,7 +372,7 @@ elif salary <= 2461.27: <field name="code">Red.Spouse.Oth.Net</field> <field name="sequence">149</field> <field name="condition_select">python</field> - <field name="condition_python">result = (employee.marital == 'married' and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field> + <field name="condition_python">result = (employee.marital in ['married', 'cohabitant'] and employee.spouse_fiscal_status=='with income' and not employee.spouse_net_revenue and employee.spouse_other_net_revenue <= 450.0) and (categories.PP + categories.PPRed + categories.FamRed + categories.ChA < 0.0)</field> <field name="amount_select">code</field> <field name="amount_python_compute">result = min(220.0, - (categories.PP + categories.PPRed + categories.FamRed + categories.ChA))</field> </record> diff --git a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml index 37fa58ae2b7..4111ab06878 100644 --- a/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml +++ b/addons/l10n_be_hr_payroll/views/l10n_be_hr_payroll_view.xml @@ -61,8 +61,8 @@ <field name="transport_mode" widget="radio"/> <field name="car_atn" attrs="{'invisible': [('transport_mode', '!=', 'company_car')]}" invisible="1"/> <field name="transport_employer_cost" invisible="1"/> - <label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}"/> - <div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}"> + <label string="Public Transport" attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_label"/> + <div attrs="{'invisible': [('transport_mode', '!=', 'public_transport')]}" name="public_transport_div"> <div> <label for="public_transport_employee_amount" class="oe_inline"/> <field name="public_transport_employee_amount" class="oe_inline"/> @@ -72,8 +72,8 @@ <field name="public_transport_reimbursed_amount" class="oe_inline"/> </div> </div> - <label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}"/> - <div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row"> + <label string="Other" attrs="{'invisible': [('transport_mode', '!=', 'others')]}" name="others_label"/> + <div attrs="{'invisible': [('transport_mode', '!=', 'others')]}" class="o_row" name="others_div"> <field name="others_reimbursed_amount" class="oe_inline" nolabel="1"/> <span>/ month</span> </div> @@ -131,8 +131,8 @@ <field name="resident_bool" eval="False"/> </xpath> <xpath expr="//field[@name='marital']" position="after"> - <field name="spouse_fiscal_status" attrs="{'invisible':[('marital','!=','married')],'required':[('marital','=','married')]}" colspan="1" help="if spouse has professionnel income or not"/> - <field name="disabled_spouse_bool" attrs="{'invisible':[('marital','!=','married')]}" colspan="1"/> + <field name="spouse_fiscal_status" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])], 'required': [('marital', 'in', ['married', 'cohabitant'])]}" colspan="1" help="if spouse has professionnel income or not"/> + <field name="disabled_spouse_bool" attrs="{'invisible': [('marital', 'not in', ['married', 'cohabitant'])]}" colspan="1"/> </xpath> <xpath expr="//field[@name='gender']" position="after"> <field name="disabled"/> @@ -148,8 +148,8 @@ <field name="other_disabled_juniors_dependent" attrs="{'invisible':[('other_dependent_people','!=',True)]}"/> </xpath> <xpath expr="//field[@name='spouse_fiscal_status']" position="after"> - <field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/> - <field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', '!=', 'married'), ('spouse_fiscal_status', '!=', 'with income')]}"/> + <field name="spouse_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/> + <field name="spouse_other_net_revenue" attrs="{'invisible': ['|', ('marital', 'not in', ['married', 'cohabitant']), ('spouse_fiscal_status', '!=', 'with income')]}"/> </xpath> </field> </record> diff --git a/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py b/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py index f774eaa0436..a6a3c10ac13 100644 --- a/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py +++ b/addons/l10n_be_hr_payroll_fleet/models/hr_contract.py @@ -21,7 +21,7 @@ class HrContract(models.Model): @api.depends('car_id', 'new_car', 'new_car_model_id') def _compute_car_atn_and_costs(self): for contract in self: - if contract.car_id: + if not contract.new_car and contract.car_id: contract.car_atn = contract.car_id.atn contract.company_car_total_depreciated_cost = contract.car_id.total_depreciated_cost elif contract.new_car and contract.new_car_model_id: diff --git a/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml b/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml index c6a7ad33abd..184772fc1e5 100644 --- a/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml +++ b/addons/l10n_be_hr_payroll_fleet/views/hr_contract_views.xml @@ -7,8 +7,8 @@ <field name="arch" type="xml"> <xpath expr="//field[@name='transport_mode']" position="after"> <field name="new_car" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('available_cars_amount', '>', 3)]}"/> - <label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}"/> - <div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}"> + <label for="car_id" attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_label"/> + <div attrs="{'invisible': ['|', ('transport_mode', '!=', 'company_car'), ('new_car', '=', True)]}" name="car_id_div"> <field name="car_id" nolabel="1" attrs="{'invisible': [('new_car', '=', True)]}"/> <field name="available_cars_amount" invisible="1"/> </div> diff --git a/addons/l10n_ch/models/res_company.py b/addons/l10n_ch/models/res_company.py index 861dc514ce7..b30e8e13135 100644 --- a/addons/l10n_ch/models/res_company.py +++ b/addons/l10n_ch/models/res_company.py @@ -6,4 +6,24 @@ from odoo import fields, models class Company(models.Model): _inherit = "res.company" + l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr') + l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr') l10n_ch_isr_print_bank_location = fields.Boolean(string='Print bank location', default=False, help='Boolean option field indicating whether or not the alternate layout (the one printing bank name and address) must be used when generating an ISR.') + l10n_ch_isr_scan_line_left = fields.Float(string='Scan line horizontal offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr') + l10n_ch_isr_scan_line_top = fields.Float(string='Scan line vertical offset (mm)', compute='_compute_l10n_ch_isr', inverse='_set_l10n_ch_isr') + + def _compute_l10n_ch_isr(self): + get_param = self.env['ir.config_parameter'].sudo().get_param + for company in self: + company.l10n_ch_isr_preprinted_account = bool(get_param('l10n_ch.isr_preprinted_account', default=False)) + company.l10n_ch_isr_preprinted_bank = bool(get_param('l10n_ch.isr_preprinted_bank', default=False)) + company.l10n_ch_isr_scan_line_top = float(get_param('l10n_ch.isr_scan_line_top', default=0)) + company.l10n_ch_isr_scan_line_left = float(get_param('l10n_ch.isr_scan_line_left', default=0)) + + def _set_l10n_ch_isr(self): + set_param = self.env['ir.config_parameter'].sudo().set_param + for company in self: + set_param("l10n_ch.isr_preprinted_account", company.l10n_ch_isr_preprinted_account) + set_param("l10n_ch.isr_preprinted_bank", company.l10n_ch_isr_preprinted_bank) + set_param("l10n_ch.isr_scan_line_top", company.l10n_ch_isr_scan_line_top) + set_param("l10n_ch.isr_scan_line_left", company.l10n_ch_isr_scan_line_left) diff --git a/addons/l10n_ch/models/res_config_settings.py b/addons/l10n_ch/models/res_config_settings.py index b1ff9537918..31bc5fc7bed 100644 --- a/addons/l10n_ch/models/res_config_settings.py +++ b/addons/l10n_ch/models/res_config_settings.py @@ -7,6 +7,14 @@ from odoo import fields, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' + l10n_ch_isr_preprinted_account = fields.Boolean(string='Preprinted account', + related="company_id.l10n_ch_isr_preprinted_account") + l10n_ch_isr_preprinted_bank = fields.Boolean(string='Preprinted bank', + related="company_id.l10n_ch_isr_preprinted_bank") l10n_ch_isr_print_bank_location = fields.Boolean(string="Print bank on ISR", related="company_id.l10n_ch_isr_print_bank_location", required=True) + l10n_ch_isr_scan_line_left = fields.Float(string='Horizontal offset', + related="company_id.l10n_ch_isr_scan_line_left") + l10n_ch_isr_scan_line_top = fields.Float(string='Vertical offset', + related="company_id.l10n_ch_isr_scan_line_top") diff --git a/addons/l10n_ch/report/isr_report.xml b/addons/l10n_ch/report/isr_report.xml index b6966836e8b..4ffa10363c6 100644 --- a/addons/l10n_ch/report/isr_report.xml +++ b/addons/l10n_ch/report/isr_report.xml @@ -61,7 +61,7 @@ and location of the bank.--> <t t-if="print_bank"> <div id="voucher-for-bank"> - <p> + <p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank"> <t t-esc="invoice.partner_bank_id.bank_id.name"/><br /> <t t-esc="invoice.partner_bank_id.bank_id.zip"/> <t t-esc="invoice.partner_bank_id.bank_id.city"/> @@ -79,12 +79,11 @@ </p> </div> - <div id="voucher-bank"> + <div id="voucher-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account"> <!--Konto/Compte/Conto--> <p id="voucher-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/> </div> - <p id="voucher-currency" t-field="invoice.currency_id.name"/> <p id="voucher-amount_units" t-esc="split_total_amount[0]"/> <p id="voucher-amount_cents" t-esc="split_total_amount[1]"/> @@ -103,7 +102,7 @@ and location of the bank.--> <t t-if="print_bank"> <div id="slip-for-bank"> - <p> + <p t-if="not invoice.company_id.l10n_ch_isr_preprinted_bank"> <t t-esc="invoice.partner_bank_id.bank_id.name"/><br /> <t t-esc="invoice.partner_bank_id.bank_id.zip"/> <t t-esc="invoice.partner_bank_id.bank_id.city"/> @@ -122,12 +121,11 @@ </p> </div> - <div id="slip-bank"> + <div id="slip-bank" t-if="not print_bank or not invoice.company_id.l10n_ch_isr_preprinted_account"> <!--Konto/Compte/Conto--> <p id="slip-bank_ref" t-field="invoice.l10n_ch_isr_postal_formatted"/> </div> - <p id="slip-currency" t-field="invoice.currency_id.name"/> <p id="slip-amount_units" t-esc="split_total_amount[0]"/> <p id="slip-amount_cents" t-esc="split_total_amount[1]"/> @@ -141,9 +139,11 @@ <address id="slip-by_customer_address" t-field="invoice.partner_id" t-options='{"widget": "contact", "fields": ["address","name"], "no_marker": True}' /> </div> - <div id="slip-optical"> + <div id="slip-optical-line"> <!--Optical reference--> - <span id="slip-optical_ref_line" t-field="invoice.l10n_ch_isr_optical_line"/> + <div t-attf-style="top: {{ invoice.company_id.l10n_ch_isr_scan_line_top }}mm; left: {{ invoice.company_id.l10n_ch_isr_scan_line_left }}mm;"> + <div t-foreach="invoice.l10n_ch_isr_optical_line" t-as="char" t-esc="char" t-attf-style="right: {{ round((char_size - char_index - 1) * 0.1, 1) }}in"/> + </div> </div> </div> </div> diff --git a/addons/l10n_ch/static/src/less/report_isr.less b/addons/l10n_ch/static/src/less/report_isr.less index be366e54ddd..913404727d2 100644 --- a/addons/l10n_ch/static/src/less/report_isr.less +++ b/addons/l10n_ch/static/src/less/report_isr.less @@ -38,6 +38,7 @@ body.l10n_ch_isr #isr { overflow: hidden; font-family: "Work Sans", Verdana, Geneva, sans-serif; + line-height: 1; font-size: 13px; p, address { @@ -58,25 +59,26 @@ body.l10n_ch_isr #isr { #voucher > * { position: absolute; /* default left margin for fields content */ - left: 1/10in; + left: 3/20in; &#voucher-for-contact { - top: 3/10in; + top: 0.25in; } &#voucher-for-bank { - top: 3/20in; + top: 0.18in; } &#voucher-for-bank + #voucher-for-contact { - top: 4/6in; + top: 0.68in; } &#voucher-bank { - top: 9/6in; + top: 1.51in; left: 11/10in; } + /* TODO: remove in master */ &#voucher-currency { top: 9/6 + 3/20in; @@ -86,24 +88,22 @@ body.l10n_ch_isr #isr { &#voucher-amount_units { width: 15/10in; - top: 11/6in; + top: 1.84in; text-align: right; - line-height: 1/6in; - font-weight: 14px; + font-size: 14px; } &#voucher-amount_cents { width: 4/10in; - top: 11/6in; - left: 19/10in; + top: 1.84in; + left: 20/10in; - line-height: 1/6in; - font-weight: 14px; + font-size: 14px; } &#voucher-by { - top: 26/12in; + top: 2.18in; } } @@ -121,25 +121,26 @@ body.l10n_ch_isr #isr { #slip > * { position: absolute; /* default left margin for fields content */ - left: 1/10in; + left: 3/20in; &#slip-for-contact { - top: 3/10in; + top: 0.25in; } &#slip-for-bank { - top: 3/20in; + top: 0.18in; } &#slip-for-bank + #slip-for-contact { - top: 4/6in; + top: 0.68in; } &#slip-bank { - top: 9/6in; + top: 1.51in; left: 11/10in; } + /* TODO: remove in master */ &#slip-currency { top: 9/6 + 3/20in; @@ -149,34 +150,52 @@ body.l10n_ch_isr #isr { &#slip-amount_units { width: 15/10in; - top: 11/6in; + top: 1.84in; text-align: right; - line-height: 1/6in; - font-weight: 14px; + font-size: 14px; } &#slip-amount_cents { width: 4/10in; - top: 11/6in; - left: 19/10in; + top: 1.84in; + left: 20/10in; - line-height: 1/6in; - font-weight: 14px; + font-size: 14px; } &#slip-reference { - width: 3 + 3/10in; - top: 7/6in; - left: 2 + 5/10in; + width: 3.3in; + top: 1.17in; + left: 2.5in; text-align: center; } &#slip-by { - top: 11/6in; - left: 2 + 5/10in; + top: 1.85in; + left: 2.55in; } + &#slip-optical-line { + right: 3/10in; + bottom: 4/6in; + + /* Positioning is set for OCRB of Tsukurimashou Font Family */ + font-family: OCRB, Courier, monospace; + font-size: 10pt; + line-height: 0.1in; + + > div { + position: relative; + + > div { + position: absolute; + bottom: 0; + } + } + } + + /* TODO: remove in master */ &#slip-optical { right: 3/10in; bottom: 4/6in; diff --git a/addons/l10n_ch/views/res_config_settings_views.xml b/addons/l10n_ch/views/res_config_settings_views.xml index 02847355c7a..2fa7a54386c 100644 --- a/addons/l10n_ch/views/res_config_settings_views.xml +++ b/addons/l10n_ch/views/res_config_settings_views.xml @@ -17,10 +17,39 @@ Print the coordinates of your bank under the 'Payment for' title of the ISR. Your address will be moved to the 'in favour of' section. </div> + <div class="content-group" attrs="{'invisible': [('l10n_ch_isr_print_bank_location', '=', False)]}"> + <div class="row mt16"> + <label for="l10n_ch_isr_preprinted_bank" class="col-md-4 o_light_label"/> + <field name="l10n_ch_isr_preprinted_bank"/> + </div> + <div class="row"> + <label for="l10n_ch_isr_preprinted_account" class="col-md-4 o_light_label"/> + <field name="l10n_ch_isr_preprinted_account"/> + </div> + </div> + </div> + </div> + <div class="col-xs-12 col-md-6 o_setting_box" id="l10n_ch-isr_print_scanline_offset"> + <div class="o_setting_left_pane"/> + <div class="o_setting_right_pane"> + <label string="ISR scan line offset"/> + <div class="text-muted"> + Offset to move the the scan line in mm + </div> + <div class="content-group"> + <div class="row mt16"> + <label for="l10n_ch_isr_scan_line_top" class="col-md-4 o_light_label"/> + <field name="l10n_ch_isr_scan_line_top"/> + </div> + <div class="row"> + <label for="l10n_ch_isr_scan_line_left" class="col-md-4 o_light_label"/> + <field name="l10n_ch_isr_scan_line_left"/> + </div> + </div> </div> </div> </xpath> </field> </record> </data> -</odoo>
\ No newline at end of file +</odoo> diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py index ac778e6be8f..bd8e4cc44c0 100644 --- a/addons/l10n_fr_fec/wizard/account_fr_fec.py +++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py @@ -220,7 +220,7 @@ class AccountFrFec(models.TransientModel): replace(MIN(aa.name), '|', '/') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') - ELSE rp.ref + ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib, @@ -278,7 +278,7 @@ class AccountFrFec(models.TransientModel): replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') - ELSE rp.ref + ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib, diff --git a/addons/l10n_multilang/models/l10n_multilang.py b/addons/l10n_multilang/models/l10n_multilang.py index bee06394b7d..0c096c4cdf3 100644 --- a/addons/l10n_multilang/models/l10n_multilang.py +++ b/addons/l10n_multilang/models/l10n_multilang.py @@ -37,11 +37,11 @@ class AccountChartTemplate(models.Model): 'type': 'model', 'res_id': out_ids[counter].id, 'lang': lang, - 'src': element.name, + 'src': element[in_field], 'value': value[element.id], }) else: - _logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element.name)) + _logger.info('Language: %s. Translation from template: there is no translation available for %s!' % (lang, element[in_field])) counter += 1 return True diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 2a421f6fdcd..97b6bf4906f 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -353,7 +353,7 @@ var Thread = Widget.extend({ } else { this.trigger('redirect', options.model, options.id); } - }, 200, true), + }, 500, true), //-------------------------------------------------------------------------- // Handlers diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 26c093c3e84..eef37598164 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -179,7 +179,7 @@ - <small class="o_mail_timestamp" t-att-title="message.date.format(date_format)"><t t-esc="message.hour"/></small> <t t-if="message.model and (message.model != 'mail.channel') and options.display_document_link"> - <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link">on <t t-esc="message.record_name"/></a> + on <a t-att-href="message.url" t-att-data-oe-model="message.model" t-att-data-oe-id="message.res_id" class="o_document_link"><t t-esc="message.record_name"/></a> </t> <t t-if="message.origin_id and (message.origin_id !== options.channel_id)"> (from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>) diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py index 348d4d04dfa..6b66bfb07b5 100644 --- a/addons/membership/models/partner.py +++ b/addons/membership/models/partner.py @@ -51,7 +51,7 @@ class Partner(models.Model): 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', - 'member_lines.date_to', 'member_lines.date_from', + 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_start(self): @@ -65,7 +65,7 @@ class Partner(models.Model): 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', - 'member_lines.date_to', 'member_lines.date_from', + 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_stop(self): @@ -79,7 +79,7 @@ class Partner(models.Model): 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', - 'member_lines.date_to', 'member_lines.date_from', + 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_cancel(self): diff --git a/addons/mrp/models/product.py b/addons/mrp/models/product.py index d9fa6fcf6f6..e08d759ff45 100644 --- a/addons/mrp/models/product.py +++ b/addons/mrp/models/product.py @@ -56,14 +56,17 @@ class ProductProduct(models.Model): mo_count = fields.Integer('# Manufacturing Orders', compute='_compute_mo_count') def _compute_bom_count(self): + # read_group_res: BOM where product_id is set + # read_group_res_tmpl: BOM where product_tmpl_id is set and product_id is not set + # The total count is the sum of both. read_group_res = self.env['mrp.bom'].read_group([('product_id', 'in', self.ids)], ['product_id'], ['product_id']) mapped_data = dict([(data['product_id'][0], data['product_id_count']) for data in read_group_res]) + read_group_res_tmpl = self.env['mrp.bom'].read_group([ + ('product_tmpl_id', 'in', self.mapped('product_tmpl_id.id')), ('product_id', '=', False) + ], ['product_tmpl_id'], ['product_tmpl_id']) + mapped_data_tmpl = dict([(data['product_tmpl_id'][0], data['product_tmpl_id_count']) for data in read_group_res_tmpl]) for product in self: - if product.product_tmpl_id.product_variant_count == 1: - bom_count = mapped_data.get(product.id, product.product_tmpl_id.bom_count) - else: - bom_count = mapped_data.get(product.id, 0) - product.bom_count = bom_count + product.bom_count = mapped_data.get(product.id, 0) + mapped_data_tmpl.get(product.product_tmpl_id.id, 0) @api.multi def _compute_used_in_bom_count(self): diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index d68df6f9934..d9617a633a2 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -313,6 +313,7 @@ class PaymentAcquirer(models.Model): 'billing_partner': billing_partner, 'billing_partner_id': billing_partner_id, 'billing_partner_name': billing_partner.name, + 'billing_partner_commercial_company_name': billing_partner.commercial_company_name, 'billing_partner_lang': billing_partner.lang, 'billing_partner_email': billing_partner.email, 'billing_partner_zip': billing_partner.zip, @@ -845,16 +846,13 @@ class PaymentToken(models.Model): 'partner_country_id': self.partner_id.country_id.id, }) - try: - kwargs.update({'3d_secure': True}) - tx.s2s_do_transaction(**kwargs) - # if 3D secure is called, then we do not refund right now - if tx.html_3ds: - return tx - except: - _logger.error('Error while validating a payment method') - finally: + kwargs.update({'3d_secure': True}) + tx.s2s_do_transaction(**kwargs) + + # if 3D secure is called, then we do not refund right now + if not tx.html_3ds: tx.s2s_do_refund() + return tx @api.multi diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js index 20216c4cdad..1e0d5e7a0c2 100644 --- a/addons/payment/static/src/js/payment_form.js +++ b/addons/payment/static/src/js/payment_form.js @@ -106,6 +106,7 @@ odoo.define('payment.payment_form', function (require) { else { checked_radio.value = data.id; // set the radio value to the new card id form.submit(); + return; } } // if the server has returned false, we display an error diff --git a/addons/payment/tests/common.py b/addons/payment/tests/common.py index 4caa6206dc8..4cdad303018 100644 --- a/addons/payment/tests/common.py +++ b/addons/payment/tests/common.py @@ -25,6 +25,7 @@ class PaymentAcquirerCommon(common.TransactionCase): 'partner_country_id': self.country_belgium.id, 'partner_country_name': 'Belgium', 'billing_partner_name': 'Norbert Buyer', + 'billing_partner_commercial_company_name': 'Big Company', 'billing_partner_lang': 'en_US', 'billing_partner_email': 'norbert.buyer@example.com', 'billing_partner_address': 'Huge Street 2/543', diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml index 1b6246290fa..9e53f94d101 100644 --- a/addons/payment/views/payment_templates.xml +++ b/addons/payment/views/payment_templates.xml @@ -84,7 +84,7 @@ <t t-call="{{acq.sudo().get_s2s_form_xml_id()}}"> <t t-set="id" t-value="acq.id"/> <t t-set="partner_id" t-value="partner_id"/> - <t t-set="return_url" t-value="''"/> + <t t-if="not return_url" t-set="return_url" t-value="''"/> </t> </div> </div> diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py index dbc24ced0c0..3b15e51d44f 100644 --- a/addons/payment_adyen/models/payment.py +++ b/addons/payment_adyen/models/payment.py @@ -78,7 +78,7 @@ class AcquirerAdyen(models.Model): ] hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii')) - raw_values = {k: values.get(k.encode('ascii'), '') for k in keys if k in values} + raw_values = {k: values.get(k, '') for k in keys if k in values} raw_values_ordered = OrderedDict(sorted(raw_values.items(), key=lambda t: t[0])) return signParams(raw_values_ordered) @@ -151,8 +151,8 @@ class AcquirerAdyen(models.Model): 'sessionValidity': tmp_date, 'resURL': urls.url_join(base_url, AdyenController._return_url), 'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False, - 'merchantSig': self._adyen_generate_merchant_sig('in', values), }) + values['merchantSig'] = self._adyen_generate_merchant_sig('in', values) return values diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index c354f2e6aa9..b10f2824733 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -127,8 +127,18 @@ class PaymentAcquirerAuthorize(models.Model): for field_name in mandatory_fields: if not data.get(field_name): error[field_name] = 'missing' - if data['cc_expiry'] and datetime.now().strftime('%y%m') > datetime.strptime(data['cc_expiry'], '%m / %y').strftime('%y%m'): - return False + if data['cc_expiry']: + # FIX we split the date into their components and check if there is two components containing only digits + # this fixes multiples crashes, if there was no space between the '/' and the components the code was crashing + # the code was also crashing if the customer was proving non digits to the date. + cc_expiry = [i.strip() for i in data['cc_expiry'].split('/')] + if len(cc_expiry) != 2 or any(not i.isdigit() for i in cc_expiry): + return False + try: + if datetime.now().strftime('%y%m') > datetime.strptime('/'.join(cc_expiry), '%m/%y').strftime('%y%m'): + return False + except ValueError: + return False return False if error else True @api.multi diff --git a/addons/payment_authorize/tests/test_authorize.py b/addons/payment_authorize/tests/test_authorize.py index f903f05b759..242fb20d265 100644 --- a/addons/payment_authorize/tests/test_authorize.py +++ b/addons/payment_authorize/tests/test_authorize.py @@ -63,6 +63,7 @@ class AuthorizeForm(AuthorizeCommon): 'x_invoice_num': 'SO004', 'x_first_name': 'Norbert', 'x_last_name': 'Buyer', + 'x_company': 'Big Company', 'x_address': 'Huge Street 2/543', 'x_city': 'Sin City', 'x_zip': '1000', diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml index dcc88b9df37..8002b5aae57 100644 --- a/addons/payment_authorize/views/payment_authorize_templates.xml +++ b/addons/payment_authorize/views/payment_authorize_templates.xml @@ -23,6 +23,7 @@ <!-- Billing Information--> <input type='hidden' name='x_first_name' t-att-value="billing_first_name"/> <input type='hidden' name='x_last_name' t-att-value="billing_last_name"/> + <input type="hidden" name="x_company" t-att-value="billing_partner_commercial_company_name"/> <input type='hidden' name='x_address' t-att-value="billing_address"/> <input type='hidden' name='x_city' t-att-value="billing_city"/> <input type='hidden' name='x_zip' t-att-value="billing_zip_code"/> diff --git a/addons/payment_ogone/models/payment.py b/addons/payment_ogone/models/payment.py index 09df4b52bc4..8771addc151 100644 --- a/addons/payment_ogone/models/payment.py +++ b/addons/payment_ogone/models/payment.py @@ -214,8 +214,8 @@ class PaymentTxOgone(models.Model): _inherit = 'payment.transaction' # ogone status _ogone_valid_tx_status = [5, 9, 8] - _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99, 81, 82] - _ogone_pending_tx_status = [46] # 3DS HTML response + _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99] + _ogone_pending_tx_status = [46, 81, 82] # 46 = 3DS HTML response _ogone_cancel_tx_status = [1] # -------------------------------------------------- @@ -283,7 +283,7 @@ class PaymentTxOgone(models.Model): return invalid_parameters def _ogone_form_validate(self, data): - if self.state in ['done', 'refunded']: + if self.state in ['done', 'refunding', 'refunded']: _logger.info('Ogone: trying to validate an already validated tx (ref %s)', self.reference) return True @@ -466,6 +466,9 @@ class PaymentTxOgone(models.Model): if self.payment_token_id: self.payment_token_id.verified = True self.execute_callback() + # if this transaction is a validation one, then we refund the money we just withdrawn + if self.type == 'validation': + self.s2s_do_refund() return True elif status in self._ogone_cancel_tx_status: self.write({ @@ -474,11 +477,13 @@ class PaymentTxOgone(models.Model): }) elif status in self._ogone_pending_tx_status: new_state = 'refunding' if self.state == 'refunding' else 'pending' - self.write({ + vals = { 'state': new_state, 'acquirer_reference': tree.get('PAYID'), - 'html_3ds': ustr(base64.b64decode(tree.HTML_ANSWER.text)), - }) + } + if status == 46: # HTML 3DS + vals['html_3ds'] = ustr(base64.b64decode(tree.HTML_ANSWER.text)) + self.write(vals) elif status in self._ogone_wait_tx_status and tries > 0: time.sleep(0.5) self.write({'acquirer_reference': tree.get('PAYID')}) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 4b24fdf00e6..f59d6f81264 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -194,7 +194,7 @@ exports.PosModel = Backbone.Model.extend({ }, },{ model: 'res.country', - fields: ['name'], + fields: ['name', 'vat_label'], loaded: function(self,countries){ self.countries = countries; self.company.country = null; @@ -2119,6 +2119,7 @@ exports.Order = Backbone.Model.extend({ company_registry: company.company_registry, contact_address: company.partner_id[1], vat: company.vat, + vat_label: company.country.vat_label, name: company.name, phone: company.phone, logo: this.pos.company_logo_base64, @@ -2263,7 +2264,9 @@ exports.Order = Backbone.Model.extend({ } }) - unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded; + if (mapped_included_taxes.length > 0) { + unit_price = line.compute_all(mapped_included_taxes, unit_price, 1, this.pos.currency.rounding, true).total_excluded; + } line.set_unit_price(unit_price); } diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py index b8ff6f8f2fd..9fb2c6b94ac 100644 --- a/addons/portal/controllers/mail.py +++ b/addons/portal/controllers/mail.py @@ -6,7 +6,7 @@ from werkzeug.exceptions import NotFound, Forbidden from odoo import http from odoo.http import request from odoo.osv import expression -from odoo.tools import consteq +from odoo.tools import consteq, plaintext2html def _has_token_access(res_model, res_id, token=''): @@ -59,6 +59,8 @@ class PortalChatter(http.Controller): def portal_chatter_post(self, res_model, res_id, message, **kw): url = request.httprequest.referrer if message: + # message is received in plaintext and saved in html + message = plaintext2html(message) _message_post_helper(res_model, int(res_id), message, **kw) url = url + "#discussion" return request.redirect(url) diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 1d5d3a79f3f..d93ca1f6d27 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -352,7 +352,7 @@ the message will be posted with the identity of the partner_id of the object --> <template id="message_thread"> - <div id="discussion" class="hidden-print o_portal_chatter" + <div id="discussion" class="hidden-print o_portal_chatter o_not_editable" t-att-data-token="token" t-att-data-res_model="object._name" t-att-data-res_id="object.id" t-att-data-pager_step="message_per_page or 10" t-att-data-allow_composer="'0' if disable_composer else '1'"> </div> </template> diff --git a/addons/pos_restaurant/static/src/xml/printbill.xml b/addons/pos_restaurant/static/src/xml/printbill.xml index 2771409e5e1..0c3f5ae7530 100644 --- a/addons/pos_restaurant/static/src/xml/printbill.xml +++ b/addons/pos_restaurant/static/src/xml/printbill.xml @@ -54,7 +54,7 @@ <div>Tel:<t t-esc='receipt.company.phone' /></div> </t> <t t-if='receipt.company.vat'> - <div><t t-esc="receipt.company.country_id.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div> + <div><t t-esc="receipt.company.vat_label or 'TIN'"/>:<t t-esc='receipt.company.vat' /></div> </t> <t t-if='receipt.company.email'> <div><t t-esc='receipt.company.email' /></div> diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 0e0a8a0c103..ffa74c60037 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -119,6 +119,7 @@ class ProductTemplate(models.Model): active = fields.Boolean('Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.") color = fields.Integer('Color Index') + is_product_variant = fields.Boolean(string='Is a product variant', compute='_compute_is_product_variant') attribute_line_ids = fields.One2many('product.attribute.line', 'product_tmpl_id', 'Product Attributes') product_variant_ids = fields.One2many('product.product', 'product_tmpl_id', 'Products', required=True) # performance: product_variant_id provides prefetching on the first product variant only @@ -236,6 +237,13 @@ class ProductTemplate(models.Model): for template in (self - unique_variants): template.weight = 0.0 + def _compute_is_product_variant(self): + for template in self: + if template._name == 'product.template': + template.is_product_variant = False + else: + template.is_product_variant = True + @api.model def _get_weight_uom_id_from_ir_config_parameter(self): """ Get the unit of measure to interpret the `weight` field. By default, we considerer diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml index 6532067bb57..006711c5d39 100644 --- a/addons/product/views/product_views.xml +++ b/addons/product/views/product_views.xml @@ -16,6 +16,7 @@ </header> <sheet> <field name='product_variant_count' invisible='1'/> + <field name='is_product_variant' invisible='1'/> <field name="id" invisible="True"/> <div class="oe_button_box" name="button_box"> <button name="toggle_active" type="object" @@ -48,8 +49,8 @@ </group> <group name="group_standard_price"> <field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/> - <label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1)]}"/> - <div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1)]}"> + <label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"/> + <div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '>', 1), ('is_product_variant', '=', False)]}"> <field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/> </div> <field name="company_id" groups="base.group_multi_company" @@ -340,7 +341,8 @@ </field> </field> <xpath expr="//div[hasclass('oe_title')]" position="inside"> - <field name="attribute_value_ids" widget="many2many_tags" groups="product.group_product_variant"/> + <field name="attribute_value_ids" widget="many2many_tags" readonly="1" + groups="product.group_product_variant"/> </xpath> </field> </record> diff --git a/addons/product_extended/models/product.py b/addons/product_extended/models/product.py index 0efd659dd79..19ef8e08492 100644 --- a/addons/product_extended/models/product.py +++ b/addons/product_extended/models/product.py @@ -37,9 +37,7 @@ class ProductProduct(models.Model): price = 0.0 result, result2 = bom.explode(self, 1) for sbom, sbom_data in result2: - if not sbom.attribute_value_ids: - # No attribute_value_ids means the bom line is not variant specific - price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty'] + price += sbom.product_id.uom_id._compute_price(sbom.product_id.standard_price, sbom.product_uom_id) * sbom_data['qty'] if bom.routing_id: # FIXME master: remove me if hasattr(self.env['mrp.workcenter'], 'costs_hour'): diff --git a/addons/product_extended/views/product_views.xml b/addons/product_extended/views/product_views.xml index 4e065e19604..2b7e4eca022 100644 --- a/addons/product_extended/views/product_views.xml +++ b/addons/product_extended/views/product_views.xml @@ -35,4 +35,24 @@ </div> </field> </record> + + <record id="product_product_view_form_easy_inherit_extended" model="ir.ui.view"> + <field name="name">product.product.view.form.easy.inherit.extended</field> + <field name="model">product.product</field> + <field name="inherit_id" ref="product.product_variant_easy_edit_view"/> + <field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/> + <field name="arch" type="xml"> + <field name="standard_price" position="after"> + <field name="cost_method" invisible='1'/> + <field name="bom_count" invisible='1'/> + <button name="compute_price" + string="Compute from BOM" type="object" + attrs="{'invisible': ['|', ('cost_method', '!=', 'standard'), ('bom_count', '=', 0)]}" + help="Compute the price of the product using products and operations of related bill of materials, for manufactured products only." + class="oe_link oe_read_only" + colspan="2"/> + </field> + </field> + </record> + </odoo> diff --git a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py index 90776a534f4..a7e484f18d4 100644 --- a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py +++ b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py @@ -33,8 +33,7 @@ class TestTimesheetHolidays(TestTimesheet): # leave dates : from next monday to next wednesday (to avoid crashing tests on weekend, when # there is no work days in working calendar) # NOTE: second and millisecond can add a working days - today = datetime.combine(datetime.today(), time.min) - self.leave_start_datetime = today.replace(hour=7) + relativedelta(weeks=0, days=1, weekday=0) + self.leave_start_datetime = datetime(2018, 2, 5, 7, 0, 0, 0) # this is monday self.leave_end_datetime = self.leave_start_datetime + relativedelta(days=3) # all company have those internal project/task (created by default) diff --git a/addons/purchase/data/mail_template_data.xml b/addons/purchase/data/mail_template_data.xml index dc51e377149..d3c075c3ff0 100644 --- a/addons/purchase/data/mail_template_data.xml +++ b/addons/purchase/data/mail_template_data.xml @@ -15,7 +15,7 @@ <field name="lang">${object.partner_id.lang}</field> <field name="body_html"><![CDATA[ <p>Dear -% if object.partner_id.is_company and object.child_ids: +% if object.partner_id.is_company and object.partner_id.child_ids: ${object.partner_id.child_ids[0].name} % else : ${object.partner_id.name} @@ -55,7 +55,7 @@ from ${object.company_id.name}. <field name="report_name">PO_${(object.name or '').replace('/','_')}</field> <field name="lang">${object.partner_id.lang}</field> <field name="body_html"><![CDATA[ -<p>Dear ${object.partner_id.name} +<p>Dear ${object.partner_id.name} % if object.partner_id.parent_id: (<i>${object.partner_id.parent_id.name}</i>) % endif diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py index fa38fa44bae..295632baffb 100644 --- a/addons/purchase/models/stock.py +++ b/addons/purchase/models/stock.py @@ -11,17 +11,6 @@ class StockPicking(models.Model): purchase_id = fields.Many2one('purchase.order', related='move_lines.purchase_line_id.order_id', string="Purchase Orders", readonly=True) - @api.model - def _create_backorder(self, backorder_moves=[]): - res = super(StockPicking, self)._create_backorder(backorder_moves) - for picking in self: - if picking.picking_type_id.code == 'incoming': - for backorder in self.search([('backorder_id', '=', picking.id)]): - backorder.message_post_with_view('mail.message_origin_link', - values={'self': backorder, 'origin': backorder.purchase_id}, - subtype_id=self.env.ref('mail.mt_note').id) - return res - class StockMove(models.Model): _inherit = 'stock.move' diff --git a/addons/sale/__init__.py b/addons/sale/__init__.py index 17753815848..abfa039b9ef 100644 --- a/addons/sale/__init__.py +++ b/addons/sale/__init__.py @@ -16,8 +16,6 @@ def uninstall_hook(cr, registry): db_registry = odoo.modules.registry.Registry.new(dbname) with api.Environment.manage(), db_registry.cursor() as cr: env = api.Environment(cr, SUPERUSER_ID, {}) - if 'account.invoice.report' in env: - env['account.invoice.report'].init() if 'crm.team' in env: recs = env['crm.team'].search([]) for rec in recs: diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index e16fe051da9..4ffddfeef20 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -575,7 +575,7 @@ class SaleOrder(models.Model): report_pages.append([]) # Append category to current report page report_pages[-1].append({ - 'name': category and category.name or 'Uncategorized', + 'name': category and category.name or _('Uncategorized'), 'subtotal': category and category.subtotal, 'pagebreak': category and category.pagebreak, 'lines': list(lines) diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index 46cc2639fa5..3c518b9a755 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -114,7 +114,7 @@ <span class="label label-danger"><i class="fa fa-fw fa-warning"></i><span class="hidden-xs"> Problem</span></span> </t> <t t-if="order.state == 'done'"> - <span class="label label-default hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span> + <span class="label label-success hidden-xs"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Done</span></span> </t> </td> <td><span t-field="order.amount_total" t-options='{"widget": "monetary", "display_currency": order.pricelist_id.currency_id}'/></td> @@ -148,7 +148,7 @@ <span class="label label-default"><i class="fa fa-fw fa-remove"/> Cancelled</span> </t> <t t-if="order.state == 'done'"> - <span class="label label-default"><i class="fa fa-fw fa-remove"/> Done</span> + <span class="label label-success"><i class="fa fa-fw fa-check" /> Done</span> </t> </h4> </div> diff --git a/addons/sale_margin/__init__.py b/addons/sale_margin/__init__.py index a8b54e88b39..0d226707e04 100644 --- a/addons/sale_margin/__init__.py +++ b/addons/sale_margin/__init__.py @@ -1,20 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from functools import partial -import openerp -from openerp import api, SUPERUSER_ID - from . import models # noqa from . import report # noqa - - -def uninstall_hook(cr, registry): - def recreate_view(dbname): - db_registry = openerp.modules.registry.Registry.new(dbname) - with api.Environment.manage(), db_registry.cursor() as cr: - env = api.Environment(cr, SUPERUSER_ID, {}) - if 'sale.report' in env: - env['sale.report'].init() - - cr.after("commit", partial(recreate_view, cr.dbname)) diff --git a/addons/sale_margin/__manifest__.py b/addons/sale_margin/__manifest__.py index 7ead9ff4c02..0da258e4932 100644 --- a/addons/sale_margin/__manifest__.py +++ b/addons/sale_margin/__manifest__.py @@ -15,5 +15,4 @@ Price and Cost Price. 'depends':['sale_management'], 'demo':['data/sale_margin_demo.xml'], 'data':['security/ir.model.access.csv','views/sale_margin_view.xml'], - 'uninstall_hook': "uninstall_hook", } diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py index ebb1ee59d9c..cef63155645 100644 --- a/addons/sale_stock/models/stock.py +++ b/addons/sale_stock/models/stock.py @@ -50,17 +50,6 @@ class StockPicking(models.Model): sale_id = fields.Many2one(related="group_id.sale_id", string="Sales Order", store=True) - @api.multi - def _create_backorder(self, backorder_moves=[]): - res = super(StockPicking, self)._create_backorder(backorder_moves) - for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'): - backorder = picking.search([('backorder_id', '=', picking.id)]) - if backorder.sale_id: - backorder.message_post_with_view( - 'mail.message_origin_link', - values={'self': backorder, 'origin': backorder.sale_id}, - subtype_id=self.env.ref('mail.mt_note').id) - return res def _log_less_quantities_than_expected(self, moves): """ Log an activity on sale order that are linked to moves. The @@ -102,4 +91,4 @@ class StockPicking(models.Model): documents = self._log_activity_get_documents(moves, 'sale_line_id', 'DOWN', _keys_in_sorted, _keys_in_groupby) self._log_activity(_render_note_exception_quantity, documents) - return super(StockPicking, self)._log_less_quantities_than_expected(moves) + return super(StockPicking, self)._log_less_quantities_than_expected(moves)
\ No newline at end of file diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml index 70fe78291f1..bc37a9bb572 100644 --- a/addons/stock/data/stock_data.xml +++ b/addons/stock/data/stock_data.xml @@ -120,7 +120,6 @@ <record id="warehouse0" model="stock.warehouse"> - <field model="res.company" name="name" search="[]" use="name"/> <field name="partner_id" ref="base.main_partner"/> <field name="code">WH</field> </record> diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py index 3dbea80703b..1bf167d5731 100644 --- a/addons/stock/models/stock_move.py +++ b/addons/stock/models/stock_move.py @@ -1068,18 +1068,8 @@ class StockMove(models.Model): return moves_todo if picking: - moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel')) - if moves_to_backorder: - backorder_picking = picking.copy({ - 'name': '/', - 'move_lines': [], - 'move_line_ids': [], - 'backorder_id': picking.id - }) - picking.message_post(body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (backorder_picking.id, backorder_picking.name)) - moves_to_backorder.write({'picking_id': backorder_picking.id}) - moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id}) - moves_to_backorder._action_assign() + picking._create_backorder() + return moves_todo def unlink(self): diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py index 1147c9e6a96..f09d9ca1160 100644 --- a/addons/stock/models/stock_picking.py +++ b/addons/stock/models/stock_picking.py @@ -378,9 +378,9 @@ class Picking(models.Model): @api.depends('move_lines.date_expected') def _compute_scheduled_date(self): if self.move_type == 'direct': - self.scheduled_date = min(self.move_lines.mapped('date_expected') or [False]) + self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) else: - self.scheduled_date = max(self.move_lines.mapped('date_expected') or [False]) + self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) @api.one def _set_scheduled_date(self): @@ -813,31 +813,25 @@ class Picking(models.Model): @api.multi def _create_backorder(self, backorder_moves=[]): - """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines. + """ Move all non-done lines into a new backorder picking. """ - # TDE note: o2o conversion, todo multi backorders = self.env['stock.picking'] for picking in self: - backorder_moves = backorder_moves or picking.move_lines - if self._context.get('do_only_split'): - not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', [])) - else: - not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel')) - if not not_done_bo_moves: - continue - backorder_picking = picking.copy({ - 'name': '/', - 'move_lines': [], - 'move_line_ids': [], - 'backorder_id': picking.id - }) - picking.message_post(body=_("Back order <em>%s</em> <b>created</b>.") % (backorder_picking.name)) - not_done_bo_moves.write({'picking_id': backorder_picking.id}) - if not picking.date_done: - picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}) - backorder_picking.action_confirm() - backorder_picking.action_assign() - backorders |= backorder_picking + moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel')) + if moves_to_backorder: + backorder_picking = picking.copy({ + 'name': '/', + 'move_lines': [], + 'move_line_ids': [], + 'backorder_id': picking.id + }) + picking.message_post( + body=_('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % ( + backorder_picking.id, backorder_picking.name)) + moves_to_backorder.write({'picking_id': backorder_picking.id}) + moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id}) + backorder_picking.action_assign() + backorders |= backorder_picking return backorders def _log_activity_get_documents(self, orig_obj_changes, stream_field, stream, sorted_method=False, groupby_method=False): diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py index 73368db6ad4..5711400941e 100644 --- a/addons/stock/models/stock_warehouse.py +++ b/addons/stock/models/stock_warehouse.py @@ -21,7 +21,7 @@ class Warehouse(models.Model): # namedtuple used in helper methods generating values for routes Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type']) - name = fields.Char('Warehouse Name', index=True, required=True) + name = fields.Char('Warehouse Name', index=True, required=True, default=lambda self: self.env['res.company']._company_default_get('stock.inventory').name) active = fields.Boolean('Active', default=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'), @@ -684,11 +684,11 @@ class Warehouse(models.Model): def _get_sequence_values(self): return { - 'in_type_id': {'name': self.name + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5}, - 'out_type_id': {'name': self.name + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5}, - 'pack_type_id': {'name': self.name + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5}, - 'pick_type_id': {'name': self.name + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5}, - 'int_type_id': {'name': self.name + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5}, + 'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5}, + 'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5}, + 'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5}, + 'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5}, + 'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5}, } def _format_rulename(self, from_loc, dest_loc, suffix): diff --git a/addons/stock/report/report_deliveryslip.xml b/addons/stock/report/report_deliveryslip.xml index 60627d9f596..a4020c61b99 100644 --- a/addons/stock/report/report_deliveryslip.xml +++ b/addons/stock/report/report_deliveryslip.xml @@ -53,20 +53,19 @@ <thead> <tr> <th><strong>Product</strong></th> - <th><strong>Ordered Quantity</strong></th> + <th><strong>Quantity</strong></th> </tr> </thead> <tbody> <tr t-foreach="o.move_lines" t-as="move"> <td><span t-field="move.product_id"/></td> <td> - <span t-field="move.ordered_qty"/> + <span t-field="move.quantity_done"/> <span t-field="move.product_uom"/> </td> </tr> </tbody> </table> - <t t-set="backorder" t-value="False"/> <table class="table table-condensed mt48" t-if="o.move_line_ids"> <t t-set="has_serial_number" t-value="o.move_line_ids.mapped('lot_id')" groups="stock.group_production_lot"/> <thead> @@ -75,10 +74,7 @@ <th name="lot_serial" t-if="has_serial_number"> Lot/Serial Number </th> - <th class="text-center"><strong>Ordered Quantity</strong></th> - <th t-if="any([move_line.state == 'done' for move_line in o.move_line_ids])" class="text-right"> - <strong>Delivered Quantity</strong> - </th> + <th class="text-center"><strong>Quantity</strong></th> </tr> </thead> <tbody> @@ -112,13 +108,6 @@ </td> </t> <td class="text-center"> - <span t-field="move_line.ordered_qty"/> - <span t-field="move_line.product_uom_id"/> - </td> - <td class="text-right" t-if="move_line.state == 'done'"> - <t t-if="move_line.ordered_qty != move_line.qty_done"> - <t t-set="backorder" t-value="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"/> - </t> <span t-field="move_line.qty_done"/> <span t-field="move_line.product_uom_id"/> </td> @@ -129,7 +118,7 @@ This shipment is a backorder of <t t-esc="o.backorder_id.name"/>. </p> <p> - <t t-if="backorder"> + <t t-if="o.search([('backorder_id', '=', o.id), ('state', 'not in', ['cancel', 'done'])])"> All items couldn't be shipped, the remaining ones will be shipped as soon as they become available. </t> </p> diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml index 9c5c7552d4b..cbb472a946f 100644 --- a/addons/stock/report/report_stockpicking_operations.xml +++ b/addons/stock/report/report_stockpicking_operations.xml @@ -127,13 +127,13 @@ <span t-field="ml.lot_id"/> <span t-if="not ml.lot_id" t-field="ml.lot_name"/> </div> - <div class="col-xs-8"> - <span t-field="ml.qty_done"/> - <t t-if="o.state!='done'"> - / <span t-field="ml.product_uom_qty"/> - </t> - </div> </t> + <div class="col-xs-8"> + <span t-field="ml.qty_done"/> + <t t-if="o.state!='done'"> + / <span t-field="ml.product_uom_qty"/> + </t> + </div> </td> <td width="30%" align="right"> <div> diff --git a/addons/stock/views/stock_move_views.xml b/addons/stock/views/stock_move_views.xml index 1a012255ae1..cd945d3bfe0 100644 --- a/addons/stock/views/stock_move_views.xml +++ b/addons/stock/views/stock_move_views.xml @@ -73,7 +73,6 @@ <kanban class="o_kanban_mobile"> <field name="name"/> <field name="product_id"/> - <field name="date"/> <field name="priority"/> <field name="state"/> <templates> diff --git a/addons/stock/wizard/stock_change_product_qty.py b/addons/stock/wizard/stock_change_product_qty.py index b29e4080e3b..1528d134079 100644 --- a/addons/stock/wizard/stock_change_product_qty.py +++ b/addons/stock/wizard/stock_change_product_qty.py @@ -94,7 +94,7 @@ class ProductChangeQuantity(models.TransientModel): else: inventory_filter = 'none' inventory = Inventory.create({ - 'name': _('INV: %s') % tools.ustr(wizard.product_id.name), + 'name': _('INV: %s') % tools.ustr(wizard.product_id.display_name), 'filter': inventory_filter, 'product_id': wizard.product_id.id, 'location_id': wizard.location_id.id, diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index a680bd4d8c1..a128f5a6942 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -5,7 +5,7 @@ from collections import defaultdict from odoo import api, fields, models, _ from odoo.exceptions import UserError -from odoo.tools import float_compare, float_round, pycompat +from odoo.tools import float_compare, float_round, float_is_zero, pycompat import logging _logger = logging.getLogger(__name__) @@ -353,8 +353,11 @@ class StockMove(models.Model): std_price_update = {} for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move.product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id] + rounding = move.product_id.uom_id.rounding - if product_tot_qty_available == 0: + if float_is_zero(product_tot_qty_available, precision_rounding=rounding): + new_std_price = move._get_price_unit() + elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py index a1eb43c76c6..49ff9488376 100644 --- a/addons/stock_landed_costs/models/stock_landed_cost.py +++ b/addons/stock_landed_costs/models/stock_landed_cost.py @@ -21,7 +21,7 @@ class LandedCost(models.Model): _inherit = 'mail.thread' name = fields.Char( - 'Name', default=lambda self: self.env['ir.sequence'].next_by_code('stock.landed.cost'), + 'Name', default=lambda self: _('New'), copy=False, readonly=True, track_visibility='always') date = fields.Date( 'Date', default=fields.Date.context_today, @@ -57,6 +57,12 @@ class LandedCost(models.Model): def _compute_total_amount(self): self.amount_total = sum(line.price_unit for line in self.cost_lines) + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost') + return super(LandedCost, self).create(vals) + @api.multi def unlink(self): self.button_cancel() diff --git a/addons/survey/models/survey.py b/addons/survey/models/survey.py index 60c387fd395..ab7ac5daa7c 100644 --- a/addons/survey/models/survey.py +++ b/addons/survey/models/survey.py @@ -227,9 +227,8 @@ class Survey(models.Model): # Calculate and return statistics for choice if question.type in ['simple_choice', 'multiple_choice']: - answers = {} comments = [] - [answers.update({label.id: {'text': label.value, 'count': 0, 'answer_id': label.id}}) for label in question.labels_ids] + answers = OrderedDict((label.id, {'text': label.value, 'count': 0, 'answer_id': label.id}) for label in question.labels_ids) for input_line in question.user_input_line_ids: if input_line.answer_type == 'suggestion' and answers.get(input_line.value_suggested.id) and (not(current_filters) or input_line.user_input_id.id in current_filters): answers[input_line.value_suggested.id]['count'] += 1 diff --git a/addons/web/static/lib/ace/ace.odoo-custom.js b/addons/web/static/lib/ace/ace.odoo-custom.js index d8f84294316..49af24f5074 100644 --- a/addons/web/static/lib/ace/ace.odoo-custom.js +++ b/addons/web/static/lib/ace/ace.odoo-custom.js @@ -1173,15 +1173,16 @@ define("ace/lib/regexp", ["require", "exports", "module"], function(e, t, n) { if (!t.onCompositionEnd || t.$readOnly) return; var r = h; h = !1; - var i = setTimeout(function() { - i = null; + var i2 = setTimeout(function() { + i2 = null; var e = n.value.replace(/\x01/g, ""); if (h) return; e == r.lastValue ? w() : !r.lastValue && e && (w(), A(e)) }); k = function(n) { - return i && clearTimeout(i), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i && t.undo(), n) - }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range) + return i2 && clearTimeout(i2), n = n.replace(/\x01/g, ""), n == r.lastValue ? "" : (r.lastValue && i2 && t.undo(), n) + }, t.onCompositionEnd(), t.removeListener("mousedown", F), e.type == "compositionend" && r.range && t.selection.setRange(r.range), + i.isChrome && i.isChrome >= 53 && O() }, I = o.delayedCall(j, 50); r.addListener(n, "compositionstart", B), i.isGecko ? r.addListener(n, "text", function() { diff --git a/addons/web/static/src/js/chrome/menu.js b/addons/web/static/src/js/chrome/menu.js index 1e5e2922186..1e253ba54f7 100644 --- a/addons/web/static/src/js/chrome/menu.js +++ b/addons/web/static/src/js/chrome/menu.js @@ -147,15 +147,22 @@ var Menu = Widget.extend({ }); }, /** - * Call open_menu with the first menu_item matching an action_id + * Call open_menu on a menu_item that matches the action_id + * + * If `menuID` is a match on this action, open this menu_item. + * Otherwise open the first menu_item that matches the action_id. * * @param {Number} id the action_id to match + * @param {Number} [menuID] a menu ID that may match with provided action */ - open_action: function (id) { + open_action: function (id, menuID) { var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]'); - var menu_id = $menu.data('menu'); - if (menu_id) { - this.open_menu(menu_id); + if (!(menuID && $menu.filter("[data-menu='" + menuID + "']").length)) { + // menuID doesn't match action, so pick first menu_item + menuID = $menu.data('menu'); + } + if (menuID) { + this.open_menu(menuID); } }, /** diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js index 76aadf49463..db440cd742f 100644 --- a/addons/web/static/src/js/chrome/web_client.js +++ b/addons/web/static/src/js/chrome/web_client.js @@ -135,7 +135,7 @@ return AbstractWebClient.extend({ self.action_manager.loadState(state, !!self._current_state).then(function () { var action = self.action_manager.getCurrentAction(); if (action) { - self.menu.open_action(action.id); + self.menu.open_action(action.id, state.menu_id); } }); } @@ -156,7 +156,7 @@ return AbstractWebClient.extend({ var completed = $.Deferred(); self.action_manager.doAction(result, { clear_breadcrumbs: true, - action_menu_id: self.menu.current_menu, + action_menu_id: options.id, }).fail(function() { self.menu.open_menu(options.previous_menu_id); }).always(function() { diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index 75aa1acd164..4b46db8a8ff 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2007,8 +2007,9 @@ var FieldToggleBoolean = AbstractField.extend({ * @private */ _render: function () { - var className = this.value ? 'o_toggle_button_success' : 'text-muted'; - this.$('i').addClass('fa fa-circle ' + className); + this.$('i') + .toggleClass('o_toggle_button_success', !!this.value) + .toggleClass('text-muted', !this.value); var title = this.value ? this.attrs.options.active : this.attrs.options.inactive; this.$el.attr('title', title); }, diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index f81485a6b08..d5add00e38c 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -878,10 +878,15 @@ var FieldX2Many = AbstractField.extend({ this.pager = new Pager(this, this.value.count, this.value.offset + 1, this.value.limit, { single_page_hidden: true, withAccessKey: false, + validate: function () { + var isList = self.view.arch.tag === 'tree'; + // TODO: we should have some common method in the basic renderer... + return isList ? self.renderer.unselectRow() : $.when(); + }, }); this.pager.on('pager_changed', this, function (new_state) { - this.trigger_up('load', { - id: this.value.id, + self.trigger_up('load', { + id: self.value.id, limit: new_state.limit, offset: new_state.current_min - 1, on_success: function (value) { @@ -930,6 +935,7 @@ var FieldX2Many = AbstractField.extend({ * did not agree to discard. */ _saveLine: function (recordID) { + var self = this; var def = $.Deferred(); var fieldNames = this.renderer.canBeSaved(recordID); if (fieldNames.length) { @@ -941,7 +947,9 @@ var FieldX2Many = AbstractField.extend({ } else { this.renderer.setRowMode(recordID, 'readonly').done(def.resolve.bind(def)); } - return def; + return def.then(function () { + self.pager.updateState({ size: self.value.count }); + }); }, /** * Parses the 'columnInvisibleFields' attribute to search for the domains @@ -1178,7 +1186,20 @@ var FieldOne2Many = FieldX2Many.extend({ return this._super.apply(this, arguments).then(function () { if (ev && ev.target === self && ev.data.changes && self.view.arch.tag === 'tree') { if (ev.data.changes[self.name].operation === 'CREATE') { - var index = self.editable === 'top' ? 0 : self.value.data.length - 1; + var index = 0; + if (self.editable !== 'top') { + index = self.value.data.length - 1; + // we consider that a new record, created in the bottom, + // does not count as a record worth mentioning in the + // pager, at least not until the line has been saved. + // We prevent the pager from increasing its size, which + // means that the pager is not displayed when we just + // reach the limit. For example, if limit is 3, if we + // have 3 records, and we click on add, we will see the + // 4 records on the same page, but we do not want a + // pager. + self.pager.updateState({ size: self.value.count - 1}); + } var newID = self.value.data[index].id; self.renderer.editRecord(newID); } diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index 21259671505..c6afa9c4921 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -359,6 +359,11 @@ var BasicModel = AbstractModel.extend({ elem._changes = null; elem._isDirty = false; } + elem.offset = 0; + if (elem.tempLimitIncrement) { + elem.limit -= elem.tempLimitIncrement; + delete elem.tempLimitIncrement; + } }); }, /** @@ -809,6 +814,7 @@ var BasicModel = AbstractModel.extend({ this.discardChanges(id, {rollback: false}); } } else if (element._changes) { + delete element.tempLimitIncrement; _.each(element._changes, function (change) { delete change.isNew; }); @@ -1640,6 +1646,12 @@ var BasicModel = AbstractModel.extend({ // handle multiple add: command[2] may be a dict of values (1 // record added) or an array of dict of values var data = _.isArray(command.ids) ? command.ids : [command.ids]; + + // Ensure the local data repository (list) boundaries can handle incoming records (data) + if (data.length + list.res_ids.length > list.limit) { + list.limit = data.length + list.res_ids.length; + } + var list_records = {}; _.each(data, function (d) { rec = self._makeDataPoint({ @@ -1684,6 +1696,10 @@ var BasicModel = AbstractModel.extend({ case 'CREATE': var options = {position: command.position}; def = this._addX2ManyDefaultRecord(list, options).then(function (id) { + if (command.position === 'bottom' && list.orderedResIDs.length >= list.limit) { + list.tempLimitIncrement = (list.tempLimitIncrement || 0) + 1; + list.limit += 1; + } // FIXME: hack for lunch widget, which does useless default_get and onchange if (command.data) { return self._applyChange(id, command.data); @@ -3824,7 +3840,8 @@ var BasicModel = AbstractModel.extend({ // generate the current count and res_ids list by applying the changes var currentCount = list.count; var currentResIDs = list.res_ids; - var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount; + var effectiveLimit = (list.limit || 0) - (list.tempLimitIncrement || 0); + var upperBound = effectiveLimit ? Math.min(list.offset + effectiveLimit, currentCount) : currentCount; var fieldNames = list.getFieldNames(); for (var i = list.offset; i < upperBound; i++) { var resId = currentResIDs[i]; diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js index b289663fb19..b328b88f5e5 100644 --- a/addons/web/static/src/js/views/form/form_renderer.js +++ b/addons/web/static/src/js/views/form/form_renderer.js @@ -610,6 +610,10 @@ var FormRenderer = BasicRenderer.extend({ var $result = $('<div/>', {class: 'o_group'}); var colSize = Math.max(1, Math.round(12 / (parseInt(node.attrs.col, 10) || 2))); + if (node.attrs.string) { + var $sep = $('<div/>', {class: 'o_horizontal_separator'}).text(node.attrs.string); + $result.append($sep); + } $result.append(_.map(node.children, function (child) { if (child.tag === 'newline') { return $('<br/>'); diff --git a/addons/web/static/src/js/views/view_dialogs.js b/addons/web/static/src/js/views/view_dialogs.js index 1b74068d44d..d4ae709008c 100644 --- a/addons/web/static/src/js/views/view_dialogs.js +++ b/addons/web/static/src/js/views/view_dialogs.js @@ -252,6 +252,7 @@ var SelectCreateDialog = ViewDialog.extend({ } }, selection_changed: function (event) { + event.stopPropagation(); this.$footer.find(".o_select_button").prop('disabled', !event.data.selection.length); }, search: function (event) { diff --git a/addons/web/static/src/less/kanban_view.less b/addons/web/static/src/less/kanban_view.less index f53a5d185c3..34999256fa5 100644 --- a/addons/web/static/src/less/kanban_view.less +++ b/addons/web/static/src/less/kanban_view.less @@ -230,7 +230,7 @@ height: 20px; } .oe_kanban_bottom_left { - .o-flex(1, 0, auto); + .o-flex(1, 1, auto); > * { margin-right: 6px; @@ -243,7 +243,7 @@ } } .oe_kanban_bottom_right{ - .o-flex(0, 0, auto); + .o-flex(0, 1, auto); .oe_kanban_avatar { width: 20px; diff --git a/addons/web/static/src/less/kanban_view_mobile.less b/addons/web/static/src/less/kanban_view_mobile.less index 72ba087e0c2..d09b6968925 100644 --- a/addons/web/static/src/less/kanban_view_mobile.less +++ b/addons/web/static/src/less/kanban_view_mobile.less @@ -29,7 +29,8 @@ } } - &.o_kanban_small_column .o_kanban_group:not(.o_column_folded) { + // [class] to get same specificity as elsewhere (kanban_view.less) + &[class] .o_kanban_group:not(.o_column_folded) { .o-position-absolute(@top: @o-kanban-mobile-tabs-height, @left: 0, @bottom: 0); overflow-x: scroll; width: 100%; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index fd263a07eb6..3ef2a213398 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -209,7 +209,7 @@ <t t-name="WebClient.DebugManager"> <li class="o_debug_manager"> - <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown"> + <a href="#" title="Open Developer Tools" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-bug"/> </a> <ul class="dropdown-menu o_debug_dropdown" role="menu"/> @@ -325,12 +325,12 @@ <t t-name="Sidebar"> <t t-foreach="widget.sections" t-as="section"> <div class="btn-group o_dropdown"> - <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button t-if="section.name != 'buttons'" class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <t t-if="section.name == 'files'" t-raw="widget.items[section.name].length || ''"/> <t t-esc="section.label"/> <span class="caret"/> </button> <t t-if="section.name == 'buttons'" t-foreach="widget.items[section.name]" t-as="item" t-att-class="item.classname"> - <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url" target="_blank"> + <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'" target="_blank"> <t t-raw="item.label"/> </a> </t> @@ -352,7 +352,7 @@ <t t-raw="item.write_uid[1]"/> <t t-esc="item.write_date_string"/> </t> </t> - <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url"> + <a t-att-title="item.title or None" t-att-data-section="section.name" t-att-data-index="item_index" t-att-href="item.url or '#'"> <t t-raw="item.label"/> <span t-if="section.name == 'files' and widget.options.editable and !item.callback" class="fa fa-trash-o o_sidebar_delete_attachment" t-att-data-id="item.id" title="Delete this attachment"/> </a> @@ -469,7 +469,7 @@ </form> <t t-name="GraphView.buttons"> <div class="btn-group btn-group-sm" role="group"> - <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> Measures <span class="caret"/> </button> <ul class="dropdown-menu o_graph_measures_list"> @@ -498,7 +498,7 @@ </div> <t t-name="PivotView.buttons"> <div class="btn-group btn-group-sm" role="group"> - <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="btn btn-primary btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> Measures <span class="caret"/> </button> <ul class="dropdown-menu o_pivot_measures_list"> @@ -852,7 +852,7 @@ <t t-call="FieldStatus.content.button"/> </li> </ul> - <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown">More <span class="caret"/></button> + <button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button> </t> <t t-foreach="selection_unfolded.reverse()" t-as="i"> <t t-call="FieldStatus.content.button"/> @@ -985,7 +985,7 @@ <ul class="dropdown-menu o_searchview_autocomplete" role="menu"></ul> </t> <div t-name="SearchView.FilterMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-filter"/> Filters <span class="caret"/> </button> <ul class="dropdown-menu o_filters_menu" role="menu"> @@ -1097,7 +1097,7 @@ </select> </t> <div t-name="SearchView.GroupByMenu" class="btn-group hidden-xs o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-bars"/> Group By <span class="caret"/> </button> <ul class="dropdown-menu o_group_by_menu" role="menu"> @@ -1118,7 +1118,7 @@ </li> </t> <div t-name="SearchView.FavoriteMenu" class="btn-group o_dropdown"> - <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown"> + <button class="o_dropdown_toggler_btn btn btn-sm dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="fa fa-star"/> Favorites <span class="caret"/> </button> <ul class="dropdown-menu o_favorites_menu" role="menu"> @@ -1247,8 +1247,8 @@ <span class="o_stat_text"><t t-esc="text"/></span> </t> <t t-name="toggle_button"> - <button type="button" class="o_icon_button" t-att-title="widget.string"> - <i t-attf-class="fa #{widget.icon}" t-att-title="widget.string"/> + <button type="button" class="o_icon_button" t-att-title="widget.string" t-att-aria-label="widget.string"> + <i class="fa fa-circle" t-att-title="widget.string"/> </button> </t> @@ -1262,8 +1262,8 @@ <t t-set="att_prev" t-value="{'accesskey': 'p'}" /> <t t-set="att_next" t-value="{'accesskey': 'n'}" /> </t> - <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev"/> - <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next"/> + <button type="button" class="fa fa-chevron-left btn btn-icon o_pager_previous" t-att="att_prev" aria-label="Previous"/> + <button type="button" class="fa fa-chevron-right btn btn-icon o_pager_next" t-att="att_next" aria-label="Next"/> </span> </div> diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js index 8488076b3f0..a60e3419bdb 100644 --- a/addons/web/static/tests/fields/basic_fields_tests.js +++ b/addons/web/static/tests/fields/basic_fields_tests.js @@ -387,7 +387,6 @@ QUnit.module('basic_fields', { '<field name="bar" widget="toggle_button" ' + 'options="{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}"/>' + '</tree>', - res_id: 2, }); assert.strictEqual(list.$('button i.fa.fa-circle.o_toggle_button_success').length, 4, @@ -411,6 +410,81 @@ QUnit.module('basic_fields', { list.destroy(); }); + QUnit.test('toggle_button in form view (edit mode)', function (assert) { + assert.expect(6); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + form.$('.o_field_widget[name=bar]').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps([]); + + // save + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should still be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); + + QUnit.test('toggle_button in form view (readonly mode)', function (assert) { + assert.expect(4); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form>' + + '<field name="bar" widget="toggle_button" ' + + 'options="{\'active\': \'Active value\', \'inactive\': \'Inactive value\'}"/>' + + '</form>', + mockRPC: function (route, args) { + if (args.method === 'write') { + assert.step('write'); + } + return this._super.apply(this, arguments); + }, + res_id: 2, + }); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.o_toggle_button_success:not(.text-muted)').length, + 1, "should be green"); + + // click on the button to toggle the value + form.$('.o_field_widget[name=bar]').click(); + + assert.strictEqual(form.$('.o_field_widget[name=bar] i.text-muted:not(.o_toggle_button_success)').length, + 1, "should be gray"); + assert.verifySteps(['write']); + + form.destroy(); + }); QUnit.module('FieldFloat'); diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index 9b8df65ab29..dbdde1cb566 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -2078,7 +2078,7 @@ QUnit.module('relational_fields', { data: this.data, arch: '<form>' + '<field name="p">' + - '<tree editable="bttom">' + + '<tree editable="bottom">' + '<field name="trululu"/>' + '</tree>' + '</field>' + @@ -2173,6 +2173,92 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('many2many list add *many* records, remove, re-add', function (assert) { + assert.expect(5); + + this.data.partner.fields.timmy.domain = [['color', '=', 2]]; + this.data.partner.fields.timmy.onChange = true; + this.data.partner_type.fields.product_ids = {string: "Product", type: "many2many", relation: 'product'}; + + for (var i=0; i<50; i++) { + var new_record_partner_type = {id: 100+i, display_name: "batch" + i, color: 2}; + this.data.partner_type.records.push(new_record_partner_type); + } + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="timmy" widget="many2many">' + + '<tree>' + + '<field name="display_name"/>' + + '<field name="product_ids" widget="many2many_tags"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'partner_type,false,list': '<tree><field name="display_name"/></tree>', + 'partner_type,false,search': '<search><field name="display_name"/><field name="color"/></search>', + }, + mockRPC: function (route, args) { + if (args.method === 'get_formview_id') { + assert.deepEqual(args.args[0], [1], "should call get_formview_id with correct id"); + return $.when(false); + } + return this._super(route, args); + }, + }); + + // First round: add 51 records in batch + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + var $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('thead input[type=checkbox]').click(); //select all the records we created in batch + 'gold' + + $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); + + var m2m_records = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_data_cell:not(.o_many2many_tags_cell)'); + assert.equal(m2m_records.length, 51, + 'We should have added all the records present in the search view to the m2m field'); // the 50 in batch + 'gold' + + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_save').click(); + + // Secound round: remove one record + form.$buttons.find('.btn.btn-primary.btn-sm.o_form_button_edit').click(); + var trash_buttons = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_list_record_delete'); + + trash_buttons.first().click(); + + var pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); + assert.equal(pager_limit.text(), '50', + 'We should have 50 records in the m2m field'); + + // Third round: re-add 1 records + form.$('.o_field_x2many_list_row_add a').click(); + + $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('thead input[type=checkbox]').click(); + + $modal.find('.btn.btn-sm.btn-primary.o_select_button').click(); + + pager_limit = form.$('.o_field_many2many.o_field_widget.o_field_x2many.o_field_x2many_list .o_pager_limit'); + assert.equal(pager_limit.text(), '51', + 'We should have 51 records in the m2m field'); + + form.destroy(); + }); + QUnit.module('FieldOne2Many'); QUnit.test('one2many basic properties', function (assert) { @@ -2784,7 +2870,7 @@ QUnit.module('relational_fields', { }); QUnit.test('onchange followed by edition on the second page', function (assert) { - assert.expect(20); + assert.expect(12); var ids = []; for (var i=1; i<85; i++) { @@ -2854,10 +2940,32 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#39))').index(), 2, "should display '#39' at the third line"); form.destroy(); + }); + + QUnit.test('onchange followed by edition on the second page (part 2)', function (assert) { + assert.expect(8); + + var ids = []; + for (var i=1; i<85; i++) { + var id = 10 + i; + ids.push(id); + this.data.turtle.records.push({ + id: id, + turtle_int: id/3|0, + turtle_foo: "#" + i, + }); + } + ids.splice(41, 0, 1, 2, 3); + this.data.partner.records[0].turtles = ids; + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [[5]].concat(obj.turtles); + }, + }; // bottom order - form = createView({ + var form = createView({ View: FormView, model: 'partner', data: this.data, @@ -2889,16 +2997,16 @@ QUnit.module('relational_fields', { form.$('.o_field_x2many_list_row_add a').click(); - assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); + assert.strictEqual(form.$('.o_data_row').length, 41, "should display 41 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); - assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position"); + assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 40, "should display the create line at the last position"); form.$('.o_data_row input:first').val('value 3').trigger('input'); form.$('.o_field_x2many_list_row_add a').click(); - assert.strictEqual(form.$('.o_data_row').length, 40, "should display 39 records and the create line"); + assert.strictEqual(form.$('.o_data_row').length, 42, "should display 42 records and the create line"); assert.strictEqual(form.$('.o_data_row:has(.o_data_cell:contains(#76))').index(), 38, "should display '#76' at the penultimate line"); - assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 39, "should display the create line at the last position"); + assert.strictEqual(form.$('.o_data_row:has(.o_field_char)').index(), 41, "should display the create line at the last position"); form.destroy(); }); @@ -3979,6 +4087,37 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable one2many list, pager is updated', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, then click in form view to confirm it + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$el.click(); + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 5', + "pager should display the correct total"); + form.destroy(); + }); + + QUnit.test('one2many list (non editable): edition', function (assert) { assert.expect(12); @@ -4287,6 +4426,185 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editable one2many list, adding line when only one page', function (assert) { + assert.expect(1); + + this.data.partner.records[0].turtles=[1,2,3]; + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, to reach the page size limit + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // the record currently being added should not count in the pager + assert.ok(!form.$('.o_cp_pager').is(':visible'), + "pager should not be visible"); + form.destroy(); + }); + + QUnit.test('editable one2many list, adding line, then discarding', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record, then discard + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$buttons.find('.o_form_button_cancel').click(); + + // confirm the discard operation + $('.modal .modal-footer .btn-primary').click(); // click on confirm + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should still be visible"); + form.destroy(); + }); + + QUnit.test('editable one2many list, required field and pager', function (assert) { + assert.expect(1); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.turtle.fields.turtle_foo.required = true; + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a (empty) record + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // go on next page. The new record is not valid and should be discarded + form.$('.o_pager_next').click(); + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "should have 1 data rows"); + + form.destroy(); + }); + + QUnit.test('editable one2many list, required field, pager and confirm discard', function (assert) { + assert.expect(3); + + this.data.turtle.records.push({id:4, turtle_foo: 'stephen hawking'}); + this.data.turtle.fields.turtle_foo.required = true; + this.data.partner.records[0].turtles = [1,2,3,4]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '<field name="turtle_int"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a record with a dirty state, but not valid + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('input[name="turtle_int"]').val(4321).trigger('input'); + + // go to next page. The new record is not valid, but dirty. we should + // see a confirm dialog + form.$('.o_pager_next').click(); + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should still display the correct total"); + + // click on cancel + $('.modal .modal-footer .btn-default').click(); // click on cancel + + assert.strictEqual(form.$('.o_cp_pager').text().trim(), '1-3 / 4', + "pager should again display the correct total"); + assert.strictEqual(form.$('.o_field_one2many input.o_field_invalid').length, 1, + "there should be an invalid input in the one2many"); + form.destroy(); + }); + + QUnit.test('editable one2many list, adding, discarding, and pager', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1]; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + // add a 4 records record (to make the pager appear) + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_x2many_list_row_add a').click(); + + // go on next page + form.$('.o_pager_next').click(); + + // discard + form.$buttons.find('.o_form_button_cancel').click(); + $('.modal .modal-footer .btn-primary').click(); + + assert.strictEqual(form.$('tr.o_data_row').length, 1, + "should have 1 data row"); + assert.ok(!form.$('.o_cp_pager').is(':visible'), + "pager should not be visible"); + + form.destroy(); + }); + QUnit.test('unselecting a line with missing required data', function (assert) { assert.expect(5); @@ -7602,6 +7920,42 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('editing tabbed one2many (editable=bottom), again...', function (assert) { + assert.expect(1); + + this.data.partner.records[0].turtles = []; + for (var i = 0; i < 9; i++) { + var id = 100 + i; + this.data.turtle.records.push({id: id, turtle_foo: 'turtle' + (id-99)}); + this.data.partner.records[0].turtles.push(id); + } + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree editable="bottom" limit="3">' + + '<field name="turtle_foo"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + }); + + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_data_row input[name="turtle_foo"]').val('rainbow dash').trigger('input'); + form.$('.o_x2m_control_panel .o_pager_next').click(); + form.$('.o_x2m_control_panel .o_pager_next').click(); + + assert.strictEqual(form.$('tr.o_data_row').length, 3, + "should have 3 data rows on the current page"); + form.destroy(); + }); + QUnit.test('editing tabbed one2many (editable=top)', function (assert) { assert.expect(15); @@ -8217,6 +8571,49 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('add a new line after limit is reached should behave nicely', function (assert) { + assert.expect(2); + + this.data.partner.records[0].turtles = [1,2,3]; + + this.data.partner.onchanges = { + turtles: function (obj) { + obj.turtles = [ + [5], + [1, 1, {turtle_foo: "yop"}], + [1, 2, {turtle_foo: "blip"}], + [1, 3, {turtle_foo: "kawa"}], + [0, obj.turtles[3][2], {turtle_foo: "abc"}], + ]; + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'<form string="Partners">' + + '<field name="turtles">' + + '<tree limit="3" editable="bottom">' + + '<field name="turtle_foo" required="1"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + form.$('.o_field_x2many_list_row_add a').click(); + assert.strictEqual(form.$('.o_data_row').length, 4, 'should have 4 data rows'); + form.$('.o_input[name="turtle_foo"]').val('a').trigger('input'); + assert.strictEqual(form.$('.o_data_row').length, 4, + 'should still have 4 data rows (the limit is increased to 4)'); + + form.destroy(); + }); + QUnit.test('onchange in a one2Many, but not in main record, with non inline view', function (assert) { assert.expect(6); diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js index e9b652659ff..192efa338bd 100644 --- a/addons/web/static/tests/views/form_tests.js +++ b/addons/web/static/tests/views/form_tests.js @@ -5202,6 +5202,46 @@ QUnit.module('Views', { form.destroy(); }); + QUnit.test('outer and inner groups string attribute', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch: '<form string="Partners">' + + '<sheet>' + + '<group string="parent group" class="parent_group">' + + '<group string="child group 1" class="group_1">' + + '<field name="bar"/>' + + '</group>' + + '<group string="child group 2" class="group_2">' + + '<field name="bar"/>' + + '</group>' + + '</group>' + + '</sheet>' + + '</form>', + res_id: 1, + }); + + var $parentGroup = form.$('.parent_group'); + var $group1 = form.$('.group_1'); + var $group2 = form.$('.group_2'); + + assert.strictEqual(form.$('table.o_inner_group').length, 2, + "should contain two inner groups"); + assert.strictEqual($group1.find('.o_horizontal_separator').length, 1, + "inner group should contain one string separator"); + assert.strictEqual($group1.find('.o_horizontal_separator:contains(child group 1)').length, 1, + "first inner group should contain 'child group 1' string"); + assert.strictEqual($group2.find('.o_horizontal_separator:contains(child group 2)').length, 1, + "second inner group should contain 'child group 2' string"); + assert.strictEqual($parentGroup.find('> div.o_horizontal_separator:contains(parent group)').length, 1, + "outer group should contain 'parent group' string"); + + form.destroy(); + }); + QUnit.test('form group with newline tag inside', function (assert) { assert.expect(6); diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js index 6ec4f50ab4b..39484795f5a 100644 --- a/addons/web/static/tests/views/view_dialogs_tests.js +++ b/addons/web/static/tests/views/view_dialogs_tests.js @@ -5,6 +5,9 @@ var dialogs = require('web.view_dialogs'); var ListController = require('web.ListController'); var testUtils = require('web.test_utils'); var Widget = require('web.Widget'); +var FormView = require('web.FormView'); + +var createView = testUtils.createView; QUnit.module('Views', { beforeEach: function () { @@ -14,6 +17,7 @@ QUnit.module('Views', { display_name: { string: "Displayed name", type: "char" }, foo: {string: "Foo", type: 'char'}, bar: {string: "Bar", type: "boolean"}, + instrument: {string: 'Instruments', type: 'many2one', relation: 'instrument'}, }, records: [ {id: 1, foo: 'blip', display_name: 'blipblip', bar: true}, @@ -21,8 +25,34 @@ QUnit.module('Views', { {id: 3, foo: 'piou piou', display_name: "Jack O'Neill", bar: true}, ], }, + instrument: { + fields: { + name: {string: "name", type: "char"}, + badassery: {string: 'level', type: 'many2many', relation: 'badassery', domain: [['level', '=', 'Awsome']]}, + }, + }, + + badassery: { + fields: { + level: {string: 'level', type: "char"}, + }, + records: [ + {id: 1, level: 'Awsome'}, + ], + }, + + product: { + fields : { + name: {string: "name", type: "char" }, + partner : {string: 'Doors', type: 'one2many', relation: 'partner'}, + }, + records: [ + {id: 1, name: 'The end'}, + ], + }, }; }, + }, function () { QUnit.module('view_dialogs'); @@ -242,6 +272,98 @@ QUnit.module('Views', { parent.destroy(); }); + QUnit.test('SelectCreateDialog cascade x2many in create mode', function (assert) { + assert.expect(5); + + var form = createView({ + View: FormView, + model: 'product', + data: this.data, + arch: '<form>' + + '<field name="name"/>' + + '<field name="partner" widget="one2many_list" >' + + '<tree editable="top">' + + '<field name="display_name"/>' + + '<field name="instrument"/>' + + '</tree>' + + '</field>' + + '</form>', + res_id: 1, + archs: { + 'partner,false,form': '<form>' + + '<field name="name"/>' + + '<field name="instrument" widget="one2many_list" mode="tree"/>' + + '</form>', + + 'instrument,false,form': '<form>'+ + '<field name="name"/>'+ + '<field name="badassery">' + + '<tree>'+ + '<field name="level"/>'+ + '</tree>' + + '</field>' + + '</form>', + + 'badassery,false,list': '<tree>'+ + '<field name="level"/>'+ + '</tree>', + + 'badassery,false,search': '<search>'+ + '<field name="level"/>'+ + '</search>', + }, + + mockRPC: function(route, args) { + if (route === '/web/dataset/call_kw/partner/get_formview_id') { + return $.when(false); + } + if (route === '/web/dataset/call_kw/instrument/get_formview_id') { + return $.when(false); + } + if (route === '/web/dataset/call_kw/instrument/create') { + assert.deepEqual(args.args, [{badassery: [[6, false, [1]]], name: false}], + 'The method create should have been called with the right arguments'); + return $.when(false); + } + return this._super(route, args); + }, + }); + + form.$buttons.find('.o_form_button_edit').click(); + form.$('.o_field_x2many_list_row_add a').click(); + form.$('.o_field_widget .o_field_many2one[name=instrument] input').click(); + $('ul.ui-autocomplete.ui-front.ui-menu.ui-widget.ui-widget-content li.o_m2o_dropdown_option').first().click(); + + var $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + $modal.find('.o_field_x2many_list_row_add a').click(); + + var $modals = $('.modal-dialog.modal-lg'); + + assert.equal($modals.length, 2, + 'There should be two modals'); + + var $second_modal = $modals.not($modal); + $second_modal.find('.o_list_view.table.table-condensed.table-striped.o_list_view_ungrouped .o_data_row input[type=checkbox]').click(); + + $second_modal.find('.o_select_button').click(); + + $modal = $('.modal-dialog.modal-lg'); + + assert.equal($modal.length, 1, + 'There should be one modal'); + + assert.equal($modal.find('.o_data_cell').text(), 'Awsome', + 'There should be one item in the list of the modal'); + + $modal.find('.btn.btn-sm.btn-primary').click(); + + form.destroy(); + }); + QUnit.test('SelectCreateDialog: save current search', function (assert) { assert.expect(4); diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index b14c58ae938..68051f8c07c 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -147,7 +147,10 @@ class Web_Editor(http.Controller): Attachments = request.env['ir.attachment'] # registry for the attachment table res_model = kwargs.get('res_model', 'ir.ui.view') - res_id = res_model != 'ir.ui.view' and kwargs.get('res_id') or None + if res_model != 'ir.ui.view' and kwargs.get('res_id'): + res_id = int(kwargs['res_id']) + else: + res_id = None uploads = [] message = None diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 188b563f238..097f3d3b12f 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -1233,7 +1233,7 @@ registry.many2one = SnippetOption.extend({ method: 'search_read', args: [domain, this.Model === 'res.partner' ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']], kwargs: { - order: 'name DESC', + order: [{name: 'name', asc: false}], limit: 5, context: weContext.get(), }, diff --git a/addons/web_editor/static/src/less/web_editor.backend.less b/addons/web_editor/static/src/less/web_editor.backend.less index 76da07437a5..a9d3c4e2198 100644 --- a/addons/web_editor/static/src/less/web_editor.backend.less +++ b/addons/web_editor/static/src/less/web_editor.backend.less @@ -22,6 +22,7 @@ margin: 0; padding: 0; border: 0; + word-wrap: break-word; ul > li > p { margin: 0px; } @@ -57,9 +58,9 @@ position: static !important; top: 0 !important; left: 0 !important; - overflow: visible !important; width: auto !important; overflow: hidden !important; + transform: none !important; } } diff --git a/addons/web_editor/static/src/less/web_editor.variables.less b/addons/web_editor/static/src/less/web_editor.variables.less index 382969d96b3..7b6f9565b4c 100644 --- a/addons/web_editor/static/src/less/web_editor.variables.less +++ b/addons/web_editor/static/src/less/web_editor.variables.less @@ -94,6 +94,12 @@ border-radius: 0; } + .has-error { + .form-control { + border-color: @state-danger-text; + } + } + select.form-control { .o-appearance(none); background: url('data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPScxLjEnIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZycgeG1 sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnIHdpZHRoPScyNCcgaGVpZ2 h0PScyNCcgdmlld0JveD0nMCAwIDI0IDI0Jz48cGF0aCBpZD0nc3ZnXzEnIGQ9J203LjQwNiw3L jgyOGw0LjU5NCw0LjU5NGw0LjU5NCwtNC41OTRsMC40MDYsMS40MDZsLTUsNC43NjZsLTUsLTQu NzY2bDAuNDA2LC0xLjQwNnonIGZpbGw9JyM4ODgnLz48L3N2Zz4='); diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js index 6a4d7c7a609..86626229867 100644 --- a/addons/website/static/src/js/backend/dashboard.js +++ b/addons/website/static/src/js/backend/dashboard.js @@ -214,6 +214,7 @@ var Dashboard = Widget.extend(ControlPanelMixin, { self.handle_analytics_auth($analytics_components); gapi.analytics.auth.on('signIn', function() { + delete window.onOriginError; self.handle_analytics_auth($analytics_components); }); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index d7c29359b95..602556c03cc 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -877,7 +877,7 @@ registry.socialShare = Animation.extend({ var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag) var social_network = { 'facebook': 'https://www.facebook.com/sharer/sharer.php?u=' + url, - 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url), + 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + url + '&text=' + encodeURIComponent(title + hashtags + ' - ') + url, 'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + url + '&title=' + encodeURIComponent(title), 'google-plus': 'https://plus.google.com/share?url=' + url, }; diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index 331edf06906..0b5af478da7 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -63,9 +63,10 @@ options.registry.company_data = options.Class.extend({ start: function () { var proto = options.registry.company_data.prototype; var def; + var self = this; if (proto.__link === undefined) { def = this._rpc({route: '/web/session/get_session_info'}).then(function (session) { - return this._rpc({ + return self._rpc({ model: 'res.users', method: 'read', args: [session.uid, ['company_id']], diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js index a931cc057ef..c66b340c699 100644 --- a/addons/website/static/src/js/menu/content.js +++ b/addons/website/static/src/js/menu/content.js @@ -157,7 +157,7 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({ }, }; if (this.page.date_publish) { - datepickersOptions.defaultDate = this.page.date_publish; + datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish); } this.$('#date_publish_container').datetimepicker(datepickersOptions); @@ -183,9 +183,15 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({ var context = weContext.get(); var url = this.$('#page_url').val(); - var date_publish = this.$('#date_publish').val(); - if (date_publish !== '') { - date_publish = time.datetime_to_str(new Date(date_publish)); + var $date_publish = this.$("#date_publish"); + $date_publish.closest(".form-group").removeClass('has-error'); + var date_publish = $date_publish.val(); + if (date_publish !== "") { + date_publish = this._parse_date(date_publish); + if (!date_publish) { + $date_publish.closest(".form-group").addClass('has-error'); + return; + } } var params = { id: this.page.id, @@ -280,6 +286,28 @@ var PagePropertiesDialog = weWidgets.Dialog.extend({ id: m[2] | 0, }; }, + /** + * Converts a string representing the browser datetime + * (exemple: Albanian: '2018-Qer-22 15.12.35.') + * to a string representing UTC in Odoo's datetime string format + * (exemple: '2018-04-22 13:12:35'). + * + * The time zone of the datetime string is assumed to be the one of the + * browser and it will be converted to UTC (standard for Odoo). + * + * @private + * @param {String} value A string representing a datetime. + * @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise. + */ + _parse_date: function (value) { + var datetime = moment(value, time.getLangDatetimeFormat(), true); + if (datetime.isValid()) { + return time.datetime_to_str(datetime.toDate()); + } + else { + return false; + } + }, //-------------------------------------------------------------------------- // Handlers diff --git a/addons/website_event_sale/views/event_templates.xml b/addons/website_event_sale/views/event_templates.xml index 3af11508112..789ea5995b2 100644 --- a/addons/website_event_sale/views/event_templates.xml +++ b/addons/website_event_sale/views/event_templates.xml @@ -39,7 +39,8 @@ </t> <t t-set="price"> <t t-if="ticket.price or editable"> - <t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) > 1"> + <t t-if="(ticket.price-website.get_current_pricelist().currency_id.compute(ticket.price_reduce, event.company_id.sudo().currency_id)) > 1 + and website.get_current_pricelist().discount_policy == 'without_discount'"> <del class="text-danger mr4" t-field="ticket.price" t-options='{ "widget": "monetary", "from_currency": event.company_id.sudo().currency_id, diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 3cf961ada09..ca6f4147418 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -10,6 +10,7 @@ from odoo.addons.base.models.ir_qweb_fields import nl2br from odoo.addons.http_routing.models.ir_http import slug from odoo.addons.website.controllers.main import QueryURL from odoo.exceptions import ValidationError +from odoo.addons.website.controllers.main import Website from odoo.addons.website_form.controllers.main import WebsiteForm _logger = logging.getLogger(__name__) @@ -116,7 +117,19 @@ class WebsiteSaleForm(WebsiteForm): return json.dumps({'id': order.id}) +class Website(Website): + @http.route() + def get_switchable_related_views(self, key): + views = super(Website, self).get_switchable_related_views(key) + if key == 'website_sale.product': + if not request.env.user.has_group('product.group_product_variant'): + view_product_variants = request.env.ref('website_sale.product_variants') + views[:] = [v for v in views if v['id'] != view_product_variants.id] + return views + + class WebsiteSale(http.Controller): + def _get_compute_currency_and_context(self): pricelist_context = dict(request.env.context) pricelist = False diff --git a/addons/website_sale/data/demo.xml b/addons/website_sale/data/demo.xml index 7d6b4ee7524..9fa440df145 100644 --- a/addons/website_sale/data/demo.xml +++ b/addons/website_sale/data/demo.xml @@ -722,7 +722,6 @@ Weight: 31 grams</field> </record> <record model="product.pricelist" id="product.list0"> - <field name="name">USD</field> <field name="selectable" eval="True" /> <field name="website_id" ref="website.default_website" /> <field name="sequence">3</field> diff --git a/addons/website_sale/tests/test_website_sale_pricelist.py b/addons/website_sale/tests/test_website_sale_pricelist.py index 754a49432d6..2995ca139cf 100644 --- a/addons/website_sale/tests/test_website_sale_pricelist.py +++ b/addons/website_sale/tests/test_website_sale_pricelist.py @@ -57,11 +57,11 @@ class TestWebsitePriceList(TransactionCase): current_pl = False country_list = { - False: ['USD', 'EUR', 'Benelux', 'Canada'], + False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'], 'BE': ['EUR', 'Benelux'], 'IT': ['EUR'], 'CA': ['Canada'], - 'US': ['USD', 'EUR', 'Benelux', 'Canada'] + 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada'] } for country, result in country_list.items(): pls = self.get_pl(show, current_pl, country) @@ -73,10 +73,10 @@ class TestWebsitePriceList(TransactionCase): current_pl = False country_list = { - False: ['USD', 'EUR', 'Benelux', 'Christmas', 'Canada'], + False: ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'], 'BE': ['EUR', 'Benelux', 'Christmas'], 'IT': ['EUR', 'Christmas'], - 'US': ['USD', 'EUR', 'Benelux', 'Christmas', 'Canada'], + 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Christmas', 'Canada'], 'CA': ['Canada'] } @@ -116,11 +116,11 @@ class TestWebsitePriceList(TransactionCase): current_pl = False country_list = { - False: ['USD', 'EUR', 'Benelux', 'Canada'], + False: ['Public Pricelist', 'EUR', 'Benelux', 'Canada'], 'BE': ['EUR', 'Benelux'], 'IT': ['EUR'], 'CA': ['EUR', 'Canada'], - 'US': ['USD', 'EUR', 'Benelux', 'Canada'] + 'US': ['Public Pricelist', 'EUR', 'Benelux', 'Canada'] } for country, result in country_list.items(): pls = self.get_pl(show, current_pl, country) diff --git a/addons/website_sale/views/sale_report_views.xml b/addons/website_sale/views/sale_report_views.xml index 256472179b7..61b04df2c2b 100644 --- a/addons/website_sale/views/sale_report_views.xml +++ b/addons/website_sale/views/sale_report_views.xml @@ -31,7 +31,7 @@ <field name="name">sale.report.view.pivot.website</field> <field name="model">sale.report</field> <field name="arch" type="xml"> - <pivot string="Sales Report"> + <pivot string="Sales Report" disable_linking="True"> <field name="date" type="row"/> <field name="state" type="col"/> <field name="price_subtotal" type="measure"/> diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 662d2212838..1ca35623f09 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -415,7 +415,7 @@ <div class="col-sm-7"> <span itemprop="image" t-attf-content="{{request.httprequest.url_root}}web/image/product.template/{{product.id}}/image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image'}" groups="!website_sale.group_website_multi_image"/> <t groups="website_sale.group_website_multi_image"> - <t t-set="variant_img" t-value="product.product_variant_id.image_variant"/> + <t t-set="variant_img" t-value="any(product.mapped('product_variant_ids.image_variant'))"/> <t t-set="image_ids" t-value="product.product_image_ids"/> <div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0"> <div class="carousel-outer"> @@ -1224,7 +1224,7 @@ <input type="hidden" name="partner_id" t-att-value="partner_id or '0'" /> <input type="hidden" name="callback" t-att-value="callback" /> <!-- Example --> - <input type="hidden" name="field_required" t-att-value="mode[1] == 'billing' and 'phone,name' or ''" /> + <input type="hidden" name="field_required" t-att-value="'phone,name'" /> <div class="clearfix"/> <div style='padding: 0 15px'> diff --git a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js index 0241c2d7d2f..c5a9f593b6e 100644 --- a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js +++ b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js @@ -1,7 +1,7 @@ odoo.define('website_sale_comparison.comparison', function (require) { "use strict"; -require('web.dom_ready') +require('web.dom_ready'); var ajax = require('web.ajax'); var core = require('web.core'); var _t = core._t; @@ -55,7 +55,11 @@ var ProductComparison = Widget.extend({ if (self.comparelist_product_ids.length < self.product_compare_limit) { var prod = $(this).data('product-product-id'); if (e.currentTarget.classList.contains('o_add_compare_dyn')) { - prod = parseInt($(this).parent().find('.product_id').val()); + prod = $(this).parent().find('.product_id').val(); + if (!prod) { // case List View Variants + prod = $(this).parent().find('input:checked').first().val(); + } + prod = parseInt(prod, 10); } self.add_new_products(prod); website_sale_utils.animate_clone($('#comparelist .o_product_panel_header'), $(this).closest('form'), -50, 10); @@ -68,6 +72,12 @@ var ProductComparison = Widget.extend({ $('body').on('click', '.comparator-popover .o_comparelist_products .o_remove', function (e){ self.rm_from_comparelist(e); }); + $('body').on('click', '.o_comparelist_remove', function (e){ + self.rm_from_comparelist(e); + var new_link = '/shop/compare/?products=' + self.comparelist_product_ids.toString(); + window.location = _.isEmpty(self.comparelist_product_ids) ? '/shop' : new_link; + }); + $("#o_comparelist_table tr").click(function(){ $($(this).data('target')).children().slideToggle(100); $(this).find('.fa-chevron-circle-down, .fa-chevron-circle-right').toggleClass('fa-chevron-circle-down fa-chevron-circle-right'); diff --git a/addons/website_sale_comparison/views/website_sale_comparison_template.xml b/addons/website_sale_comparison/views/website_sale_comparison_template.xml index 258b419897b..f472c5db811 100644 --- a/addons/website_sale_comparison/views/website_sale_comparison_template.xml +++ b/addons/website_sale_comparison/views/website_sale_comparison_template.xml @@ -74,7 +74,7 @@ <thead> <td class='td-top-left'></td> <td t-foreach="products" t-as="product"> - <a t-attf-href="/shop/compare/?products=#{','.join([str(id) for id in products._ids if id != product.id])}" t-att-data-product_product_id="product.id" class="o_comparelist_remove" t-if="len(products) > 2"> + <a href="#" t-att-data-product_product_id="product.id" class="o_comparelist_remove" t-if="len(products) > 2"> <strong class='pull-right'>x</strong> </a> <a t-att-href="'/shop/product/%s' % slug(product.product_tmpl_id)"> diff --git a/addons/website_sale_options/static/src/js/website_sale.test.js b/addons/website_sale_options/static/src/js/website_sale.test.js index b94f4493a2a..1c4cb821445 100644 --- a/addons/website_sale_options/static/src/js/website_sale.test.js +++ b/addons/website_sale_options/static/src/js/website_sale.test.js @@ -47,8 +47,39 @@ tour.register('shop_customize', { }, { content: "open customize menu", - trigger: '#customize-menu > a', + trigger: '#customize-menu', extra_trigger: "#product_detail", + run: function() { + // ENABLE VARIANT GROUP + $('body').addClass('notReady'); + var ajax = odoo.__DEBUG__.services['web.ajax']; + var get_group_payload = { + model: 'ir.model.data', + method: 'xmlid_to_res_id', + args: ['product.group_product_variant', false], + kwargs: {} + }; + ajax.jsonpRpc('/web/dataset/call_kw', 'call', get_group_payload).then(function(group_id) { + ajax.jsonpRpc('/web/dataset/call_kw', 'call', { + model: 'res.groups', + method: 'write', + args: [group_id, {'users': [[4, 1]]}], + kwargs: {} + }).then(function() { + location.reload(true); + }); + }); + }, + }, + { + content: "open customize menu", + trigger: '#customize-menu > a', + extra_trigger: 'body:not(.notReady)', + }, + { + content: "check page loaded after enable variant group", + trigger: '#customize-menu a:contains(List View of Variants)', + run: function () {}, // it's a check }, { content: "click on 'List View of Variants'", diff --git a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js index 8f0c3ec0359..34ed9c47957 100644 --- a/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js +++ b/addons/website_sale_wishlist/static/src/js/website_sale_wishlist.js @@ -20,7 +20,7 @@ var ProductWishlist = Widget.extend({ init: function(){ var self = this; this.wishlist_product_ids = []; - $.get('/shop/wishlist', {'count': 1}).then(function(res) { + var wish_loading = $.get('/shop/wishlist', {'count': 1}).then(function(res) { self.wishlist_product_ids = JSON.parse(res); self.update_wishlist_view(); }); @@ -44,12 +44,37 @@ var ProductWishlist = Widget.extend({ else { $el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled'); } + $el.data('product-product-id', parseInt($product_id.val(), 10)); + + }); + + // manage "List View of variants" + $('.oe_website_sale').on('change', 'input.js_product_change', function(ev) { + var product_id = ev.currentTarget.value; + var $el = $(ev.target).closest('.js_add_cart_variants').find("[data-action='o_wishlist']"); + + if (!_.contains(self.wishlist_product_ids, parseInt(product_id, 10))) { + $el.prop("disabled", false).removeClass('disabled').removeAttr('disabled'); + } + else { + $el.prop("disabled", true).addClass('disabled').attr('disabled', 'disabled'); + } + $el.data('product-product-id', product_id); }); + wish_loading.then(function() { + if ($('input.js_product_change').length) { // manage "List View of variants" + $('input.js_product_change:checked').first().trigger('change'); + } + else { + $('input.js_variant_change').trigger('change'); + } + }); + }, add_new_products:function($el, e){ var self = this; - var product_id = $el.data('product-product-id'); - if (e.currentTarget.classList.contains('o_add_wishlist_dyn')) { + var product_id = parseInt($el.data('product-product-id'), 10); + if (!product_id && e.currentTarget.classList.contains('o_add_wishlist_dyn')) { product_id = parseInt($el.parent().find('.product_id').val()); } if (!_.contains(self.wishlist_product_ids, product_id)) { |
