diff --git a/addons/account/models/account.py b/addons/account/models/account.py index a582d77d399..87d5d7e7f1f 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -231,7 +231,7 @@ class AccountJournal(models.Model): #groups_id = fields.Many2many('res.groups', 'account_journal_group_rel', 'journal_id', 'group_id', string='Groups') currency_id = fields.Many2one('res.currency', help='The currency used to enter statement', string="Currency", oldname='currency') - company_id = fields.Many2one('res.company', string='Company', required=True, index=1, default=lambda self: self.env.user.company_id, + company_id = fields.Many2one('res.company', string='Company', required=True, index=True, default=lambda self: self.env.user.company_id, help="Company related to this journal") refund_sequence = fields.Boolean(string='Dedicated Refund Sequence', help="Check this box if you don't want to share the same sequence for invoices and refunds made from this journal", default=False) @@ -632,7 +632,16 @@ class AccountTax(models.Model): if self.amount_type == 'fixed': # Use copysign to take into account the sign of the base amount which includes the sign # of the quantity and the sign of the price_unit - return math.copysign(quantity, base_amount) * self.amount + # Amount is the fixed price for the tax, it can be negative + # Base amount included the sign of the quantity and the sign of the unit price and when + # a product is returned, it can be done either by changing the sign of quantity or by changing the + # sign of the price unit. + # When the price unit is equal to 0, the sign of the quantity is absorbed in base_amount then + # a "else" case is needed. + if base_amount: + return math.copysign(quantity, base_amount) * self.amount + else: + return quantity * self.amount if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): return base_amount * self.amount / 100 if self.amount_type == 'percent' and self.price_include: diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py index b33a52a7671..dcc272ad1f2 100644 --- a/addons/account/models/account_bank_statement.py +++ b/addons/account/models/account_bank_statement.py @@ -129,7 +129,7 @@ class AccountBankStatement(models.Model): name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True) reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)") - date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, select=True, copy=False, default=fields.Date.context_today) + date = fields.Date(required=True, states={'confirm': [('readonly', True)]}, index=True, copy=False, default=fields.Date.context_today) date_done = fields.Datetime(string="Closed On") balance_start = fields.Monetary(string='Starting Balance', states={'confirm': [('readonly', True)]}, default=_default_opening_balance) balance_end_real = fields.Monetary('Ending Balance', states={'confirm': [('readonly', True)]}) @@ -289,58 +289,54 @@ class AccountBankStatement(models.Model): """ statements = self bsl_obj = self.env['account.bank.statement.line'] - # NB : The field account_id can be used at the statement line creation/import to avoid the reconciliation process on it later on, # this is why we filter out statements lines where account_id is set - st_lines_filter = [('journal_entry_ids', '=', False), ('account_id', '=', False)] + + sql_query = """SELECT stl.id + FROM account_bank_statement_line stl + WHERE account_id IS NULL AND not exists (select 1 from account_move m where m.statement_line_id = stl.id) + AND company_id = %s + """ + params = (self.env.user.company_id.id,) if statements: - st_lines_filter += [('statement_id', 'in', statements.ids)] + sql_query += ' AND stl.statement_id IN %s' + params += (tuple(statements.ids),) + sql_query += ' ORDER BY stl.id' + self.env.cr.execute(sql_query, params) + st_lines_left = self.env['account.bank.statement.line'].browse([line.get('id') for line in self.env.cr.dictfetchall()]) - # Try to automatically reconcile statement lines - automatic_reconciliation_entries = [] - st_lines_left = self.env['account.bank.statement.line'] - for st_line in bsl_obj.search(st_lines_filter): - res = st_line.auto_reconcile() - if not res: - st_lines_left = (st_lines_left | st_line) - else: - automatic_reconciliation_entries.append(res.ids) - - # Try to set statement line's partner - for st_line in st_lines_left: - if st_line.name and not st_line.partner_id: - additional_domain = [('ref', '=', st_line.name)] - match_recs = st_line.get_move_lines_for_reconciliation(limit=1, additional_domain=additional_domain, overlook_partner=True) - if match_recs and match_recs[0].partner_id: - st_line.write({'partner_id': match_recs[0].partner_id.id}) - - # Collect various informations for the reconciliation widget - notifications = [] - num_auto_reconciled = len(automatic_reconciliation_entries) - if num_auto_reconciled > 0: - auto_reconciled_message = num_auto_reconciled > 1 \ - and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ - or _("1 transaction was automatically reconciled.") - notifications += [{ - 'type': 'info', - 'message': auto_reconciled_message, - 'details': { - 'name': _("Automatically reconciled items"), - 'model': 'account.move', - 'ids': automatic_reconciliation_entries - } - }] - - lines = [] - for el in statements: - lines.extend(el.line_ids.ids) - lines = list(set(lines)) + #try to assign partner to bank_statement_line + stl_to_assign_partner = [stl.id for stl in st_lines_left if not stl.partner_id] + refs = list(set([st.name for st in st_lines_left if not stl.partner_id])) + if st_lines_left and stl_to_assign_partner and refs: + sql_query = """SELECT aml.partner_id, aml.ref, stl.id + FROM account_move_line aml + JOIN account_account acc ON acc.id = aml.account_id + JOIN account_bank_statement_line stl ON aml.ref = stl.name + WHERE (aml.company_id = %s + AND aml.partner_id IS NOT NULL) + AND ( + (aml.statement_id IS NULL AND aml.account_id IN %s) + OR + (acc.internal_type IN ('payable', 'receivable') AND aml.reconciled = false) + ) + AND aml.ref IN %s + """ + params = (self.env.user.company_id.id, (st_lines_left[0].journal_id.default_credit_account_id.id, st_lines_left[0].journal_id.default_debit_account_id.id), tuple(refs)) + if statements: + sql_query += 'AND stl.id IN %s' + params += (tuple(stl_to_assign_partner),) + self.env.cr.execute(sql_query, params) + results = self.env.cr.dictfetchall() + st_line = self.env['account.bank.statement.line'] + for line in results: + st_line.browse(line.get('id')).write({'partner_id': line.get('partner_id')}) return { 'st_lines_ids': st_lines_left.ids, - 'notifications': notifications, + 'notifications': [], 'statement_name': len(statements) == 1 and statements[0].name or False, - 'num_already_reconciled_lines': statements and bsl_obj.search_count([('journal_entry_ids', '!=', False), ('id', 'in', lines)]) or 0, + 'num_already_reconciled_lines': 0, } @api.multi @@ -449,6 +445,40 @@ class AccountBankStatementLine(models.Model): # Reconciliation interface methods #################################################### + @api.multi + def reconciliation_widget_auto_reconcile(self, num_already_reconciled_lines): + automatic_reconciliation_entries = self.env['account.bank.statement.line'] + unreconciled = self.env['account.bank.statement.line'] + for stl in self: + res = stl.auto_reconcile() + if res: + automatic_reconciliation_entries += stl + else: + unreconciled += stl + + # Collect various informations for the reconciliation widget + notifications = [] + num_auto_reconciled = len(automatic_reconciliation_entries) + if num_auto_reconciled > 0: + auto_reconciled_message = num_auto_reconciled > 1 \ + and _("%d transactions were automatically reconciled.") % num_auto_reconciled \ + or _("1 transaction was automatically reconciled.") + notifications += [{ + 'type': 'info', + 'message': auto_reconciled_message, + 'details': { + 'name': _("Automatically reconciled items"), + 'model': 'account.move', + 'ids': automatic_reconciliation_entries.ids + } + }] + return { + 'st_lines_ids': unreconciled.ids, + 'notifications': notifications, + 'statement_name': False, + 'num_already_reconciled_lines': num_auto_reconciled + num_already_reconciled_lines, + } + @api.multi def get_data_for_reconciliation_widget(self, excluded_ids=None): """ Returns the data required to display a reconciliation widget, for each statement line in self """ @@ -561,86 +591,66 @@ class AccountBankStatementLine(models.Model): return self.env['account.move.line'].search(domain, offset=offset, limit=limit, order="date_maturity asc, id asc") - def _get_domain_maker_move_line_amount(self): - """ Returns a function that can create the appropriate domain to search on move.line amount based on statement.line currency/amount """ - company_currency = self.journal_id.company_id.currency_id - st_line_currency = self.currency_id or self.journal_id.currency_id - currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False - field = currency and 'amount_residual_currency' or 'amount_residual' - precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places - - def ret(comparator, amount, p=precision, f=field, c=currency): - if comparator == '<': - if amount < 0: - domain = [(f, '<', 0), (f, '>', amount)] - else: - domain = [(f, '>', 0), (f, '<', amount)] - elif comparator == '=': - if f == 'amount_residual': - liquidity_field = amount > 0 and 'debit' or 'credit' - domain = [ - '|', (f, '=', float_round(amount, precision_digits=p)), - '&', ('account_id.internal_type', '=', 'liquidity'), - (liquidity_field, '=', amount), - ] - else: - domain = [ - '|', (f, '=', float_round(amount, precision_digits=p)), - '&', ('account_id.internal_type', '=', 'liquidity'), - ('amount_currency', '=', amount), - ] - else: - raise UserError(_("Programmation error : domain_maker_move_line_amount requires comparator '=' or '<'")) - domain += [('currency_id', '=', c)] - return domain - - return ret + def _get_common_sql_query(self, overlook_partner = False, excluded_ids = None, split = False): + acc_type = "acc.internal_type IN ('payable', 'receivable')" if (self.partner_id or overlook_partner) else "acc.reconcile = true" + select_clause = "SELECT aml.id " + from_clause = "FROM account_move_line aml JOIN account_account acc ON acc.id = aml.account_id " + where_clause = """WHERE aml.company_id = %(company_id)s + AND ( + (aml.statement_id IS NULL AND aml.account_id IN %(account_payable_receivable)s) + OR + ("""+acc_type+""" AND aml.reconciled = false) + )""" + where_clause = where_clause + ' AND aml.partner_id = %(partner_id)s' if self.partner_id else where_clause + where_clause = where_clause + ' AND aml.id NOT IN %(excluded_ids)s' if excluded_ids else where_clause + if split: + return select_clause, from_clause, where_clause + return select_clause + from_clause + where_clause def get_reconciliation_proposition(self, excluded_ids=None): """ Returns move lines that constitute the best guess to reconcile a statement line Note: it only looks for move lines in the same currency as the statement line. """ + self.ensure_one() + if not excluded_ids: + excluded_ids = [] + amount = self.amount_currency or self.amount + company_currency = self.journal_id.company_id.currency_id + st_line_currency = self.currency_id or self.journal_id.currency_id + currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False + precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places + params = {'company_id': self.env.user.company_id.id, + 'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id), + 'amount': float_round(amount, precision_digits=precision), + 'partner_id': self.partner_id.id, + 'excluded_ids': tuple(excluded_ids), + 'ref': self.name, + } # Look for structured communication match if self.name: - overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway - domain = [('ref', '=', self.name)] - match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) - if match_recs and len(match_recs) == 1: - return match_recs - elif len(match_recs) == 0: - move = self.env['account.move'].search([('name', '=', self.name)], limit=1) - if move: - domain = [('move_id', '=', move.id)] - match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=2, additional_domain=domain, overlook_partner=overlook_partner) - if match_recs and len(match_recs) == 1: - return match_recs - - # How to compare statement line amount and move lines amount - amount_domain_maker = self._get_domain_maker_move_line_amount() - amount = self.amount_currency or self.amount + add_to_select = ", CASE WHEN aml.ref = %(ref)s THEN 1 ELSE 2 END as temp_field_order " + add_to_from = " JOIN account_move m ON m.id = aml.move_id " + select_clause, from_clause, where_clause = self._get_common_sql_query(overlook_partner=True, excluded_ids=excluded_ids, split=True) + sql_query = select_clause + add_to_select + from_clause + add_to_from + where_clause + sql_query += " AND (aml.ref= %(ref)s or m.name = %(ref)s) \ + ORDER BY temp_field_order, date_maturity asc, aml.id asc" + self.env.cr.execute(sql_query, params) + results = self.env.cr.fetchone() + if results: + return self.env['account.move.line'].browse(results[0]) # Look for a single move line with the same amount - match_recs = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=1, additional_domain=amount_domain_maker('=', amount)) - if match_recs: - return match_recs + field = currency and 'amount_residual_currency' or 'amount_residual' + liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit' + sql_query = self._get_common_sql_query(excluded_ids=excluded_ids) + \ + " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \ + ORDER BY date_maturity asc, aml.id asc LIMIT 1" + self.env.cr.execute(sql_query, params) + results = self.env.cr.fetchone() + if results: + return self.env['account.move.line'].browse(results[0]) - if not self.partner_id: - return self.env['account.move.line'] - - # Select move lines until their total amount is greater than the statement line amount - domain = [('reconciled', '=', False)] - domain += [('account_id.user_type_id.type', '=', amount > 0 and 'receivable' or 'payable')] # Make sure we can't mix receivable and payable - domain += amount_domain_maker('<', amount) # Will also enforce > 0 - mv_lines = self.get_move_lines_for_reconciliation(excluded_ids=excluded_ids, limit=5, additional_domain=domain) - st_line_currency = self.currency_id or self.journal_id.currency_id or self.journal_id.company_id.currency_id - ret = self.env['account.move.line'] - total = 0 - for line in mv_lines: - total += line.currency_id and line.amount_residual_currency or line.amount_residual - if float_compare(total, abs(amount), precision_digits=st_line_currency.rounding) != -1: - break - ret = (ret | line) - return ret + return self.env['account.move.line'] def _get_move_lines_for_auto_reconcile(self): """ Returns the move lines that the method auto_reconcile can use to try to reconcile the statement line """ @@ -654,28 +664,44 @@ class AccountBankStatementLine(models.Model): self.ensure_one() match_recs = self.env['account.move.line'] - # How to compare statement line amount and move lines amount - amount_domain_maker = self._get_domain_maker_move_line_amount() - equal_amount_domain = amount_domain_maker('=', self.amount_currency or self.amount) - + amount = self.amount_currency or self.amount + company_currency = self.journal_id.company_id.currency_id + st_line_currency = self.currency_id or self.journal_id.currency_id + currency = (st_line_currency and st_line_currency != company_currency) and st_line_currency.id or False + precision = st_line_currency and st_line_currency.decimal_places or company_currency.decimal_places + params = {'company_id': self.env.user.company_id.id, + 'account_payable_receivable': (self.journal_id.default_credit_account_id.id, self.journal_id.default_debit_account_id.id), + 'amount': float_round(amount, precision_digits=precision), + 'partner_id': self.partner_id.id, + 'ref': self.name, + } + field = currency and 'amount_residual_currency' or 'amount_residual' + liquidity_field = currency and 'amount_currency' or amount > 0 and 'debit' or 'credit' # Look for structured communication match if self.name: - overlook_partner = not self.partner_id # If the transaction has no partner, look for match in payable and receivable account anyway - domain = equal_amount_domain + [('ref', '=', self.name)] - match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=domain, overlook_partner=overlook_partner) - if match_recs and len(match_recs) != 1: + sql_query = self._get_common_sql_query() + \ + " AND aml.ref = %(ref)s AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \ + ORDER BY date_maturity asc, aml.id asc" + self.env.cr.execute(sql_query, params) + match_recs = self.env.cr.dictfetchall() + if len(match_recs) > 1: return False # Look for a single move line with the same partner, the same amount if not match_recs: if self.partner_id: - match_recs = self.get_move_lines_for_reconciliation(limit=2, additional_domain=equal_amount_domain) - if match_recs and len(match_recs) != 1: + sql_query = self._get_common_sql_query() + \ + " AND ("+field+" = %(amount)s OR (acc.internal_type = 'liquidity' AND "+liquidity_field+" = %(amount)s)) \ + ORDER BY date_maturity asc, aml.id asc" + self.env.cr.execute(sql_query, params) + match_recs = self.env.cr.dictfetchall() + if len(match_recs) > 1: return False if not match_recs: return False + match_recs = self.env['account.move.line'].browse([aml.get('id') for aml in match_recs]) # Now reconcile counterpart_aml_dicts = [] payment_aml_rec = self.env['account.move.line'] diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py index a4b29bedcfc..b1bf44aa4ae 100644 --- a/addons/account/models/account_journal_dashboard.py +++ b/addons/account/models/account_journal_dashboard.py @@ -117,7 +117,7 @@ class account_journal(models.Model): for i in range(0,6): if i == 0: query += "("+select_sql_clause+" and date < '"+start_date.strftime(DF)+"')" - elif i == 6: + elif i == 5: query += " UNION ALL ("+select_sql_clause+" and date >= '"+start_date.strftime(DF)+"')" else: next_date = start_date + timedelta(days=7) @@ -303,6 +303,7 @@ class account_journal(models.Model): invoice_type = _journal_invoice_type_map[(self.type, self._context.get('invoice_type'))] ctx = self._context.copy() + ctx.pop('group_by', None) ctx.update({ 'journal_type': self.type, 'default_journal_id': self.id, @@ -336,6 +337,7 @@ class account_journal(models.Model): 'default_payment_type': payment_type, 'default_journal_id': self.id }) + ctx.pop('group_by', None) action_rec = self.env['ir.model.data'].xmlid_to_object('account.action_account_payments') if action_rec: action = action_rec.read([])[0] @@ -351,6 +353,7 @@ class account_journal(models.Model): ctx = dict(self.env.context, default_journal_id=self.id) if ctx.get('search_default_journal', False): ctx.update(search_default_journal_id=self.id) + ctx.pop('group_by', None) ir_model_obj = self.pool['ir.model.data'] model, action_id = ir_model_obj.get_object_reference(self._cr, self._uid, 'account', action_name) action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index f8a5007c7e8..8efbd9e3c7a 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -89,7 +89,7 @@ class AccountMove(models.Model): company_id = fields.Many2one('res.company', related='journal_id.company_id', string='Company', store=True, readonly=True, default=lambda self: self.env.user.company_id) matched_percentage = fields.Float('Percentage Matched', compute='_compute_matched_percentage', digits=0, store=True, readonly=True, help="Technical field used in cash basis method") - statement_line_id = fields.Many2one('account.bank.statement.line', string='Bank statement line reconciled with this entry', copy=False, readonly=True) + statement_line_id = fields.Many2one('account.bank.statement.line', index=True, string='Bank statement line reconciled with this entry', copy=False, readonly=True) # Dummy Account field to search on account.move by account_id dummy_account_id = fields.Many2one('account.account', related='line_ids.account_id', string='Account', store=False) @@ -230,6 +230,16 @@ class AccountMoveLine(models.Model): _description = "Journal Item" _order = "date desc, id desc" + def init(self, cr): + """ change index on partner_id to a multi-column index on (partner_id, ref), the new index will behave in the + same way when we search on partner_id, with the addition of being optimal when having a query that will + search on partner_id and ref at the same time (which is the case when we open the bank reconciliation widget) + """ + cr.execute('DROP INDEX IF EXISTS account_move_line_partner_id_index') + cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('account_move_line_partner_id_ref_idx',)) + if not cr.fetchone(): + cr.execute('CREATE INDEX account_move_line_partner_id_ref_idx ON account_move_line (partner_id, ref)') + @api.depends('debit', 'credit', 'amount_currency', 'currency_id', 'matched_debit_ids', 'matched_credit_ids', 'matched_debit_ids.amount', 'matched_credit_ids.amount', 'account_id.currency_id', 'move_id.state') def _amount_residual(self): """ Computes the residual amount of a move line from a reconciliable account in the company currency and the line's currency. @@ -363,7 +373,7 @@ class AccountMoveLine(models.Model): move_id = fields.Many2one('account.move', string='Journal Entry', ondelete="cascade", help="The move of this entry line.", index=True, required=True, auto_join=True) narration = fields.Text(related='move_id.narration', string='Internal Note') - ref = fields.Char(related='move_id.ref', string='Partner Reference', store=True, copy=False) + ref = fields.Char(related='move_id.ref', string='Partner Reference', store=True, copy=False, index=True) payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry") statement_id = fields.Many2one('account.bank.statement', string='Statement', help="The bank statement used for bank reconciliation", index=True, copy=False) @@ -389,7 +399,7 @@ class AccountMoveLine(models.Model): # TODO: put the invoice link and partner_id on the account_move invoice_id = fields.Many2one('account.invoice', oldname="invoice") - partner_id = fields.Many2one('res.partner', string='Partner', index=True, ondelete='restrict') + partner_id = fields.Many2one('res.partner', string='Partner', ondelete='restrict') user_type_id = fields.Many2one('account.account.type', related='account_id.user_type_id', index=True, store=True, oldname="user_type") _sql_constraints = [ @@ -1134,8 +1144,6 @@ class AccountMoveLine(models.Model): @api.multi def write(self, vals): - if vals.get('tax_line_id') or vals.get('tax_ids'): - raise UserError(_('You cannot change the tax, you should remove and recreate lines.')) if ('account_id' in vals) and self.env['account.account'].browse(vals['account_id']).deprecated: raise UserError(_('You cannot use deprecated account.')) if any(key in vals for key in ('account_id', 'journal_id', 'date', 'move_id', 'debit', 'credit')): @@ -1420,11 +1428,11 @@ class AccountPartialReconcile(models.Model): exchange_partial_rec_id = rate_diff_partial_rec.id #mark the reference of the full reconciliation on the partial ones and on the entries self.env['account.full.reconcile'].with_context(check_move_validity=False).create({ - 'partial_reconcile_ids': [(6, 0, partial_rec_ids)], - 'reconciled_line_ids': [(6, 0, aml_ids)], + 'partial_reconcile_ids': [(4, p_id) for p_id in partial_rec_ids], + 'reconciled_line_ids': [(4, a_id) for a_id in aml_ids], 'exchange_move_id': exchange_move_id, 'exchange_partial_rec_id': exchange_partial_rec_id, - }) + }) return res @api.multi diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py index cfa771b5b42..a4b5a0e9d13 100644 --- a/addons/account/models/partner.py +++ b/addons/account/models/partner.py @@ -338,7 +338,7 @@ class ResPartner(models.Model): else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines - overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<=', date), '&', ('date_maturity', '=', False), ('date', '<=', date)] + overdue_domain = ['|', '&', ('date_maturity', '!=', False), ('date_maturity', '<', date), '&', ('date_maturity', '=', False), ('date', '<', date)] if overdue_only: domain += overdue_domain return domain diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index 26657212f35..cb84ee74b23 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -1127,6 +1127,7 @@ var bankStatementReconciliation = abstractReconciliation.extend({ this.reconciliation_menu_id = false; // Used to update the needaction badge // The same move line cannot be selected for multiple reconciliations this.excluded_move_lines_ids = {}; + this.widget_childrens = []; }, serverPreprocessResultHandler: function(data) { @@ -1178,44 +1179,64 @@ var bankStatementReconciliation = abstractReconciliation.extend({ single_statement: self.single_statement, total_lines: self.num_already_reconciled_lines+self.lines.length })); - self.updateProgressbar(); - var reconciliations_to_show = self.lines.slice(0, self.num_reconciliations_fetched_in_batch); - self.last_displayed_reconciliation_index = reconciliations_to_show.length; - self.$(".reconciliation_lines_container").css("opacity", 0); - - // If everything is reconciled, show end message - if (self.lines.length === 0) { - if (self.notifications) - self.displayNotifications(self.notifications, 0); - self.displayDoneMessage(true); - return; - } - - // Display the reconciliations - return self.model_bank_statement_line - .call("get_data_for_reconciliation_widget", [reconciliations_to_show]) - .then(function (data) { - var child_promises = []; - var datum = data.shift(); - if (datum !== undefined) - child_promises.push(self.displayReconciliation(datum.st_line.id, 'match', false, true, datum.st_line, datum.reconciliation_proposition)); - while ((datum = data.shift()) !== undefined) - child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition)); - - // When reconciliations are instanciated, make an entrance - $.when.apply($, child_promises).then(function(){ - self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed, function() { - if (self.notifications) { - self.displayNotifications(self.notifications); - self.updateShowMoreButton(); - } + self.$el.find('.js_automatic_reconciliation').click(function() { + // Let odoo try to reconcile entries for the user + self.model_bank_statement_line + .call("reconciliation_widget_auto_reconcile", [self.lines || undefined, self.num_already_reconciled_lines]) + .then(function(data){ self.serverPreprocessResultHandler(data); }) + .then(function(){ self.$('.js_automatic_reconciliation').hide(); + return self.display_reconciliation_propositions(); }); - }); - }); + + }); + return self.display_reconciliation_propositions(); }); }); }, + display_reconciliation_propositions: function() { + var self = this; + self.updateProgressbar(); + var reconciliations_to_show = self.lines.slice(0, self.num_reconciliations_fetched_in_batch); + self.last_displayed_reconciliation_index = reconciliations_to_show.length; + self.$(".reconciliation_lines_container").css("opacity", 0); + // Delete previous bankStatementReconciliationLine + $.each(self.widget_childrens, function(index, child) { + child.destroy(); + }); + + // If everything is reconciled, show end message + if (self.lines.length === 0) { + self.$(".reconciliation_lines_container").hide(); + if (self.notifications) + self.displayNotifications(self.notifications, 0); + self.displayDoneMessage(true); + return; + } + + // Display the reconciliations + return self.model_bank_statement_line + .call("get_data_for_reconciliation_widget", [reconciliations_to_show]) + .then(function (data) { + var child_promises = []; + var datum = data.shift(); + if (datum !== undefined) + child_promises.push(self.displayReconciliation(datum.st_line.id, 'match', false, true, datum.st_line, datum.reconciliation_proposition)); + while ((datum = data.shift()) !== undefined) + child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition)); + + // When reconciliations are instanciated, make an entrance + $.when.apply($, child_promises).then(function(){ + self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed, function() { + if (self.notifications) { + self.displayNotifications(self.notifications); + self.updateShowMoreButton(); + } + }); + }); + }); + }, + statementNameClickHandler: function() { if (! this.single_statement) return; this.$(".statement_name span").hide(); @@ -1320,6 +1341,7 @@ var bankStatementReconciliation = abstractReconciliation.extend({ reconciliation_proposition: initial_data_provided ? reconciliation_proposition : undefined, }; var widget = new self.children_widget(self, context); + this.widget_childrens.push(widget); return widget.appendTo(self.$(".reconciliation_lines_container")); }, diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml index d91f042bcda..5c5b59c5758 100644 --- a/addons/account/static/src/xml/account_reconciliation.xml +++ b/addons/account/static/src/xml/account_reconciliation.xml @@ -12,6 +12,7 @@
+
diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py index b1429b0cc1c..76d6dcfe2d7 100644 --- a/addons/account_asset/account_asset.py +++ b/addons/account_asset/account_asset.py @@ -6,6 +6,7 @@ from dateutil.relativedelta import relativedelta from openerp import api, fields, models, _ from openerp.exceptions import UserError, ValidationError from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DF +from openerp.tools import float_compare class AccountAssetCategory(models.Model): @@ -417,11 +418,12 @@ class AccountAssetDepreciationLine(models.Model): categ_type = line.asset_id.category_id.type debit_account = line.asset_id.category_id.account_asset_id.id credit_account = line.asset_id.category_id.account_depreciation_id.id + prec = self.env['decimal.precision'].precision_get('Account') move_line_1 = { 'name': asset_name, 'account_id': credit_account, - 'debit': 0.0, - 'credit': amount, + 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, + 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': journal_id, 'partner_id': partner_id, 'currency_id': company_currency != current_currency and current_currency.id or False, @@ -432,8 +434,8 @@ class AccountAssetDepreciationLine(models.Model): move_line_2 = { 'name': asset_name, 'account_id': debit_account, - 'credit': 0.0, - 'debit': amount, + 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, + 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': journal_id, 'partner_id': partner_id, 'currency_id': company_currency != current_currency and current_currency.id or False, diff --git a/addons/account_asset/wizard/account_asset_change_duration.py b/addons/account_asset/wizard/account_asset_change_duration.py index 8564968434c..cbfc01a58bf 100644 --- a/addons/account_asset/wizard/account_asset_change_duration.py +++ b/addons/account_asset/wizard/account_asset_change_duration.py @@ -54,7 +54,8 @@ class AssetModify(models.TransientModel): if 'method_end' in fields and asset.method_time == 'end': res.update({'method_end': asset.method_end}) if self.env.context.get('active_id'): - res['asset_method_time'] = self._get_asset_method_time() + asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id')) + res['asset_method_time'] = asset.method_time return res @api.multi diff --git a/addons/account_voucher/account_voucher.py b/addons/account_voucher/account_voucher.py index 8116df538c4..2ba2602ad25 100644 --- a/addons/account_voucher/account_voucher.py +++ b/addons/account_voucher/account_voucher.py @@ -102,8 +102,8 @@ class AccountVoucher(models.Model): pay_now = fields.Selection([ ('pay_now', 'Pay Directly'), ('pay_later', 'Pay Later'), - ], 'Payment', select=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later') - date_due = fields.Date('Due Date', readonly=True, select=True, states={'draft': [('readonly', False)]}) + ], 'Payment', index=True, readonly=True, states={'draft': [('readonly', False)]}, default='pay_later') + date_due = fields.Date('Due Date', readonly=True, index=True, states={'draft': [('readonly', False)]}) @api.onchange('partner_id', 'pay_now') def onchange_partner_id(self): diff --git a/addons/base_import/static/src/js/import.js b/addons/base_import/static/src/js/import.js index 496a25bc9ab..7b5b801b8be 100644 --- a/addons/base_import/static/src/js/import.js +++ b/addons/base_import/static/src/js/import.js @@ -411,7 +411,7 @@ var DataImport = Widget.extend(ControlPanelMixin, { return $.when([{ type: 'error', record: false, - message: error.data.arguments[1], + message: error.data.arguments && error.data.arguments[1] || error.message, }]); }) ; }, diff --git a/addons/event/models/event.py b/addons/event/models/event.py index 447edb098a8..f4bf2ac7491 100644 --- a/addons/event/models/event.py +++ b/addons/event/models/event.py @@ -291,7 +291,7 @@ class event_registration(models.Model): string='Status', default='draft', readonly=True, copy=False, track_visibility='onchange') email = fields.Char(string='Email') phone = fields.Char(string='Phone') - name = fields.Char(string='Attendee Name', select=True) + name = fields.Char(string='Attendee Name', index=True) @api.one @api.constrains('event_id', 'state') diff --git a/addons/event_sale/wizard/event_edit_registration.py b/addons/event_sale/wizard/event_edit_registration.py index 98f6553f5a6..0526db91646 100644 --- a/addons/event_sale/wizard/event_edit_registration.py +++ b/addons/event_sale/wizard/event_edit_registration.py @@ -68,7 +68,7 @@ class RegistrationEditorLine(models.TransientModel): event_ticket_id = fields.Many2one('event.event.ticket', string='Event Ticket') email = fields.Char(string='Email') phone = fields.Char(string='Phone') - name = fields.Char(string='Name', select=True) + name = fields.Char(string='Name', index=True) @api.one def get_registration_data(self): diff --git a/addons/hr/hr.py b/addons/hr/hr.py index 25ca70fcfb2..14ead8fd5ce 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -261,7 +261,7 @@ class hr_employee(osv.osv): user_field_lst.append(name) return user_field_lst - _constraints = [(osv.osv._check_recursion, _('Error! You cannot create recursive hierarchy of Employee(s).'), ['parent_id']),] + _constraints = [(osv.osv._check_recursion, 'Error! You cannot create recursive hierarchy of Employee(s).', ['parent_id']),] class hr_department(osv.osv): @@ -291,7 +291,7 @@ class hr_department(osv.osv): } _constraints = [ - (osv.osv._check_recursion, _('Error! You cannot create recursive departments.'), ['parent_id']) + (osv.osv._check_recursion, 'Error! You cannot create recursive departments.', ['parent_id']) ] def name_get(self, cr, uid, ids, context=None): diff --git a/addons/hr_equipment/models/hr_equipment.py b/addons/hr_equipment/models/hr_equipment.py index fa7ebe2e95f..0d48366224f 100644 --- a/addons/hr_equipment/models/hr_equipment.py +++ b/addons/hr_equipment/models/hr_equipment.py @@ -236,7 +236,7 @@ class HrEquipmentRequest(models.Model): employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get) department_id = fields.Many2one('hr.department', string='Department') category_id = fields.Many2one('hr.equipment.category', string='Category') - equipment_id = fields.Many2one('hr.equipment', string='Asset', select=True) + equipment_id = fields.Many2one('hr.equipment', string='Asset', index=True) user_id = fields.Many2one('res.users', string='Assigned to', track_visibility='onchange') stage_id = fields.Many2one('hr.equipment.stage', string='Stage', track_visibility='onchange', default=_default_stage) priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority') diff --git a/addons/hr_payroll_account/hr_payroll_account.py b/addons/hr_payroll_account/hr_payroll_account.py index fa07751f290..c27df320165 100644 --- a/addons/hr_payroll_account/hr_payroll_account.py +++ b/addons/hr_payroll_account/hr_payroll_account.py @@ -1,7 +1,5 @@ #-*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import time -from datetime import date, datetime, timedelta from openerp import api from openerp.osv import fields, osv @@ -94,13 +92,12 @@ class hr_payslip(osv.osv): move_pool = self.pool.get('account.move') hr_payslip_line_pool = self.pool['hr.payslip.line'] precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Payroll') - timenow = time.strftime('%Y-%m-%d') for slip in self.browse(cr, uid, ids, context=context): line_ids = [] debit_sum = 0.0 credit_sum = 0.0 - date = timenow + date = slip.date or slip.date_to name = _('Payslip of %s') % (slip.employee_id.name) move = { @@ -152,7 +149,6 @@ class hr_payslip(osv.osv): raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name)) adjust_credit = (0, 0, { 'name': _('Adjustment Entry'), - 'date': timenow, 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, diff --git a/addons/hr_recruitment/models/hr_recruitment.py b/addons/hr_recruitment/models/hr_recruitment.py index c29b3cc0308..cf234b3106e 100644 --- a/addons/hr_recruitment/models/hr_recruitment.py +++ b/addons/hr_recruitment/models/hr_recruitment.py @@ -120,20 +120,20 @@ class Applicant(models.Model): help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma") probability = fields.Float("Probability") partner_id = fields.Many2one('res.partner', "Contact") - create_date = fields.Datetime("Creation Date", readonly=True, select=True) + create_date = fields.Datetime("Creation Date", readonly=True, index=True) write_date = fields.Datetime("Update Date", readonly=True) stage_id = fields.Many2one('hr.recruitment.stage', 'Stage', track_visibility='onchange', domain="['|', ('job_id', '=', False), ('job_id', '=', job_id)]", - copy=False, select=1, + copy=False, index=True, default=_default_stage_id) last_stage_id = fields.Many2one('hr.recruitment.stage', "Last Stage", help="Stage of the applicant before being in the current stage. Used for lost cases analysis.") categ_ids = fields.Many2many('hr.applicant.category', string="Tags") company_id = fields.Many2one('res.company', "Company", default=_default_company_id) user_id = fields.Many2one('res.users', "Responsible", track_visibility="onchange", default=lambda self: self.env.uid) - date_closed = fields.Datetime("Closed", readonly=True, select=True) - date_open = fields.Datetime("Assigned", readonly=True, select=True) - date_last_stage_update = fields.Datetime("Last Stage Update", select=True, default=fields.Datetime.now) + date_closed = fields.Datetime("Closed", readonly=True, index=True) + date_open = fields.Datetime("Assigned", readonly=True, index=True) + date_last_stage_update = fields.Datetime("Last Stage Update", index=True, default=fields.Datetime.now) date_action = fields.Date("Next Action Date") title_action = fields.Char("Next Action", size=64) priority = fields.Selection(AVAILABLE_PRIORITIES, "Appreciation", default='0') diff --git a/addons/lunch/models/lunch.py b/addons/lunch/models/lunch.py index 2f22f69659e..38b3535c163 100644 --- a/addons/lunch/models/lunch.py +++ b/addons/lunch/models/lunch.py @@ -147,7 +147,7 @@ class LunchOrderLine(models.Model): ('confirmed', 'Received'), ('ordered', 'Ordered'), ('cancelled', 'Cancelled')], - 'Status', readonly=True, select=True, default='new') + 'Status', readonly=True, index=True, default='new') cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id') @@ -235,7 +235,7 @@ class LunchAlert(models.Model): alert_type = fields.Selection([('specific', 'Specific Day'), ('week', 'Every Week'), ('days', 'Every Day')], - string='Recurrency', required=True, select=True, default='specific') + string='Recurrency', required=True, index=True, default='specific') specific_day = fields.Date('Day', default=fields.Date.context_today) monday = fields.Boolean('Monday') tuesday = fields.Boolean('Tuesday') diff --git a/addons/mail/controllers/bus.py b/addons/mail/controllers/bus.py index 0249c823ec4..058f7e331de 100644 --- a/addons/mail/controllers/bus.py +++ b/addons/mail/controllers/bus.py @@ -23,6 +23,7 @@ class MailChatController(openerp.addons.bus.controllers.main.BusController): partner_id = request.env.user.partner_id.id if partner_id: + channels = list(channels) # do not alter original list for mail_channel in request.env['mail.channel'].search([('channel_partner_ids', 'in', [partner_id])]): channels.append((request.db, 'mail.channel', mail_channel.id)) # personal and needaction channel diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 249cc4dc139..f73c6ad0e05 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -45,7 +45,7 @@ class Channel(models.Model): ('channel', 'Channel')], 'Channel Type', default='channel') description = fields.Text('Description') - uuid = fields.Char('UUID', size=50, select=True, default=lambda self: '%s' % uuid.uuid4()) + uuid = fields.Char('UUID', size=50, index=True, default=lambda self: '%s' % uuid.uuid4()) email_send = fields.Boolean('Send messages by email', default=False) # multi users channel channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen') diff --git a/addons/mail/models/mail_followers.py b/addons/mail/models/mail_followers.py index 2dd62582b1d..eda2c0647a4 100644 --- a/addons/mail/models/mail_followers.py +++ b/addons/mail/models/mail_followers.py @@ -18,13 +18,13 @@ class Followers(models.Model): _description = 'Document Followers' res_model = fields.Char( - 'Related Document Model', required=True, select=1, help='Model of the followed resource') + 'Related Document Model', required=True, index=True, help='Model of the followed resource') res_id = fields.Integer( - 'Related Document ID', select=1, help='Id of the followed resource') + 'Related Document ID', index=True, help='Id of the followed resource') partner_id = fields.Many2one( - 'res.partner', string='Related Partner', ondelete='cascade', select=1) + 'res.partner', string='Related Partner', ondelete='cascade', index=True) channel_id = fields.Many2one( - 'mail.channel', string='Listener', ondelete='cascade', select=1) + 'mail.channel', string='Listener', ondelete='cascade', index=True) subtype_ids = fields.Many2many( 'mail.message.subtype', string='Subtype', help="Message subtypes followed, meaning subtypes that will be pushed onto the user's Wall.") diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py index 0962bd8c111..1073016a019 100644 --- a/addons/mail/models/mail_message.py +++ b/addons/mail/models/mail_message.py @@ -57,12 +57,12 @@ class Message(models.Model): help='Attachments are linked to a document through model / res_id and to the message ' 'through this field.') parent_id = fields.Many2one( - 'mail.message', 'Parent Message', select=True, ondelete='set null', + 'mail.message', 'Parent Message', index=True, ondelete='set null', help="Initial thread message.") child_ids = fields.One2many('mail.message', 'parent_id', 'Child Messages') # related document - model = fields.Char('Related Document Model', select=1) - res_id = fields.Integer('Related Document ID', select=1) + model = fields.Char('Related Document Model', index=True) + res_id = fields.Integer('Related Document ID', index=True) record_name = fields.Char('Message Record Name', help="Name get of the related document.") # characteristics message_type = fields.Selection([ @@ -73,13 +73,13 @@ class Message(models.Model): help="Message type: email for email message, notification for system " "message, comment for other messages such as user replies", oldname='type') - subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', select=1) + subtype_id = fields.Many2one('mail.message.subtype', 'Subtype', ondelete='set null', index=True) # origin email_from = fields.Char( 'From', default=_get_default_from, help="Email address of the sender. This field is set when no matching partner is found and replaces the author_id field in the chatter.") author_id = fields.Many2one( - 'res.partner', 'Author', select=1, + 'res.partner', 'Author', index=True, ondelete='set null', default=_get_default_author, help="Author of the message. If not set, email_from may hold an email address that did not match any partner.") author_avatar = fields.Binary("Author's avatar", related='author_id.image_small') @@ -108,7 +108,7 @@ class Message(models.Model): no_auto_thread = fields.Boolean( 'No threading for answers', help='Answers do not go in the original document discussion thread. This has an impact on the generated message-id.') - message_id = fields.Char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False) + message_id = fields.Char('Message-Id', help='Message unique identifier', index=True, readonly=1, copy=False) reply_to = fields.Char('Reply-To', help='Reply email address. Setting the reply_to bypasses the automatic thread creation.') mail_server_id = fields.Many2one('ir.mail_server', 'Outgoing mail server') diff --git a/addons/mail/models/mail_template.py b/addons/mail/models/mail_template.py index f1579e1d943..7ef203f6275 100644 --- a/addons/mail/models/mail_template.py +++ b/addons/mail/models/mail_template.py @@ -116,7 +116,7 @@ class MailTemplate(models.Model): name = fields.Char('Name') model_id = fields.Many2one('ir.model', 'Applies to', help="The type of document this template can be used with") - model = fields.Char('Related Document Model', related='model_id.model', select=True, store=True, readonly=True) + model = fields.Char('Related Document Model', related='model_id.model', index=True, store=True, readonly=True) lang = fields.Char('Language', help="Optional translation language (ISO code) to select when sending out an email. " "If not set, the english version will be used. " diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py index 87c26590550..8619c69404d 100644 --- a/addons/mail/models/mail_tracking_value.py +++ b/addons/mail/models/mail_tracking_value.py @@ -25,7 +25,7 @@ class MailTracking(models.Model): new_value_text = fields.Text('New Value Text', readonly=1) new_value_datetime = fields.Datetime('New Value Datetime', readonly=1) - mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, select=True, ondelete='cascade') + mail_message_id = fields.Many2one('mail.message', 'Message ID', required=True, index=True, ondelete='cascade') @api.model def create_tracking_values(self, initial_value, new_value, col_name, col_info): diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py index bb6e8e9aeb4..a8bbccebfb5 100644 --- a/addons/mail/wizard/invite.py +++ b/addons/mail/wizard/invite.py @@ -25,8 +25,8 @@ class Invite(models.TransientModel): result['message'] = _('

Hello,

%s invited you to follow a new document.

') % user_name return result - res_model = fields.Char('Related Document Model', required=True, select=1, help='Model of the followed resource') - res_id = fields.Integer('Related Document ID', select=1, help='Id of the followed resource') + res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource') + res_id = fields.Integer('Related Document ID', index=True, help='Id of the followed resource') partner_ids = fields.Many2many('res.partner', string='Recipients', help="List of partners that will be added as follower of the current document.") channel_ids = fields.Many2many('mail.channel', string='Channels', help='List of channels that will be added as listeners of the current document.', domain=[('channel_type', '=', 'channel')]) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index 34d59736185..5acc2ebfa66 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -120,7 +120,7 @@ class MailComposer(models.TransientModel): auto_delete = fields.Boolean('Delete Emails', help='Delete sent emails (mass mailing only)') auto_delete_message = fields.Boolean('Delete Message Copy', help='Do not keep a copy of the email in the document communication history (mass mailing only)') template_id = fields.Many2one( - 'mail.template', 'Use template', select=True, + 'mail.template', 'Use template', index=True, domain="[('model', '=', model)]") # mail_message updated fields message_type = fields.Selection(default="comment") diff --git a/addons/mrp/views/report_mrpbomstructure.xml b/addons/mrp/views/report_mrpbomstructure.xml index f1ad7090f77..a5ece4b4f2b 100644 --- a/addons/mrp/views/report_mrpbomstructure.xml +++ b/addons/mrp/views/report_mrpbomstructure.xml @@ -31,8 +31,7 @@ - [ - ] + diff --git a/addons/procurement_jit/sale.py b/addons/procurement_jit/sale.py index b7ab9d40433..de43c848415 100644 --- a/addons/procurement_jit/sale.py +++ b/addons/procurement_jit/sale.py @@ -11,8 +11,8 @@ class SaleOrderLine(models.Model): res = super(SaleOrderLine, self)._action_procurement_create() orders = list(set(x.order_id for x in self)) for order in orders: - reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or ((x.state=='partially_available') and not x.printed)) + reassign = order.picking_ids.filtered(lambda x: x.state=='confirmed' or ((x.state in ['partially_available', 'waiting']) and not x.printed)) if reassign: reassign.do_unreserve() reassign.action_assign() - return res \ No newline at end of file + return res diff --git a/addons/purchase/invoice.py b/addons/purchase/invoice.py index 8e82e290e1c..554c5a7d7da 100644 --- a/addons/purchase/invoice.py +++ b/addons/purchase/invoice.py @@ -87,6 +87,22 @@ class AccountInvoice(models.Model): if purchase_ids: self.origin = ', '.join(purchase_ids.mapped('name')) + @api.onchange('partner_id', 'company_id') + def _onchange_partner_id(self): + res = super(AccountInvoice, self)._onchange_partner_id() + if not self.env.context.get('default_journal_id') and self.partner_id and self.currency_id and\ + self.type in ['in_invoice', 'in_refund'] and\ + self.currency_id != self.partner_id.property_purchase_currency_id: + journal_domain = [ + ('type', '=', 'purchase'), + ('company_id', '=', self.company_id.id), + ('currency_id', '=', self.partner_id.property_purchase_currency_id.id), + ] + default_journal_id = self.env['account.journal'].search(journal_domain, limit=1) + if default_journal_id: + self.journal_id = default_journal_id + return res + @api.model def invoice_line_move_line_get(self): res = super(AccountInvoice, self).invoice_line_move_line_get() @@ -163,6 +179,6 @@ class AccountInvoiceLine(models.Model): """ Override AccountInvoice_line to add the link to the purchase order line it is related to""" _inherit = 'account.invoice.line' - purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', select=True, readonly=True) + purchase_line_id = fields.Many2one('purchase.order.line', 'Purchase Order Line', ondelete='set null', index=True, readonly=True) purchase_id = fields.Many2one('purchase.order', related='purchase_line_id.order_id', string='Purchase Order', store=False, readonly=True, help='Associated Purchase Order. Filled in automatically when a PO is chosen on the vendor bill.') diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 407976486f0..39441246b3f 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -93,7 +93,7 @@ class PurchaseOrder(models.Model): 'cancel': [('readonly', True)], } - name = fields.Char('Order Reference', required=True, select=True, copy=False, default='New') + name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New') origin = fields.Char('Source Document', copy=False,\ help="Reference of the document that generated this purchase order " "request (e.g. a sale order or an internal procurement request)") @@ -102,9 +102,9 @@ class PurchaseOrder(models.Model): "It's used to do the matching when you receive the " "products as this reference is usually written on the " "delivery order sent by your vendor.") - date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, select=True, copy=False, default=fields.Datetime.now,\ + date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") - date_approve = fields.Date('Approval Date', readonly=1, select=True, copy=False) + date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always') dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\ help="Put an address if you want to deliver directly from the vendor to the customer. "\ @@ -118,7 +118,7 @@ class PurchaseOrder(models.Model): ('purchase', 'Purchase Order'), ('done', 'Locked'), ('cancel', 'Cancelled') - ], string='Status', readonly=True, select=True, copy=False, default='draft', track_visibility='onchange') + ], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange') order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True) notes = fields.Text('Terms and Conditions') @@ -133,7 +133,7 @@ class PurchaseOrder(models.Model): picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0) picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False) - date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', required=True, select=True, oldname='minimum_planned_date') + date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', required=True, index=True, oldname='minimum_planned_date') amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') @@ -145,13 +145,13 @@ class PurchaseOrder(models.Model): product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product') create_uid = fields.Many2one('res.users', 'Responsible') - company_id = fields.Many2one('res.company', 'Company', required=True, select=1, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) + company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\ help="This will determine picking type of incoming shipment") default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\ help="Technical field used to display the Drop Ship Address", readonly=True) - group_id = fields.Many2one('procurement.group', string="Procurement Group") + group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False) @api.model def name_search(self, name, args=None, operator='ilike', limit=100): @@ -281,6 +281,8 @@ class PurchaseOrder(models.Model): @api.multi def button_confirm(self): for order in self: + if order.state not in ['draft', 'sent']: + continue order._add_supplier_to_product() # Deal with double validation process if order.company_id.po_double_validation == 'one_step'\ @@ -516,7 +518,7 @@ class PurchaseOrderLine(models.Model): name = fields.Text(string='Description', required=True) product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) - date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True) + date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True) taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True) product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) @@ -527,7 +529,7 @@ class PurchaseOrderLine(models.Model): price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True) - order_id = fields.Many2one('purchase.order', string='Order Reference', select=True, required=True, ondelete='cascade') + order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '=', 'normal')]) company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) state = fields.Selection(related='order_id.state', stored=True) diff --git a/addons/purchase/purchase_view.xml b/addons/purchase/purchase_view.xml index a9bc1fa4341..7c561c0d6d1 100644 --- a/addons/purchase/purchase_view.xml +++ b/addons/purchase/purchase_view.xml @@ -250,7 +250,7 @@ - + diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index bf491896911..49ff8ab6338 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -257,11 +257,6 @@ class purchase_requisition(osv.osv): if not confirm: raise UserError(_('You have no line selected for buying.')) - #check for complete RFQ - for quotation in tender.purchase_ids: - if (self.check_valid_quotation(cr, uid, quotation, context=context)): - #Set PO state to confirm - po.button_confirm(cr, uid, [quotation.id], context=context) #get other confirmed lines per supplier for po_line in tender.po_line_ids: diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 7054c8add8e..b51ee81f169 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1555,12 +1555,18 @@ class stock_picking(models.Model): def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None): """ Unreserve quants then try to reassign quants.""" + if context is None: + context = {} stock_move_obj = self.pool.get('stock.move') if not move_ids: self.do_unreserve(cr, uid, [picking.id], context=context) self.action_assign(cr, uid, [picking.id], context=context) else: - stock_move_obj.do_unreserve(cr, uid, move_ids, context=context) + if 'no_state_change' in context: + move = stock_move_obj.browse(cr, uid, move_ids, context=context) + stock_move_obj.do_unreserve(cr, uid, [m.id for m in move if m.reserved_quant_ids], context=context) + else: + stock_move_obj.do_unreserve(cr, uid, move_ids, context=context) stock_move_obj.action_assign(cr, uid, move_ids, no_prepare=True, context=context) def do_new_transfer(self, cr, uid, ids, context=None): @@ -1667,6 +1673,7 @@ class stock_picking(models.Model): if moves_reassign and (picking.location_id.usage not in ("supplier", "production", "inventory")): ctx = dict(context) ctx['reserve_only_ops'] = True #unnecessary to assign other quants than those involved with pack operations as they will be unreserved anyways. + ctx['no_state_change'] = True self.rereserve_quants(cr, uid, picking, move_ids=picking.move_lines.ids, context=ctx) self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) @@ -2047,10 +2054,11 @@ class stock_move(osv.osv): if move.state in ('done', 'cancel'): raise UserError(_('Cannot unreserve a done move')) quant_obj.quants_unreserve(cr, uid, move, context=context) - if self.find_move_ancestors(cr, uid, move, context=context): - self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context) - else: - self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context) + if not context.get('no_state_change'): + if self.find_move_ancestors(cr, uid, move, context=context): + self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context) + else: + self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context) def _prepare_procurement_from_move(self, cr, uid, move, context=None): origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or move.picking_id.name or "/") @@ -2625,6 +2633,8 @@ class stock_move(osv.osv): operations = set() move_qty = {} for move in self.browse(cr, uid, ids, context=context): + if move.picking_id: + pickings.add(move.picking_id.id) move_qty[move.id] = move.product_qty for link in move.linked_move_operation_ids: operations.add(link.operation_id) @@ -4890,11 +4900,6 @@ class stock_picking_type(osv.osv): name = record.name if record.warehouse_id: name = record.warehouse_id.name + ': ' +name - if context.get('special_shortened_wh_name'): - if record.warehouse_id: - name = record.warehouse_id.name - else: - name = _('Customer') + ' (' + record.name + ')' res.append((record.id, name)) return res diff --git a/addons/warning/warning.py b/addons/warning/warning.py index 0c971a2a82e..bead8a1b3a0 100644 --- a/addons/warning/warning.py +++ b/addons/warning/warning.py @@ -233,6 +233,16 @@ class sale_order_line(osv.osv): return result @api.onchange('product_id') + def product_id_change(self): + warning = self.onchange_product_id_warning() + product_info = self.product_id + if product_info.sale_line_warn != 'no-message': + if product_info.sale_line_warn == 'block': + return warning + result = super(sale_order_line, self).product_id_change() + result['warning'] = warning and warning.get('warning') + return result + def onchange_product_id_warning(self): if not self.product_id: return diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 645c1d9b32d..616b7352053 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -25,11 +25,6 @@ import werkzeug.utils import werkzeug.wrappers from openerp.api import Environment -try: - import xlwt -except ImportError: - xlwt = None - import openerp import openerp.modules.registry from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound @@ -37,7 +32,7 @@ from openerp.modules import get_resource_path from openerp.tools import topological_sort from openerp.tools.translate import _ from openerp.tools import ustr -from openerp.tools.misc import str2bool +from openerp.tools.misc import str2bool, xlwt from openerp import http from openerp.http import request, serialize_exception as _serialize_exception from openerp.exceptions import AccessError diff --git a/addons/web/controllers/pivot.py b/addons/web/controllers/pivot.py index 571cff94229..41c113bbbdd 100644 --- a/addons/web/controllers/pivot.py +++ b/addons/web/controllers/pivot.py @@ -1,13 +1,10 @@ from openerp import http import json from openerp.http import request, serialize_exception as _serialize_exception +from openerp.tools.misc import xlwt from cStringIO import StringIO from collections import deque -try: - import xlwt -except ImportError: - xlwt = None class TableExporter(http.Controller): @@ -21,7 +18,7 @@ class TableExporter(http.Controller): jdata = json.loads(data) nbr_measures = jdata['nbr_measures'] workbook = xlwt.Workbook() - worksheet = workbook.add_sheet(jdata['title'][:30]) + worksheet = workbook.add_sheet(jdata['title']) header_bold = xlwt.easyxf("font: bold on; pattern: pattern solid, fore_colour gray25;") header_plain = xlwt.easyxf("pattern: pattern solid, fore_colour gray25;") bold = xlwt.easyxf("font: bold on;") diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index 3e9396a4e96..60df888ea1d 100644 --- a/addons/web/static/src/js/framework/data.js +++ b/addons/web/static/src/js/framework/data.js @@ -688,7 +688,7 @@ var DataSetSearch = DataSet.extend({ .limit(options.limit || false); q = q.order_by.apply(q, this._sort); - return q.all().done(function (records) { + return this.orderer.add(q.all()).done(function (records) { // FIXME: not sure about that one, *could* have discarded count q.count().done(function (count) { self._length = count; }); self.ids = _(records).pluck('id'); diff --git a/addons/web/static/src/js/views/form_widgets.js b/addons/web/static/src/js/views/form_widgets.js index b1f9058c825..8f046859ee8 100644 --- a/addons/web/static/src/js/views/form_widgets.js +++ b/addons/web/static/src/js/views/form_widgets.js @@ -478,6 +478,7 @@ var FieldCharDomain = common.AbstractField.extend(common.ReinitializeFieldMixin, this.$('.o_debug_input').val(this.get('value')); } } else { + this.$('.o_form_input').val(''); this.$('.o_count').text(_t('No selected record')); var $arrow = this.$('button span').detach(); this.$('button').text(_('Select records ')).append($("").addClass('fa fa-arrow-right')); @@ -1073,7 +1074,7 @@ var FieldRadio = common.AbstractField.extend(common.ReinitializeFieldMixin, { render_value: function () { var self = this; this.$el.toggleClass("oe_readonly", this.get('effective_readonly')); - this.$("input").filter(function () {return this.value == self.get_value();}).prop("checked", true); + this.$("input").prop("checked", false).filter(function () {return this.value == self.get_value();}).prop("checked", true); this.$(".oe_radio_readonly").text(this.get('value') ? this.get('value')[1] : ""); } }); @@ -1528,13 +1529,17 @@ var FieldStatus = common.AbstractField.extend({ return fields; }); }, - on_click_stage: function (ev) { + on_click_stage: _.debounce(function (ev) { var self = this; var $li = $(ev.currentTarget); + var ul = $li.closest('.oe_form_field_status'); if (this.view.is_disabled) { return; } var val; + if (ul.attr('disabled')) { + return; + } if (this.field.type == "many2one") { val = parseInt($li.data("id"), 10); } @@ -1551,13 +1556,16 @@ var FieldStatus = common.AbstractField.extend({ this.view.recursive_save().done(function() { var change = {}; change[self.name] = val; + ul.attr('disabled', true); self.view.dataset.write(self.view.datarecord.id, change).done(function() { self.view.reload(); + }).always(function() { + ul.removeAttr('disabled'); }); }); } } - }, + }, 300), }); var FieldMonetary = FieldFloat.extend({ diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html index 8b6fd4f09e7..7bbed8f3c20 100644 --- a/addons/web/views/database_manager.html +++ b/addons/web/views/database_manager.html @@ -32,7 +32,9 @@ var modal = $(this).parentsUntil('body', '.modal'); if (modal.hasClass('o_database_backup')) { $(modal).modal('hide'); - $('.list-group').before("
The backup may take some time before being ready
"); + if (!$('.alert-backup-long').length) { + $('.list-group').before("
The backup may take some time before being ready
"); + } } }); }); diff --git a/addons/web_editor/tests/test_ui.py b/addons/web_editor/tests/test_ui.py index 8fb85d6569f..f4d6d0dd2cb 100644 --- a/addons/web_editor/tests/test_ui.py +++ b/addons/web_editor/tests/test_ui.py @@ -1,6 +1,7 @@ import openerp.tests - +@openerp.tests.common.at_install(False) +@openerp.tests.common.post_install(True) class TestUi(openerp.tests.HttpCase): post_install = True diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 5a6b0d20604..7229aa310b0 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -561,6 +561,10 @@
+ + + +

