diff --git a/addons/.bzrignore b/addons/.bzrignore deleted file mode 100644 index 8c348710117..00000000000 --- a/addons/.bzrignore +++ /dev/null @@ -1,2 +0,0 @@ -.* -**/node_modules diff --git a/addons/account/account_bank_statement.py b/addons/account/account_bank_statement.py index 75940d2d83d..1ddab4bece9 100644 --- a/addons/account/account_bank_statement.py +++ b/addons/account/account_bank_statement.py @@ -26,6 +26,9 @@ from openerp.report import report_sxw class account_bank_statement(osv.osv): def create(self, cr, uid, vals, context=None): + if vals.get('name', '/') == '/': + journal_id = vals.get('journal_id', self._default_journal_id(cr, uid, context=context)) + vals['name'] = self._compute_default_statement_name(cr, uid, journal_id, context=context) if 'line_ids' in vals: for idx, line in enumerate(vals['line_ids']): line[2]['sequence'] = idx + 1 @@ -65,17 +68,14 @@ class account_bank_statement(osv.osv): return periods[0] return False - def _compute_default_statement_name(self, cr, uid, context=None): + def _compute_default_statement_name(self, cr, uid, journal_id, context=None): if context is None: context = {} obj_seq = self.pool.get('ir.sequence') - default_journal_id = self._default_journal_id(cr, uid, context=context) - if default_journal_id != False: - period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context) - context['fiscalyear_id'] = period.fiscalyear_id.id - journal = self.pool.get('account.journal').browse(cr, uid, default_journal_id, None) - return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context) - return obj_seq.next_by_code(cr, uid, 'account.bank.statement', context=context) + period = self.pool.get('account.period').browse(cr, uid, self._get_period(cr, uid, context=context), context=context) + context['fiscalyear_id'] = period.fiscalyear_id.id + journal = self.pool.get('account.journal').browse(cr, uid, journal_id, None) + return obj_seq.next_by_id(cr, uid, journal.sequence_id.id, context=context) def _currency(self, cursor, user, ids, name, args, context=None): res = {} @@ -150,7 +150,7 @@ class account_bank_statement(osv.osv): } _defaults = { - 'name': _compute_default_statement_name, + 'name': '/', 'date': fields.date.context_today, 'state': 'draft', 'journal_id': _default_journal_id, @@ -213,11 +213,8 @@ class account_bank_statement(osv.osv): def _get_counter_part_account(sefl, cr, uid, st_line, context=None): """Retrieve the account to use in the counterpart move. - This method may be overridden to implement custom move generation (making sure to - call super() to establish a clean extension chain). - :param browse_record st_line: account.bank.statement.line record to - create the move from. + :param browse_record st_line: account.bank.statement.line record to create the move from. :return: int/long of the account.account to use as counterpart """ if st_line.amount >= 0: @@ -226,26 +223,19 @@ class account_bank_statement(osv.osv): def _get_counter_part_partner(sefl, cr, uid, st_line, context=None): """Retrieve the partner to use in the counterpart move. - This method may be overridden to implement custom move generation (making sure to - call super() to establish a clean extension chain). - :param browse_record st_line: account.bank.statement.line record to - create the move from. + :param browse_record st_line: account.bank.statement.line record to create the move from. :return: int/long of the res.partner to use as counterpart """ return st_line.partner_id and st_line.partner_id.id or False def _prepare_bank_move_line(self, cr, uid, st_line, move_id, amount, company_currency_id, context=None): """Compute the args to build the dict of values to create the counter part move line from a - statement line by calling the _prepare_move_line_vals. This method may be - overridden to implement custom move generation (making sure to call super() to - establish a clean extension chain). + statement line by calling the _prepare_move_line_vals. - :param browse_record st_line: account.bank.statement.line record to - create the move from. + :param browse_record st_line: account.bank.statement.line record to create the move from. :param int/long move_id: ID of the account.move to link the move line :param float amount: amount of the move line - :param int/long account_id: ID of account to use as counter part :param int/long company_currency_id: ID of currency of the concerned company :return: dict of value to create() the bank account.move.line """ @@ -258,7 +248,6 @@ class account_bank_statement(osv.osv): if st_line.statement_id.currency.id != company_currency_id: amt_cur = st_line.amount cur_id = st_line.currency_id or st_line.statement_id.currency.id - # TODO : FIXME the amount should be in the journal currency if st_line.currency_id and st_line.amount_currency: amt_cur = st_line.amount_currency cur_id = st_line.currency_id.id @@ -269,9 +258,7 @@ class account_bank_statement(osv.osv): def _prepare_move_line_vals(self, cr, uid, st_line, move_id, debit, credit, currency_id=False, amount_currency=False, account_id=False, partner_id=False, context=None): """Prepare the dict of values to create the move line from a - statement line. All non-mandatory args will replace the default computed one. - This method may be overridden to implement custom move generation (making sure to - call super() to establish a clean extension chain). + statement line. :param browse_record st_line: account.bank.statement.line record to create the move from. @@ -342,21 +329,29 @@ class account_bank_statement(osv.osv): move_ids.append(st_line.journal_entry_id.id) self.pool.get('account.move').post(cr, uid, move_ids, context=context) self.message_post(cr, uid, [st.id], body=_('Statement %s confirmed, journal items were created.') % (st.name,), context=context) + self.link_bank_to_partner(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}, context=context) def button_cancel(self, cr, uid, ids, context=None): - done = [] account_move_obj = self.pool.get('account.move') + reconcile_pool = self.pool.get('account.move.reconcile') + move_line_pool = self.pool.get('account.move.line') + move_ids = [] for st in self.browse(cr, uid, ids, context=context): - if st.state=='draft': - continue - move_ids = [] for line in st.line_ids: - move_ids += [x.id for x in line.move_ids] + if line.journal_entry_id: + move_ids.append(line.journal_entry_id.id) + for aml in line.journal_entry_id.line_id: + if aml.reconcile_id: + move_lines = [l.id for l in aml.reconcile_id.line_id] + move_lines.remove(aml.id) + reconcile_pool.unlink(cr, uid, [aml.reconcile_id.id], context=context) + if len(move_lines) >= 2: + move_line_pool.reconcile_partial(cr, uid, move_lines, 'auto', context=context) + if move_ids: account_move_obj.button_cancel(cr, uid, move_ids, context=context) account_move_obj.unlink(cr, uid, move_ids, context) - done.append(st.id) - return self.write(cr, uid, done, {'state':'draft'}, context=context) + return self.write(cr, uid, ids, {'state': 'draft'}, context=context) def _compute_balance_end_real(self, cr, uid, journal_id, context=None): res = False @@ -417,18 +412,35 @@ class account_bank_statement(osv.osv): def number_of_lines_reconciled(self, cr, uid, id, context=None): bsl_obj = self.pool.get('account.bank.statement.line') - return bsl_obj.search_count(cr, uid, [('statement_id','=',id), ('journal_entry_id','!=',False)], context=context) - + return bsl_obj.search_count(cr, uid, [('statement_id', '=', id), ('journal_entry_id', '!=', False)], context=context) + def get_format_currency_js_function(self, cr, uid, id, context=None): """ Returns a string that can be used to instanciate a javascript function. - That function formats a number according to the statement's journal currency """ + That function formats a number according to the statement line's currency or the statement currency""" company_currency = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id - currency_obj = id and self.browse(cr, uid, id, context=context).journal_id.currency or company_currency + st = id and self.browse(cr, uid, id, context=context) + if not st: + return + statement_currency = st.journal_id.currency or company_currency digits = 2 # TODO : from currency_obj - if currency_obj.position == 'after': - return "return amount.toFixed("+str(digits)+") + ' "+currency_obj.symbol+"';" - elif currency_obj.position == 'before': - return "return '"+currency_obj.symbol+" ' + amount.toFixed("+str(digits)+");" + function = "" + done_currencies = [] + for st_line in st.line_ids: + st_line_currency = st_line.currency_id or statement_currency + if st_line_currency.id not in done_currencies: + if st_line_currency.position == 'after': + return_str = "return amount.toFixed(" + str(digits) + ") + ' " + st_line_currency.symbol + "';" + else: + return_str = "return '" + st_line_currency.symbol + " ' + amount.toFixed(" + str(digits) + ");" + function += "if (currency_id === " + str(st_line_currency.id) + "){ " + return_str + " }" + done_currencies.append(st_line_currency.id) + return function + + def link_bank_to_partner(self, cr, uid, ids, context=None): + for statement in self.browse(cr, uid, ids, context=context): + for st_line in statement.line_ids: + if st_line.bank_account_id and st_line.partner_id and st_line.bank_account_id.partner_id.id != st_line.partner_id.id: + self.pool.get('res.partner.bank').write(cr, uid, [st_line.bank_account_id.id], {'partner_id': st_line.partner_id.id}, context=context) class account_bank_statement_line(osv.osv): @@ -444,35 +456,40 @@ class account_bank_statement_line(osv.osv): } for mv_line in reconciliation_data['reconciliation_proposition']: mv_line_ids_selected.append(mv_line['id']) - ret.append(reconciliation_data); - + ret.append(reconciliation_data) + # Check if, now that 'candidate' move lines were selected, there are moves left for statement lines - for reconciliation_data in ret: - if not reconciliation_data['st_line']['has_no_partner']: - if self.get_move_lines_counterparts(cr, uid, reconciliation_data['st_line']['id'], excluded_ids=mv_line_ids_selected, count=True, context=context) == 0: - reconciliation_data['st_line']['no_match'] = True + #for reconciliation_data in ret: + # if not reconciliation_data['st_line']['has_no_partner']: + # st_line = self.browse(cr, uid, reconciliation_data['st_line']['id'], context=context) + # if not self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=mv_line_ids_selected, count=True, context=context): + # reconciliation_data['st_line']['no_match'] = True return ret def get_statement_line_for_reconciliation(self, cr, uid, id, context=None): """ Returns the data required by the bank statement reconciliation use case """ line = self.browse(cr, uid, id, context=context) statement_currency = line.journal_id.currency or line.journal_id.company_id.currency_id + amount = line.amount rml_parser = report_sxw.rml_parse(cr, uid, 'statement_line_widget', context=context) amount_str = line.amount > 0 and line.amount or -line.amount amount_str = rml_parser.formatLang(amount_str, currency_obj=statement_currency) amount_currency_str = "" if line.amount_currency and line.currency_id: - amount_currency_str = rml_parser.formatLang(line.amount_currency, currency_obj=line.currency_id) + amount_currency_str = amount_str + amount_str = rml_parser.formatLang(line.amount_currency, currency_obj=line.currency_id) + amount = line.amount_currency - dict = { + data = { 'id': line.id, 'ref': line.ref, 'note': line.note or "", 'name': line.name, 'date': line.date, - 'amount': line.amount, + 'amount': amount, 'amount_str': amount_str, - 'no_match': self.get_move_lines_counterparts(cr, uid, id, count=True, context=context) == 0 and line.partner_id.id, + 'currency_id': line.currency_id.id or statement_currency.id, + 'no_match': self.get_move_lines_counterparts(cr, uid, line, count=True, context=context) == 0, 'partner_id': line.partner_id.id, 'statement_id': line.statement_id.id, 'account_code': line.journal_id.default_debit_account_id.code, @@ -482,93 +499,116 @@ class account_bank_statement_line(osv.osv): 'has_no_partner': not line.partner_id.id, } if line.partner_id.id: - if line.amount > 0: - dict['open_balance_account_id'] = line.partner_id.property_account_receivable.id - else: - dict['open_balance_account_id'] = line.partner_id.property_account_payable.id - return dict + data['open_balance_account_id'] = line.partner_id.property_account_payable.id + if amount > 0: + data['open_balance_account_id'] = line.partner_id.property_account_receivable.id + return data + + def search_structured_com(self, cr, uid, st_line, context=None): + if not st_line.ref: + return + domain = [('ref', '=', st_line.ref)] + if st_line.partner_id: + domain += [('partner_id', '=', st_line.partner_id.id)] + ids = self.pool.get('account.move.line').search(cr, uid, domain, limit=1, context=context) + return ids and ids[0] or False def get_reconciliation_proposition(self, cr, uid, id, excluded_ids=[], context=None): """ Returns move lines that constitute the best guess to reconcile a statement line. """ st_line = self.browse(cr, uid, id, context=context) company_currency = st_line.journal_id.company_id.currency_id.id statement_currency = st_line.journal_id.currency.id or company_currency - # either use the unsigned debit/credit fields or the signed amount_currency field sign = 1 if statement_currency == company_currency: + amount_field = 'credit' if st_line.amount > 0: amount_field = 'debit' - else: - amount_field = 'credit' else: amount_field = 'amount_currency' if st_line.amount < 0: sign = -1 + # look for structured communication + exact_match_id = self.search_structured_com(cr, uid, st_line, context=context) + if exact_match_id: + return self.make_counter_part_lines(cr, uid, st_line, [exact_match_id], count=False, context=context) + #we don't propose anything if there is no partner detected + if not st_line.partner_id.id: + return [] # look for exact match - exact_match_id = self.get_move_lines_counterparts(cr, uid, id, excluded_ids=excluded_ids, limit=1, additional_domain=[(amount_field,'=',(sign*st_line.amount))]) + exact_match_id = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, limit=1, additional_domain=[(amount_field, '=', (sign * st_line.amount))]) if exact_match_id: return exact_match_id # select oldest move lines if sign == -1: - mv_lines = self.get_move_lines_counterparts(cr, uid, id, excluded_ids=excluded_ids, limit=50, additional_domain=[(amount_field,'<',0)]) + mv_lines = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, limit=50, additional_domain=[(amount_field, '<', 0)]) else: - mv_lines = self.get_move_lines_counterparts(cr, uid, id, excluded_ids=excluded_ids, limit=50, additional_domain=[(amount_field,'>',0)]) + mv_lines = self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids=excluded_ids, limit=50, additional_domain=[(amount_field, '>', 0)]) ret = [] total = 0 # get_move_lines_counterparts inverts debit and credit amount_field = 'debit' if amount_field == 'credit' else 'credit' for line in mv_lines: - if total + line[amount_field] <= st_line.amount: + if total + line[amount_field] <= abs(st_line.amount): ret.append(line) total += line[amount_field] - else: + if total >= abs(st_line.amount): break - return ret - def get_move_lines_counterparts(self, cr, uid, id, excluded_ids=[], str="", offset=0, limit=None, count=False, additional_domain=[], context=None): + def get_move_lines_counterparts_id(self, cr, uid, st_line_id, excluded_ids=[], filter_str="", offset=0, limit=None, count=False, additional_domain=[], context=None): + st_line = self.browse(cr, uid, st_line_id, context=context) + return self.get_move_lines_counterparts(cr, uid, st_line, excluded_ids, filter_str, offset, limit, count, additional_domain, context=context) + + def get_move_lines_counterparts(self, cr, uid, st_line, excluded_ids=[], filter_str="", offset=0, limit=None, count=False, additional_domain=[], context=None): """ Find the move lines that could be used to reconcile a statement line and returns the counterpart that could be created to reconcile them If count is true, only returns the count. - :param integer id: the id of the statement line + :param st_line: the browse record of the statement line :param integers list excluded_ids: ids of move lines that should not be fetched - :param string str: string to filter lines + :param string filter_str: string to filter lines :param integer offset: offset of the request :param integer limit: number of lines to fetch :param boolean count: just return the number of records :param tuples list domain: additional domain restrictions """ - if context is None: - context = {} - - rml_parser = report_sxw.rml_parse(cr, uid, 'statement_line_counterpart_widget', context=context) - st_line = self.browse(cr, uid, id, context=context) - company_currency = st_line.journal_id.company_id.currency_id - statement_currency = st_line.journal_id.currency or company_currency mv_line_pool = self.pool.get('account.move.line') - currency_obj = self.pool.get('res.currency') domain = additional_domain + [ - ('partner_id', '=', st_line.partner_id.id), ('reconcile_id', '=', False), - ('state','=','valid'), - '|',('account_id.type', '=', 'receivable'), - ('account_id.type', '=', 'payable'), #Let the front-end warn the user if he tries to mix payable and receivable in the same reconciliation + ('state', '=', 'valid'), ] + if st_line.partner_id.id: + domain += [('partner_id', '=', st_line.partner_id.id), + '|', ('account_id.type', '=', 'receivable'), + ('account_id.type', '=', 'payable')] + else: + domain += [('account_id.reconcile', '=', True)] + #domain += [('account_id.reconcile', '=', True), ('account_id.type', '=', 'other')] if excluded_ids: domain.append(('id', 'not in', excluded_ids)) - if str: - domain += ['|', ('move_id.name', 'ilike', str), ('move_id.ref', 'ilike', str)] + if filter_str: + if not st_line.partner_id: + domain += [ '|', ('partner_id.name', 'ilike', filter_str)] + domain += ['|', ('move_id.name', 'ilike', filter_str), ('move_id.ref', 'ilike', filter_str)] + line_ids = mv_line_pool.search(cr, uid, domain, offset=offset, limit=limit, order="date_maturity asc, id asc", context=context) + return self.make_counter_part_lines(cr, uid, st_line, line_ids, count=count, context=context) + + def make_counter_part_lines(self, cr, uid, st_line, line_ids, count=False, context=None): + if context is None: + context = {} + mv_line_pool = self.pool.get('account.move.line') + currency_obj = self.pool.get('res.currency') + company_currency = st_line.journal_id.company_id.currency_id + statement_currency = st_line.journal_id.currency or company_currency + rml_parser = report_sxw.rml_parse(cr, uid, 'statement_line_counterpart_widget', context=context) #partially reconciled lines can be displayed only once reconcile_partial_ids = [] - ids = mv_line_pool.search(cr, uid, domain, offset=offset, limit=limit, order="date_maturity asc, id asc", context=context) - if count: nb_lines = 0 - for line in mv_line_pool.browse(cr, uid, ids, context=context): + for line in mv_line_pool.browse(cr, uid, line_ids, context=context): if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids: continue nb_lines += 1 @@ -577,7 +617,7 @@ class account_bank_statement_line(osv.osv): return nb_lines else: ret = [] - for line in mv_line_pool.browse(cr, uid, ids, context=context): + for line in mv_line_pool.browse(cr, uid, line_ids, context=context): if line.reconcile_partial_id and line.reconcile_partial_id.id in reconcile_partial_ids: continue amount_currency_str = "" @@ -595,8 +635,12 @@ class account_bank_statement_line(osv.osv): 'period_name': line.period_id.name, 'journal_name': line.journal_id.name, 'amount_currency_str': amount_currency_str, + 'partner_id': line.partner_id.id, + 'partner_name': line.partner_id.name, + 'has_no_partner': not bool(st_line.partner_id.id), } - if statement_currency.id != company_currency.id and line.currency_id and line.currency_id.id == statement_currency.id: + st_line_currency = st_line.currency_id or statement_currency + if st_line.currency_id and line.currency_id and line.currency_id.id == st_line.currency_id.id: if line.amount_residual_currency < 0: ret_line['debit'] = 0 ret_line['credit'] = -line.amount_residual_currency @@ -613,21 +657,46 @@ class account_bank_statement_line(osv.osv): ret_line['credit'] = line.amount_residual if line.debit != 0 else 0 ctx = context.copy() ctx.update({'date': st_line.date}) - ret_line['debit'] = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, ret_line['debit'], context=ctx) - ret_line['credit'] = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, ret_line['credit'], context=ctx) - ret_line['debit_str'] = rml_parser.formatLang(ret_line['debit'], currency_obj=statement_currency) - ret_line['credit_str'] = rml_parser.formatLang(ret_line['credit'], currency_obj=statement_currency) + ret_line['debit'] = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, ret_line['debit'], context=ctx) + ret_line['credit'] = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, ret_line['credit'], context=ctx) + ret_line['debit_str'] = rml_parser.formatLang(ret_line['debit'], currency_obj=st_line_currency) + ret_line['credit_str'] = rml_parser.formatLang(ret_line['credit'], currency_obj=st_line_currency) ret.append(ret_line) if line.reconcile_partial_id: reconcile_partial_ids.append(line.reconcile_partial_id.id) return ret + def get_currency_rate_line(self, cr, uid, st_line, currency_diff, move_id, context=None): + if currency_diff < 0: + account_id = st_line.company_id.expense_currency_exchange_account_id.id + if not account_id: + raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + else: + account_id = st_line.company_id.income_currency_exchange_account_id.id + if not account_id: + raise osv.except_osv(_('Insufficient Configuration!'), _("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates.")) + return { + 'move_id': move_id, + 'name': _('change') + ': ' + (st_line.name or '/'), + 'period_id': st_line.statement_id.period_id.id, + 'journal_id': st_line.journal_id.id, + 'partner_id': st_line.partner_id.id, + 'company_id': st_line.company_id.id, + 'statement_id': st_line.statement_id.id, + 'debit': currency_diff < 0 and -currency_diff or 0, + 'credit': currency_diff > 0 and currency_diff or 0, + 'date': st_line.date, + 'account_id': account_id + } + def process_reconciliation(self, cr, uid, id, mv_line_dicts, context=None): """ Creates a move line for each item of mv_line_dicts and for the statement line. Reconcile a new move line with its counterpart_move_line_id if specified. Finally, mark the statement line as reconciled by putting the newly created move id in the column journal_entry_id. :param int id: id of the bank statement line :param list of dicts mv_line_dicts: move lines to create. If counterpart_move_line_id is specified, reconcile with it """ + if context is None: + context = {} st_line = self.browse(cr, uid, id, context=context) company_currency = st_line.journal_id.company_id.currency_id statement_currency = st_line.journal_id.currency or company_currency @@ -637,7 +706,7 @@ class account_bank_statement_line(osv.osv): currency_obj = self.pool.get('res.currency') # Checks - if st_line.journal_entry_id.id != False: + if st_line.journal_entry_id.id: raise osv.except_osv(_('Error!'), _('The bank statement line was already reconciled.')) for mv_line_dict in mv_line_dicts: for field in ['debit', 'credit', 'amount_currency']: @@ -657,37 +726,50 @@ class account_bank_statement_line(osv.osv): amount = currency_obj.compute(cr, uid, st_line.statement_id.currency.id, company_currency.id, st_line.amount, context=context) bank_st_move_vals = bs_obj._prepare_bank_move_line(cr, uid, st_line, move_id, amount, company_currency.id, context=context) aml_obj.create(cr, uid, bank_st_move_vals, context=context) - st_line_currency_rate = bank_st_move_vals['amount_currency'] and statement_currency.id == company_currency.id and (bank_st_move_vals['amount_currency'] / st_line.amount) or False - st_line_currency = bank_st_move_vals['currency_id'] # Complete the dicts - st_line_statement_id = st_line.statement_id.id - st_line_journal_id = st_line.journal_id.id - st_line_partner_id = st_line.partner_id.id - st_line_company_id = st_line.company_id.id - st_line_period_id = st_line.statement_id.period_id.id + st_line_currency = st_line.currency_id or statement_currency + st_line_currency_rate = st_line.currency_id and statement_currency.id == company_currency.id and (st_line.amount_currency / st_line.amount) or False + to_create = [] for mv_line_dict in mv_line_dicts: mv_line_dict['ref'] = move_name mv_line_dict['move_id'] = move_id - mv_line_dict['period_id'] = st_line_period_id - mv_line_dict['journal_id'] = st_line_journal_id - mv_line_dict['partner_id'] = st_line_partner_id - mv_line_dict['company_id'] = st_line_company_id - mv_line_dict['statement_id'] = st_line_statement_id + mv_line_dict['period_id'] = st_line.statement_id.period_id.id + mv_line_dict['journal_id'] = st_line.journal_id.id + mv_line_dict['company_id'] = st_line.company_id.id + mv_line_dict['statement_id'] = st_line.statement_id.id if mv_line_dict.get('counterpart_move_line_id'): mv_line = aml_obj.browse(cr, uid, mv_line_dict['counterpart_move_line_id'], context=context) mv_line_dict['account_id'] = mv_line.account_id.id - if statement_currency.id != company_currency.id: + if st_line_currency.id != company_currency.id: mv_line_dict['amount_currency'] = mv_line_dict['debit'] - mv_line_dict['credit'] - mv_line_dict['currency_id'] = statement_currency.id - mv_line_dict['debit'] = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['debit']) - mv_line_dict['credit'] = currency_obj.compute(cr, uid, statement_currency.id, company_currency.id, mv_line_dict['credit']) - elif st_line_currency and st_line_currency_rate: - mv_line_dict['amount_currency'] = self.pool.get('res.currency').round(cr, uid, st_line.currency_id, (mv_line_dict['debit'] - mv_line_dict['credit']) * st_line_currency_rate) - mv_line_dict['currency_id'] = st_line_currency - + mv_line_dict['currency_id'] = st_line_currency.id + if st_line.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate: + debit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['debit'] / st_line_currency_rate) + credit_at_current_rate = self.pool.get('res.currency').round(cr, uid, company_currency, mv_line_dict['credit'] / st_line_currency_rate) + else: + debit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=context) + credit_at_current_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=context) + if mv_line_dict.get('counterpart_move_line_id'): + #post an account line that use the same currency rate than the counterpart (to balance the account) and post the difference in another line + ctx = context.copy() + ctx['date'] = mv_line.date + debit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['debit'], context=ctx) + credit_at_old_rate = currency_obj.compute(cr, uid, st_line_currency.id, company_currency.id, mv_line_dict['credit'], context=ctx) + mv_line_dict['credit'] = credit_at_old_rate + mv_line_dict['debit'] = debit_at_old_rate + if debit_at_old_rate - debit_at_current_rate: + currency_diff = debit_at_current_rate - debit_at_old_rate + to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) + if credit_at_old_rate - credit_at_current_rate: + currency_diff = credit_at_current_rate - credit_at_old_rate + to_create.append(self.get_currency_rate_line(cr, uid, st_line, currency_diff, move_id, context=context)) + else: + mv_line_dict['debit'] = debit_at_current_rate + mv_line_dict['credit'] = credit_at_current_rate + to_create.append(mv_line_dict) # Create move lines move_line_pairs_to_reconcile = [] - for mv_line_dict in mv_line_dicts: + for mv_line_dict in to_create: counterpart_move_line_id = None # NB : this attribute is irrelevant for aml_obj.create() and needs to be removed from the dict if mv_line_dict.get('counterpart_move_line_id'): counterpart_move_line_id = mv_line_dict['counterpart_move_line_id'] @@ -723,7 +805,7 @@ class account_bank_statement_line(osv.osv): 'bank_account_id': fields.many2one('res.partner.bank','Bank Account'), 'statement_id': fields.many2one('account.bank.statement', 'Statement', select=True, required=True, ondelete='cascade'), 'journal_id': fields.related('statement_id', 'journal_id', type='many2one', relation='account.journal', string='Journal', store=True, readonly=True), - 'ref': fields.char('Reference', size=32), + 'ref': fields.char('Structured Communication'), 'note': fields.text('Notes'), 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of bank statement lines."), 'company_id': fields.related('statement_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True), diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index eb43830f8f6..96d63c545c6 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -295,7 +295,8 @@ class account_invoice(osv.osv): }, multi='all'), 'currency_id': fields.many2one('res.currency', 'Currency', required=True, readonly=True, states={'draft':[('readonly',False)]}, track_visibility='always'), - 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}), + 'journal_id': fields.many2one('account.journal', 'Journal', required=True, readonly=True, states={'draft':[('readonly',False)]}, + domain="[('type', 'in', {'out_invoice': ['sale'], 'out_refund': ['sale_refund'], 'in_refund': ['purchase_refund'], 'in_invoice': ['purchase']}.get(type, [])), ('company_id', '=', company_id)]"), 'company_id': fields.many2one('res.company', 'Company', required=True, change_default=True, readonly=True, states={'draft':[('readonly',False)]}), 'check_total': fields.float('Verification Total', digits_compute=dp.get_precision('Account'), readonly=True, states={'draft':[('readonly',False)]}), 'reconciled': fields.function(_reconciled, string='Paid/Reconciled', type='boolean', diff --git a/addons/account/account_move_line.py b/addons/account/account_move_line.py index 3d8d8302f32..eb705b42995 100644 --- a/addons/account/account_move_line.py +++ b/addons/account/account_move_line.py @@ -127,8 +127,8 @@ class account_move_line(osv.osv): if move_line.reconcile_id: continue - if not move_line.account_id.type in ('payable', 'receivable'): - #this function does not suport to be used on move lines not related to payable or receivable accounts + if not move_line.account_id.reconcile: + #this function does not suport to be used on move lines not related to a reconcilable account continue if move_line.currency_id: @@ -741,6 +741,8 @@ class account_move_line(osv.osv): def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): if context is None: context = {} + if context.get('fiscalyear'): + args.append(('period_id.fiscalyear_id', '=', context.get('fiscalyear', False))) if context and context.get('next_partner_only', False): if not context.get('partner_id', False): partner = self.list_partners_to_reconcile(cr, uid, context=context) @@ -823,7 +825,7 @@ class account_move_line(osv.osv): 'line_partial_ids': map(lambda x: (4,x,False), merges+unmerge) }, context=context) move_rec_obj.reconcile_partial_check(cr, uid, [r_id] + merges_rec, context=context) - return True + return r_id def reconcile(self, cr, uid, ids, type='auto', writeoff_acc_id=False, writeoff_period_id=False, writeoff_journal_id=False, context=None): account_obj = self.pool.get('account.account') diff --git a/addons/account/account_view.xml b/addons/account/account_view.xml index 8afa19a63b0..1d340001c55 100644 --- a/addons/account/account_view.xml +++ b/addons/account/account_view.xml @@ -351,7 +351,7 @@ diff --git a/addons/account/demo/account_bank_statement.xml b/addons/account/demo/account_bank_statement.xml index 693dbc876b5..b79d8b95cd6 100644 --- a/addons/account/demo/account_bank_statement.xml +++ b/addons/account/demo/account_bank_statement.xml @@ -15,7 +15,7 @@ - 001 + @@ -26,7 +26,7 @@ - 002 + SAJ2014002 @@ -37,7 +37,7 @@ - 003 + @@ -47,7 +47,7 @@ - 004 + diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.css b/addons/account/static/src/css/account_bank_statement_reconciliation.css index 626d1f32dab..05caeab2655 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.css +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.css @@ -120,8 +120,6 @@ cursor: pointer; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line.no_match:not(.no_partner) .toggle_match { visibility: hidden !important; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line.no_partner .partner_name, .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line.no_partner .line_open_balance { - display: none !important; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line > table > tbody > tr:nth-child(1) > td table { margin-bottom: 10px; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line table.details td:first-child { @@ -202,10 +200,6 @@ cursor: pointer; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view td:nth-child(6) { border-left: 1px solid black; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view tr.initial_line > td:nth-child(5) { - border-top: 1px solid black; } - .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .accounting_view tr.initial_line > td:nth-child(6) { - border-top: 1px solid black; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match .match_controls { padding: 0 0 5px 18px; } .openerp .oe_bank_statement_reconciliation .oe_bank_statement_reconciliation_line .match .match_controls .filter { diff --git a/addons/account/static/src/css/account_bank_statement_reconciliation.scss b/addons/account/static/src/css/account_bank_statement_reconciliation.scss index 3b98689bcbc..248536b7a9b 100644 --- a/addons/account/static/src/css/account_bank_statement_reconciliation.scss +++ b/addons/account/static/src/css/account_bank_statement_reconciliation.scss @@ -194,12 +194,6 @@ $initialLineBackground: #f0f0f0; } } - &.no_partner { - .partner_name, .line_open_balance { - display: none !important; - } - } - /* gap between accounting_view and action view */ > table > tbody > tr:nth-child(1) > td table { margin-bottom: 10px; @@ -341,10 +335,6 @@ $initialLineBackground: #f0f0f0; // accounting "T" td:nth-child(6) { border-left: $accountingBorder; } - tr.initial_line > td { - &:nth-child(5) { border-top: $accountingBorder; } - &:nth-child(6) { border-top: $accountingBorder; } - } } diff --git a/addons/account/static/src/js/account_widgets.js b/addons/account/static/src/js/account_widgets.js index a7e83a2146b..8f5786e978f 100644 --- a/addons/account/static/src/js/account_widgets.js +++ b/addons/account/static/src/js/account_widgets.js @@ -171,7 +171,7 @@ openerp.account = function (instance) { deferred_promises.push(self.model_bank_statement .call("get_format_currency_js_function", [self.statement_id]) .then(function(data){ - self.formatCurrency = new Function("amount", data); + self.formatCurrency = new Function("amount, currency_id", data); }) ); @@ -245,7 +245,7 @@ openerp.account = function (instance) { keyboardShortcutsHandler: function(e) { var self = this; - if (e.which === 13 && (e.ctrlKey || e.metaKey)) { + if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) { $.each(self.getChildren(), function(i, o){ if (o.is_valid && o.persistAndDestroy()) { self.lines_reconciled_with_ctrl_enter++; @@ -789,6 +789,9 @@ openerp.account = function (instance) { line.q_amount = (line.debit !== 0 ? "- "+line.q_debit : "") + (line.credit !== 0 ? line.q_credit : ""); line.q_popover = QWeb.render("bank_statement_reconciliation_move_line_details", {line: line}); line.q_label = line.name; + if (line.has_no_partner){ + line.q_label = line.partner_name + ': ' +line.q_label; + } // WARNING : pretty much of a ugly hack // The value of account_move.ref is either the move's communication or it's name without the slashes @@ -981,6 +984,7 @@ openerp.account = function (instance) { lineOpenBalanceClickHandler: function() { var self = this; if (self.get("mode") === "create") { + self.addLineBeingEdited(); self.set("mode", "match"); } else { self.set("mode", "create"); @@ -1038,7 +1042,8 @@ openerp.account = function (instance) { var slice_start = self.get("pager_index") * self.max_move_lines_displayed; var slice_end = (self.get("pager_index")+1) * self.max_move_lines_displayed; _( _.filter(self.mv_lines_deselected, function(o){ - return o.name.indexOf(self.filter) !== -1 || o.ref.indexOf(self.filter) !== -1 }) + return o.q_label.indexOf(self.filter) !== -1 || (o.ref && o.ref.indexOf(self.filter) !== -1) + }) .slice(slice_start, slice_end)).each(function(line){ var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); self.bindPopoverTo($line.find(".line_info_button")); @@ -1057,7 +1062,6 @@ openerp.account = function (instance) { updatePagerControls: function() { var self = this; - if (self.get("pager_index") === 0) self.$(".pager_control_left").addClass("disabled"); else @@ -1075,7 +1079,7 @@ openerp.account = function (instance) { balanceChanged: function() { var self = this; var balance = self.get("balance"); - + self.$(".tbody_open_balance").empty(); // Special case hack : no identified partner if (self.st_line.has_no_partner) { if (Math.abs(balance).toFixed(3) === "0.000") { @@ -1088,19 +1092,23 @@ openerp.account = function (instance) { self.$(".button_ok").attr("disabled", "disabled"); self.$(".button_ok").text("OK"); self.is_valid = false; + var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : ""); + var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : ""); + var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {debit: debit, credit: credit, account_code: self.map_account_id_code[self.st_line.open_balance_account_id]})); + $line.find('.js_open_balance')[0].innerHTML = "Choose counterpart"; + self.$(".tbody_open_balance").append($line); } return; } - self.$(".tbody_open_balance").empty(); if (Math.abs(balance).toFixed(3) === "0.000") { self.$(".button_ok").addClass("oe_highlight"); self.$(".button_ok").text("OK"); } else { self.$(".button_ok").removeClass("oe_highlight"); self.$(".button_ok").text("Keep open"); - var debit = (balance > 0 ? self.formatCurrency(balance) : ""); - var credit = (balance < 0 ? self.formatCurrency(-1*balance) : ""); + var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : ""); + var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : ""); var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {debit: debit, credit: credit, account_code: self.map_account_id_code[self.st_line.open_balance_account_id]})); self.$(".tbody_open_balance").append($line); } @@ -1111,21 +1119,15 @@ openerp.account = function (instance) { self.$(".action_pane.active").removeClass("active"); - // Special case hack : if no_partner, either inactive or create + // Special case hack : if no_partner and mode == inactive if (self.st_line.has_no_partner) { if (self.get("mode") === "inactive") { self.$(".match").slideUp(self.animation_speed); self.$(".create").slideUp(self.animation_speed); self.$(".toggle_match").removeClass("visible_toggle"); self.el.dataset.mode = "inactive"; - } else { - self.initializeCreateForm(); - self.$(".match").slideUp(self.animation_speed); - self.$(".create").slideDown(self.animation_speed); - self.$(".toggle_match").addClass("visible_toggle"); - self.el.dataset.mode = "create"; - } - return; + return; + } } if (self.get("mode") === "inactive") { @@ -1198,6 +1200,8 @@ openerp.account = function (instance) { var self = this; var line_created_being_edited = self.get("line_created_being_edited"); line_created_being_edited[0][elt.corresponding_property] = val.newValue; + + line_created_being_edited[0].currency_id = self.st_line.currency_id; // Specific cases if (elt === self.account_id_field) @@ -1215,7 +1219,7 @@ openerp.account = function (instance) { var tax = data.taxes[0]; var tax_account_id = (amount > 0 ? tax.account_collected_id : tax.account_paid_id) line_created_being_edited[0].amount = (data.total.toFixed(3) === amount.toFixed(3) ? amount : data.total); - line_created_being_edited[1] = {id: line_created_being_edited[0].id, account_id: tax_account_id, account_num: self.map_account_id_code[tax_account_id], label: tax.name, amount: tax.amount, no_remove_action: true}; + line_created_being_edited[1] = {id: line_created_being_edited[0].id, account_id: tax_account_id, account_num: self.map_account_id_code[tax_account_id], label: tax.name, amount: tax.amount, no_remove_action: true, currency_id: self.st_line.currency_id}; } ); } else { @@ -1228,10 +1232,9 @@ openerp.account = function (instance) { $.when(deferred_tax).then(function(){ // Format amounts if (line_created_being_edited[0].amount) - line_created_being_edited[0].amount_str = self.formatCurrency(Math.abs(line_created_being_edited[0].amount)); + line_created_being_edited[0].amount_str = self.formatCurrency(Math.abs(line_created_being_edited[0].amount), line_created_being_edited[0].currency_id); if (line_created_being_edited[1] && line_created_being_edited[1].amount) - line_created_being_edited[1].amount_str = self.formatCurrency(Math.abs(line_created_being_edited[1].amount)); - + line_created_being_edited[1].amount_str = self.formatCurrency(Math.abs(line_created_being_edited[1].amount), line_created_being_edited[0].currency_id); self.set("line_created_being_edited", line_created_being_edited); self.createdLinesChanged(); // TODO For some reason, previous line doesn't trigger change handler }); @@ -1268,10 +1271,10 @@ openerp.account = function (instance) { line.initial_amount = line.debit !== 0 ? line.debit : -1 * line.credit; if (balance < 0) { line.debit -= balance; - line.debit_str = self.formatCurrency(line.debit); + line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id); } else { line.credit -= balance; - line.credit_str = self.formatCurrency(line.credit); + line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id); } line.propose_partial_reconcile = false; line.partial_reconcile = true; @@ -1291,12 +1294,13 @@ openerp.account = function (instance) { }, unpartialReconcileLine: function(line) { + var self = this; if (line.initial_amount > 0) { line.debit = line.initial_amount; - line.debit_str = this.formatCurrency(line.debit); + line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id); } else { line.credit = -1 * line.initial_amount; - line.credit_str = this.formatCurrency(line.credit); + line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id); } line.propose_partial_reconcile = true; line.partial_reconcile = false; @@ -1359,16 +1363,15 @@ openerp.account = function (instance) { if (limit > 0) { // Load move lines deferred_move_lines = self.model_bank_statement_line - .call("get_move_lines_counterparts", [self.st_line.id, excluded_ids, self.filter, offset, limit]) + .call("get_move_lines_counterparts_id", [self.st_line.id, excluded_ids, self.filter, offset, limit]) .then(function (lines) { _(lines).each(self.decorateMoveLine.bind(self)); move_lines = lines; }); } - // Fetch the number of move lines corresponding to this statement line and this filter var deferred_total_move_lines_num = self.model_bank_statement_line - .call("get_move_lines_counterparts", [self.st_line.id, excluded_ids, self.filter, offset, limit, true]) + .call("get_move_lines_counterparts_id", [self.st_line.id, excluded_ids, self.filter, 0, undefined, true]) .then(function(num){ move_lines_num = num; }); diff --git a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml index f311207ad8c..b5a8492545d 100644 --- a/addons/account/static/src/xml/account_bank_statement_reconciliation.xml +++ b/addons/account/static/src/xml/account_bank_statement_reconciliation.xml @@ -185,7 +185,7 @@ - Open balance + Open balance diff --git a/addons/account/views/report_agedpartnerbalance.xml b/addons/account/views/report_agedpartnerbalance.xml index 019f32a35de..c1f9047e330 100644 --- a/addons/account/views/report_agedpartnerbalance.xml +++ b/addons/account/views/report_agedpartnerbalance.xml @@ -99,7 +99,7 @@ - + diff --git a/addons/account/wizard/account_chart.py b/addons/account/wizard/account_chart.py index 38df2f7484d..de652a947f0 100644 --- a/addons/account/wizard/account_chart.py +++ b/addons/account/wizard/account_chart.py @@ -62,9 +62,10 @@ class account_chart(osv.osv_memory): ORDER BY p.date_stop DESC LIMIT 1) AS period_stop''', (fiscalyear_id, fiscalyear_id)) periods = [i[0] for i in cr.fetchall()] - if periods and len(periods) > 1: + if periods: start_period = periods[0] - end_period = periods[1] + if len(periods) > 1: + end_period = periods[1] res['value'] = {'period_from': start_period, 'period_to': end_period} else: res['value'] = {'period_from': False, 'period_to': False} diff --git a/addons/account_analytic_analysis/account_analytic_analysis.py b/addons/account_analytic_analysis/account_analytic_analysis.py index 519660fb708..9550f77bcab 100644 --- a/addons/account_analytic_analysis/account_analytic_analysis.py +++ b/addons/account_analytic_analysis/account_analytic_analysis.py @@ -22,7 +22,6 @@ from dateutil.relativedelta import relativedelta import datetime import logging import time -import traceback from openerp.osv import osv, fields from openerp.osv.orm import intersect, except_orm @@ -73,6 +72,7 @@ class account_analytic_invoice_line(osv.osv): result = {} res = self.pool.get('product.product').browse(cr, uid, product, context=local_context) + price = False if price_unit is not False: price = price_unit elif pricelist_id: @@ -746,29 +746,32 @@ class account_analytic_account(osv.osv): contract_ids = ids else: contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')]) - for contract in self.browse(cr, uid, contract_ids, context=context): - try: - invoice_values = self._prepare_invoice(cr, uid, contract, context=context) - invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context)) - next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d") - interval = contract.recurring_interval - if contract.recurring_rule_type == 'daily': - new_date = next_date+relativedelta(days=+interval) - elif contract.recurring_rule_type == 'weekly': - new_date = next_date+relativedelta(weeks=+interval) - elif contract.recurring_rule_type == 'monthly': - new_date = next_date+relativedelta(months=+interval) - else: - new_date = next_date+relativedelta(years=+interval) - self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context) - if automatic: - cr.commit() - except Exception: - if automatic: - cr.rollback() - _logger.error(traceback.format_exc()) - else: - raise + if contract_ids: + cr.execute('SELECT company_id, array_agg(id) as ids FROM account_analytic_account WHERE id IN %s GROUP BY company_id', (tuple(contract_ids),)) + for company_id, ids in cr.fetchall(): + for contract in self.browse(cr, uid, ids, context=dict(context, company_id=company_id, force_company=company_id)): + try: + invoice_values = self._prepare_invoice(cr, uid, contract, context=context) + invoice_ids.append(self.pool['account.invoice'].create(cr, uid, invoice_values, context=context)) + next_date = datetime.datetime.strptime(contract.recurring_next_date or current_date, "%Y-%m-%d") + interval = contract.recurring_interval + if contract.recurring_rule_type == 'daily': + new_date = next_date+relativedelta(days=+interval) + elif contract.recurring_rule_type == 'weekly': + new_date = next_date+relativedelta(weeks=+interval) + elif contract.recurring_rule_type == 'monthly': + new_date = next_date+relativedelta(months=+interval) + else: + new_date = next_date+relativedelta(years=+interval) + self.write(cr, uid, [contract.id], {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, context=context) + if automatic: + cr.commit() + except Exception: + if automatic: + cr.rollback() + _logger.exception('Fail to create recurring invoice for contract %s', contract.code) + else: + raise return invoice_ids class account_analytic_account_summary_user(osv.osv): diff --git a/addons/account_voucher/voucher_sales_purchase_view.xml b/addons/account_voucher/voucher_sales_purchase_view.xml index d0064d6336c..f8c2d65f63b 100644 --- a/addons/account_voucher/voucher_sales_purchase_view.xml +++ b/addons/account_voucher/voucher_sales_purchase_view.xml @@ -248,7 +248,7 @@ - + diff --git a/addons/auth_crypt/auth_crypt.py b/addons/auth_crypt/auth_crypt.py index 4651d27fe7d..6c9deb51e92 100644 --- a/addons/auth_crypt/auth_crypt.py +++ b/addons/auth_crypt/auth_crypt.py @@ -1,137 +1,41 @@ -# -# Implements encrypting functions. -# -# Copyright (c) 2008, F S 3 Consulting Inc. -# -# Maintainer: -# Alec Joseph Rivera (agifs3.ph) -# refactored by Antony Lesuisse openerp.com> -# - -import hashlib -import hmac import logging -from random import sample -from string import ascii_letters, digits + +from passlib.context import CryptContext import openerp from openerp.osv import fields, osv _logger = logging.getLogger(__name__) -magic_md5 = '$1$' -magic_sha256 = '$5$' - -def gen_salt(length=8, symbols=None): - if symbols is None: - symbols = ascii_letters + digits - return ''.join(sample(symbols, length)) - -def md5crypt( raw_pw, salt, magic=magic_md5 ): - """ md5crypt FreeBSD crypt(3) based on but different from md5 - - The md5crypt is based on Mark Johnson's md5crypt.py, which in turn is - based on FreeBSD src/lib/libcrypt/crypt.c (1.2) by Poul-Henning Kamp. - Mark's port can be found in ActiveState ASPN Python Cookbook. Kudos to - Poul and Mark. -agi - - Original license: - - * "THE BEER-WARE LICENSE" (Revision 42): - * - * wrote this file. As long as you retain this - * notice you can do whatever you want with this stuff. If we meet some - * day, and you think this stuff is worth it, you can buy me a beer in - * return. - * - * Poul-Henning Kamp - """ - raw_pw = raw_pw.encode('utf-8') - salt = salt.encode('utf-8') - hash = hashlib.md5() - hash.update( raw_pw + magic + salt ) - st = hashlib.md5() - st.update( raw_pw + salt + raw_pw) - stretch = st.digest() - - for i in range( 0, len( raw_pw ) ): - hash.update( stretch[i % 16] ) - - i = len( raw_pw ) - - while i: - if i & 1: - hash.update('\x00') - else: - hash.update( raw_pw[0] ) - i >>= 1 - - saltedmd5 = hash.digest() - - for i in range( 1000 ): - hash = hashlib.md5() - - if i & 1: - hash.update( raw_pw ) - else: - hash.update( saltedmd5 ) - - if i % 3: - hash.update( salt ) - if i % 7: - hash.update( raw_pw ) - if i & 1: - hash.update( saltedmd5 ) - else: - hash.update( raw_pw ) - - saltedmd5 = hash.digest() - - itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' - - rearranged = '' - for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)): - v = ord( saltedmd5[a] ) << 16 | ord( saltedmd5[b] ) << 8 | ord( saltedmd5[c] ) - - for i in range(4): - rearranged += itoa64[v & 0x3f] - v >>= 6 - - v = ord( saltedmd5[11] ) - - for i in range( 2 ): - rearranged += itoa64[v & 0x3f] - v >>= 6 - - return magic + salt + '$' + rearranged - -def sh256crypt(cls, password, salt, magic=magic_sha256): - iterations = 1000 - # see http://en.wikipedia.org/wiki/PBKDF2 - result = password.encode('utf8') - for i in xrange(cls.iterations): - result = hmac.HMAC(result, salt, hashlib.sha256).digest() # uses HMAC (RFC 2104) to apply salt - result = result.encode('base64') # doesnt seem to be crypt(3) compatible - return '%s%s$%s' % (magic_sha256, salt, result) +default_crypt_context = CryptContext( + # kdf which can be verified by the context. The default encryption kdf is + # the first of the list + ['pbkdf2_sha512', 'md5_crypt'], + # deprecated algorithms are still verified as usual, but ``needs_update`` + # will indicate that the stored hash should be replaced by a more recent + # algorithm. Passlib 1.6 supports an `auto` value which deprecates any + # algorithm but the default, but Debian only provides 1.5 so... + deprecated=['md5_crypt'], +) class res_users(osv.osv): _inherit = "res.users" + def init(self, cr): + _logger.info("Hashing passwords, may be slow for databases with many users...") + cr.execute("SELECT id, password FROM res_users" + " WHERE password IS NOT NULL" + " AND password != ''") + for uid, pwd in cr.fetchall(): + self._set_password(cr, openerp.SUPERUSER_ID, uid, pwd) + def set_pw(self, cr, uid, id, name, value, args, context): if value: - encrypted = md5crypt(value, gen_salt()) - cr.execute("update res_users set password='', password_crypt=%s where id=%s", (encrypted, id)) - del value + self._set_password(cr, uid, id, value, context=context) def get_pw( self, cr, uid, ids, name, args, context ): cr.execute('select id, password from res_users where id in %s', (tuple(map(int, ids)),)) - stored_pws = cr.fetchall() - res = {} - - for id, stored_pw in stored_pws: - res[id] = stored_pw - - return res + return dict(cr.fetchall()) _columns = { 'password': fields.function(get_pw, fnct_inv=set_pw, type='char', string='Password', invisible=True, store=True), @@ -141,27 +45,51 @@ class res_users(osv.osv): def check_credentials(self, cr, uid, password): # convert to base_crypt if needed cr.execute('SELECT password, password_crypt FROM res_users WHERE id=%s AND active', (uid,)) + encrypted = None if cr.rowcount: - stored_password, stored_password_crypt = cr.fetchone() - if stored_password and not stored_password_crypt: - salt = gen_salt() - stored_password_crypt = md5crypt(stored_password, salt) - cr.execute("UPDATE res_users SET password='', password_crypt=%s WHERE id=%s", (stored_password_crypt, uid)) + stored, encrypted = cr.fetchone() + if stored and not encrypted: + self._set_password(cr, uid, uid, stored) try: return super(res_users, self).check_credentials(cr, uid, password) except openerp.exceptions.AccessDenied: - # check md5crypt - if stored_password_crypt: - if stored_password_crypt[:len(magic_md5)] == magic_md5: - salt = stored_password_crypt[len(magic_md5):11] - if stored_password_crypt == md5crypt(password, salt): - return - elif stored_password_crypt[:len(magic_md5)] == magic_sha256: - salt = stored_password_crypt[len(magic_md5):11] - if stored_password_crypt == md5crypt(password, salt): - return - # Reraise password incorrect + if encrypted: + valid_pass, replacement = self._crypt_context(cr, uid, uid)\ + .verify_and_update(password, encrypted) + if replacement is not None: + self._set_encrypted_password(cr, uid, uid, replacement) + if valid_pass: + return + raise + def _set_password(self, cr, uid, id, password, context=None): + """ Encrypts then stores the provided plaintext password for the user + ``id`` + """ + encrypted = self._crypt_context(cr, uid, id, context=context).encrypt(password) + self._set_encrypted_password(cr, uid, id, encrypted, context=context) + + def _set_encrypted_password(self, cr, uid, id, encrypted, context=None): + """ Store the provided encrypted password to the database, and clears + any plaintext password + + :param uid: id of the current user + :param id: id of the user on which the password should be set + """ + cr.execute( + "UPDATE res_users SET password='', password_crypt=%s WHERE id=%s", + (encrypted, id)) + + def _crypt_context(self, cr, uid, id, context=None): + """ Passlib CryptContext instance used to encrypt and verify + passwords. Can be overridden if technical, legal or political matters + require different kdfs than the provided default. + + Requires a CryptContext as deprecation and upgrade notices are used + internally + """ + return default_crypt_context + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 99f134a5cbd..7652420d7c6 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -74,7 +74,7 @@ class OAuthLogin(Home): state = dict( d=request.session.db, p=provider['id'], - r=redirect, + r=werkzeug.url_quote_plus(redirect), ) token = request.params.get('token') if token: @@ -143,7 +143,7 @@ class OAuthController(http.Controller): cr.commit() action = state.get('a') menu = state.get('m') - redirect = state.get('r') + redirect = werkzeug.url_unquote_plus(state['r']) if state.get('r') else False url = '/web' if redirect: url = redirect diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index eed958e20d9..37669840818 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -102,7 +102,7 @@ openerp.calendar = function(instance) { var self = this; var action_url = ''; - action_url = _.str.sprintf('/?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id); + action_url = _.str.sprintf('/web?db=%s#id=%s&view_type=form&model=calendar.event', db, meeting_id); var reload_page = function(){ return location.replace(action_url); diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py index 641c490cabb..e38bf973cdf 100644 --- a/addons/crm_claim/crm_claim.py +++ b/addons/crm_claim/crm_claim.py @@ -45,13 +45,10 @@ class crm_claim_stage(osv.osv): help="Link between stages and sales teams. When set, this limitate the current stage to the selected sales teams."), 'case_default': fields.boolean('Common to All Teams', help="If you check this field, this stage will be proposed by default on each sales team. It will not assign this stage to existing teams."), - 'fold': fields.boolean('Hide in Views when Empty', - help="This stage is not visible, for example in status bar or kanban view, when there are no records in that stage to display."), } _defaults = { 'sequence': lambda *args: 1, - 'fold': False, } class crm_claim(osv.osv): diff --git a/addons/crm_claim/crm_claim_data.xml b/addons/crm_claim/crm_claim_data.xml index fca7ac51178..86cf988ff45 100644 --- a/addons/crm_claim/crm_claim_data.xml +++ b/addons/crm_claim/crm_claim_data.xml @@ -61,7 +61,6 @@ Rejected 29 - diff --git a/addons/crm_claim/crm_claim_view.xml b/addons/crm_claim/crm_claim_view.xml index 43df3d069bf..6180b85660d 100644 --- a/addons/crm_claim/crm_claim_view.xml +++ b/addons/crm_claim/crm_claim_view.xml @@ -51,7 +51,6 @@ - diff --git a/addons/event/report/report_event_registration.py b/addons/event/report/report_event_registration.py index e76b5c60d1f..c4a4dfd6b2d 100644 --- a/addons/event/report/report_event_registration.py +++ b/addons/event/report/report_event_registration.py @@ -32,7 +32,7 @@ class report_event_registration(osv.osv): 'draft_state': fields.integer(' # No of Draft Registrations', size=20), 'confirm_state': fields.integer(' # No of Confirmed Registrations', size=20), 'seats_max': fields.integer('Max Seats'), - 'nbevent': fields.integer('Number Of Events'), + 'nbevent': fields.integer('Number of Registrations'), 'event_type': fields.many2one('event.type', 'Event Type'), 'registration_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Attended'), ('cancel', 'Cancelled')], 'Registration State', readonly=True, required=True), 'event_state': fields.selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Event State', readonly=True, required=True), @@ -59,7 +59,7 @@ class report_event_registration(osv.osv): r.name AS name_registration, e.company_id AS company_id, e.date_begin AS event_date, - count(e.id) AS nbevent, + count(r.id) AS nbevent, CASE WHEN r.state IN ('draft') THEN r.nb_register ELSE 0 END AS draft_state, CASE WHEN r.state IN ('open','done') THEN r.nb_register ELSE 0 END AS confirm_state, e.type AS event_type, diff --git a/addons/event/static/src/css/event.css b/addons/event/static/src/css/event.css index ff5bfaba19b..53c03cc03f0 100644 --- a/addons/event/static/src/css/event.css +++ b/addons/event/static/src/css/event.css @@ -1,7 +1,7 @@ .oe_event_date{ border-top-left-radius:3px; border-top-right-radius:3px; - font-size: 48px; + font-size: 36px; height: auto; font-weight: bold; text-align: center; diff --git a/addons/event_sale/event_sale.py b/addons/event_sale/event_sale.py index ed99e6a39cc..69ad0cb6af6 100644 --- a/addons/event_sale/event_sale.py +++ b/addons/event_sale/event_sale.py @@ -245,7 +245,8 @@ class event_ticket(osv.osv): ] def onchange_product_id(self, cr, uid, ids, product_id=False, context=None): - return {'value': {'price': self.pool.get("product.product").browse(cr, uid, product_id).list_price or 0}} + price = self.pool.get("product.product").browse(cr, uid, product_id).list_price if product_id else 0 + return {'value': {'price': price}} class event_registration(osv.osv): diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index ddc08336293..faed62f8da9 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -58,7 +58,7 @@ def start_end_date_for_period(period, default_start_date=False, default_end_date end_date = default_end_date if start_date and end_date: - return (start_date.strftime(DF), end_date.strftime(DF)) + return (datetime.strftime(start_date, DF), datetime.strftime(end_date, DF)) else: return (start_date, end_date) diff --git a/addons/google_calendar/google_calendar.py b/addons/google_calendar/google_calendar.py index 543c81977aa..dd1d45c05fe 100644 --- a/addons/google_calendar/google_calendar.py +++ b/addons/google_calendar/google_calendar.py @@ -699,7 +699,7 @@ class google_calendar(osv.AbstractModel): for att in att_obj.browse(cr, uid, my_att_ids, context=context): event = att.event_id - base_event_id = att.google_internal_event_id.split('_')[0] + base_event_id = att.google_internal_event_id.rsplit('_', 1)[0] if base_event_id not in event_to_synchronize: event_to_synchronize[base_event_id] = {} @@ -721,7 +721,7 @@ class google_calendar(osv.AbstractModel): for event in all_event_from_google.values(): event_id = event.get('id') - base_event_id = event_id.split('_')[0] + base_event_id = event_id.rsplit('_', 1)[0] if base_event_id not in event_to_synchronize: event_to_synchronize[base_event_id] = {} @@ -786,7 +786,7 @@ class google_calendar(osv.AbstractModel): if actSrc == 'OE': self.delete_an_event(cr, uid, current_event[0], context=context) elif actSrc == 'GG': - new_google_event_id = event.GG.event['id'].split('_')[1] + new_google_event_id = event.GG.event['id'].rsplit('_', 1)[1] if 'T' in new_google_event_id: new_google_event_id = new_google_event_id.replace('T', '')[:-1] else: @@ -795,7 +795,8 @@ class google_calendar(osv.AbstractModel): if event.GG.status: parent_event = {} if not event_to_synchronize[base_event][0][1].OE.event_id: - event_to_synchronize[base_event][0][1].OE.event_id = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].split('_')[0])], ['event_id'], context=context_novirtual)[0].get('event_id')[0] + main_ev = att_obj.search_read(cr, uid, [('google_internal_event_id', '=', event.GG.event['id'].rsplit('_', 1)[0])], fields=['event_id'], context=context_novirtual) + event_to_synchronize[base_event][0][1].OE.event_id = main_ev[0].get('event_id')[0] parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id) res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) diff --git a/addons/hr/hr.py b/addons/hr/hr.py index 85366adfe3b..f4719f572f0 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -225,7 +225,7 @@ class hr_employee(osv.osv): "resized as a 128x128px image, with aspect ratio preserved. "\ "Use this field in form views or some kanban views."), 'image_small': fields.function(_get_image, fnct_inv=_set_image, - string="Smal-sized photo", type="binary", multi="_get_image", + string="Small-sized photo", type="binary", multi="_get_image", store = { 'hr.employee': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), }, diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 09e7373384e..0c356706407 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -40,7 +40,9 @@ - + @@ -68,7 +70,9 @@ - + diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 3d467e0b065..ed25b75aa83 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -99,7 +99,7 @@ class hr_holidays_status(osv.osv): for record in self.browse(cr, uid, ids, context=context): name = record.name if not record.limit: - name = name + (' (%d/%d)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0)) + name = name + (' (%g/%g)' % (record.leaves_taken or 0.0, record.max_leaves or 0.0)) res.append((record.id, name)) return res diff --git a/addons/l10n_be_coda/__openerp__.py b/addons/l10n_be_coda/__openerp__.py index ea1ce59eb88..6ecdb04f1d6 100644 --- a/addons/l10n_be_coda/__openerp__.py +++ b/addons/l10n_be_coda/__openerp__.py @@ -59,25 +59,7 @@ A removal of one object in the CODA processing results in the removal of the associated objects. The removal of a CODA File containing multiple Bank Statements will also remove those associated statements. -The following reconciliation logic has been implemented in the CODA processing: -------------------------------------------------------------------------------- - 1) The Company's Bank Account Number of the CODA statement is compared against - the Bank Account Number field of the Company's CODA Bank Account - configuration records (whereby bank accounts defined in type='info' - configuration records are ignored). If this is the case an 'internal transfer' - transaction is generated using the 'Internal Transfer Account' field of the - CODA File Import wizard. - 2) As a second step the 'Structured Communication' field of the CODA transaction - line is matched against the reference field of in- and outgoing invoices - (supported : Belgian Structured Communication Type). - 3) When the previous step doesn't find a match, the transaction counterparty is - located via the Bank Account Number configured on the OpenERP Customer and - Supplier records. - 4) In case the previous steps are not successful, the transaction is generated - by using the 'Default Account for Unrecognized Movement' field of the CODA - File Import wizard in order to allow further manual processing. - -In stead of a manual adjustment of the generated Bank Statements, you can also +Instead of a manual adjustment of the generated Bank Statements, you can also re-import the CODA after updating the OpenERP database with the information that was missing to allow automatic reconciliation. diff --git a/addons/l10n_be_coda/l10n_be_coda.py b/addons/l10n_be_coda/l10n_be_coda.py index 0dfa9bf9db4..c4cd8ab7c2b 100644 --- a/addons/l10n_be_coda/l10n_be_coda.py +++ b/addons/l10n_be_coda/l10n_be_coda.py @@ -28,46 +28,4 @@ class account_bank_statement(osv.osv): } -class account_bank_statement_line(osv.osv): - _inherit = 'account.bank.statement.line' - _columns = { - 'coda_account_number': fields.char('Account Number', help="The Counter Party Account Number") - } - - def create(self, cr, uid, data, context=None): - """ - This function creates a Bank Account Number if, for a bank statement line, - the partner_id field and the coda_account_number field are set, - and the account number does not exist in the database - """ - if 'partner_id' in data and data['partner_id'] and 'coda_account_number' in data and data['coda_account_number']: - acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', data['coda_account_number'])]) - if len(acc_number_ids) == 0: - try: - type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal') - type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context) - self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': data['coda_account_number'], 'partner_id': data['partner_id'], 'state': type_id.code}, context=context) - except ValueError: - pass - return super(account_bank_statement_line, self).create(cr, uid, data, context=context) - - def write(self, cr, uid, ids, vals, context=None): - super(account_bank_statement_line, self).write(cr, uid, ids, vals, context) - """ - Same as create function above, but for write function - """ - if 'partner_id' in vals: - for line in self.pool.get('account.bank.statement.line').browse(cr, uid, ids, context=context): - if line.coda_account_number: - acc_number_ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', line.coda_account_number)]) - if len(acc_number_ids) == 0: - try: - type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal') - type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context) - self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': line.coda_account_number, 'partner_id': vals['partner_id'], 'state': type_id.code}, context=context) - except ValueError: - pass - return True - - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_be_coda/l10n_be_coda_demo.xml b/addons/l10n_be_coda/l10n_be_coda_demo.xml index 63436a60a92..ce44539a715 100644 --- a/addons/l10n_be_coda/l10n_be_coda_demo.xml +++ b/addons/l10n_be_coda/l10n_be_coda_demo.xml @@ -32,5 +32,27 @@ + + + + + + + + draft + out_invoice + + + bba + +++240/2838/42818+++ + + + Otpez Laptop without OS + + 608.89 + 10 + + + diff --git a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt index bc3af59e322..d0add859663 100644 --- a/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt +++ b/addons/l10n_be_coda/test_coda_file/Ontvangen_CODA.2011-01-11-18.59.15.txt @@ -4,7 +4,7 @@ 2200010000 GKCCBEBB 1 0 2300010000BE41063012345610 PARTNER 1 0 1 3100010001OL44483FW SCTOFBIONLO001010001001PARTNER 1 0 0 -2100020000OL4414AC8BOVSOVSOVERS00000000030444501101110015000002010237 11011113501 0 +2100020000OL4414AC8BOVSOVSOVERS0000000003044450110111001500001101240283842818 11011113501 0 2200020000 BBRUBEBB 1 0 2300020000BE61310126985517 PARTNER 2 0 1 3100020001OL4414AC8BOVSOVSOVERS001500001001PARTNER 2 1 0 diff --git a/addons/l10n_be_coda/wizard/account_coda_import.py b/addons/l10n_be_coda/wizard/account_coda_import.py index e50f8537881..cac4cce452c 100644 --- a/addons/l10n_be_coda/wizard/account_coda_import.py +++ b/addons/l10n_be_coda/wizard/account_coda_import.py @@ -291,79 +291,38 @@ class account_coda_import(osv.osv_memory): if 'counterpartyAddress' in line and line['counterpartyAddress'] != '': note.append(_('Counter Party Address') + ': ' + line['counterpartyAddress']) line['name'] = "\n".join(filter(None, [line['counterpartyName'], line['communication']])) - partner = None partner_id = None - invoice = False + structured_com = "" + bank_account_id = False if line['communication_struct'] and 'communication_type' in line and line['communication_type'] == '101': - ids = self.pool.get('account.invoice').search(cr, uid, [('reference', '=', line['communication']), ('reference_type', '=', 'bba')]) - -# Gère les communications structurées -# TODO : à faire primer sur resolution_proposition : si la communication indique une facture, on la sélectionne - -# if ids: -# invoice = self.pool.get('account.invoice').browse(cr, uid, ids[0]) -# partner = invoice.partner_id -# partner_id = partner.id -# if invoice.type in ['in_invoice', 'in_refund'] and line['debit'] == '1': -# line['transaction_type'] = 'supplier' -# elif invoice.type in ['out_invoice', 'out_refund'] and line['debit'] == '0': -# line['transaction_type'] = 'customer' -# line['account'] = invoice.account_id.id -# line['reconcile'] = False -# if invoice.type in ['in_invoice', 'out_invoice']: -# iml_ids = self.pool.get('account.move.line').search(cr, uid, [('move_id', '=', invoice.move_id.id), ('reconcile_id', '=', False), ('account_id.reconcile', '=', True)]) -# if iml_ids: -# line['reconcile'] = iml_ids[0] -# if line['reconcile']: -# voucher_vals = { -# 'type': line['transaction_type'] == 'supplier' and 'payment' or 'receipt', -# 'name': line['name'], -# 'partner_id': partner_id, -# 'journal_id': statement['journal_id'].id, -# 'account_id': statement['journal_id'].default_credit_account_id.id, -# 'company_id': statement['journal_id'].company_id.id, -# 'currency_id': statement['journal_id'].company_id.currency_id.id, -# 'date': line['entryDate'], -# 'amount': abs(line['amount']), -# 'period_id': statement['period_id'], -# 'invoice_id': invoice.id, -# } -# context['invoice_id'] = invoice.id -# voucher_vals.update(self.pool.get('account.voucher').onchange_partner_id(cr, uid, [], -# partner_id=partner_id, -# journal_id=statement['journal_id'].id, -# amount=abs(line['amount']), -# currency_id=statement['journal_id'].company_id.currency_id.id, -# ttype=line['transaction_type'] == 'supplier' and 'payment' or 'receipt', -# date=line['transactionDate'], -# context=context -# )['value']) -# line_drs = [] -# for line_dr in voucher_vals['line_dr_ids']: -# line_drs.append((0, 0, line_dr)) -# voucher_vals['line_dr_ids'] = line_drs -# line_crs = [] -# for line_cr in voucher_vals['line_cr_ids']: -# line_crs.append((0, 0, line_cr)) -# voucher_vals['line_cr_ids'] = line_crs -# line['voucher_id'] = self.pool.get('account.voucher').create(cr, uid, voucher_vals, context=context) + structured_com = line['communication'] if 'counterpartyNumber' in line and line['counterpartyNumber']: ids = self.pool.get('res.partner.bank').search(cr, uid, [('acc_number', '=', str(line['counterpartyNumber']))]) - if ids and len(ids) > 0: - partner = self.pool.get('res.partner.bank').browse(cr, uid, ids[0], context=context).partner_id - partner_id = partner.id + if ids: + bank_account_id = ids[0] + partner_id = self.pool.get('res.partner.bank').browse(cr, uid, bank_account_id, context=context).partner_id.id + else: + #create the bank account, not linked to any partner. The reconciliation will link the partner manually + #chosen at the bank statement final confirmation time. + try: + type_model, type_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'bank_normal') + type_id = self.pool.get('res.partner.bank.type').browse(cr, uid, type_id, context=context) + bank_code = type_id.code + except ValueError: + bank_code = 'bank' + bank_account_id = self.pool.get('res.partner.bank').create(cr, uid, {'acc_number': str(line['counterpartyNumber']), 'state': bank_code}, context=context) if 'communication' in line and line['communication'] != '': note.append(_('Communication') + ': ' + line['communication']) data = { 'name': line['name'], - 'note': "\n".join(note), + 'note': "\n".join(note), 'date': line['entryDate'], 'amount': line['amount'], 'partner_id': partner_id, 'statement_id': statement['id'], - 'ref': line['ref'], + 'ref': structured_com, 'sequence': line['sequence'], - 'coda_account_number': line['counterpartyNumber'], + 'bank_account_id': bank_account_id, } self.pool.get('account.bank.statement.line').create(cr, uid, data, context=context) if statement['coda_note'] != '': diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py index 854593a87cc..499e6e6c4ac 100644 --- a/addons/l10n_be_invoice_bba/invoice.py +++ b/addons/l10n_be_invoice_bba/invoice.py @@ -141,7 +141,7 @@ class account_invoice(osv.osv): elif algorithm == 'random': if not self.check_bbacomm(reference): base = random.randint(1, 9999999999) - bbacomm = str(base).rjust(7, '0') + bbacomm = str(base).rjust(10, '0') base = int(bbacomm) mod = base % 97 or 97 mod = str(mod).rjust(2, '0') diff --git a/addons/l10n_hn/l10n_hn_base.xml b/addons/l10n_hn/l10n_hn_base.xml index 8b9acf5c50a..2ea77baab1b 100644 --- a/addons/l10n_hn/l10n_hn_base.xml +++ b/addons/l10n_hn/l10n_hn_base.xml @@ -25,7 +25,7 @@ ISV por Cobrar - + percent @@ -42,7 +42,7 @@ ISV por Pagar - + percent diff --git a/addons/l10n_uk/__openerp__.py b/addons/l10n_uk/__openerp__.py index 0590224c364..0556535296f 100644 --- a/addons/l10n_uk/__openerp__.py +++ b/addons/l10n_uk/__openerp__.py @@ -32,7 +32,7 @@ This is the latest UK OpenERP localisation necessary to run OpenERP accounting f - a few other adaptations""", 'author': 'SmartMode LTD', 'website': 'http://www.smartmode.co.uk', - 'depends': ['base_iban', 'base_vat', 'account_chart'], + 'depends': ['base_iban', 'base_vat', 'account_chart', 'account_anglo_saxon'], 'data': [ 'data/account.account.type.csv', 'data/account.account.template.csv', diff --git a/addons/l10n_us/__openerp__.py b/addons/l10n_us/__openerp__.py index 90c0e6fd4ad..ea5792c1dca 100644 --- a/addons/l10n_us/__openerp__.py +++ b/addons/l10n_us/__openerp__.py @@ -28,7 +28,7 @@ United States - Chart of accounts. ================================== """, 'website': 'http://www.openerp.com', - 'depends': ['account_chart'], + 'depends': ['account_chart', 'account_anglo_saxon'], 'data': [ 'l10n_us_account_type.xml', 'account_chart_template.xml', diff --git a/addons/mail/mail_followers.py b/addons/mail/mail_followers.py index ffc49414b2e..e4028111a73 100644 --- a/addons/mail/mail_followers.py +++ b/addons/mail/mail_followers.py @@ -176,7 +176,7 @@ class mail_notification(osv.Model): references = message.parent_id.message_id if message.parent_id else False # create email values - max_recipients = 100 + max_recipients = 50 chunks = [email_pids[x:x + max_recipients] for x in xrange(0, len(email_pids), max_recipients)] email_ids = [] for chunk in chunks: @@ -188,7 +188,7 @@ class mail_notification(osv.Model): 'references': references, } email_ids.append(self.pool.get('mail.mail').create(cr, uid, mail_values, context=context)) - if force_send and len(chunks) < 6: # for more than 500 followers, use the queue system + if force_send and len(chunks) < 2: # for more than 50 followers, use the queue system self.pool.get('mail.mail').send(cr, uid, email_ids, context=context) return True diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 4ae47a96774..186787c121a 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -211,3 +211,16 @@ class mail_group(osv.Model): return [] else: return super(mail_group, self).get_suggested_thread(cr, uid, removed_suggested_threads, context) + + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + res = super(mail_group, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context) + group = self.browse(cr, uid, id, context=context) + res.update({ + 'headers': { + 'Precedence': 'list', + } + }) + if group.alias_domain: + res['headers']['List-Id'] = '%s.%s' % (group.alias_name, group.alias_domain) + res['headers']['List-Post'] = '' % (group.alias_name, group.alias_domain) + return res diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index a4814140479..cc12bde2436 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -204,12 +204,15 @@ class mail_mail(osv.Model): """ body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context) body_alternative = tools.html2plaintext(body) - return { + res = { 'body': body, 'body_alternative': body_alternative, 'subject': self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context), 'email_to': self.send_get_mail_to(cr, uid, mail, partner=partner, context=context), } + if mail.model and mail.res_id and self.pool.get(mail.model) and hasattr(self.pool[mail.model], 'message_get_email_values'): + res.update(self.pool[mail.model].message_get_email_values(cr, uid, mail.res_id, mail, context=context)) + return res def send(self, cr, uid, ids, auto_commit=False, raise_exception=False, context=None): """ Sends the selected emails immediately, ignoring their current @@ -268,6 +271,9 @@ class mail_mail(osv.Model): # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: + email_headers = dict(headers) + if email.get('headers'): + email_headers.update(email['headers']) msg = ir_mail_server.build_email( email_from=mail.email_from, email_to=email.get('email_to'), @@ -282,7 +288,7 @@ class mail_mail(osv.Model): object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', - headers=headers) + headers=email_headers) res = ir_mail_server.send_email(cr, uid, msg, mail_server_id=mail.mail_server_id.id, context=context) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index dc2b98fdc93..76d3ab744d5 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -34,6 +34,7 @@ import pytz import socket import time import xmlrpclib +import re from email.message import Message from urllib import urlencode @@ -48,6 +49,8 @@ from openerp.tools.translate import _ _logger = logging.getLogger(__name__) +mail_header_msgid_re = re.compile('<[^<>]+>') + def decode_header(message, header, separator=' '): return separator.join(map(decode, filter(None, message.get_all(header, [])))) @@ -694,6 +697,16 @@ class mail_thread(osv.AbstractModel): if record.alias_domain and record.alias_name else False for record in self.browse(cr, SUPERUSER_ID, ids, context=context)] + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + """ Temporary method to create custom notification email values for a given + model and document. This should be better to have a headers field on + the mail.mail model, computed when creating the notification email, but + this cannot be done in a stable version. + + TDE FIXME: rethink this ulgy thing. """ + res = dict() + return res + #------------------------------------------------------ # Mail gateway #------------------------------------------------------ @@ -1301,13 +1314,13 @@ class mail_thread(osv.AbstractModel): msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT) if message.get('In-Reply-To'): - parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To']))]) + parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', '=', decode(message['In-Reply-To'].strip()))]) if parent_ids: msg_dict['parent_id'] = parent_ids[0] if message.get('References') and 'parent_id' not in msg_dict: - parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', - [x.strip() for x in decode(message['References']).split()])]) + msg_list = mail_header_msgid_re.findall(decode(message['References'])) + parent_ids = self.pool.get('mail.message').search(cr, uid, [('message_id', 'in', [x.strip() for x in msg_list])]) if parent_ids: msg_dict['parent_id'] = parent_ids[0] diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index cb068d566c1..e14a8f3dfe5 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -267,10 +267,7 @@ class mail_compose_message(osv.TransientModel): # mass mailing: rendering override wizard static values if mass_mail_mode and wizard.model: # always keep a copy, reset record name (avoid browsing records) - mail_values.update(notification=True, record_name=False) - if hasattr(self.pool[wizard.model], 'message_new'): - mail_values['model'] = wizard.model - mail_values['res_id'] = res_id + mail_values.update(notification=True, model=wizard.model, res_id=res_id, record_name=False) # auto deletion of mail_mail if 'mail_auto_delete' in context: mail_values['auto_delete'] = context.get('mail_auto_delete') diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index a67f88a2a28..0e44399a256 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -84,7 +84,8 @@ class MailMail(osv.Model): def send_get_email_dict(self, cr, uid, mail, partner=None, context=None): res = super(MailMail, self).send_get_email_dict(cr, uid, mail, partner, context=context) if mail.mailing_id and res.get('body') and res.get('email_to'): - email_to = tools.email_split(res.get('email_to')[0]) + emails = tools.email_split(res.get('email_to')[0]) + email_to = emails and emails[0] or False unsubscribe_url = self._get_unsubscribe_url(cr, uid, mail, email_to, context=context) if unsubscribe_url: res['body'] = tools.append_content_to_html(res['body'], unsubscribe_url, plaintext=False, container_tag='p') diff --git a/addons/mass_mailing/views/mass_mailing.xml b/addons/mass_mailing/views/mass_mailing.xml index 9806022498a..2939ebc3711 100644 --- a/addons/mass_mailing/views/mass_mailing.xml +++ b/addons/mass_mailing/views/mass_mailing.xml @@ -591,6 +591,7 @@ + diff --git a/addons/mrp/mrp.py b/addons/mrp/mrp.py index f5b6cf466a5..ca4109c1b9a 100644 --- a/addons/mrp/mrp.py +++ b/addons/mrp/mrp.py @@ -1071,8 +1071,8 @@ class mrp_production(osv.osv): return False # Take routing location as a Source Location. source_location_id = production.location_src_id.id - if production.bom_id.routing_id and production.bom_id.routing_id.location_id: - source_location_id = production.bom_id.routing_id.location_id.id + if production.routing_id and production.routing_id.location_id: + source_location_id = production.routing_id.location_id.id destination_location_id = production.product_id.property_stock_production.id if not source_location_id: diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml index 8aae0062a33..52e0f539b80 100644 --- a/addons/mrp/mrp_view.xml +++ b/addons/mrp/mrp_view.xml @@ -746,7 +746,7 @@ - + @@ -774,7 +774,7 @@ - + diff --git a/addons/mrp/wizard/mrp_product_produce_view.xml b/addons/mrp/wizard/mrp_product_produce_view.xml index 666be7d5114..859bebc2b43 100644 --- a/addons/mrp/wizard/mrp_product_produce_view.xml +++ b/addons/mrp/wizard/mrp_product_produce_view.xml @@ -18,16 +18,16 @@ + groups="stock.group_production_lot"/> - + + groups="stock.group_production_lot"/> diff --git a/addons/mrp/wizard/stock_move_view.xml b/addons/mrp/wizard/stock_move_view.xml index ebd948633c2..86588927c57 100644 --- a/addons/mrp/wizard/stock_move_view.xml +++ b/addons/mrp/wizard/stock_move_view.xml @@ -15,7 +15,7 @@ - diff --git a/addons/mrp_repair/views/report_mrprepairorder.xml b/addons/mrp_repair/views/report_mrprepairorder.xml index 2897fee1fd9..a6d00ba5b15 100644 --- a/addons/mrp_repair/views/report_mrprepairorder.xml +++ b/addons/mrp_repair/views/report_mrprepairorder.xml @@ -30,7 +30,7 @@

Repair Order N°: - Repair Quotation N°: + Repair Quotation N°:

@@ -41,7 +41,9 @@
Lot Number - + + +
Guarantee Limit: diff --git a/addons/procurement/procurement.py b/addons/procurement/procurement.py index 2ebc800ec04..e45651a30a2 100644 --- a/addons/procurement/procurement.py +++ b/addons/procurement/procurement.py @@ -64,7 +64,7 @@ class procurement_group(osv.osv): } _defaults = { 'name': lambda self, cr, uid, c: self.pool.get('ir.sequence').get(cr, uid, 'procurement.group') or '', - 'move_type': lambda self, cr, uid, c: 'one' + 'move_type': lambda self, cr, uid, c: 'direct' } class procurement_rule(osv.osv): @@ -267,7 +267,7 @@ class procurement_order(osv.osv): # # Scheduler # - def run_scheduler(self, cr, uid, use_new_cursor=False, context=None): + def run_scheduler(self, cr, uid, use_new_cursor=False, company_id = False, context=None): ''' Call the scheduler to check the procurement order. This is intented to be done for all existing companies at the same time, so we're running all the methods as SUPERUSER to avoid intercompany and access rights issues. @@ -288,8 +288,11 @@ class procurement_order(osv.osv): cr = openerp.registry(cr.dbname).cursor() # Run confirmed procurements + dom = [('state', '=', 'confirmed')] + if company_id: + dom += [('company_id', '=', company_id)] while True: - ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'confirmed')], context=context) + ids = self.search(cr, SUPERUSER_ID, dom, context=context) if not ids: break self.run(cr, SUPERUSER_ID, ids, context=context) @@ -298,8 +301,11 @@ class procurement_order(osv.osv): # Check if running procurements are done offset = 0 + dom = [('state', '=', 'running')] + if company_id: + dom += [('company_id', '=', company_id)] while True: - ids = self.search(cr, SUPERUSER_ID, [('state', '=', 'running')], offset=offset, context=context) + ids = self.search(cr, SUPERUSER_ID, dom, offset=offset, context=context) if not ids: break done = self.check(cr, SUPERUSER_ID, ids, context=context) diff --git a/addons/procurement/wizard/schedulers_all.py b/addons/procurement/wizard/schedulers_all.py index 5c712b3af38..34d4758c5d5 100644 --- a/addons/procurement/wizard/schedulers_all.py +++ b/addons/procurement/wizard/schedulers_all.py @@ -37,8 +37,12 @@ class procurement_compute_all(osv.osv_memory): """ proc_obj = self.pool.get('procurement.order') #As this function is in a new thread, i need to open a new cursor, because the old one may be closed + new_cr = self.pool.cursor() - proc_obj.run_scheduler(new_cr, uid, use_new_cursor=new_cr.dbname, context=context) + user = self.pool.get('res.users').browse(new_cr, uid, uid, context=context) + comps = [x.id for x in user.company_ids] + for comp in comps: + proc_obj.run_scheduler(new_cr, uid, use_new_cursor=new_cr.dbname, company_id = comp, context=context) #close the new cursor new_cr.close() return {} diff --git a/addons/product/product.py b/addons/product/product.py index eaed55c0b41..6fa0627db2c 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -678,6 +678,17 @@ class product_template(osv.osv): if not context or "create_product_product" not in context: self.create_variant_ids(cr, uid, [product_template_id], context=context) self._set_standard_price(cr, uid, product_template_id, vals.get('standard_price', 0.0), context=context) + + # TODO: this is needed to set given values to first variant after creation + # these fields should be moved to product as lead to confusion + related_vals = {} + if vals.get('ean13'): + related_vals['ean13'] = vals['ean13'] + if vals.get('default_code'): + related_vals['default_code'] = vals['default_code'] + if related_vals: + self.write(cr, uid, product_template_id, related_vals, context=context) + return product_template_id def write(self, cr, uid, ids, vals, context=None): diff --git a/addons/product_email_template/models/invoice.py b/addons/product_email_template/models/invoice.py index d9e82e42012..d5876b510f1 100644 --- a/addons/product_email_template/models/invoice.py +++ b/addons/product_email_template/models/invoice.py @@ -27,7 +27,7 @@ class account_invoice(osv.Model): template_values = Composer.onchange_template_id( cr, uid, composer_id, line.product_id.email_template_id.id, 'comment', 'account.invoice', invoice.id )['value'] - template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', '[]')] + template_values['attachment_ids'] = [(4, id) for id in template_values.get('attachment_ids', [])] Composer.write(cr, uid, [composer_id], template_values, context=context) Composer.send_mail(cr, uid, [composer_id], context=context) return True diff --git a/addons/product_expiry/product_expiry.py b/addons/product_expiry/product_expiry.py index e311356dbbd..6979344bbcc 100644 --- a/addons/product_expiry/product_expiry.py +++ b/addons/product_expiry/product_expiry.py @@ -92,7 +92,7 @@ class stock_quant(osv.osv): def apply_removal_strategy(self, cr, uid, location, product, qty, domain, removal_strategy, context=None): if removal_strategy == 'fefo': - order = 'removal_date, id' + order = 'removal_date, in_date, id' return self._quants_get_order(cr, uid, location, product, qty, domain, order, context=context) return super(stock_quant, self).apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context) diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml index 54f78ba8c8b..695aa80af2a 100644 --- a/addons/project/project_demo.xml +++ b/addons/project/project_demo.xml @@ -50,8 +50,8 @@ project.task + ref('base.partner_root'), + ref('base.partner_demo')])]"/> diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index bfdd83e0872..09cc4c301a4 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -700,6 +700,7 @@ class purchase_order(osv.osv): 'origin': order.name, 'route_ids': order.picking_type_id.warehouse_id and [(6, 0, [x.id for x in order.picking_type_id.warehouse_id.route_ids])] or [], 'warehouse_id':order.picking_type_id.warehouse_id.id, + 'invoice_state': order.invoice_method == 'picking' and '2binvoiced' or 'none' } diff_quantity = order_line.product_qty @@ -709,9 +710,10 @@ class purchase_order(osv.osv): tmp.update({ 'product_uom_qty': min(procurement_qty, diff_quantity), 'product_uos_qty': min(procurement_qty, diff_quantity), - 'move_dest_id': procurement.move_dest_id.id, # blabla - 'group_id': procurement.group_id.id or group_id, # blabla to check ca devrait etre bon et groupé dans le meme picking qd meme + 'move_dest_id': procurement.move_dest_id.id, #move destination is same as procurement destination + 'group_id': procurement.group_id.id or group_id, #move group is same as group of procurements if it exists, otherwise take another group 'procurement_id': procurement.id, + 'invoice_state': procurement.rule_id.invoice_state or (procurement.location_id and procurement.location_id.usage == 'customer' and procurement.invoice_state) or (order.invoice_method == 'picking' and '2binvoiced') or 'none', #dropship case takes from sale }) diff_quantity -= min(procurement_qty, diff_quantity) res.append(tmp) @@ -1299,6 +1301,7 @@ class procurement_order(osv.osv): res[procurement.id] = False else: schedule_date = self._get_purchase_schedule_date(cr, uid, procurement, company, context=context) + purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) line_vals = self._get_po_line_values_from_proc(cr, uid, procurement, partner, company, schedule_date, context=context) #look for any other draft PO for the same supplier, to attach the new line on instead of creating a new draft one available_draft_po_ids = po_obj.search(cr, uid, [ @@ -1306,6 +1309,10 @@ class procurement_order(osv.osv): ('location_id', '=', procurement.location_id.id), ('company_id', '=', procurement.company_id.id), ('dest_address_id', '=', procurement.partner_dest_id.id)], context=context) if available_draft_po_ids: po_id = available_draft_po_ids[0] + po_rec = po_obj.browse(cr, uid, po_id, context=context) + #if the product has to be ordered earlier those in the existing PO, we replace the purchase date on the order to avoid ordering it too late + if datetime.strptime(po_rec.date_order, DEFAULT_SERVER_DATE_FORMAT) > purchase_date: + po_obj.write(cr, uid, [po_id], {'date_order': purchase_date}, context=context) #look for any other PO line in the selected PO with same product and UoM to sum quantities instead of creating a new po line available_po_line_ids = po_line_obj.search(cr, uid, [('order_id', '=', po_id), ('product_id', '=', line_vals['product_id']), ('product_uom', '=', line_vals['product_uom'])], context=context) if available_po_line_ids: @@ -1318,7 +1325,6 @@ class procurement_order(osv.osv): po_line_id = po_line_obj.create(cr, SUPERUSER_ID, line_vals, context=context) linked_po_ids.append(procurement.id) else: - purchase_date = self._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context) name = seq_obj.get(cr, uid, 'purchase.order') or _('PO: %s') % procurement.name po_vals = { 'name': name, @@ -1363,6 +1369,13 @@ class product_template(osv.Model): _name = 'product.template' _inherit = 'product.template' + def _get_buy_route(self, cr, uid, context=None): + + buy_route = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'purchase.route_warehouse0_buy') + if buy_route: + return [buy_route] + return [] + def _purchase_count(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0) for template in self.browse(cr, uid, ids, context=context): @@ -1374,6 +1387,7 @@ class product_template(osv.Model): } _defaults = { 'purchase_ok': 1, + 'route_ids': _get_buy_route, } class product_product(osv.Model): @@ -1451,15 +1465,9 @@ class account_invoice_line(osv.Model): readonly=True), } -class product_product(osv.osv): - _inherit = "product.product" +class product_template(osv.osv): + _inherit = "product.template" - def _get_buy_route(self, cr, uid, context=None): - buy_route = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'purchase', 'route_warehouse0_buy')[1] - return [buy_route] - _defaults = { - 'route_ids': _get_buy_route, - } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index 69d970b7be6..26d2ff1002f 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -55,6 +55,33 @@ class stock_move(osv.osv): }) return super(stock_move, self).copy(cr, uid, id, default, context) + + def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None): + invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context) + if move.purchase_line_id: + purchase_line = move.purchase_line_id + self.pool.get('purchase.order.line').write(cr, uid, [purchase_line.id], { + 'invoice_lines': [(4, invoice_line_id)] + }, context=context) + self.pool.get('purchase.order').write(cr, uid, [purchase_line.order_id.id], { + 'invoice_ids': [(4, invoice_line_vals['invoice_id'])], + }) + return invoice_line_id + + def _get_master_data(self, cr, uid, move, company, context=None): + if move.purchase_line_id: + purchase_order = move.purchase_line_id.order_id + return purchase_order.partner_id, purchase_order.create_uid.id, purchase_order.pricelist_id.currency_id.id + return super(stock_move, self)._get_master_data(cr, uid, move, company, context=context) + + def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None): + res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) + if move.purchase_line_id: + purchase_line = move.purchase_line_id + res['invoice_line_tax_id'] = [(6, 0, [x.id for x in purchase_line.taxes_id])] + res['price_unit'] = purchase_line.price_unit + return res + class stock_picking(osv.osv): _inherit = 'stock.picking' diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 964fa1584ba..1f257376331 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -128,13 +128,12 @@ class sale_order(osv.osv): sale_clause = '' no_invoiced = False for arg in args: - if arg[1] == '=': - if arg[2]: - clause += 'AND inv.state = \'paid\'' - else: - clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id ' - sale_clause = ', sale_order AS sale ' - no_invoiced = True + if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]): + clause += 'AND inv.state = \'paid\'' + else: + clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id ' + sale_clause = ', sale_order AS sale ' + no_invoiced = True cursor.execute('SELECT rel.order_id ' \ 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \ diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index b3bbd253392..63f6068f3c2 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -388,7 +388,7 @@ class stock_move(osv.osv): return super(stock_move, self).action_cancel(cr, uid, ids, context=context) def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None): - invoice_line_id = self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context) + invoice_line_id = super(stock_move, self)._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context) if move.procurement_id and move.procurement_id.sale_line_id: sale_line = move.procurement_id.sale_line_id self.pool.get('sale.order.line').write(cr, uid, [sale_line.id], { diff --git a/addons/stock/procurement.py b/addons/stock/procurement.py index a8fc778f2e6..b8d7a03c9f2 100644 --- a/addons/stock/procurement.py +++ b/addons/stock/procurement.py @@ -261,7 +261,7 @@ class procurement_order(osv.osv): result['domain'] = "[('group_id','in',[" + ','.join(map(str, list(group_ids))) + "])]" return result - def run_scheduler(self, cr, uid, use_new_cursor=False, context=None): + def run_scheduler(self, cr, uid, use_new_cursor=False, company_id=False, context=None): ''' Call the scheduler in order to check the running procurements (super method), to check the minimum stock rules and the availability of moves. This function is intended to be run for all the companies at the same time, so @@ -286,8 +286,7 @@ class procurement_order(osv.osv): move_obj = self.pool.get('stock.move') #Minimum stock rules - company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id - self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company.id, context=context) + self._procure_orderpoint_confirm(cr, SUPERUSER_ID, use_new_cursor=False, company_id=company_id, context=context) #Search all confirmed stock_moves and try to assign them confirmed_ids = move_obj.search(cr, uid, [('state', '=', 'confirmed')], limit=None, order='priority desc, date_expected asc', context=context) @@ -331,7 +330,7 @@ class procurement_order(osv.osv): [order_point.product_id.id], context={'location': order_point.location_id.id})[order_point.product_id.id]['virtual_available'] - def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id=False, context=None): + def _procure_orderpoint_confirm(self, cr, uid, use_new_cursor=False, company_id = False, context=None): ''' Create procurement based on Orderpoint @@ -341,14 +340,15 @@ class procurement_order(osv.osv): if context is None: context = {} if use_new_cursor: - cr = openerp.registry(cr.dbname).db.cursor() + cr = openerp.registry(cr.dbname).cursor() orderpoint_obj = self.pool.get('stock.warehouse.orderpoint') procurement_obj = self.pool.get('procurement.order') offset = 0 ids = [1] + dom = company_id and [('company_id', '=', company_id)] or [] while ids: - ids = orderpoint_obj.search(cr, uid, [('company_id', '=', company_id)], offset=offset, limit=100) + ids = orderpoint_obj.search(cr, uid, dom, offset=offset, limit=100) for op in orderpoint_obj.browse(cr, uid, ids, context=context): prods = self._product_virtual_get(cr, uid, op) if prods is None: diff --git a/addons/stock/product.py b/addons/stock/product.py index 0cf7df5e836..1cd677706f6 100644 --- a/addons/stock/product.py +++ b/addons/stock/product.py @@ -171,13 +171,13 @@ class product_product(osv.osv): def _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None): res = {} for product in self.browse(cr, uid, ids, context=context): - res[product.id] = str(product.qty_available) + _(" In Stock") + res[product.id] = str(product.qty_available) + _(" On Hand") return res _columns = { 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'), 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'), - 'qty_in_stock': fields.function(_product_available_text, type='char'), + 'qty_available_text': fields.function(_product_available_text, type='char'), 'qty_available': fields.function(_product_available, multi='qty_available', type='float', digits_compute=dp.get_precision('Product Unit of Measure'), string='Quantity On Hand', @@ -277,6 +277,12 @@ class product_product(osv.osv): res['fields']['qty_available']['string'] = _('Produced Qty') return res + + def action_view_routes(self, cr, uid, ids, context=None): + template_obj = self.pool.get("product.template") + templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)])) + return template_obj.action_view_routes(cr, uid, templ_ids, context=context) + class product_template(osv.osv): _name = 'product.template' _inherit = 'product.template' diff --git a/addons/stock/product_view.xml b/addons/stock/product_view.xml index 2a7ad044fbc..c3941eb1887 100644 --- a/addons/stock/product_view.xml +++ b/addons/stock/product_view.xml @@ -193,7 +193,7 @@ - + @@ -202,13 +202,13 @@
'); + } + + if (step.backdrop || $element.parents("#website-top-navbar, .oe_navbar, .modal").size()) { + $tip.css("z-index", 2010); + } + + // button click event + $tip.find("button") + .one("click", function () { + step.busy = true; + if (!$(this).is("[data-role='next']")) { + clearTimeout(Tour.timer); + Tour.endTour(); + } + Tour.closePopover(); + }); + + Tour.repositionPopover(); + }, + repositionPopover: function() { + var popover = Tour.$element.data("bs.popover"); + var $tip = Tour.$element.data("bs.popover").tip(); + + if (popover.options.orphan) { + return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2); + } + + var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset; + offsetWidth = $tip[0].offsetWidth; + offsetHeight = $tip[0].offsetHeight; + tipOffset = $tip.offset(); + originalLeft = tipOffset.left; + originalTop = tipOffset.top; + offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight(); + if (offsetBottom < 0) { + tipOffset.top = tipOffset.top + offsetBottom; + } + offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth(); + if (offsetRight < 0) { + tipOffset.left = tipOffset.left + offsetRight; + } + if (tipOffset.top < 0) { + tipOffset.top = 0; + } + if (tipOffset.left < 0) { + tipOffset.left = 0; + } + $tip.offset(tipOffset); + if (popover.options.placement === "bottom" || popover.options.placement === "top") { + var left = Tour.$element.offset().left + Tour.$element.outerWidth()/2 - tipOffset.left; + $tip.find(".arrow").css("left", left ? left + "px" : ""); + } else if (popover.options.placement !== "auto") { + var top = Tour.$element.offset().top + Tour.$element.outerHeight()/2 - tipOffset.top; + $tip.find(".arrow").css("top", top ? top + "px" : ""); + } + }, + _load_template: false, + load_template: function () { + // don't need template to use bootstrap Tour in automatic mode + Tour._load_template = true; + if (typeof QWeb2 === "undefined") return $.when(); + var def = $.Deferred(); + openerp.qweb.add_template('/web/static/src/xml/website.tour.xml', function(err) { + if (err) { + def.reject(err); + } else { + def.resolve(); + } + }); + return def; + }, + popoverTitle: function (tour, options) { + return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover_title', options) : options.title; + }, + popover: function (options) { + return typeof QWeb2 !== "undefined" ? openerp.qweb.render('tour.popover', options) : options.title; + }, + getLang: function () { + return $("html").attr("lang") ? "/" + $("html").attr("lang").replace(/-/, '_') : ""; + }, + getState: function () { + var state = JSON.parse(localStorage.getItem("tour") || 'false') || {}; + if (state) { this.time = state.time; } + var tour_id,mode,step_id; + if (!state.id && window.location.href.indexOf("#tutorial.") > -1) { + state = { + "id": window.location.href.match(/#tutorial\.(.*)=true/)[1], + "mode": "tutorial", + "step_id": 0 + }; + window.location.hash = ""; + console.log("Tour '"+state.id+"' Begin from url hash"); + Tour.saveState(state.id, state.mode, state.step_id, 0); + } + if (!state.id) { + return; + } + state.tour = Tour.tours[state.id]; + state.step = state.tour && state.tour.steps[state.step_id === -1 ? 0 : state.step_id]; + return state; + }, + error: function (step, message) { + var state = Tour.getState(); + message += '\n tour: ' + state.id + + (step ? '\n step: ' + step.id + ": '" + (step._title || step.title) + "'" : '' ) + + '\n href: ' + window.location.href + + '\n referrer: ' + document.referrer + + (step ? '\n element: ' + Boolean(!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) : '' ) + + (step ? '\n waitNot: ' + Boolean(!step.waitNot || !$(step.waitNot).size()) : '' ) + + (step ? '\n waitFor: ' + Boolean(!step.waitFor || $(step.waitFor).size()) : '' ) + + "\n localStorage: " + JSON.stringify(localStorage) + + '\n\n' + $("body").html(); + Tour.reset(); + if (state.mode === "test") { + throw new Error(message); + } + }, + lists: function () { + var tour_ids = []; + for (var k in Tour.tours) { + tour_ids.push(k); + } + return tour_ids; + }, + saveState: function (tour_id, mode, step_id, number, wait) { + localStorage.setItem("tour", JSON.stringify({ + "id":tour_id, + "mode":mode, + "step_id":step_id || 0, + "time": this.time, + "number": number+1, + "wait": wait || 0 + })); + }, + reset: function () { + var state = Tour.getState(); + if (state && state.tour) { + for (var k in state.tour.steps) { + state.tour.steps[k].busy = false; + } + } + localStorage.removeItem("tour"); + clearTimeout(Tour.timer); + clearTimeout(Tour.testtimer); + Tour.closePopover(); + }, + running: function () { + var state = Tour.getState(); + if (!state) return; + else if (state.tour) { + if (!Tour._load_template) { + Tour.load_template().then(Tour.running); + return; + } + console.log("Tour '"+state.id+"' is running"); + Tour.registerSteps(state.tour, state.mode); + Tour.nextStep(); + } else { + if (state.mode === "test" && state.wait >= 10) { + Tour.error(state.step, "Tour '"+state.id+"' undefined"); + } + Tour.saveState(state.id, state.mode, state.step_id, state.number-1, state.wait+1); + console.log("Tour '"+state.id+"' wait for running (tour undefined)"); + setTimeout(Tour.running, Tour.retryRunningDelay); + } + }, + check: function (step) { + return (step && + (!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) && + (!step.waitNot || !$(step.waitNot).size()) && + (!step.waitFor || $(step.waitFor).size())); + }, + waitNextStep: function () { + var state = Tour.getState(); + var time = new Date().getTime(); + var timer; + var next = state.tour.steps[state.step.id+1]; + var overlaps = state.mode === "test" ? Tour.errorDelay : 0; + + window.onbeforeunload = function () { + clearTimeout(Tour.timer); + clearTimeout(Tour.testtimer); + }; + + function checkNext () { + Tour.autoTogglePopover(); + + clearTimeout(Tour.timer); + if (Tour.check(next)) { + clearTimeout(Tour.currentTimer); + // use an other timeout for cke dom loading + Tour.saveState(state.id, state.mode, state.step.id, 0); + setTimeout(function () { + Tour.nextStep(next); + }, Tour.defaultDelay); + } else if (!overlaps || new Date().getTime() - time < overlaps) { + Tour.timer = setTimeout(checkNext, Tour.defaultDelay); + } else { + Tour.error(next, "Can't reach the next step"); + } + } + checkNext(); + }, + nextStep: function (step) { + var state = Tour.getState(); + + if (!state) { + return; + } + + step = step || state.step; + var next = state.tour.steps[step.id+1]; + + if (state.mode === "test" && state.number > 3) { + Tour.error(next, "Cycling. Can't reach the next step"); + } + + Tour.saveState(state.id, state.mode, step.id, state.number); + + if (step.id !== state.step_id) { + console.log("Tour '"+state.id+"' Step: '" + (step._title || step.title) + "' (" + (new Date().getTime() - this.time) + "ms)"); + } + + Tour.autoTogglePopover(true); + + if (step.onload) { + step.onload(); + } + + if (next) { + setTimeout(function () { + if (Tour.getState()) { + Tour.waitNextStep(); + } + if (state.mode === "test") { + setTimeout(function(){ + Tour.autoNextStep(state.tour, step); + }, Tour.defaultDelay); + } + }, next.wait || 0); + } else { + setTimeout(function(){ + Tour.autoNextStep(state.tour, step); + }, Tour.defaultDelay); + Tour.endTour(); + } + }, + endTour: function () { + var state = Tour.getState(); + var test = state.step.id >= state.tour.steps.length-1; + Tour.reset(); + if (test) { + console.log("Tour '"+state.id+"' finish: ok"); + console.log('ok'); + } else { + console.log("Tour '"+state.id+"' finish: error"); + console.log('error'); + } + }, + autoNextStep: function (tour, step) { + clearTimeout(Tour.testtimer); + + function autoStep () { + if (!step) return; + + if (step.autoComplete) { + step.autoComplete(tour); + } + + $(".popover.tour [data-role='next']").click(); + + var $element = $(step.element); + if (!$element.size()) return; + + if (step.snippet) { + + Tour.autoDragAndDropSnippet($element); + + } else if ($element.is(":visible")) { + + $element.trigger($.Event("mouseenter", { srcElement: $element[0] })); + $element.trigger($.Event("mousedown", { srcElement: $element[0] })); + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + $element[0].dispatchEvent(evt); + + // trigger after for step like: mouseenter, next step click on button display with mouseenter + setTimeout(function () { + $element.trigger($.Event("mouseup", { srcElement: $element[0] })); + $element.trigger($.Event("mouseleave", { srcElement: $element[0] })); + }, 1000); + } + if (step.sampleText) { + + $element.trigger($.Event("keydown", { srcElement: $element })); + if ($element.is("input") ) { + $element.val(step.sampleText); + } if ($element.is("select")) { + $element.find("[value='"+step.sampleText+"'], option:contains('"+step.sampleText+"')").attr("selected", true); + $element.val(step.sampleText); + } else { + $element.html(step.sampleText); + } + setTimeout(function () { + $element.trigger($.Event("keyup", { srcElement: $element })); + $element.trigger($.Event("change", { srcElement: $element })); + }, self.defaultDelay<<1); + + } + } + Tour.testtimer = setTimeout(autoStep, 100); + }, + autoDragAndDropSnippet: function (selector) { + var $thumbnail = $(selector).first(); + var thumbnailPosition = $thumbnail.position(); + $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top })); + $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: document.body.scrollWidth/2, pageY: document.body.scrollHeight/2 })); + var $dropZone = $(".oe_drop_zone").first(); + var dropPosition = $dropZone.position(); + $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top })); + } +}; +openerp.Tour = Tour; + +///////////////////////////////////////////////// + +$(document).ready(Tour.running); + +}()); diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index e276a5e3995..956ec494573 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -2632,6 +2632,7 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ type_of_date: "datetime", events: { 'change .oe_datepicker_master': 'change_datetime', + 'keypress .oe_datepicker_master': 'change_datetime', }, init: function(parent) { this._super(parent); @@ -2750,8 +2751,8 @@ instance.web.DateTimeWidget = instance.web.Widget.extend({ format_client: function(v) { return instance.web.format_value(v, {"widget": this.type_of_date}); }, - change_datetime: function() { - if (this.is_valid_()) { + change_datetime: function(e) { + if ((e.type !== "keypress" || e.which === 13) && this.is_valid_()) { this.set_value_from_ui_(); this.trigger("datetime_changed"); } @@ -2809,7 +2810,9 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc }, set_dimensions: function (height, width) { this._super(height, width); - this.datewidget.$input.css('height', height); + if (!this.get("effective_readonly")) { + this.datewidget.$input.css('height', height); + } } }); @@ -3509,6 +3512,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc this.floating = false; this.current_display = null; this.is_started = false; + this.ignore_focusout = false; }, reinit_value: function(val) { this.internal_set_value(val); @@ -3640,6 +3644,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc var ed_delay = 200; var ed_duration = 15000; var anyoneLoosesFocus = function (e) { + if (self.ignore_focusout) { return; } var used = false; if (self.floating) { if (self.last_search.length > 0) { @@ -3843,11 +3848,17 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc _search_create_popup: function() { this.no_ed = true; this.ed_def.reject(); - return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); + this.ignore_focusout = true; + this.reinit_value(false); + var res = instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); + this.ignore_focusout = false; + this.no_ed = false; + return res; }, set_dimensions: function (height, width) { this._super(height, width); - this.$input.css('height', height); + if (!this.get("effective_readonly") && this.$input) + this.$input.css('height', height); } }); @@ -5505,9 +5516,13 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. this._super.apply(this, arguments); }, initialize_content: function() { + var self= this; this.$el.find('input.oe_form_binary_file').change(this.on_file_change); this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); + this.$el.find('.oe_form_binary_file_edit').click(function(event){ + self.$el.find('input.oe_form_binary_file').click(); + }); }, on_file_change: function(e) { var self = this; @@ -5675,8 +5690,6 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ return; $img.css("max-width", "" + self.options.size[0] + "px"); $img.css("max-height", "" + self.options.size[1] + "px"); - $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px"); - $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px"); }); $img.on('error', function() { $img.attr('src', self.placeholder); diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index e2c63eb191a..144de8eb21c 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -130,15 +130,7 @@ if (this.editable()) { this.$el.find('table:first').show(); this.$el.find('.oe_view_nocontent').remove(); - this.start_edition().then(function(){ - var fields = self.editor.form.fields; - self.editor.form.fields_order.some(function(field){ - if (fields[field].$el.is(':visible')){ - fields[field].$el.find("input").select(); - return true; - } - }); - }); + this.start_edition(); } else { this._super(); } @@ -243,6 +235,7 @@ return this.ensure_saved().then(function () { var $recordRow = self.groups.get_row_for(record); var cells = self.get_cells_for($recordRow); + var fields = {}; self.fields_for_resize.splice(0, self.fields_for_resize.length); return self.with_event('edit', { record: record.attributes, @@ -256,10 +249,16 @@ // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow field.$el.attr('data-fieldname', field_name); + fields[field_name] = field; self.fields_for_resize.push({field: field, cell: cell}); }, options).then(function () { $recordRow.addClass('oe_edition'); self.resize_fields(); + var focus_field = options && options.focus_field ? options.focus_field : undefined; + if (!focus_field){ + focus_field = _.find(self.editor.form.fields_order, function(field){ return fields[field] && fields[field].$el.is(':visible:has(input)'); }); + } + if (focus_field) fields[focus_field].$el.find('input').select(); return record.attributes; }); }).fail(function () { @@ -286,9 +285,7 @@ if (!this.editor.is_editing()) { return; } for(var i=0, len=this.fields_for_resize.length; i
- - - Edit - + +
+ + +
@@ -2038,4 +2039,5 @@
+ diff --git a/addons/website/static/src/xml/website.tour.xml b/addons/web/static/src/xml/website.tour.xml similarity index 90% rename from addons/website/static/src/xml/website.tour.xml rename to addons/web/static/src/xml/website.tour.xml index dcecef021bb..cae823bd04e 100644 --- a/addons/website/static/src/xml/website.tour.xml +++ b/addons/web/static/src/xml/website.tour.xml @@ -1,8 +1,8 @@ - +
-
+

@@ -21,7 +21,7 @@
- +
diff --git a/addons/web/views/database_manager.html b/addons/web/views/database_manager.html index 5734f60bc62..19ae6a23bb2 100644 --- a/addons/web/views/database_manager.html +++ b/addons/web/views/database_manager.html @@ -51,6 +51,7 @@ + diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 2b022d86f30..9de0d51526e 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -22,6 +22,7 @@ + @@ -246,6 +247,7 @@ diff --git a/addons/website_mail_group/__init__.py b/addons/website_mail_group/__init__.py index ee5959455ad..9f86759e32b 100644 --- a/addons/website_mail_group/__init__.py +++ b/addons/website_mail_group/__init__.py @@ -1 +1,2 @@ import controllers +import models diff --git a/addons/website_mail_group/controllers/main.py b/addons/website_mail_group/controllers/main.py index a28b808d9f2..59478b37fa3 100644 --- a/addons/website_mail_group/controllers/main.py +++ b/addons/website_mail_group/controllers/main.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import datetime +from dateutil import relativedelta -from openerp import tools +from openerp import tools, SUPERUSER_ID from openerp.addons.web import http from openerp.addons.website.models.website import slug from openerp.addons.web.http import request @@ -28,12 +29,23 @@ class MailGroup(http.Controller): def view(self, **post): cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') + mail_message_obj = request.registry.get('mail.message') group_ids = group_obj.search(cr, uid, [('alias_id', '!=', False), ('alias_id.alias_name', '!=', False)], context=context) - values = {'groups': group_obj.browse(cr, uid, group_ids, context)} + groups = group_obj.browse(cr, uid, group_ids, context) + # compute statistics + month_date = datetime.datetime.today() - relativedelta.relativedelta(months=1) + group_data = dict.fromkeys(group_ids, dict()) + for group in groups: + group_data[group.id]['monthly_message_nbr'] = mail_message_obj.search( + cr, SUPERUSER_ID, + [('model', '=', 'mail.group'), ('res_id', '=', group.id), ('date', '>=', month_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT))], + count=True, context=context) + values = {'groups': groups, 'group_data': group_data} return request.website.render('website_mail_group.mail_groups', values) @http.route(["/groups/subscription/"], type='json', auth="user") def subscription(self, group_id=0, action=False, **post): + """ TDE FIXME: seems dead code """ cr, uid, context = request.cr, request.uid, request.context group_obj = request.registry.get('mail.group') if action: diff --git a/addons/website_mail_group/models/__init__.py b/addons/website_mail_group/models/__init__.py new file mode 100644 index 00000000000..ea8be51acde --- /dev/null +++ b/addons/website_mail_group/models/__init__.py @@ -0,0 +1 @@ +import mail_group diff --git a/addons/website_mail_group/models/mail_group.py b/addons/website_mail_group/models/mail_group.py new file mode 100644 index 00000000000..804785b66f9 --- /dev/null +++ b/addons/website_mail_group/models/mail_group.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from openerp.osv import osv + + +class MailGroup(osv.Model): + _inherit = 'mail.group' + + def message_get_email_values(self, cr, uid, id, notif_mail=None, context=None): + res = super(MailGroup, self).message_get_email_values(cr, uid, id, notif_mail=notif_mail, context=context) + group = self.browse(cr, uid, id, context=context) + base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') + res['headers'].update({ + 'List-Archive': '<%s/groups/%s>' % (base_url, group.id), + 'List-Subscribe': '<%s/groups>' % (base_url), + 'List-Unsubscribe': '<%s/groups>' % (base_url), + }) + return res diff --git a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js index fd6a2d38cfa..8ad4f9ba796 100644 --- a/addons/website_mail_group/static/src/js/website_mail_group.snippet.js +++ b/addons/website_mail_group/static/src/js/website_mail_group.snippet.js @@ -3,8 +3,8 @@ var website = openerp.website; - website.snippet.animationRegistry.follow = website.snippet.Animation.extend({ - selector: ".js_follow", + website.snippet.animationRegistry.follow_alias = website.snippet.Animation.extend({ + selector: ".js_follow_alias", start: function (editable_mode) { var self = this; this.is_user = false; @@ -23,8 +23,8 @@ // not if editable mode to allow designer to edit alert field if (!editable_mode) { - $('.js_follow > .alert').addClass("hidden"); - $('.js_follow > .input-group-btn.hidden').removeClass("hidden"); + $('.js_follow_alias > .alert').addClass("hidden"); + $('.js_follow_alias > .input-group-btn.hidden').removeClass("hidden"); this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) { event.preventDefault(); self.on_click(); diff --git a/addons/website_mail_group/views/snippets.xml b/addons/website_mail_group/views/snippets.xml index 452f59f4902..95935cd4998 100644 --- a/addons/website_mail_group/views/snippets.xml +++ b/addons/website_mail_group/views/snippets.xml @@ -11,7 +11,7 @@ Discussion Group -
@@ -38,7 +38,7 @@
  • diff --git a/addons/website_mail_group/views/website_mail_group.xml b/addons/website_mail_group/views/website_mail_group.xml index 8ecbd6c84ed..a1065027748 100644 --- a/addons/website_mail_group/views/website_mail_group.xml +++ b/addons/website_mail_group/views/website_mail_group.xml @@ -45,7 +45,7 @@
  • participants
    - messages + messages / month
    diff --git a/addons/website_report/views/layouts.xml b/addons/website_report/views/layouts.xml index 9978d994821..03df41266be 100644 --- a/addons/website_report/views/layouts.xml +++ b/addons/website_report/views/layouts.xml @@ -62,8 +62,6 @@ - - diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 23225186dc8..a6bcd15f9e5 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -55,7 +55,7 @@ class table_compute(object): self.table[(pos/PPR)+y2][(pos%PPR)+x2] = False self.table[pos/PPR][pos%PPR] = { 'product': p, 'x':x, 'y': y, - 'class': " ".join(map(lambda x: x.html_class, p.website_style_ids)) + 'class': " ".join(map(lambda x: x.html_class or '', p.website_style_ids)) } if index<=PPG: maxy=max(maxy,y+(pos/PPR)) @@ -169,7 +169,7 @@ class website_sale(http.Controller): values = { 'search': search, - 'category': category and int(category), + 'category': category, 'attrib_values': attrib_values, 'attrib_set': attrib_set, 'pager': pager, diff --git a/addons/website_sale/static/src/js/website.tour.sale.js b/addons/website_sale/static/src/js/website.tour.sale.js index 8cfb4e2cdad..57365a826a6 100644 --- a/addons/website_sale/static/src/js/website.tour.sale.js +++ b/addons/website_sale/static/src/js/website.tour.sale.js @@ -1,9 +1,6 @@ (function () { 'use strict'; - - var website = openerp.website; - - website.Tour.register({ + openerp.Tour.register({ id: 'shop_customize', name: "Customize the page and search a product", path: '/shop', @@ -37,7 +34,7 @@ ] }); - website.Tour.register({ + openerp.Tour.register({ id: 'shop_buy_product', name: "Try to buy products", path: '/shop', diff --git a/addons/website_sale/static/src/js/website.tour.shop.js b/addons/website_sale/static/src/js/website.tour.shop.js index 3dd5b54b357..c39a8b2f9e8 100644 --- a/addons/website_sale/static/src/js/website.tour.shop.js +++ b/addons/website_sale/static/src/js/website.tour.shop.js @@ -1,10 +1,9 @@ (function () { 'use strict'; - var website = openerp.website; var _t = openerp._t; - website.Tour.register({ + openerp.Tour.register({ id: 'shop', name: _t("Create a product"), steps: [ diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py index b37b514a825..a57a2d64bca 100644 --- a/addons/website_sale/tests/test_sale_process.py +++ b/addons/website_sale/tests/test_sale_process.py @@ -3,22 +3,22 @@ import os import openerp.tests inject = [ - ("openerp.website.Tour", os.path.join(os.path.dirname(__file__), '../../website/static/src/js/website.tour.js')), - ("openerp.website.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")), + ("openerp.Tour", os.path.join(os.path.dirname(__file__), '../../web/static/src/js/tour.js')), + ("openerp.Tour.ShopTest", os.path.join(os.path.dirname(__file__), "../static/src/js/website.tour.sale.js")), ] @openerp.tests.common.at_install(False) @openerp.tests.common.post_install(True) class TestUi(openerp.tests.HttpCase): def test_01_admin_shop_tour(self): - self.phantom_js("/", "openerp.website.Tour.run('shop', 'test')", "openerp.website.Tour.tours.shop", login="admin") + self.phantom_js("/", "openerp.Tour.run('shop', 'test')", "openerp.Tour.tours.shop", login="admin") def test_02_admin_checkout(self): - self.phantom_js("/", "openerp.website.Tour.run('shop_customize', 'test')", "openerp.website.Tour.tours.shop_customize", login="admin", inject=inject) - self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="admin", inject=inject) + self.phantom_js("/", "openerp.Tour.run('shop_customize', 'test')", "openerp.Tour.tours.shop_customize", login="admin", inject=inject) + self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="admin", inject=inject) def test_03_demo_checkout(self): - self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", login="demo", inject=inject) + self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", login="demo", inject=inject) def test_04_public_checkout(self): - self.phantom_js("/", "openerp.website.Tour.run('shop_buy_product', 'test')", "openerp.website.Tour.tours.shop_buy_product", inject=inject) + self.phantom_js("/", "openerp.Tour.run('shop_buy_product', 'test')", "openerp.Tour.tours.shop_buy_product", inject=inject) diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index c43363b863e..fb5a83de1bd 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -240,7 +240,7 @@