Odoo Version

@@ -608,6 +612,7 @@
+
diff --git a/addons/website_blog/controllers/main.py b/addons/website_blog/controllers/main.py index e59da8736b9..5c0db09e5bc 100644 --- a/addons/website_blog/controllers/main.py +++ b/addons/website_blog/controllers/main.py @@ -228,8 +228,8 @@ class WebsiteBlog(http.Controller): all_post_ids = blog_post_obj.search(cr, uid, [('blog_id', '=', blog.id)], context=context) # should always return at least the current post current_blog_post_index = all_post_ids.index(blog_post.id) - next_post_id = all_post_ids[0 if current_blog_post_index == len(all_post_ids) - 1 \ - else current_blog_post_index + 1] + nb_posts = len(all_post_ids) + next_post_id = all_post_ids[(current_blog_post_index + 1) % nb_posts] if nb_posts > 1 else None next_post = next_post_id and blog_post_obj.browse(cr, uid, next_post_id, context=context) or False values = { diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py index 1dddd28a23a..c816b8914ca 100644 --- a/addons/website_form/controllers/main.py +++ b/addons/website_form/controllers/main.py @@ -216,7 +216,7 @@ class WebsiteForm(http.Controller): # If some attachments didn't match a field on the model, # we create a mail.message to link them to the record if orphan_attachment_ids: - if model.name != 'mail.mail': + if model.model != 'mail.mail': values = { 'body': _('

Attached files :

'), 'model': model.model, diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 3c0757f4793..fa23f56478d 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -209,14 +209,14 @@ class Post(models.Model): ) # history - create_date = fields.Datetime('Asked on', select=True, readonly=True) - create_uid = fields.Many2one('res.users', string='Created by', select=True, readonly=True) - write_date = fields.Datetime('Update on', select=True, readonly=True) + create_date = fields.Datetime('Asked on', index=True, readonly=True) + create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True) + write_date = fields.Datetime('Update on', index=True, readonly=True) bump_date = fields.Datetime('Bumped on', readonly=True, help="Technical field allowing to bump a question. Writing on this field will trigger " "a write on write_date and therefore bump the post. Directly writing on write_date " "is currently not supported and this field is a workaround.") - write_uid = fields.Many2one('res.users', string='Updated by', select=True, readonly=True) + write_uid = fields.Many2one('res.users', string='Updated by', index=True, readonly=True) relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True) # vote @@ -244,7 +244,7 @@ class Post(models.Model): # closing closed_reason_id = fields.Many2one('forum.post.reason', string='Reason') - closed_uid = fields.Many2one('res.users', string='Closed by', select=1) + closed_uid = fields.Many2one('res.users', string='Closed by', index=True) closed_date = fields.Datetime('Closed on', readonly=True) # karma calculation and access @@ -812,7 +812,7 @@ class Vote(models.Model): post_id = fields.Many2one('forum.post', string='Post', ondelete='cascade', required=True) user_id = fields.Many2one('res.users', string='User', required=True, default=lambda self: self._uid) vote = fields.Selection([('1', '1'), ('-1', '-1'), ('0', '0')], string='Vote', required=True, default='1') - create_date = fields.Datetime('Create Date', select=True, readonly=True) + create_date = fields.Datetime('Create Date', index=True, readonly=True) forum_id = fields.Many2one('forum.forum', string='Forum', related="post_id.forum_id", store=True) recipient_id = fields.Many2one('res.users', string='To', related="post_id.create_uid", store=True) diff --git a/addons/website_forum/models/res_users.py b/addons/website_forum/models/res_users.py index 3a5d8c92395..b389dcaa264 100644 --- a/addons/website_forum/models/res_users.py +++ b/addons/website_forum/models/res_users.py @@ -18,7 +18,7 @@ class Users(models.Model): ['country_id', 'city', 'website', 'website_description', 'website_published'])) return init_res - create_date = fields.Datetime('Create Date', readonly=True, copy=False, select=True) + create_date = fields.Datetime('Create Date', readonly=True, copy=False, index=True) karma = fields.Integer('Karma', default=0) badge_ids = fields.One2many('gamification.badge.user', 'user_id', string='Badges', copy=False) gold_badge = fields.Integer('Gold badges count', compute="_get_user_badge_level") diff --git a/addons/website_quote/static/src/css/website_quotation.css b/addons/website_quote/static/src/css/website_quotation.css index 6d32f06ab96..30ba234d14b 100644 --- a/addons/website_quote/static/src/css/website_quotation.css +++ b/addons/website_quote/static/src/css/website_quotation.css @@ -1,17 +1,11 @@ .o_website_quote .bs-sidebar { - position: fixed; + position: relative; + width: 100%; z-index : 1; background-color: #f7f5fa; border-radius: 5px; } -/* mobile preview: the left menu overlaps the quote */ -@media (max-width: 992px) { - .o_website_quote .bs-sidebar { - position: relative; - } -} - .o_website_quote .bs-sidenav { padding-top: 10px; padding-bottom: 10px; @@ -56,14 +50,6 @@ .o_website_quote .bs-sidebar .nav > .active > ul { display: block; } - .o_website_quote .bs-sidebar { - width: 213px; - } -} -@media (min-width: 1200px) { - .o_website_quote .bs-sidebar { - width: 263px; - } } @media print { body { diff --git a/addons/website_quote/static/src/js/website_quotation.js b/addons/website_quote/static/src/js/website_quotation.js index 2ff25986179..798b1ec5a76 100644 --- a/addons/website_quote/static/src/js/website_quotation.js +++ b/addons/website_quote/static/src/js/website_quotation.js @@ -2,6 +2,7 @@ odoo.define('website_quote.website_quote', function (require) { 'use strict'; var ajax = require('web.ajax'); +var config = require('web.config'); var Widget = require('web.Widget'); var website = require('website.website'); @@ -175,6 +176,22 @@ if(!$('.o_website_quote').length) { nav_menu.setElement($('[data-id="quote_sidebar"]')); nav_menu.start($('body[data-target=".navspy"]')); + var $bs_sidebar = $(".o_website_quote .bs-sidebar"); + $(window).on('resize', _.throttle(adapt_sidebar_position, 200, {leading: false})); + adapt_sidebar_position(); + + function adapt_sidebar_position() { + $bs_sidebar.css({ + position: "", + width: "", + }); + if (config.device.size_class >= config.device.SIZES.MD) { + $bs_sidebar.css({ + position: "fixed", + width: $bs_sidebar.outerWidth(), + }); + } + } }); odoo.define('website_quote.payment_method', function (require) { diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index 7c8593c48e7..2e0091b53f1 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -295,8 +295,8 @@ class website_slides(http.Controller): payload = request.httprequest.content_length # payload is total request content size so it's not exact size of file. # already add client validation this is for double check if client alter. - if (payload / 1024 / 1024 > 15): - return {'error': _('File is too big. File size cannot exceed 15MB')} + if (payload / 1024 / 1024 > 25): + return {'error': _('File is too big. File size cannot exceed 25MB')} values = dict((fname, post[fname]) for fname in [ 'name', 'url', 'tag_ids', 'slide_type', 'channel_id', diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index 6ecc0540437..e7455ca138a 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -192,7 +192,7 @@ class EmbeddedSlide(models.Model): _description = 'Embedded Slides View Counter' _rec_name = 'slide_id' - slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, select=1) + slide_id = fields.Many2one('slide.slide', string="Presentation", required=True, index=True) url = fields.Char('Third Party Website URL', required=True) count_views = fields.Integer('# Views', default=1) diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js index ea7ec99e401..b8fbe164421 100644 --- a/addons/website_slides/static/src/js/slides_upload.js +++ b/addons/website_slides/static/src/js/slides_upload.js @@ -86,8 +86,8 @@ var SlideDialog = Widget.extend({ this.reset_file(); return; } - if (file.size / 1024 / 1024 > 15) { - this.display_alert(_t("File is too big. File size cannot exceed 15MB")); + if (file.size / 1024 / 1024 > 25) { + this.display_alert(_t("File is too big. File size cannot exceed 25MB")); this.reset_file(); return; } diff --git a/doc/_extensions/odoo/layout.html b/doc/_extensions/odoo/layout.html index a7ed5078040..000019b32cf 100644 --- a/doc/_extensions/odoo/layout.html +++ b/doc/_extensions/odoo/layout.html @@ -230,7 +230,7 @@
{% if pagename != master_doc %}
- {% if 'has-toc' not in meta %} + {% if 'has-toc' not in meta and not (pagename in toc) %} {% endif %} -
+
{% endif %} {% block body %} {% endblock %} {% if pagename != master_doc %}
diff --git a/doc/_extensions/odoo/static/style.css b/doc/_extensions/odoo/static/style.css index 1d48dea23b0..d521ff30677 100644 --- a/doc/_extensions/odoo/static/style.css +++ b/doc/_extensions/odoo/static/style.css @@ -12353,6 +12353,13 @@ article.doc-body section.doc-content:first-of-type > p:first-child { font-size: 22.5px; } } +article.doc-body.index-category { + min-height: 300px; + min-height: 30vh; +} +article.doc-body.index-category li.toctree-l1 { + padding: 5px 0; +} .content-switcher { margin-top: 1.5em; } diff --git a/doc/_extensions/odoo/static/style.less b/doc/_extensions/odoo/static/style.less index 44811836068..5fd9fe17804 100644 --- a/doc/_extensions/odoo/static/style.less +++ b/doc/_extensions/odoo/static/style.less @@ -742,6 +742,15 @@ article.doc-body { .lead; } } + + &.index-category { + min-height: 300px; + min-height: 30vh; + + li.toctree-l1 { + padding: 5px 0; + } + } } .content-switcher { diff --git a/doc/cla/corporate/BeOpen.md b/doc/cla/corporate/BeOpen.md new file mode 100644 index 00000000000..ae1486a2db6 --- /dev/null +++ b/doc/cla/corporate/BeOpen.md @@ -0,0 +1,15 @@ +Belgium, 2016-08-09 + +BeOpen agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Kim Huysmans kim.huysmans@rhea.be https://github.com/huysmki + +List of contributors: + +Kim Huysmans kim.huysmans@rhea.be https://github.com/huysmki diff --git a/doc/cla/corporate/iraiser.md b/doc/cla/corporate/iraiser.md new file mode 100644 index 00000000000..72e852aedaf --- /dev/null +++ b/doc/cla/corporate/iraiser.md @@ -0,0 +1,17 @@ +France, 2016-05-18 + +iRaiser agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Bruno PLANCHER bplancher@iraiser.eu https://github.com/bplancher + +List of contributors: + +Bruno PLANCHER bplancher@iraiser.eu https://github.com/bplancher +Damien NICOLAS dnicolas@iraiser.eu https://github.com/gordonzola + diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst index 76f1d81cb38..7684abc3345 100644 --- a/doc/setup/deploy.rst +++ b/doc/setup/deploy.rst @@ -231,6 +231,12 @@ just about any SSL termination proxy, but requires the following setup: * your SSL termination proxy should also automatically redirect non-secure connections to the secure port +.. warning:: + + In case you are using the Point of Sale module in combinaison with a `POSBox`_, + you must disable the HTTPS configuration for the route ``/pos/web`` to avoid + mix-content errors. + Configuration sample -------------------- @@ -422,3 +428,4 @@ which will generate a 32 characters pseudorandom printable string. .. _use an SSH tunnel: http://www.postgresql.org/docs/9.3/static/ssh-tunnels.html .. _WSGI: http://wsgi.readthedocs.org/ +.. _POSBox: https://www.odoo.com/page/point-of-sale-hardware#part_2 \ No newline at end of file diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 6d830bd63b3..a2e1455fa15 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -625,9 +625,10 @@ class ir_model_constraint(Model): ids.reverse() for data in self.browse(cr, uid, ids, context): model = data.model.model - model_obj = self.pool.get(model) - if not model_obj: - continue + if model in self.pool: + table = self.pool[model]._table + else: + table = model.replace('.', '_') name = openerp.tools.ustr(data.name) typ = data.type @@ -641,17 +642,17 @@ class ir_model_constraint(Model): if typ == 'f': # test if FK exists on this table (it could be on a related m2m table, in which case we ignore it) cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) - WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, model_obj._table)) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('f', name, table)) if cr.fetchone(): - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (table, name),) _logger.info('Dropped FK CONSTRAINT %s@%s', name, model) if typ == 'u': # test if constraint exists cr.execute("""SELECT 1 from pg_constraint cs JOIN pg_class cl ON (cs.conrelid = cl.oid) - WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, model_obj._table)) + WHERE cs.contype=%s and cs.conname=%s and cl.relname=%s""", ('u', name, table)) if cr.fetchone(): - cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (model_obj._table, name),) + cr.execute('ALTER TABLE "%s" DROP CONSTRAINT "%s"' % (table, name),) _logger.info('Dropped CONSTRAINT %s@%s', name, model) self.unlink(cr, uid, ids, context) diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 1f719522d9d..491ba81d590 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -227,10 +227,12 @@ class CurrencyRate(models.Model): _description = "Currency Rate" _order = "name desc" - name = fields.Datetime(string='Date', required=True, index=True, default=lambda self:fields.Date.today() + ' 00:00:00') + name = fields.Datetime(string='Date', required=True, index=True, + default=lambda self: fields.Date.today() + ' 00:00:00') rate = fields.Float(digits=(12, 6), help='The rate of the currency to the currency of rate 1') currency_id = fields.Many2one('res.currency', string='Currency', readonly=True) - company_id = fields.Many2one('res.company', string='Company') + company_id = fields.Many2one('res.company', string='Company', + default=lambda self: self.env.user._get_company()) @api.model def name_search(self, name, args=None, operator='ilike', limit=80): diff --git a/openerp/addons/base/res/res_currency_view.xml b/openerp/addons/base/res/res_currency_view.xml index f31343b7e6f..cc9f670011d 100644 --- a/openerp/addons/base/res/res_currency_view.xml +++ b/openerp/addons/base/res/res_currency_view.xml @@ -23,6 +23,28 @@ + + res.currency.rate.form + res.currency.rate + +
+ + + + + + + + + + + + + +
+
+
+