diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 43ab40a77e9..da7ea8e6152 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -1142,6 +1142,9 @@ class AccountMoveLine(models.Model): if 'company_ids' in context: domain += [('company_id', 'in', context['company_ids'])] + if context.get('reconcile_date'): + domain += ['|', ('reconciled', '=', False), '|', ('matched_debit_ids.create_date', '>', context['reconcile_date']), ('matched_credit_ids.create_date', '>', context['reconcile_date'])] + where_clause = "" where_clause_params = [] tables = '' diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py index d7670f2f621..6dfcd6a7e8b 100644 --- a/addons/account/report/account_aged_partner_balance.py +++ b/addons/account/report/account_aged_partner_balance.py @@ -61,8 +61,8 @@ class ReportAgedPartnerBalance(models.AbstractModel): AND (COALESCE(l.date_maturity,l.date) > %s)\ AND (l.partner_id IN %s)\ AND l.reconciled IS FALSE\ - AND (l.date <= %s)\ - GROUP BY l.partner_id', (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from,)) + AND l.date <= %s \ + GROUP BY l.partner_id', (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from)) partner_totals = cr.fetchall() for partner_id, amount in partner_totals: future_past[partner_id] = amount @@ -85,50 +85,29 @@ class ReportAgedPartnerBalance(models.AbstractModel): args_list += (form[str(i)]['stop'],) args_list += (date_from,) - cr.execute('''SELECT l.partner_id, SUM(l.debit - l.credit), l.id + query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND (l.partner_id IN %s) - AND l.reconciled IS FALSE AND ''' + dates_query + ''' AND (l.date <= %s) - GROUP BY l.partner_id, l.id''', args_list) - partners_partial = cr.fetchall() - partners_amount = dict((partner_id, 0) for partner_id, amount, line_id in partners_partial) - - for partner_id, amount, line_id in partners_partial: - partial_reconcile_ids = [] - line = self.env['account.move.line'].browse(line_id) + GROUP BY l.partner_id, l.id''' + cr.execute(query, args_list) + partners_amount = {} + aml_ids = cr.fetchall() + aml_ids = aml_ids and aml_ids[0] or [] + for line in self.env['account.move.line'].browse(aml_ids): + if line.partner_id.id not in partners_amount: + partners_amount[line.partner_id.id] = 0.0 + line_amount = line.amount_residual for partial_line in (line.matched_debit_ids + line.matched_credit_ids): - if not partial_line.credit_move_id.id in partial_reconcile_ids: - partial_reconcile_ids.append(partial_line.credit_move_id.id) - if not partial_line.debit_move_id.id in partial_reconcile_ids: - partial_reconcile_ids.append(partial_line.debit_move_id.id) - if partial_reconcile_ids: - # in case of partial reconciliation, we want to keep the left amount in the oldest period - cr.execute('''SELECT MIN(COALESCE(date_maturity, date)) from account_move_line where id = %s''', (line_id,)) - date = cr.fetchall() - partial = False - if 'BETWEEN' in dates_query: - partial = date and args_list[-3] <= date[0][0] <= args_list[-2] - elif '>=' in dates_query: - partial = date and date[0][0] >= form[str(i)]['start'] - else: - partial = date and date[0][0] <= form[str(i)]['stop'] - if partial: - # partial reconcilation - limit_date = 'COALESCE(l.date_maturity, l.date) <= %s' - cr.execute('''SELECT SUM(l.debit - l.credit) - FROM account_move_line AS l, account_move AS am - WHERE l.move_id = am.id AND am.state IN %s - AND l.id IN %s - AND ''' + limit_date, (tuple(move_state), tuple(partial_reconcile_ids), date_from)) - unreconciled_amount = cr.fetchall() - partners_amount[partner_id] += unreconciled_amount[0][0] - else: - partners_amount[partner_id] += amount + if partial_line.create_date > date_from + ' 23:59:59': + line_amount = line.balance + break + + partners_amount[line.partner_id.id] += line_amount history.append(partners_amount) for partner in partners: @@ -148,12 +127,9 @@ class ReportAgedPartnerBalance(models.AbstractModel): # Adding counter self.total_account[(i)] = self.total_account[(i)] + (during and during[0] or 0) values[str(i)] = during and during[0] or 0.0 - total = False - if totals.has_key( partner['id'] ): - total = [ totals[partner['id']] ] - values['total'] = total and total[0] or 0.0 + values['total'] = sum([values['direction']] + [values[str(i)] for i in range(5)]) ## Add for total - self.total_account[(i+1)] = self.total_account[(i+1)] + (total and total[0] or 0.0) + self.total_account[(i + 1)] += values['total'] values['name'] = partner['name'] res.append(values) diff --git a/addons/account_extra_reports/__init__.py b/addons/account_extra_reports/__init__.py new file mode 100644 index 00000000000..b7771cae031 --- /dev/null +++ b/addons/account_extra_reports/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import report +import wizard diff --git a/addons/account_extra_reports/__openerp__.py b/addons/account_extra_reports/__openerp__.py new file mode 100644 index 00000000000..763ff021503 --- /dev/null +++ b/addons/account_extra_reports/__openerp__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Extra Accounting Reports', + 'version': '1.1', + 'category': 'Accounting & Finance', + 'description': """ +Extra Accounting Reports. +==================================== + +This module adds two new reports: +* Sale/Purchase Journal report +* Partner Ledger + """, + 'website': 'https://www.odoo.com/page/accounting', + 'depends': ['account_accountant'], + 'data': [ + 'wizard/account_report_print_journal_view.xml', + 'views/report_journal.xml', + 'wizard/account_report_partner_ledger_view.xml', + 'views/report_partnerledger.xml', + 'data/account_report.xml' + ], + 'demo': [], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/account_extra_reports/data/account_report.xml b/addons/account_extra_reports/data/account_report.xml new file mode 100644 index 00000000000..1264d7138bc --- /dev/null +++ b/addons/account_extra_reports/data/account_report.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/addons/account_extra_reports/report/__init__.py b/addons/account_extra_reports/report/__init__.py new file mode 100644 index 00000000000..e424cac8b5d --- /dev/null +++ b/addons/account_extra_reports/report/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_journal +import account_partner_ledger diff --git a/addons/account_extra_reports/report/account_journal.py b/addons/account_extra_reports/report/account_journal.py new file mode 100644 index 00000000000..77d7d8222cf --- /dev/null +++ b/addons/account_extra_reports/report/account_journal.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +import time +from openerp import api, models + + +class ReportJournal(models.AbstractModel): + _name = 'report.account_extra_reports.report_journal' + + def lines(self, target_move, journal_ids, sort_selection, data): + if isinstance(journal_ids, int): + journal_ids = [journal_ids] + + move_state = ['draft', 'posted'] + if target_move == 'posted': + move_state = ['posted'] + + query_get_clause = self._get_query_get_clause(data) + params = [tuple(move_state), tuple(journal_ids)] + query_get_clause[2] + query = 'SELECT "account_move_line".id FROM ' + query_get_clause[0] + ', account_move am WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' ORDER BY ' + if sort_selection == 'date': + query += '"account_move_line".date' + else: + query += 'am.name' + query += ', "account_move_line".move_id' + self.env.cr.execute(query, tuple(params)) + ids = map(lambda x: x[0], self.env.cr.fetchall()) + return self.env['account.move.line'].browse(ids) + + def _sum_debit(self, data, journal_id): + move_state = ['draft', 'posted'] + if data['form'].get('target_move', 'all') == 'posted': + move_state = ['posted'] + + query_get_clause = self._get_query_get_clause(data) + params = [tuple(move_state), tuple(journal_id.ids)] + query_get_clause[2] + self.env.cr.execute('SELECT SUM(debit) FROM ' + query_get_clause[0] + ', account_move am ' + 'WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' ', + tuple(params)) + return self.env.cr.fetchone()[0] or 0.0 + + def _sum_credit(self, data, journal_id): + move_state = ['draft', 'posted'] + if data['form'].get('target_move', 'all') == 'posted': + move_state = ['posted'] + + query_get_clause = self._get_query_get_clause(data) + params = [tuple(move_state), tuple(journal_id.ids)] + query_get_clause[2] + self.env.cr.execute('SELECT SUM(credit) FROM ' + query_get_clause[0] + ', account_move am ' + 'WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' ', + tuple(params)) + return self.env.cr.fetchone()[0] or 0.0 + + def _get_taxes(self, data, journal_id): + move_state = ['draft', 'posted'] + if data['form'].get('target_move', 'all') == 'posted': + move_state = ['posted'] + + query_get_clause = self._get_query_get_clause(data) + params = [tuple(move_state), tuple(journal_id.ids)] + query_get_clause[2] + self.env.cr.execute('SELECT DISTINCT tax_line_id FROM ' + query_get_clause[0] + ', account_move am ' + 'WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' AND tax_line_id is not null', + tuple(params)) + ids = map(lambda x: x[0], self.env.cr.fetchall()) + taxes = self.env['account.tax'].browse(ids) + res = {} + for tax in taxes: + res[tax] = {} + self.env.cr.execute('SELECT sum(debit - credit) FROM ' + query_get_clause[0] + ', account_move am ' + 'WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' AND tax_line_id = %s', + tuple(params + [tax.id])) + res[tax]['tax_amount'] = self.env.cr.fetchone()[0] or 0.0 + amls = self.env['account.move.line'].search([('tax_ids', 'in', [tax.id])]) + self.env.cr.execute('SELECT sum(debit - credit) FROM ' + query_get_clause[0] + ', account_move am ' + 'WHERE "account_move_line".move_id=am.id AND am.state IN %s AND "account_move_line".journal_id IN %s AND ' + query_get_clause[1] + ' AND "account_move_line".id in %s', + tuple(params + [tuple(amls.ids)])) + res[tax]['base_amount'] = self.env.cr.fetchone()[0] or 0.0 + if journal_id.type == 'sale': + #sales operation are credits + res[tax]['base_amount'] = res[tax]['base_amount'] * -1 + res[tax]['tax_amount'] = res[tax]['tax_amount'] * -1 + return res + + def _get_query_get_clause(self, data): + return self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() + + @api.multi + def render_html(self, data): + target_move = data['form'].get('target_move', 'all') + sort_selection = data['form'].get('sort_selection', 'date') + + res = {} + for journal in data['form']['journal_ids']: + res[journal] = self.with_context(data['form'].get('used_context', {})).lines(target_move, journal, sort_selection, data) + docargs = { + 'doc_ids': data['form']['journal_ids'], + 'doc_model': self.env['account.journal'], + 'data': data, + 'docs': self.env['account.journal'].browse(data['form']['journal_ids']), + 'time': time, + 'lines': res, + 'sum_credit': self._sum_credit, + 'sum_debit': self._sum_debit, + 'get_taxes': self._get_taxes, + } + return self.env['report'].render('account_extra_reports.report_journal', docargs) diff --git a/addons/account_extra_reports/report/account_partner_ledger.py b/addons/account_extra_reports/report/account_partner_ledger.py new file mode 100644 index 00000000000..a5ca5ed2a3c --- /dev/null +++ b/addons/account_extra_reports/report/account_partner_ledger.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- + +import time +from openerp import api, models + + +class ReportPartnerLedger(models.AbstractModel): + _name = 'report.account_extra_reports.report_partnerledger' + + def _lines(self, data, partner): + full_account = [] + query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] + query = """ + SELECT "account_move_line".id, "account_move_line".date, j.code, acc.code as a_code, acc.name as a_name, "account_move_line".ref, m.name as move_name, "account_move_line".name, "account_move_line".debit, "account_move_line".credit, "account_move_line".amount_currency,"account_move_line".currency_id, c.symbol AS currency_code + FROM """ + query_get_data[0] + """ + LEFT JOIN account_journal j ON ("account_move_line".journal_id = j.id) + LEFT JOIN account_account acc ON ("account_move_line".account_id = acc.id) + LEFT JOIN res_currency c ON ("account_move_line".currency_id=c.id) + LEFT JOIN account_move m ON (m.id="account_move_line".move_id) + WHERE "account_move_line".partner_id = %s + AND m.state IN %s + AND "account_move_line".account_id IN %s AND """ + query_get_data[1] + reconcile_clause + """ + ORDER BY "account_move_line".date""" + self.env.cr.execute(query, tuple(params)) + res = self.env.cr.dictfetchall() + sum = 0.0 + for r in res: + r['displayed_name'] = '-'.join( + r['move_name'] not in ['', '/'] and [r['move_name']] or [] + + r['ref'] not in ['', '/'] and [r['ref']] or [] + + r['name'] not in ['', '/'] and [r['name']] or [] + ) + sum += r['debit'] - r['credit'] + r['progress'] = sum + full_account.append(r) + return full_account + + def _sum_partner(self, data, partner, field): + if field not in ['debit', 'credit', 'debit - credit']: + return + result = 0.0 + query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + + params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] + query = """SELECT sum(""" + field + """) + FROM """ + query_get_data[0] + """, account_move AS m + WHERE "account_move_line".partner_id = %s + AND m.id = "account_move_line".move_id + AND m.state IN %s + AND account_id IN %s + AND """ + query_get_data[1] + reconcile_clause + self.env.cr.execute(query, tuple(params)) + + contemp = self.env.cr.fetchone() + if contemp is not None: + result = contemp[0] or 0.0 + return result + + @api.multi + def render_html(self, data): + data['computed'] = {} + + obj_partner = self.env['res.partner'] + query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get() + data['computed']['move_state'] = ['draft', 'posted'] + if data['form'].get('target_move', 'all') == 'posted': + data['computed']['move_state'] = ['posted'] + result_selection = data['form'].get('result_selection', 'customer') + if result_selection == 'supplier': + data['computed']['ACCOUNT_TYPE'] = ['payable'] + elif result_selection == 'customer': + data['computed']['ACCOUNT_TYPE'] = ['receivable'] + else: + data['computed']['ACCOUNT_TYPE'] = ['payable', 'receivable'] + + self.env.cr.execute(""" + SELECT a.id + FROM account_account a + WHERE a.internal_type IN %s + AND NOT a.deprecated""", (tuple(data['computed']['ACCOUNT_TYPE']),)) + data['computed']['account_ids'] = [a for (a,) in self.env.cr.fetchall()] + params = [tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2] + reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false ' + query = """ + SELECT DISTINCT "account_move_line".partner_id + FROM """ + query_get_data[0] + """, account_account AS account, account_move AS am + WHERE "account_move_line".partner_id IS NOT NULL + AND "account_move_line".account_id = account.id + AND am.id = "account_move_line".move_id + AND am.state IN %s + AND "account_move_line".account_id IN %s + AND NOT account.deprecated + AND """ + query_get_data[1] + reconcile_clause + self.env.cr.execute(query, tuple(params)) + partner_ids = [res['partner_id'] for res in self.env.cr.dictfetchall()] + partners = obj_partner.browse(partner_ids) + partners = sorted(partners, key=lambda x: (x.ref, x.name)) + + docargs = { + 'doc_ids': partner_ids, + 'doc_model': self.env['res.partner'], + 'data': data, + 'docs': partners, + 'time': time, + 'lines': self._lines, + 'sum_partner': self._sum_partner, + } + return self.env['report'].render('account_extra_reports.report_partnerledger', docargs) diff --git a/addons/account_extra_reports/views/report_journal.xml b/addons/account_extra_reports/views/report_journal.xml new file mode 100644 index 00000000000..73926363f09 --- /dev/null +++ b/addons/account_extra_reports/views/report_journal.xml @@ -0,0 +1,103 @@ + + + + diff --git a/addons/account_extra_reports/views/report_partnerledger.xml b/addons/account_extra_reports/views/report_partnerledger.xml new file mode 100644 index 00000000000..e8cb8c3e263 --- /dev/null +++ b/addons/account_extra_reports/views/report_partnerledger.xml @@ -0,0 +1,91 @@ + + + + diff --git a/addons/account_extra_reports/wizard/__init__.py b/addons/account_extra_reports/wizard/__init__.py new file mode 100644 index 00000000000..7755d1ec568 --- /dev/null +++ b/addons/account_extra_reports/wizard/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import account_report_common_journal +import account_report_print_journal +import account_report_partner_ledger diff --git a/addons/account_extra_reports/wizard/account_report_common_journal.py b/addons/account_extra_reports/wizard/account_report_common_journal.py new file mode 100644 index 00000000000..2108d655152 --- /dev/null +++ b/addons/account_extra_reports/wizard/account_report_common_journal.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +from openerp import api, fields, models + + +class AccountCommonJournalReport(models.TransientModel): + _name = 'account.common.journal.report' + _description = 'Account Common Journal Report' + _inherit = "account.common.report" + + amount_currency = fields.Boolean('With Currency', help="Print Report with the currency column if the currency differs from the company currency.") + + @api.multi + def pre_print_report(self, data): + data['form'].update({'amount_currency': self.amount_currency}) + return data diff --git a/addons/account_extra_reports/wizard/account_report_partner_ledger.py b/addons/account_extra_reports/wizard/account_report_partner_ledger.py new file mode 100644 index 00000000000..0a92f43eca7 --- /dev/null +++ b/addons/account_extra_reports/wizard/account_report_partner_ledger.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- + +from openerp import fields, models, _ +from openerp.exceptions import UserError + + +class AccountPartnerLedger(models.TransientModel): + _inherit = "account.common.partner.report" + _name = "account.report.partner.ledger" + _description = "Account Partner Ledger" + + amount_currency = fields.Boolean("With Currency", help="It adds the currency column on report if the currency differs from the company currency.") + reconciled = fields.Boolean('Reconciled Entries') + + def _print_report(self, data): + data = self.pre_print_report(data) + data['form'].update({'reconciled': self.reconciled, 'amount_currency': self.amount_currency}) + return self.env['report'].get_action(self, 'account_extra_reports.report_partnerledger', data=data) diff --git a/addons/account_extra_reports/wizard/account_report_partner_ledger_view.xml b/addons/account_extra_reports/wizard/account_report_partner_ledger_view.xml new file mode 100644 index 00000000000..d50e048de63 --- /dev/null +++ b/addons/account_extra_reports/wizard/account_report_partner_ledger_view.xml @@ -0,0 +1,48 @@ + + + + + Partner Ledger + account.report.partner.ledger + + + + + + + + + + + + + + + + Partner Ledger + ir.actions.act_window + account.report.partner.ledger + form + form + + new + + + + + Partner Ledger + client_print_multi + + action + account.account + + + + + diff --git a/addons/account_extra_reports/wizard/account_report_print_journal.py b/addons/account_extra_reports/wizard/account_report_print_journal.py new file mode 100644 index 00000000000..d8b465b2a96 --- /dev/null +++ b/addons/account_extra_reports/wizard/account_report_print_journal.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from openerp import fields, models + + +class AccountPrintJournal(models.TransientModel): + _inherit = "account.common.journal.report" + _name = "account.print.journal" + _description = "Account Print Journal" + + sort_selection = fields.Selection([('date', 'Date'), ('move_name', 'Journal Entry Number'),], 'Entries Sorted by', required=True, default='move_name') + journal_ids = fields.Many2many('account.journal', string='Journals', required=True, default=lambda self: self.env['account.journal'].search([('type', 'in', ['sale', 'purchase'])])) + + def _print_report(self, data): + data = self.pre_print_report(data) + data['form'].update({'sort_selection': self.sort_selection}) + return self.env['report'].with_context(landscape=True).get_action(self, 'account_extra_reports.report_journal', data=data) diff --git a/addons/account_extra_reports/wizard/account_report_print_journal_view.xml b/addons/account_extra_reports/wizard/account_report_print_journal_view.xml new file mode 100644 index 00000000000..e40cac57f26 --- /dev/null +++ b/addons/account_extra_reports/wizard/account_report_print_journal_view.xml @@ -0,0 +1,40 @@ + + + + + Sale/Purchase Journal + account.print.journal + + + + + + + + + + [('type', 'in', ('sale', 'purchase'))] + + + + + + + Sale/Purchase Journal + ir.actions.act_window + account.print.journal + form + form + + new + + + + + diff --git a/addons/barcodes/barcodes.py b/addons/barcodes/barcodes.py index c2c8f910949..a288fd2e55c 100644 --- a/addons/barcodes/barcodes.py +++ b/addons/barcodes/barcodes.py @@ -188,7 +188,7 @@ class barcode_rule(models.Model): @api.model def _get_type_selection(self): - return [('alias','Alias'),('product','Unit Product')] + return [('alias', _('Alias')), ('product', _('Unit Product'))] _columns = { 'name': fields.char('Rule Name', size=32, required=True, help='An internal identification for this barcode nomenclature rule'), diff --git a/addons/barcodes/i18n/barcodes.pot b/addons/barcodes/i18n/barcodes.pot index 5018c7b5270..248fdaac767 100644 --- a/addons/barcodes/i18n/barcodes.pot +++ b/addons/barcodes/i18n/barcodes.pot @@ -53,7 +53,9 @@ msgid "A barcode nomenclature defines how the point of sale identify and interpr msgstr "" #. module: barcodes +#: code:addons/barcodes/barcodes.py:191 #: model:ir.model.fields,field_description:barcodes.field_barcode_rule_alias +#, python-format msgid "Alias" msgstr "" @@ -267,6 +269,12 @@ msgstr "" msgid "UPC/EAN Conversion" msgstr "" +#. module: barcodes +#: code:addons/barcodes/barcodes.py:191 +#, python-format +msgid "Unit Product" +msgstr "" + #. module: barcodes #: model:ir.model.fields,help:barcodes.field_barcode_rule_sequence msgid "Used to order rules such that rules with a smaller sequence match first" diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index 3e526b10db4..1d0ce4a0161 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -1534,74 +1534,63 @@ class calendar_event(osv.Model): return super(calendar_event, self)._name_search(cr, user, name=name, args=args, operator=operator, context=context, limit=limit, name_get_uid=name_get_uid) def write(self, cr, uid, ids, values, context=None): - def _only_changes_to_apply_on_real_ids(field_names): - ''' return True if changes are only to be made on the real ids''' - for field in field_names: - if field in ['start', 'start_date', 'start_datetime', 'stop', 'stop_date', 'stop_datetime', 'active']: - return True - return False - + context = context or {} if not isinstance(ids, (tuple, list)): ids = [ids] - context = context or {} - self._set_date(cr, uid, values, id=ids[0], context=context) + values0 = values - for one_ids in ids: - if isinstance(one_ids, (basestring, int, long)): - if len(str(one_ids).split('-')) == 1: - ids = [int(one_ids)] - else: - ids = [one_ids] + # process events one by one + for event_id in ids: + # make a copy, since _set_date() modifies values depending on event + values = dict(values0) + self._set_date(cr, uid, values, event_id, context=context) - res = False - new_id = False - - # Special write of complex IDS - for event_id in list(ids): - if len(str(event_id).split('-')) == 1: - continue - - ids.remove(event_id) - real_event_id = calendar_id2real_id(event_id) - - # if we are setting the recurrency flag to False or if we are only changing fields that - # should be only updated on the real ID and not on the virtual (like message_follower_ids): - # then set real ids to be updated. - if not values.get('recurrency', True) or not _only_changes_to_apply_on_real_ids(values.keys()): - ids.append(real_event_id) - continue + # special write of complex IDS + real_ids = [] + new_ids = [] + if '-' not in str(event_id): + real_ids = [int(event_id)] else: - data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration']) - if data.get('rrule'): - new_id = self._detach_one_event(cr, uid, event_id, values, context=None) + real_event_id = calendar_id2real_id(event_id) - res = super(calendar_event, self).write(cr, uid, [int(event_id) for event_id in ids], values, context=context) - - # set end_date for calendar searching - if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \ - (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')): - for id in ids: - final_date = self._get_recurrency_end_date(cr, uid, id, context=context) - super(calendar_event, self).write(cr, uid, [id], {'final_date': final_date}, context=context) - - attendees_create = False - if values.get('partner_ids', False): - attendees_create = self.create_attendees(cr, uid, ids, context) - - if (values.get('start_date') or values.get('start_datetime', False)) and values.get('active', True): - the_id = new_id or (ids and int(ids[0])) - if the_id: - if attendees_create: - attendees_create = attendees_create[the_id] - mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids'])) + # if we are setting the recurrency flag to False or if we are only changing fields that + # should be only updated on the real ID and not on the virtual (like message_follower_ids): + # then set real ids to be updated. + blacklisted = any(key in values for key in ('start', 'stop', 'active')) + if not values.get('recurrency', True) or not blacklisted: + real_ids = [real_event_id] else: - mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids] + data = self.read(cr, uid, event_id, ['start', 'stop', 'rrule', 'duration']) + if data.get('rrule'): + new_ids = [self._detach_one_event(cr, uid, event_id, values, context=None)] - if mail_to_ids: - current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context) - return res or True and False + super(calendar_event, self).write(cr, uid, real_ids, values, context=context) + + # set end_date for calendar searching + if values.get('recurrency') and values.get('end_type', 'count') in ('count', unicode('count')) and \ + (values.get('rrule_type') or values.get('count') or values.get('start') or values.get('stop')): + for id in real_ids: + final_date = self._get_recurrency_end_date(cr, uid, id, context=context) + super(calendar_event, self).write(cr, uid, [id], {'final_date': final_date}, context=context) + + attendees_create = False + if values.get('partner_ids', False): + attendees_create = self.create_attendees(cr, uid, real_ids + new_ids, context) + + if (values.get('start_date') or values.get('start_datetime')) and values.get('active', True): + for the_id in real_ids + new_ids: + if attendees_create: + attendees_create = attendees_create[the_id] + mail_to_ids = list(set(attendees_create['old_attendee_ids']) - set(attendees_create['removed_attendee_ids'])) + else: + mail_to_ids = [att.id for att in self.browse(cr, uid, the_id, context=context).attendee_ids] + + if mail_to_ids: + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, mail_to_ids, template_xmlid='calendar_template_meeting_changedate', email_from=current_user.email, context=context) + + return True def create(self, cr, uid, vals, context=None): if context is None: diff --git a/addons/calendar/test/test_calendar_recurrent_event_case2.yml b/addons/calendar/test/test_calendar_recurrent_event_case2.yml index bd5faac0086..0908edfb2a8 100644 --- a/addons/calendar/test/test_calendar_recurrent_event_case2.yml +++ b/addons/calendar/test/test_calendar_recurrent_event_case2.yml @@ -91,5 +91,12 @@ meetings = self.browse(cr, uid, meeting_ids, context) for meeting in meetings: assert meeting.description == 'Review code of the module: sync_google_calendar.', 'Description not changed for id: %s' %meeting.id - - +- + I update the description of two meetings, and check that both have been updated +- + !python {model: calendar.event}: | + DESCRIPTION = "Some description" + ids = [ref('calendar_event_testmeeting0'), ref('calendar_event_sprintreview0')] + self.write(cr, uid, ids, {'description': DESCRIPTION}, context=context) + for i, event in enumerate(self.browse(cr, uid, ids, context)): + assert event.description == DESCRIPTION, "Event %d has not been updated" % i diff --git a/addons/crm/sales_team.py b/addons/crm/sales_team.py index 1ae14e831e8..bad0c676f4b 100644 --- a/addons/crm/sales_team.py +++ b/addons/crm/sales_team.py @@ -122,7 +122,7 @@ class crm_team(osv.Model): def action_your_pipeline(self, cr, uid, context=None): IrModelData = self.pool['ir.model.data'] - action = IrModelData.xmlid_to_object(cr, uid, 'crm.crm_lead_opportunities_tree_view').read(['name', 'help', 'res_model', 'target', 'domain', 'context', 'type', 'search_view_id']) + action = IrModelData.xmlid_to_object(cr, uid, 'crm.crm_lead_opportunities_tree_view', context=context).read(['name', 'help', 'res_model', 'target', 'domain', 'context', 'type', 'search_view_id']) if not action: action = {} else: diff --git a/addons/google_drive/google_drive_demo.xml b/addons/google_drive/google_drive_demo.xml index ebe73f40ec2..d222da057fa 100644 --- a/addons/google_drive/google_drive_demo.xml +++ b/addons/google_drive/google_drive_demo.xml @@ -15,7 +15,7 @@ Partner Review - https://docs.google.com/spreadsheet/ccc?key=0Ah2qnrLAoZmUdGRvdVdmS1VoSDctWk1kd18taGZ4ckE#gid=0 + https://docs.google.com/spreadsheet/ccc?key=1eIM-q7PX9wp1vEUUdUvrmAe8V3Do60vXDETulNmI9mA#gid=0 Partner Review %(name)s diff --git a/addons/hr_equipment/i18n/hr_equipment.pot b/addons/hr_equipment/i18n/hr_equipment.pot index b50652925d5..6eefd52c633 100644 --- a/addons/hr_equipment/i18n/hr_equipment.pot +++ b/addons/hr_equipment/i18n/hr_equipment.pot @@ -105,7 +105,7 @@ msgstr "" #. module: hr_equipment #: model:ir.model,name:hr_equipment.model_hr_equipment_category -#: model:ir.model.fields,field_description:hr_equipment.field_hr_equipment_category_id_5383 +#: model:ir.model.fields,field_description:hr_equipment.field_hr_equipment_category_id_6493 msgid "Asset Category" msgstr "" diff --git a/addons/hr_payroll_account/hr_payroll_account.py b/addons/hr_payroll_account/hr_payroll_account.py index 7a24fb27459..0951d8b2a94 100644 --- a/addons/hr_payroll_account/hr_payroll_account.py +++ b/addons/hr_payroll_account/hr_payroll_account.py @@ -66,7 +66,7 @@ class hr_payslip(osv.osv): def onchange_contract_id(self, cr, uid, ids, date_from, date_to, employee_id=False, contract_id=False, context=None): contract_obj = self.pool.get('hr.contract') res = super(hr_payslip, self).onchange_contract_id(cr, uid, ids, date_from=date_from, date_to=date_to, employee_id=employee_id, contract_id=contract_id, context=context) - journal_id = contract_id and contract_obj.browse(cr, uid, contract_id, context=context).journal_id.id or False + journal_id = contract_id and contract_obj.browse(cr, uid, contract_id, context=context).journal_id.id or (not contract_id and self._get_default_journal(cr, uid, context=None)) res['value'].update({'journal_id': journal_id}) return res diff --git a/addons/im_livechat/static/src/js/im_livechat.js b/addons/im_livechat/static/src/js/im_livechat.js index 9ccb50c10f8..84f626883c0 100644 --- a/addons/im_livechat/static/src/js/im_livechat.js +++ b/addons/im_livechat/static/src/js/im_livechat.js @@ -23,7 +23,7 @@ var LivechatButton = Widget.extend({ init: function (parent, server_url, options) { this._super(parent); this.options = _.defaults(options || {}, { - placeholder: _t('Ask something ...'), + input_placeholder: _t('Ask something ...'), default_username: _t("Visitor"), button_text: _t("Chat with one of our collaborators"), default_message: _t("How may I help you?"), @@ -91,10 +91,14 @@ var LivechatButton = Widget.extend({ return $.when.apply($, defs); }, - open_chat: function () { + open_chat: _.debounce(function () { + if (this.opening_chat) { + return; + } var self = this; var cookie = utils.get_cookie('im_livechat_session'); var def; + this.opening_chat = true; clearTimeout(this.auto_popup_timeout); if (cookie) { def = $.when(JSON.parse(cookie)); @@ -120,13 +124,16 @@ var LivechatButton = Widget.extend({ utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60); utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60); } + }).always(function () { + self.opening_chat = false; }); - }, + }, 200, true), open_chat_window: function (channel) { var self = this; var options = { display_stars: false, + placeholder: this.options.input_placeholder || "", }; var is_folded = (channel.state === 'folded'); this.chat_window = new ChatWindow(this, channel.id, channel.name, is_folded, channel.message_unread_counter, options); @@ -136,7 +143,10 @@ var LivechatButton = Widget.extend({ }); this.chat_window.on("close_chat_session", this, function () { var input_disabled = this.chat_window.$(".o_chat_input input").prop('disabled'); - if (this.messages.length > 1 && !input_disabled) { + var ask_fb = !input_disabled && _.find(this.messages, function (msg) { + return msg.id !== '_welcome'; + }); + if (ask_fb) { this.chat_window.toggle_fold(false); this.ask_feedback(); } else { @@ -212,15 +222,17 @@ var LivechatButton = Widget.extend({ }, send_welcome_message: function () { - this.add_message({ - id: 1, - attachment_ids: [], - author_id: this.channel.operator_pid, - body: this.options.default_message, - channel_ids: [this.channel.id], - date: time.datetime_to_str(new Date()), - tracking_value_ids: [], - }, {prepend: true}); + if (this.options.default_message) { + this.add_message({ + id: '_welcome', + attachment_ids: [], + author_id: this.channel.operator_pid, + body: this.options.default_message, + channel_ids: [this.channel.id], + date: time.datetime_to_str(new Date()), + tracking_value_ids: [], + }, {prepend: true}); + } }, ask_feedback: function () { diff --git a/addons/mail/i18n/mail.pot b/addons/mail/i18n/mail.pot index ff2e24c407a..a6ab1f857c3 100644 --- a/addons/mail/i18n/mail.pot +++ b/addons/mail/i18n/mail.pot @@ -258,6 +258,13 @@ msgstr "" msgid "@ Inbox" msgstr "" +#. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/systray.xml:20 +#, python-format +msgid "@Inbox" +msgstr "" + #. module: mail #: model:ir.model.fields,help:mail.field_mail_alias_alias_defaults #: model:ir.model.fields,help:mail.field_mail_channel_alias_defaults @@ -419,6 +426,13 @@ msgstr "" msgid "Aliases" msgstr "" +#. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/systray.xml:12 +#, python-format +msgid "All" +msgstr "" + #. module: mail #: selection:res.partner,notify_email:0 msgid "All Messages" @@ -658,7 +672,10 @@ msgid "Channels/Partner" msgstr "" #. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/systray.xml:13 #: model:ir.actions.client,name:mail.mail_channel_action_client_chat +#, python-format msgid "Chat" msgstr "" @@ -1476,7 +1493,8 @@ msgstr "" #. module: mail #. openerp-web -#: code:addons/mail/static/src/js/client_action.js:31 +#: code:addons/mail/static/src/js/client_action.js:30 +#: code:addons/mail/static/src/xml/client_action.xml:123 #, python-format msgid "Invite people" msgstr "" @@ -1813,6 +1831,8 @@ msgid "Message-Id" msgstr "" #. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/systray.xml:6 #: model:ir.actions.act_window,name:mail.action_view_mail_message #: model:ir.model.fields,field_description:mail.field_mail_channel_message_ids #: model:ir.model.fields,field_description:mail.field_mail_thread_message_ids @@ -1820,6 +1840,7 @@ msgstr "" #: model:ir.model.fields,field_description:mail.field_res_users_message_ids #: model:ir.ui.menu,name:mail.menu_mail_message #: model:ir.ui.view,arch_db:mail.view_message_tree +#, python-format msgid "Messages" msgstr "" @@ -1931,8 +1952,11 @@ msgstr "" #. module: mail #. openerp-web -#: code:addons/mail/static/src/js/chat_manager.js:99 +#: code:addons/mail/static/src/js/chat_manager.js:54 +#: code:addons/mail/static/src/js/window_manager.js:124 #: code:addons/mail/static/src/xml/chatter.xml:67 +#: code:addons/mail/static/src/xml/client_action.xml:127 +#: code:addons/mail/static/src/xml/systray.xml:16 #, python-format msgid "New message" msgstr "" @@ -2574,6 +2598,13 @@ msgstr "" msgid "Send Email" msgstr "" +#. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/client_action.xml:127 +#, python-format +msgid "Send mail" +msgstr "" + #. module: mail #: code:addons/mail/models/mail_template.py:237 #, python-format @@ -2887,6 +2918,13 @@ msgstr "" msgid "To (Partners)" msgstr "" +#. module: mail +#. openerp-web +#: code:addons/mail/static/src/xml/chat_window.xml:24 +#, python-format +msgid "To:" +msgstr "" + #. module: mail #. openerp-web #: code:addons/mail/static/src/xml/chatter.xml:30 @@ -3000,6 +3038,11 @@ msgstr "" msgid "Unread Messages Counter" msgstr "" +#. module: mail +#: model:ir.ui.view,arch_db:mail.view_message_search +msgid "Unread messages" +msgstr "" + #. module: mail #. openerp-web #: code:addons/mail/static/src/xml/client_action.xml:126 diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index e4422e9286a..eaadfb99c04 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -445,7 +445,7 @@ class Channel(models.Model): if self.channel_message_ids.ids: last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id}) - self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id}) + self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id}) return last_message_id @api.multi diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 28dbe2332a7..b7a5b73145c 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -169,6 +169,7 @@ class MailThread(models.AbstractModel): @api.multi def _get_message_unread(self): res = dict((res_id, 0) for res_id in self.ids) + partner_id = self.env.user.partner_id.id # search for unread messages, directly in SQL to improve performances self._cr.execute(""" SELECT msg.res_id FROM mail_message msg @@ -176,8 +177,8 @@ class MailThread(models.AbstractModel): ON rel.mail_message_id = msg.id RIGHT JOIN mail_channel_partner cp ON (cp.channel_id = rel.mail_channel_id AND cp.partner_id = %s AND (cp.seen_message_id < msg.id)) - WHERE msg.model = %s AND msg.res_id in %s""", - (self.env.user.partner_id.id, self._name, tuple(self.ids),)) + WHERE msg.model = %s AND msg.res_id in %s AND msg.author_id != %s""", + (partner_id, self._name, tuple(self.ids), partner_id,)) for result in self._cr.fetchall(): res[result[0]] += 1 diff --git a/addons/mail/models/update.py b/addons/mail/models/update.py index d5d46242eb6..d8e7875c62e 100644 --- a/addons/mail/models/update.py +++ b/addons/mail/models/update.py @@ -109,8 +109,9 @@ class publisher_warranty_contract(AbstractModel): pass if result.get('enterprise_info'): # Update expiration date - self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'database.expiration_date', result.get('enterprise_info').get('expiration_date'), ['base.group_user']) - self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'database.expiration_reason', result.get('enterprise_info').get('expiration_reason', 'trial'), ['base.group_system']) + self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'database.expiration_date', result['enterprise_info'].get('expiration_date'), ['base.group_user']) + self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'database.expiration_reason', result['enterprise_info'].get('expiration_reason', 'trial'), ['base.group_system']) + self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID, 'database.enterprise_code', result['enterprise_info'].get('enterprise_code'), ['base.group_user']) except Exception: if cron_mode: diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js index aff51c721cf..75b1819ddd2 100644 --- a/addons/mail/static/src/js/chat_manager.js +++ b/addons/mail/static/src/js/chat_manager.js @@ -118,7 +118,7 @@ function strip_html (node, transform_children) { function inline (node, transform_children) { if (node.nodeType === 3) return node.data; if (node.tagName === "BR") return " "; - if (node.tagName.match(/^(A|P|DIV)$/)) return transform_children(); + if (node.tagName.match(/^(A|P|DIV|PRE)$/)) return transform_children(); node.innerHTML = transform_children(); return node.outerHTML; } @@ -133,7 +133,7 @@ function add_message (data, options) { if (!msg) { msg = make_message(data); - // Keep the array ordered by date when inserting the new message + // Keep the array ordered by id when inserting the new message messages.splice(_.sortedIndex(messages, msg, 'id'), 0, msg); _.each(msg.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); @@ -457,9 +457,7 @@ function update_channel_unread_counter (channel, counter) { } var channel_seen = _.throttle(function (channel) { - return ChannelModel.call('channel_seen', [[channel.id]]).then(function (last_seen_message_id) { - channel.last_seen_message_id = last_seen_message_id; - }); + return ChannelModel.call('channel_seen', [[channel.id]], {}, {shadow: true}); }, 3000); // Notification handlers @@ -573,7 +571,7 @@ function on_mark_as_read_notification (data) { _.each(data.channel_ids, function (channel_id) { var channel = chat_manager.get_channel(channel_id); if (channel) { - channel.needaction_counter -= data.message_ids.length; + channel.needaction_counter = Math.max(channel.needaction_counter - data.message_ids.length, 0); } }); } else { // if no channel_ids specified, this is a 'mark all read' in the inbox @@ -581,7 +579,7 @@ function on_mark_as_read_notification (data) { channel.needaction_counter = 0; }); } - needaction_counter -= data.message_ids.length; + needaction_counter = Math.max(needaction_counter - data.message_ids.length, 0); chat_manager.bus.trigger('update_needaction', needaction_counter); } @@ -609,8 +607,11 @@ function on_mark_as_unread_notification (data) { function on_channel_seen_notification (data) { var channel = chat_manager.get_channel(data.id); - if (channel && channel.unread_counter) { - update_channel_unread_counter(channel, 0); + if (channel) { + channel.last_seen_message_id = data.last_message_id; + if (channel.unread_counter) { + update_channel_unread_counter(channel, 0); + } } } @@ -781,7 +782,7 @@ var chat_manager = { } if (!channel.members_deferred) { channel.members_deferred = ChannelModel - .call("channel_fetch_listeners", [channel.uuid]) + .call("channel_fetch_listeners", [channel.uuid], {}, {shadow: true}) .then(function (members) { var suggestions = []; _.each(mention_partner_suggestions, function (partners) { @@ -790,7 +791,7 @@ var chat_manager = { })); }); - return [members].concat(suggestions); + return [members]; }); } return channel.members_deferred; @@ -807,12 +808,27 @@ var chat_manager = { return chat_unread_counter; }, + get_last_seen_message: function (channel) { + if (channel.last_seen_message_id) { + var messages = channel.cache['[]'].messages; + var msg = _.findWhere(messages, {id: channel.last_seen_message_id}); + if (msg) { + var i = _.sortedIndex(messages, msg, 'id') + 1; + while (i < messages.length && messages[i].author_id && messages[i].author_id[0] === session.partner_id) { + msg = messages[i]; + i++; + } + return msg; + } + } + }, + get_discuss_ids: function () { return discuss_ids; }, detach_channel: function (channel) { - return ChannelModel.call("channel_minimize", [channel.uuid, true]); + return ChannelModel.call("channel_minimize", [channel.uuid, true], {}, {shadow: true}); }, remove_chatter_messages: function (model) { messages = _.reject(messages, function (message) { @@ -864,7 +880,7 @@ var chat_manager = { }, close_chat_session: function (channel_id) { var channel = this.get_channel(channel_id); - ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}); + ChannelModel.call("channel_fold", [], {uuid : channel.uuid, state : "closed"}, {shadow: true}); }, fold_channel: function (channel_id, folded) { var args = { @@ -873,7 +889,7 @@ var chat_manager = { if (_.isBoolean(folded)) { args.state = folded ? 'folded' : 'open'; } - return ChannelModel.call("channel_fold", [], args); + return ChannelModel.call("channel_fold", [], args, {shadow: true}); }, /** * Special redirection handling for given model and id @@ -948,8 +964,8 @@ var chat_manager = { return parse_and_transform(message_body, inline); }, - search_partner: function (search_val) { - return PartnerModel.call('im_search', [search_val, 20]).then(function(result) { + search_partner: function (search_val, limit) { + return PartnerModel.call('im_search', [search_val, limit || 20], {}, {shadow: true}).then(function(result) { var values = []; _.each(result, function(user) { var escaped_name = _.escape(user.name); @@ -994,8 +1010,8 @@ function init () { }); var ir_model = new Model("ir.model.data"); - var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"]); - var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"]); + var load_menu_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_menu_root_chat"], {}, {shadow: true}); + var load_action_id = ir_model.call("xmlid_to_res_id", ["mail.mail_channel_action_client_chat"], {}, {shadow: true}); bus.on('notification', null, on_notification); diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 851c7fa56d2..95ac9dd8897 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -24,10 +24,10 @@ return Widget.extend({ this._super(parent); this.title = title; this.channel_id = channel_id; - this.placeholder = _t("Say something"); this.folded = is_folded; this.options = _.defaults(options || {}, { display_stars: true, + placeholder: _t("Say something"), }); this.unread_msgs = unread_msgs || 0; this.is_hidden = false; @@ -46,6 +46,8 @@ return Widget.extend({ if (this.folded) { this.$el.css('height', HEIGHT_FOLDED); + } else { + this.focus_input(); } var def = this.thread.replace(this.$('.o_chat_content')); return $.when(this._super(), def); @@ -67,9 +69,15 @@ return Widget.extend({ this.folded = _.isBoolean(fold) ? fold : !this.folded; if (!this.folded) { this.thread.scroll_to(); + this.focus_input(); } this.fold(); }, + focus_input: function () { + if (!config.device.touch) { + this.$input.focus(); + } + }, do_show: function () { this.is_hidden = false; this._super.apply(this, arguments); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index c0613666be3..dc528b2b1aa 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -451,6 +451,7 @@ var ChatterComposer = composer.BasicComposer.extend({ record_name: false, is_log: false, internal_subtypes: [], + default_body: '', }); if (this.options.is_log) { this.options.send_text = _('Log'); @@ -467,6 +468,13 @@ var ChatterComposer = composer.BasicComposer.extend({ return $.when(this._super.apply(this, arguments), this.message_get_suggested_recipients()); }, + start: function () { + var self = this; + return this._super().then(function () { + self.$input.val(self.options.default_body); + }); + }, + should_send: function () { return false; }, @@ -866,6 +874,8 @@ var Chatter = form_common.AbstractField.extend({ // destroy current composer, if any if (this.composer) { this.composer.destroy(); + this.composer = undefined; + this.mute_new_message_button(false); } // fetch and render messages of current document @@ -895,6 +905,7 @@ var Chatter = form_common.AbstractField.extend({ internal_subtypes: this.options.internal_subtypes, is_log: options && options.is_log, record_name: this.record_name, + default_body: old_composer && old_composer.$input.val(), }); this.composer.on('input_focused', this, function () { this.composer.mention_set_prefetched_partners(this.mention_suggestions || []); @@ -910,11 +921,19 @@ var Chatter = form_common.AbstractField.extend({ self.composer.on('post_message', self, self.on_post_message); self.composer.on('need_refresh', self, self.refresh_followers); }); - + this.mute_new_message_button(true); }, close_composer: function () { if (this.composer.is_empty()) { this.composer.do_hide(); + this.mute_new_message_button(false); + } + }, + mute_new_message_button: function (mute) { + if (mute) { + this.$('.o_chatter_button_new_message').removeClass('btn-primary').addClass('btn-default'); + } else if (!mute) { + this.$('.o_chatter_button_new_message').removeClass('btn-default').addClass('btn-primary'); } }, diff --git a/addons/mail/static/src/js/client_action.js b/addons/mail/static/src/js/client_action.js index 38cc26e8997..82b36f3425d 100644 --- a/addons/mail/static/src/js/client_action.js +++ b/addons/mail/static/src/js/client_action.js @@ -171,8 +171,8 @@ var ChatAction = Widget.extend(ControlPanelMixin, { this.searchview = new SearchView(this, dataset, view_id, {}, options); this.searchview.on('search_data', this, this.on_search); - this.basic_composer = new composer.BasicComposer(this); - this.extended_composer = new composer.ExtendedComposer(this); + this.basic_composer = new composer.BasicComposer(this, {mention_partners_restricted: true}); + this.extended_composer = new composer.ExtendedComposer(this, {mention_partners_restricted: true}); this.thread = new ChatThread(this, { display_help: true, shorten_messages: false, @@ -423,13 +423,11 @@ var ChatAction = Widget.extend(ControlPanelMixin, { // Compute position of the 'New messages' separator, only once when joining // a channel to keep it in the thread when new messages arrive if (_.isUndefined(this.messages_separator_position)) { - var msg_id = this.last_seen_message_id; if (!this.unread_counter) { this.messages_separator_position = false; // no unread message -> don't display separator - } else if ((msg_id === false) || !_.findWhere(messages, {id: msg_id})) { - this.messages_separator_position = 'top'; // all displayed messages are unread } else { - this.messages_separator_position = msg_id; // last read message is msg_id + var msg = chat_manager.get_last_seen_message(this.channel); + this.messages_separator_position = msg ? msg.id : 'top'; } } return { diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js index dae8cdc4241..5d6de2bad50 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -331,6 +331,7 @@ var BasicComposer = Widget.extend({ input_max_height: 150, input_min_height: 28, mention_fetch_limit: 8, + mention_partners_restricted: false, // set to true to only suggest prefetched partners send_text: _('Send'), }); this.context = this.options.context; @@ -620,7 +621,7 @@ var BasicComposer = Widget.extend({ } } }); - if (!suggestions.length) { + if (!suggestions.length && !self.options.mention_partners_restricted) { // no result found among prefetched partners, fetch other suggestions var kwargs = { limit: limit, diff --git a/addons/mail/static/src/js/window_manager.js b/addons/mail/static/src/js/window_manager.js index 05afc816712..65488129884 100644 --- a/addons/mail/static/src/js/window_manager.js +++ b/addons/mail/static/src/js/window_manager.js @@ -343,8 +343,10 @@ core.bus.on('web_client_ready', null, function () { var chat_session = _.findWhere(chat_sessions, {id: channel.id}); if (!chat_session || chat_session.window.folded) { chat_manager.detach_channel(channel); - } else if (chat_session && chat_session.window.is_hidden) { + } else if (chat_session.window.is_hidden) { make_session_visible(chat_session); + } else { + chat_session.window.focus_input(); } }); @@ -370,7 +372,7 @@ return ChatWindow.extend({ self.$('.o_chat_search_input input') .autocomplete({ source: function(request, response) { - chat_manager.search_partner(request.term).done(response); + chat_manager.search_partner(request.term, 10).done(response); }, select: function(event, ui) { self.trigger('open_dm_session', ui.item.id); diff --git a/addons/mail/static/src/less/client_action.less b/addons/mail/static/src/less/client_action.less index 8d5ecccd2d3..aee37ab5e9f 100644 --- a/addons/mail/static/src/less/client_action.less +++ b/addons/mail/static/src/less/client_action.less @@ -352,6 +352,7 @@ padding: 5px; cursor: pointer; font-size: 12px; + overflow: hidden; &~.o_mail_channel_preview { border-top: 1px solid lightgray; } diff --git a/addons/mail/static/src/less/thread.less b/addons/mail/static/src/less/thread.less index e523e6ffe2d..be86e577b08 100644 --- a/addons/mail/static/src/less/thread.less +++ b/addons/mail/static/src/less/thread.less @@ -97,6 +97,9 @@ .o_thread_message_core { .o-flex(1, 1, auto); + min-width: 0; + max-width: 100%; + word-wrap: break-word; &.o_mail_note { background-color: @gray-lighter; @@ -115,12 +118,16 @@ display: none; } - > p { + p { margin: 0 0 9px; // Required by the old design to override a general rule on p's &:last-child { margin-bottom: 0; } } + a { + display: inline-block; + word-break: break-all; + } .o_mail_body_long { display: none; diff --git a/addons/mail/static/src/xml/chat_window.xml b/addons/mail/static/src/xml/chat_window.xml index f5c7273d832..eb003cd59cd 100644 --- a/addons/mail/static/src/xml/chat_window.xml +++ b/addons/mail/static/src/xml/chat_window.xml @@ -12,7 +12,7 @@
- +
diff --git a/addons/mail/static/src/xml/client_action.xml b/addons/mail/static/src/xml/client_action.xml index 04d45eb9bdc..1e38fe3d6dd 100644 --- a/addons/mail/static/src/xml/client_action.xml +++ b/addons/mail/static/src/xml/client_action.xml @@ -124,7 +124,7 @@ - + diff --git a/addons/mass_mailing/models/mass_mailing_stats.py b/addons/mass_mailing/models/mass_mailing_stats.py index 586e1ff1f90..ede39b7570b 100644 --- a/addons/mass_mailing/models/mass_mailing_stats.py +++ b/addons/mass_mailing/models/mass_mailing_stats.py @@ -38,6 +38,8 @@ class MailMailStats(osv.Model): if not self.pool.get(stat.model): continue target = self.pool[stat.model].browse(cr, uid, stat.res_id, context=context) + if not target or not target.exists(): + continue email = '' for email_field in ('email', 'email_from'): if email_field in target and target[email_field]: diff --git a/addons/point_of_sale/i18n/point_of_sale.pot b/addons/point_of_sale/i18n/point_of_sale.pot index 0d58856e2d5..10804208f64 100644 --- a/addons/point_of_sale/i18n/point_of_sale.pot +++ b/addons/point_of_sale/i18n/point_of_sale.pot @@ -856,6 +856,12 @@ msgstr "" msgid "Cashdrawer" msgstr "" +#. module: point_of_sale +#: code:addons/point_of_sale/point_of_sale.py:1604 +#, python-format +msgid "Cashier" +msgstr "" + #. module: point_of_sale #: model:ir.actions.act_window,help:point_of_sale.product_pos_category_action msgid "Categories are used to browse your products through the\n" @@ -1371,6 +1377,12 @@ msgstr "" msgid "Discount:" msgstr "" +#. module: point_of_sale +#: code:addons/point_of_sale/point_of_sale.py:1602 +#, python-format +msgid "Discounted Product" +msgstr "" + #. module: point_of_sale #. openerp-web #: code:addons/point_of_sale/static/src/xml/pos.xml:791 @@ -2830,6 +2842,12 @@ msgstr "" msgid "Price" msgstr "" +#. module: point_of_sale +#: code:addons/point_of_sale/point_of_sale.py:1601 +#, python-format +msgid "Priced Product" +msgstr "" + #. module: point_of_sale #: model:ir.model.fields,field_description:point_of_sale.field_pos_config_pricelist_id #: model:ir.model.fields,field_description:point_of_sale.field_pos_order_pricelist_id @@ -4129,6 +4147,12 @@ msgstr "" msgid "Warehouse" msgstr "" +#. module: point_of_sale +#: code:addons/point_of_sale/point_of_sale.py:1600 +#, python-format +msgid "Weighted Product" +msgstr "" + #. module: point_of_sale #. openerp-web #: code:addons/point_of_sale/static/src/xml/pos.xml:1180 diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index aeae93a52af..a46c1ed09bc 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -1594,10 +1594,10 @@ class barcode_rule(models.Model): def _get_type_selection(self): types = sets.Set(super(barcode_rule,self)._get_type_selection()) types.update([ - ('weight','Weighted Product'), - ('price','Priced Product'), - ('discount','Discounted Product'), - ('client','Client'), - ('cashier','Cashier') + ('weight', _('Weighted Product')), + ('price', _('Priced Product')), + ('discount', _('Discounted Product')), + ('client', _('Client')), + ('cashier', _('Cashier')) ]) return list(types) diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css index b301995fb95..cdc92f94def 100644 --- a/addons/point_of_sale/static/src/css/pos.css +++ b/addons/point_of_sale/static/src/css/pos.css @@ -885,7 +885,20 @@ td { display: inline-block; margin: 9px 5px; } +.pos .category-list-scroller{ + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + width:100%; + height:100%; + max-height:60vh; + overflow: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + -webkit-transform: translate3d(0,0,0); +} .pos .category-list { text-align: left; padding: 10px; diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index d2beb9525e9..0c7d90612ca 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -443,6 +443,8 @@ var Chrome = PosBaseWidget.extend({ this.logo_click_time = 0; this.logo_click_count = 0; + this.previous_touch_y_coordinate = -1; + this.widget = {}; // contains references to subwidgets instances this.cleanup_dom(); @@ -537,12 +539,40 @@ var Chrome = PosBaseWidget.extend({ } }, + _scrollable: function(element, scrolling_down){ + var $element = $(element); + var scrollable = true; + + if (! scrolling_down && $element.scrollTop() <= 0) { + scrollable = false; + } else if (scrolling_down && $element.scrollTop() + $element.height() >= element.scrollHeight) { + scrollable = false; + } + + return scrollable; + }, + disable_rubberbanding: function(){ + var self = this; + + document.body.addEventListener('touchstart', function(event){ + self.previous_touch_y_coordinate = event.touches[0].clientY; + }); + // prevent the pos body from being scrollable. document.body.addEventListener('touchmove',function(event){ var node = event.target; + var current_touch_y_coordinate = event.touches[0].clientY; + var scrolling_down; + + if (current_touch_y_coordinate < self.previous_touch_y_coordinate) { + scrolling_down = true; + } else { + scrolling_down = false; + } + while(node){ - if(node.classList && node.classList.contains('touch-scrollable')){ + if(node.classList && node.classList.contains('touch-scrollable') && self._scrollable(node, scrolling_down)){ return; } node = node.parentNode; diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index f15174bbf6c..7d2aa8dd192 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -194,7 +194,9 @@
-
+
+
+
diff --git a/addons/pos_mercury/i18n/pos_mercury.pot b/addons/pos_mercury/i18n/pos_mercury.pot index d5abf2200f6..cd84e327443 100644 --- a/addons/pos_mercury/i18n/pos_mercury.pot +++ b/addons/pos_mercury/i18n/pos_mercury.pot @@ -151,6 +151,12 @@ msgstr "" msgid "Created on" msgstr "" +#. module: pos_mercury +#: code:addons/pos_mercury/models/pos_mercury.py:17 +#, python-format +msgid "Credit Card" +msgstr "" + #. module: pos_mercury #. openerp-web #: code:addons/pos_mercury/static/src/js/pos_mercury.js:177 diff --git a/addons/pos_mercury/models/pos_mercury.py b/addons/pos_mercury/models/pos_mercury.py index 63f531ea230..0abf956d726 100644 --- a/addons/pos_mercury/models/pos_mercury.py +++ b/addons/pos_mercury/models/pos_mercury.py @@ -4,7 +4,7 @@ import logging import sets -from openerp import models, fields, api +from openerp import models, fields, api, _ _logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ class barcode_rule(models.Model): def _get_type_selection(self): types = sets.Set(super(barcode_rule, self)._get_type_selection()) types.update([ - ('credit', 'Credit Card') + ('credit', _('Credit Card')) ]) return list(types) diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index fa568cc2f48..d45219cdeea 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -757,6 +757,9 @@ class ProcurementOrder(models.Model): taxes = self.product_id.supplier_taxes_id fpos = po.fiscal_position_id taxes_id = fpos.map_tax(taxes).ids if fpos else [] + if taxes_id: + companies = self.env['res.company'].search([('id', 'child_of', self.company_id.id)]) + taxes_id = taxes_id.filtered(lambda x: x.company_id.id in companies).ids price_unit = self.env['account.tax']._fix_tax_included_price(seller.price, self.product_id.supplier_taxes_id, taxes_id) if seller else 0.0 if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id: @@ -802,6 +805,7 @@ class ProcurementOrder(models.Model): 'partner_id': partner.id, 'picking_type_id': self.rule_id.picking_type_id.id, 'company_id': self.company_id.id, + 'currency_id': partner.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id, 'dest_address_id': self.partner_dest_id.id, 'origin': self.origin, 'payment_term_id': partner.property_supplier_payment_term_id.id, diff --git a/addons/stock/i18n/stock.pot b/addons/stock/i18n/stock.pot index 4cfb78af2f2..5fe7bccf6d6 100644 --- a/addons/stock/i18n/stock.pot +++ b/addons/stock/i18n/stock.pot @@ -2926,6 +2926,7 @@ msgid "Localization" msgstr "" #. module: stock +#: code:addons/stock/stock.py:4912 #: model:ir.model.fields,field_description:stock.field_product_product_location_id #: model:ir.model.fields,field_description:stock.field_product_template_location_id #: model:ir.model.fields,field_description:stock.field_stock_change_product_qty_location_id @@ -2939,6 +2940,7 @@ msgstr "" #: model:ir.ui.view,arch_db:stock.quant_search_view #: model:ir.ui.view,arch_db:stock.view_move_search #: model:ir.ui.view,arch_db:stock.warehouse_orderpoint_search +#, python-format msgid "Location" msgstr "" @@ -2997,6 +2999,7 @@ msgid "Logistics" msgstr "" #. module: stock +#: code:addons/stock/stock.py:4913 #: model:ir.model.fields,field_description:stock.field_stock_move_restrict_lot_id #: model:ir.model.fields,field_description:stock.field_stock_move_scrap_restrict_lot_id #: model:ir.model.fields,field_description:stock.field_stock_quant_lot_id @@ -3005,6 +3008,7 @@ msgstr "" #: model:ir.ui.view,arch_db:stock.report_lot_barcode #: model:ir.ui.view,arch_db:stock.report_package_barcode #: model:ir.ui.view,arch_db:stock.report_picking +#, python-format msgid "Lot" msgstr "" @@ -3748,11 +3752,13 @@ msgid "Pack Type" msgstr "" #. module: stock +#: code:addons/stock/stock.py:4914 #: model:ir.model.fields,field_description:stock.field_stock_quant_package_id #: model:ir.ui.view,arch_db:stock.quant_package_search_view #: model:ir.ui.view,arch_db:stock.quant_search_view #: model:ir.ui.view,arch_db:stock.view_quant_package_form #: model:ir.ui.view,arch_db:stock.view_quant_package_tree +#, python-format msgid "Package" msgstr "" @@ -5800,6 +5806,12 @@ msgstr "" msgid "USPS integration" msgstr "" +#. module: stock +#: code:addons/stock/stock.py:730 +#, python-format +msgid "Under no circumstances should you delete or change quants yourselves!" +msgstr "" + #. module: stock #: model:ir.model.fields,help:stock.field_stock_production_lot_name msgid "Unique Serial Number" diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 7bc33307cdb..483d2fe30ed 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1636,7 +1636,7 @@ class stock_picking(models.Model): """ if not context: context = {} - + notrack_context = dict(context, mail_notrack=True) stock_move_obj = self.pool.get('stock.move') self.create_lots_for_picking(cr, uid, ids, context=context) for picking in self.browse(cr, uid, ids, context=context): @@ -1664,7 +1664,7 @@ class stock_picking(models.Model): todo_move_ids.append(move.id) elif float_compare(remaining_qty,0, precision_rounding = move.product_id.uom_id.rounding) > 0 and \ float_compare(remaining_qty, move.product_qty, precision_rounding = move.product_id.uom_id.rounding) < 0: - new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context) + new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=notrack_context) todo_move_ids.append(move.id) #Assign move as it was assigned before toassign_move_ids.append(new_move) @@ -1674,7 +1674,7 @@ class stock_picking(models.Model): self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context) self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context) if todo_move_ids and not context.get('do_only_split'): - self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context) + self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=notrack_context) elif context.get('do_only_split'): context = dict(context, split=todo_move_ids) self._create_backorder(cr, uid, picking, context=context) @@ -2103,14 +2103,29 @@ class stock_move(osv.osv): self.pool['procurement.order'].run(cr, uid, res, context=context) return res + def create(self, cr, uid, vals, context=None): + if context is None: + context = {} + picking_obj = self.pool['stock.picking'] + track = not context.get('mail_notrack') and vals.get('picking_id') + if track: + picking = picking_obj.browse(cr, uid, vals['picking_id'], context=context) + initial_values = {picking.id: {'state': picking.state}} + res = super(stock_move, self).create(cr, uid, vals, context=context) + if track: + picking_obj.message_track(cr, uid, [vals['picking_id']], picking_obj.fields_get(cr, uid, ['state'], context=context), initial_values, context=context) + return res + def write(self, cr, uid, ids, vals, context=None): if context is None: context = {} if isinstance(ids, (int, long)): ids = [ids] + picking_obj = self.pool['stock.picking'] # Check that we do not modify a stock.move which is done frozen_fields = set(['product_qty', 'product_uom', 'location_id', 'location_dest_id', 'product_id']) - for move in self.browse(cr, uid, ids, context=context): + moves = self.browse(cr, uid, ids, context=context) + for move in moves: if move.state == 'done': if frozen_fields.intersection(vals): raise UserError(_('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).')) @@ -2147,7 +2162,18 @@ class stock_move(osv.osv): #Note that, for pulled moves we intentionally don't propagate on the procurement. if propagated_changes_dict: self.write(cr, uid, [move.move_dest_id.id], propagated_changes_dict, context=context) - return super(stock_move, self).write(cr, uid, ids, vals, context=context) + track_pickings = not context.get('mail_notrack') and any(field in vals for field in ['state', 'picking_id', 'partially_available']) + if track_pickings: + to_track_picking_ids = set([move.picking_id.id for move in moves if move.picking_id]) + if vals.get('picking_id'): + to_track_picking_ids.add(vals['picking_id']) + to_track_picking_ids = list(to_track_picking_ids) + pickings = picking_obj.browse(cr, uid, to_track_picking_ids, context=context) + initial_values = dict((picking.id, {'state': picking.state}) for picking in pickings) + res = super(stock_move, self).write(cr, uid, ids, vals, context=context) + if track_pickings: + picking_obj.message_track(cr, uid, to_track_picking_ids, picking_obj.fields_get(cr, uid, ['state'], context=context), initial_values, context=context) + return res def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom): """ On change of product quantity finds UoM @@ -2306,7 +2332,7 @@ class stock_move(osv.osv): for state, write_ids in states.items(): if len(write_ids): - self.write(cr, uid, write_ids, {'state': state}) + self.write(cr, uid, write_ids, {'state': state}, context=context) #assign picking in batch for all confirmed move that share the same details for key, move_ids in to_assign.items(): self._picking_assign(cr, uid, move_ids, context=context) @@ -2443,7 +2469,7 @@ class stock_move(osv.osv): """ procurement_obj = self.pool.get('procurement.order') context = context or {} - procs_to_check = [] + procs_to_check = set() for move in self.browse(cr, uid, ids, context=context): if move.state == 'done': raise UserError(_('You cannot cancel a stock move that has been set to \'Done\'.')) @@ -2462,11 +2488,11 @@ class stock_move(osv.osv): self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context) if move.procurement_id: # Does the same as procurement check, only eliminating a refresh - procs_to_check.append(move.procurement_id.id) + procs_to_check.add(move.procurement_id.id) res = self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context) if procs_to_check: - procurement_obj.check(cr, uid, procs_to_check, context=context) + procurement_obj.check(cr, uid, list(procs_to_check), context=context) return res def _check_package_from_moves(self, cr, uid, ids, context=None): @@ -4908,10 +4934,10 @@ class barcode_rule(models.Model): def _get_type_selection(self): types = sets.Set(super(barcode_rule,self)._get_type_selection()) types.update([ - ('weight','Weighted Product'), - ('location','Location'), - ('lot','Lot'), - ('package','Package') + ('weight', _('Weighted Product')), + ('location', _('Location')), + ('lot', _('Lot')), + ('package', _('Package')) ]) return list(types) diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index 46a72f01ad4..922598e9456 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -50,7 +50,7 @@ class stock_history(osv.osv): if 'inventory_value' in fields: group_lines = {} for line in res: - domain = line.get('__domain', []) + domain = line.get('__domain', domain) group_lines.setdefault(str(domain), self.search(cr, uid, domain, context=context)) line_ids = set() for ids in group_lines.values(): @@ -75,7 +75,7 @@ class stock_history(osv.osv): histories_dict[(history['product_id'], history['company_id'])] = history['cost'] for line in res: inv_value = 0.0 - lines = group_lines.get(str(line.get('__domain', []))) + lines = group_lines.get(str(line.get('__domain', domain))) for line_id in lines: line_rec = lines_dict[line_id] product = products_dict[line_rec['product_id']] diff --git a/addons/survey/survey.py b/addons/survey/survey.py index cd9f1f19236..cde6f7c8167 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -357,7 +357,7 @@ class survey_survey(osv.Model): for cell in product(rows.keys(), answers.keys()): res[cell] = 0 for input_line in question.user_input_line_ids: - if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters): + if input_line.answer_type == 'suggestion' and (not(current_filters) or input_line.user_input_id.id in current_filters) and input_line.value_suggested_row: res[(input_line.value_suggested_row.id, input_line.value_suggested.id)] += 1 if input_line.answer_type == 'text' and (not(current_filters) or input_line.user_input_id.id in current_filters): comments.append(input_line) diff --git a/addons/web/static/lib/moment/locale/af.js b/addons/web/static/lib/moment/locale/af.js index 2777e58e77f..ec9533a4b73 100644 --- a/addons/web/static/lib/moment/locale/af.js +++ b/addons/web/static/lib/moment/locale/af.js @@ -17,6 +17,10 @@ weekdays : "Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"), weekdaysShort : "Son_Maa_Din_Woe_Don_Vry_Sat".split("_"), weekdaysMin : "So_Ma_Di_Wo_Do_Vr_Sa".split("_"), + meridiemParse: /vm|nm/i, + isPM : function (input) { + return /^nm$/i.test(input); + }, meridiem : function (hours, minutes, isLower) { if (hours < 12) { return isLower ? 'vm' : 'VM'; diff --git a/addons/web/static/lib/moment/locale/ar-sa.js b/addons/web/static/lib/moment/locale/ar-sa.js index 64e209137ea..7a42d59851c 100644 --- a/addons/web/static/lib/moment/locale/ar-sa.js +++ b/addons/web/static/lib/moment/locale/ar-sa.js @@ -48,6 +48,10 @@ LLL : "D MMMM YYYY LT", LLLL : "dddd D MMMM YYYY LT" }, + meridiemParse: /ص|م/, + isPM : function (input) { + return 'م' === input; + }, meridiem : function (hour, minute, isLower) { if (hour < 12) { return "ص"; @@ -79,7 +83,7 @@ yy : "%d سنوات" }, preparse: function (string) { - return string.replace(/[۰-۹]/g, function (match) { + return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { return numberMap[match]; }).replace(/،/g, ','); }, diff --git a/addons/web/static/lib/moment/locale/ar.js b/addons/web/static/lib/moment/locale/ar.js index 2af64ee3a92..f113199a7b7 100644 --- a/addons/web/static/lib/moment/locale/ar.js +++ b/addons/web/static/lib/moment/locale/ar.js @@ -49,6 +49,10 @@ LLL : "D MMMM YYYY LT", LLLL : "dddd D MMMM YYYY LT" }, + meridiemParse: /ص|م/, + isPM : function (input) { + return 'م' === input; + }, meridiem : function (hour, minute, isLower) { if (hour < 12) { return "ص"; @@ -80,7 +84,7 @@ yy : "%d سنوات" }, preparse: function (string) { - return string.replace(/[۰-۹]/g, function (match) { + return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { return numberMap[match]; }).replace(/،/g, ','); }, diff --git a/addons/web/static/lib/moment/locale/az.js b/addons/web/static/lib/moment/locale/az.js index a6a5aff964e..c4f66f1d2ca 100644 --- a/addons/web/static/lib/moment/locale/az.js +++ b/addons/web/static/lib/moment/locale/az.js @@ -72,6 +72,10 @@ y : "bir il", yy : "%d il" }, + meridiemParse: /gecə|səhər|gündüz|axşam/, + isPM : function (input) { + return /^(gündüz|axşam)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "gecə"; diff --git a/addons/web/static/lib/moment/locale/be.js b/addons/web/static/lib/moment/locale/be.js index 6e0aef1b8d1..cda5495a19e 100644 --- a/addons/web/static/lib/moment/locale/be.js +++ b/addons/web/static/lib/moment/locale/be.js @@ -113,8 +113,10 @@ y : "год", yy : relativeTimeWithPlural }, - - + meridiemParse: /ночы|раніцы|дня|вечара/, + isPM : function (input) { + return /^(дня|вечара)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "ночы"; diff --git a/addons/web/static/lib/moment/locale/bn.js b/addons/web/static/lib/moment/locale/bn.js index 8ceb8eb0a4f..947205616d0 100644 --- a/addons/web/static/lib/moment/locale/bn.js +++ b/addons/web/static/lib/moment/locale/bn.js @@ -82,6 +82,10 @@ return symbolMap[match]; }); }, + meridiemParse: /রাত|শকাল|দুপুর|বিকেল|রাত/, + isPM: function (input) { + return /^(দুপুর|বিকেল|রাত)$/.test(input); + }, //Bengali is a vast language its spoken //in different forms in various parts of the world. //I have just generalized with most common one used diff --git a/addons/web/static/lib/moment/locale/bo.js b/addons/web/static/lib/moment/locale/bo.js index f1567abf5ee..0542887aa4b 100644 --- a/addons/web/static/lib/moment/locale/bo.js +++ b/addons/web/static/lib/moment/locale/bo.js @@ -82,6 +82,10 @@ return symbolMap[match]; }); }, + meridiemParse: /མཚན་མོ|ཞོགས་ཀས|ཉིན་གུང|དགོང་དག|མཚན་མོ/, + isPM: function (input) { + return /^(ཉིན་གུང|དགོང་དག|མཚན་མོ)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "མཚན་མོ"; diff --git a/addons/web/static/lib/moment/locale/eo.js b/addons/web/static/lib/moment/locale/eo.js index 318385b9502..43ce8e40c21 100644 --- a/addons/web/static/lib/moment/locale/eo.js +++ b/addons/web/static/lib/moment/locale/eo.js @@ -26,6 +26,10 @@ LLL : "D[-an de] MMMM, YYYY LT", LLLL : "dddd, [la] D[-an de] MMMM, YYYY LT" }, + meridiemParse: /[ap]\.t\.m/i, + isPM: function (input) { + return input.charAt(0).toLowerCase() === 'p'; + }, meridiem : function (hours, minutes, isLower) { if (hours > 11) { return isLower ? 'p.t.m.' : 'P.T.M.'; diff --git a/addons/web/static/lib/moment/locale/fa.js b/addons/web/static/lib/moment/locale/fa.js index 09c7909514f..af5e3a0e059 100644 --- a/addons/web/static/lib/moment/locale/fa.js +++ b/addons/web/static/lib/moment/locale/fa.js @@ -48,6 +48,10 @@ LLL : 'D MMMM YYYY LT', LLLL : 'dddd, D MMMM YYYY LT' }, + meridiemParse: /قبل از ظهر|بعد از ظهر/, + isPM: function (input) { + return /بعد از ظهر/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 12) { return "قبل از ظهر"; diff --git a/addons/web/static/lib/moment/locale/hi.js b/addons/web/static/lib/moment/locale/hi.js index 6dd7098c27d..eb440d9cdfa 100644 --- a/addons/web/static/lib/moment/locale/hi.js +++ b/addons/web/static/lib/moment/locale/hi.js @@ -84,6 +84,11 @@ }, // Hindi notation for meridiems are quite fuzzy in practice. While there exists // a rigid notion of a 'Pahar' it is not used as rigidly in modern Hindi. + meridiemParse: /रात|सुबह|दोपहर|शाम|रात/, + isPM: function (input) { + // TODO: This is incorrect (look at cutoffs). We need a better isPM interface. + return /^(दोपहर|शाम|रात)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "रात"; diff --git a/addons/web/static/lib/moment/locale/hu.js b/addons/web/static/lib/moment/locale/hu.js index 910f0868b5a..e695dc94db8 100644 --- a/addons/web/static/lib/moment/locale/hu.js +++ b/addons/web/static/lib/moment/locale/hu.js @@ -62,6 +62,10 @@ LLL : "YYYY. MMMM D., LT", LLLL : "YYYY. MMMM D., dddd LT" }, + meridiemParse: /de|du/i, + isPM: function (input) { + return input.charAt(1).toLowerCase() === 'u'; + }, meridiem : function (hours, minutes, isLower) { if (hours < 12) { return isLower === true ? 'de' : 'DE'; diff --git a/addons/web/static/lib/moment/locale/hy-am.js b/addons/web/static/lib/moment/locale/hy-am.js index b6984a2794a..56ad52db51d 100644 --- a/addons/web/static/lib/moment/locale/hy-am.js +++ b/addons/web/static/lib/moment/locale/hy-am.js @@ -77,6 +77,10 @@ yy : "%d տարի" }, + meridiemParse: /գիշերվա|առավոտվա|ցերեկվա|երեկոյան/, + isPM: function (input) { + return /^(ցերեկվա|երեկոյան)$/.test(input); + }, meridiem : function (hour) { if (hour < 4) { return "գիշերվա"; diff --git a/addons/web/static/lib/moment/locale/id.js b/addons/web/static/lib/moment/locale/id.js index 6043f30a239..ebff3271682 100644 --- a/addons/web/static/lib/moment/locale/id.js +++ b/addons/web/static/lib/moment/locale/id.js @@ -25,6 +25,11 @@ LLL : "D MMMM YYYY [pukul] LT", LLLL : "dddd, D MMMM YYYY [pukul] LT" }, + meridiemParse: /pagi|siang|sore|malam/, + isPM: function (input) { + // TODO: This is incorrect (look at cutoffs). + return /^(siang|sore|malam)$/.test(input); + }, meridiem : function (hours, minutes, isLower) { if (hours < 11) { return 'pagi'; diff --git a/addons/web/static/lib/moment/locale/ja.js b/addons/web/static/lib/moment/locale/ja.js index 34c4b890d8e..2b4a7e7f7bb 100644 --- a/addons/web/static/lib/moment/locale/ja.js +++ b/addons/web/static/lib/moment/locale/ja.js @@ -24,6 +24,10 @@ LLL : "YYYY年M月D日LT", LLLL : "YYYY年M月D日LT dddd" }, + meridiemParse: /午前|午後/i, + isPM : function (input) { + return input === '午後'; + }, meridiem : function (hour, minute, isLower) { if (hour < 12) { return "午前"; diff --git a/addons/web/static/lib/moment/locale/ko.js b/addons/web/static/lib/moment/locale/ko.js index 7de2e51033c..c13cad1daf6 100644 --- a/addons/web/static/lib/moment/locale/ko.js +++ b/addons/web/static/lib/moment/locale/ko.js @@ -27,9 +27,6 @@ LLL : "YYYY년 MMMM D일 LT", LLLL : "YYYY년 MMMM D일 dddd LT" }, - meridiem : function (hour, minute, isUpper) { - return hour < 12 ? '오전' : '오후'; - }, calendar : { sameDay : '오늘 LT', nextDay : '내일 LT', @@ -55,9 +52,12 @@ yy : "%d년" }, ordinal : '%d일', - meridiemParse : /(오전|오후)/, + meridiemParse : /오전|오후/, isPM : function (token) { return token === "오후"; + }, + meridiem : function (hour, minute, isUpper) { + return hour < 12 ? '오전' : '오후'; } }); })); diff --git a/addons/web/static/lib/moment/locale/ml.js b/addons/web/static/lib/moment/locale/ml.js index d3cee1d3f30..2398d3be592 100644 --- a/addons/web/static/lib/moment/locale/ml.js +++ b/addons/web/static/lib/moment/locale/ml.js @@ -47,6 +47,10 @@ y : "ഒരു വർഷം", yy : "%d വർഷം" }, + meridiemParse: /രാത്രി|രാവിലെ|ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി/i, + isPM : function (input) { + return /^(ഉച്ച കഴിഞ്ഞ്|വൈകുന്നേരം|രാത്രി)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "രാത്രി"; diff --git a/addons/web/static/lib/moment/locale/mr.js b/addons/web/static/lib/moment/locale/mr.js index 8cbfe7cf97d..92a54f087cc 100644 --- a/addons/web/static/lib/moment/locale/mr.js +++ b/addons/web/static/lib/moment/locale/mr.js @@ -82,6 +82,11 @@ return symbolMap[match]; }); }, + meridiemParse: /रात्री|सकाळी|दुपारी|सायंकाळी|रात्री/, + isPM : function (input) { + // TODO: This is wrong. + return /^(दुपारी|सायंकाळी|रात्री)$/.test(input); + }, meridiem: function (hour, minute, isLower) { if (hour < 4) { diff --git a/addons/web/static/lib/moment/locale/ms-my.js b/addons/web/static/lib/moment/locale/ms-my.js index eee412f6bcd..ed6a2e25794 100644 --- a/addons/web/static/lib/moment/locale/ms-my.js +++ b/addons/web/static/lib/moment/locale/ms-my.js @@ -24,6 +24,11 @@ LLL : "D MMMM YYYY [pukul] LT", LLLL : "dddd, D MMMM YYYY [pukul] LT" }, + meridiemParse: /pagi|tengahari|petang|malam/, + isPM: function (input) { + // TODO: This is wrong. + return /^(tengahari|petang|malam)$/.test(input); + }, meridiem : function (hours, minutes, isLower) { if (hours < 11) { return 'pagi'; diff --git a/addons/web/static/lib/moment/locale/ne.js b/addons/web/static/lib/moment/locale/ne.js index 836fb4d476f..9f6d73e3b35 100644 --- a/addons/web/static/lib/moment/locale/ne.js +++ b/addons/web/static/lib/moment/locale/ne.js @@ -59,6 +59,11 @@ return symbolMap[match]; }); }, + meridiemParse: /राती|बिहान|दिउँसो|बेलुका|साँझ|राती/, + isPM : function (input) { + // TODO: This is wrong. + return /^(दिउँसो|बेलुका|साँझ|राती)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 3) { return "राती"; diff --git a/addons/web/static/lib/moment/locale/sq.js b/addons/web/static/lib/moment/locale/sq.js index 4a3dfea725a..cf30d684034 100644 --- a/addons/web/static/lib/moment/locale/sq.js +++ b/addons/web/static/lib/moment/locale/sq.js @@ -19,6 +19,10 @@ weekdays : "E Diel_E Hënë_E Martë_E Mërkurë_E Enjte_E Premte_E Shtunë".split("_"), weekdaysShort : "Die_Hën_Mar_Mër_Enj_Pre_Sht".split("_"), weekdaysMin : "D_H_Ma_Më_E_P_Sh".split("_"), + meridiemParse: /PD|MD/, + isPM: function (input) { + return input.charAt(0) === 'M'; + }, meridiem : function (hours, minutes, isLower) { return hours < 12 ? 'PD' : 'MD'; }, diff --git a/addons/web/static/lib/moment/locale/ta.js b/addons/web/static/lib/moment/locale/ta.js index 963d40349f4..441d1fa929e 100644 --- a/addons/web/static/lib/moment/locale/ta.js +++ b/addons/web/static/lib/moment/locale/ta.js @@ -89,6 +89,8 @@ // refer http://ta.wikipedia.org/s/1er1 + // TODO: This is pretty wrong (when hour is equal to 6 10, 14, 18, 20, + // 24 (0). Also it doesn't split at 12 (noon). meridiem : function (hour, minute, isLower) { if (hour >= 6 && hour <= 10) { return " காலை"; diff --git a/addons/web/static/lib/moment/locale/th.js b/addons/web/static/lib/moment/locale/th.js index 30b41e69aa0..447f46ff56e 100644 --- a/addons/web/static/lib/moment/locale/th.js +++ b/addons/web/static/lib/moment/locale/th.js @@ -24,6 +24,10 @@ LLL : "D MMMM YYYY เวลา LT", LLLL : "วันddddที่ D MMMM YYYY เวลา LT" }, + meridiemParse: /ก่อนเที่ยง|หลังเที่ยง/, + isPM: function (input) { + return input === 'หลังเที่ยง'; + }, meridiem : function (hour, minute, isLower) { if (hour < 12) { return "ก่อนเที่ยง"; diff --git a/addons/web/static/lib/moment/locale/uk.js b/addons/web/static/lib/moment/locale/uk.js index f27d9f3e311..0ee509a8e30 100644 --- a/addons/web/static/lib/moment/locale/uk.js +++ b/addons/web/static/lib/moment/locale/uk.js @@ -122,6 +122,10 @@ // M. E.: those two are virtually unused but a user might want to implement them for his/her website for some reason + meridiemParse: /ночі|ранку|дня|вечора/, + isPM: function (input) { + return /^(дня|вечора)$/.test(input); + }, meridiem : function (hour, minute, isLower) { if (hour < 4) { return "ночі"; diff --git a/addons/web/static/lib/moment/locale/zh-cn.js b/addons/web/static/lib/moment/locale/zh-cn.js index e73acd70497..96aac691fee 100644 --- a/addons/web/static/lib/moment/locale/zh-cn.js +++ b/addons/web/static/lib/moment/locale/zh-cn.js @@ -29,6 +29,11 @@ lll : "YYYY年MMMD日LT", llll : "YYYY年MMMD日ddddLT" }, + meridiemParse: /凌晨|早上|上午|中午|下午|晚上/, + isPM: function (input) { + // TODO: This is wrong. + return /^(中午|下午|晚上)$/.test(input); + }, meridiem : function (hour, minute, isLower) { var hm = hour * 100 + minute; if (hm < 600) { diff --git a/addons/web/static/lib/moment/locale/zh-tw.js b/addons/web/static/lib/moment/locale/zh-tw.js index edb1fb9879c..1eea9173074 100644 --- a/addons/web/static/lib/moment/locale/zh-tw.js +++ b/addons/web/static/lib/moment/locale/zh-tw.js @@ -28,6 +28,11 @@ lll : "YYYY年MMMD日LT", llll : "YYYY年MMMD日ddddLT" }, + meridiemParse: /早上|上午|中午|下午|晚上/, + isPM: function (input) { + // TODO: This is wrong. + return /^(中午|下午|晚上)$/.test(input); + }, meridiem : function (hour, minute, isLower) { var hm = hour * 100 + minute; if (hm < 900) { diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index ee1876d9f4f..1cfc7bb69ef 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -295,15 +295,13 @@ QWeb2.Engine = (function() { } req.open('GET', s, async); if (async) { - req.onreadystatechange = function() { - if (req.readyState == 4) { - if (req.status == 200) { - callback(null, self._parse_from_request(req)); - } else { - callback(new Error("Can't load template " + s + ", http status " + req.status)); - } + req.addEventListener("load", function() { + if (req.status == 200) { + callback(null, self._parse_from_request(req)); + } else { + callback(new Error("Can't load template " + s + ", http status " + req.status)); } - }; + }); } req.send(null); if (!async) { diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index 66330006380..600751b8b83 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -1160,6 +1160,9 @@ var One2ManyListView = X2ManyListView.extend({ return this._super(); }, _on_focus_one2many: function () { + if(!this.editor.is_editing()) { + return; + } this.dataset.x2m.internal_dataset_changed = true; this._dataset_changed = false; this.__focus = true; diff --git a/addons/web/static/src/js/views/pivot_view.js b/addons/web/static/src/js/views/pivot_view.js index deb2c16870f..6d0a75da569 100644 --- a/addons/web/static/src/js/views/pivot_view.js +++ b/addons/web/static/src/js/views/pivot_view.js @@ -17,7 +17,6 @@ var View = require('web.View'); var _lt = core._lt; var _t = core._t; var QWeb = core.qweb; -var total = _t("Total"); var PivotView = View.extend({ template: 'PivotView', @@ -184,7 +183,7 @@ var PivotView = View.extend({ } } }); - this.measures.__count__ = {string: "Quantity", type: "integer"}; + this.measures.__count__ = {string: _t("Quantity"), type: "integer"}; }, do_search: function (domain, context, group_by) { if (!this.ready) { @@ -509,6 +508,7 @@ var PivotView = View.extend({ return name + (numbers[id] > 1 ? " (" + numbers[id] + ")" : ""); }, make_header: function (data_pt, root, i, j, parent_header) { + var total = _t("Total"); var attrs = data_pt.attributes, value = attrs.value, title = value.length ? value[value.length - 1] : total; @@ -537,6 +537,7 @@ var PivotView = View.extend({ }, get_header: function (data_pt, root, i, j, parent) { var path; + var total = _t("Total"); if (parent) { path = parent.path.concat(data_pt.attributes.value.slice(i,j)); } else { @@ -798,6 +799,10 @@ var PivotView = View.extend({ nbr_measures: nbr_measures, title: this.title, }; + if(table.measure_row.length + 1 > 256) { + c.show_message(_t("For Excel compatibility, data cannot be exported if there is more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures.")); + return; + } session.get_file({ url: '/web/pivot/export_xls', data: {data: JSON.stringify(table)}, @@ -834,4 +839,3 @@ core.view_registry.add('pivot', PivotView); return PivotView; }); - diff --git a/addons/web_calendar/static/src/js/widgets.js b/addons/web_calendar/static/src/js/widgets.js index 175a68aab80..e5d0f5ae4b8 100644 --- a/addons/web_calendar/static/src/js/widgets.js +++ b/addons/web_calendar/static/src/js/widgets.js @@ -53,7 +53,7 @@ var QuickCreate = Dialog.extend({ return _t("Create"); } var title = (_.isUndefined(parent.field_widget)) ? - (parent.string || parent.name) : + (parent.title || parent.string || parent.name) : (parent.field_widget.string || parent.field_widget.name || ''); return _t("Create: ") + title; }, diff --git a/addons/web_kanban/static/src/js/kanban_column.js b/addons/web_kanban/static/src/js/kanban_column.js index 877b234d3a6..754f5dc812c 100644 --- a/addons/web_kanban/static/src/js/kanban_column.js +++ b/addons/web_kanban/static/src/js/kanban_column.js @@ -164,7 +164,7 @@ var KanbanColumn = Widget.extend({ }, update_column: function () { - var title = this.folded ? this.title + ' (' + this.records.length + ')' : this.title; + var title = this.folded ? this.title + ' (' + this.dataset.size() + ')' : this.title; this.$header.find('.o_column_title').text(title); this.$header.find('.o-kanban-count').text(this.records.length); diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index 915f575e0f4..3ae65d76be9 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -437,6 +437,7 @@ var KanbanView = View.extend({ this.$el.sortable({ axis: 'x', items: '> .o_kanban_group', + handle: '.o_kanban_header', cursor: 'move', revert: 150, delay: 100, diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js index 37bbcd71871..608d6b1549f 100644 --- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js +++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js @@ -83,8 +83,8 @@ base.ready().done(function () { }, }, { - title: "Check that the subtotal is 3,856.96", - element: '#order_total_untaxed .oe_currency_value:contains("3,856.96")', + title: "Check that the subtotal is 5,500.00", + element: '#order_total_untaxed .oe_currency_value:contains("5,500.00")', }, { title: "Select `Wire Transfer` payment method", diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 91569312d10..2f6b6d4b844 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -483,7 +483,7 @@ class Post(models.Model): elif post.state == 'pending' and not post.parent_id: # TDE FIXME: in master, you should probably use a subtype; # however here we remove subtype but set partner_ids - partners = post.sudo().message_partner_ids.filtered(lambda partner: partner.user_ids and partner.user_ids.karma >= post.forum_id.karma_moderate) + partners = post.sudo().message_partner_ids.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids)) note_subtype = self.sudo().env.ref('mail.mt_note') body = _('

A new question %s has been asked on %s and require your validation. Click here to access the question.

') % \ (post.name, post.forum_id.name, base_url, slug(post.forum_id), slug(post)) diff --git a/addons/website_sale/models/payment.py b/addons/website_sale/models/payment.py index 563a8597a20..f632292e8f7 100644 --- a/addons/website_sale/models/payment.py +++ b/addons/website_sale/models/payment.py @@ -24,7 +24,7 @@ class PaymentTransaction(orm.Model): tx = getattr(self, tx_find_method_name)(cr, uid, data, context=context) if tx and tx.state == 'done' and tx.acquirer_id.auto_confirm == 'at_pay_confirm' and tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']: self.pool['sale.order'].action_confirm(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=dict(context, send_email=True)) - elif tx and tx.state not in ['cancel'] and tx.sale_order_id and tx.sale_order_id.state in ['draft']: + elif tx and tx.state not in ['cancel', 'error'] and tx.sale_order_id and tx.sale_order_id.state in ['draft']: self.pool['sale.order'].force_quotation_send(cr, SUPERUSER_ID, [tx.sale_order_id.id], context=context) return res diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index ab8e7bdb8f4..7e07c31f5f4 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -199,9 +199,8 @@ class website(orm.Model): :returns: pricelist recordset """ isocountry = request.session.geoip and request.session.geoip.get('country_code') or False - user_id = self.pool['res.users'].browse(cr, uid, request.uid or uid, context=context) pl_ids = self._get_pl(cr, uid, isocountry, show_visible, - user_id.partner_id.property_product_pricelist.id, + request.website.user_id.sudo().partner_id.property_product_pricelist.id, request.session.get('website_sale_current_pl'), request.website.website_pricelist_ids) return self.pool['product.pricelist'].browse(cr, uid, pl_ids, context=context) @@ -220,16 +219,33 @@ class website(orm.Model): """ :returns: The current pricelist record """ - pl_id = request.session.get('website_sale_current_pl') - if pl_id: - return self.pool['product.pricelist'].browse(cr, uid, [pl_id], context=context)[0] + # The list of available pricelists for this user. + # If the user is signed in, and has a pricelist set different than the public user pricelist + # then this pricelist will always be considered as available + available_pricelists = self.get_pricelist_available(cr, uid, context=context) + if request.session.get('website_sale_current_pl'): + # `website_sale_current_pl` is set only if the user specifically chose it: + # - Either, he chose it from the pricelist selection + # - Either, he entered a coupon code + return self.pool['product.pricelist'].browse(cr, uid, [request.session['website_sale_current_pl']], context=context)[0] else: - available_pricelists = self.get_pricelist_available(cr, uid, context=context) - if available_pricelists: + partner = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id + # If the user has a saved cart, it take the pricelist of this cart, except if + # the order is no longer draft (It has already been confirmed, or cancelled, ...) + pl = partner.last_website_so_id and partner.last_website_so_id.state == 'draft' and partner.last_website_so_id.pricelist_id + if not pl: + # The pricelist of the user set on its partner form. + # If the user is not signed in, it's the public user pricelist + pl = partner.property_product_pricelist + if available_pricelists and pl not in available_pricelists: + # If there is at least one pricelist in the available pricelists + # and the chosen pricelist is not within them + # it then choose the first available pricelist. + # This can only happen when the pricelist is the public user pricelist and this pricelist is not in the available pricelist for this localization + # If the user is signed in, and has a special pricelist (different than the public user pricelist), + # then this special pricelist is amongs these available pricelists, and therefore it won't fall in this case. pl = available_pricelists[0] - else: - pl = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.property_product_pricelist - request.session['website_sale_current_pl'] = pl.id + return pl def sale_product_domain(self, cr, uid, ids, context=None): @@ -251,7 +267,12 @@ class website(orm.Model): """ partner = self.get_partner(cr, uid) sale_order_obj = self.pool['sale.order'] - sale_order_id = request.session.get('sale_order_id') or (partner.last_website_so_id.id if partner.last_website_so_id and partner.last_website_so_id.state == 'draft' else False) + sale_order_id = request.session.get('sale_order_id') + if not sale_order_id: + last_order = partner.last_website_so_id + available_pricelists = self.get_pricelist_available(cr, uid, context=context) + # Do not reload the cart of this user last visit if the cart is no longer draft or uses a pricelist no longer available. + sale_order_id = last_order and last_order.state == 'draft' and last_order.pricelist_id in available_pricelists and last_order.id sale_order = None # Test validity of the sale_order_id diff --git a/addons/website_slides/data/website_slides_demo.xml b/addons/website_slides/data/website_slides_demo.xml index 123396db9b9..cd7e4fbd016 100644 --- a/addons/website_slides/data/website_slides_demo.xml +++ b/addons/website_slides/data/website_slides_demo.xml @@ -74,8 +74,8 @@ - Odoo POS Indiegogo Campaign - https://www.youtube.com/watch?v=C_Laaz4mORg + Awesome Timesheet by Odoo + https://www.youtube.com/watch?v=v2O0YZU19ms diff --git a/addons/website_slides/models/slides.py b/addons/website_slides/models/slides.py index f4173ade0ab..ed6e2720947 100644 --- a/addons/website_slides/models/slides.py +++ b/addons/website_slides/models/slides.py @@ -485,7 +485,10 @@ class Slide(models.Model): return fetch_res values = {'slide_type': 'video', 'document_id': document_id} - youtube_values = fetch_res['values'].get('items', list(dict()))[0] + items = fetch_res['values'].get('items') + if not items: + return {'error': _('Please enter valid Youtube or Google Doc URL')} + youtube_values = items[0] if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: diff --git a/doc/setup.rst b/doc/setup.rst index 6a901cc8ff8..f200108964e 100644 --- a/doc/setup.rst +++ b/doc/setup.rst @@ -10,3 +10,4 @@ Setting Up setup/install setup/deploy setup/cdn + setup/enterprise diff --git a/doc/setup/enterprise.rst b/doc/setup/enterprise.rst new file mode 100644 index 00000000000..b44dff361d4 --- /dev/null +++ b/doc/setup/enterprise.rst @@ -0,0 +1,110 @@ +============================ +From Community to Enterprise +============================ + +Depending on your current installation, there are multiple ways to upgrade +your community version. +In any case the basic guidelines are: + +* Backup your community database + + .. image:: enterprise/db_manager.png + :class: img-responsive + +* Shutdown your server + +* Upgrade the web module + +* Restart your server + +* Enter your Odoo Enterprise Subscription code + +.. image:: enterprise/enterprise_code.png + :class: img-responsive + +On Linux, using an installer +============================ + +* Backup your community database + +* Stop the odoo service + + .. code-block:: console + + $ sudo service odoo stop + +* Install the enterprise .deb (it should install over the community package) + + .. code-block:: console + + $ sudo dpkg -i + +* Update your database to the enterprise packages using + + .. code-block:: console + + $ python /usr/bin/odoo.py -d -u web --stop-after-init + +* You should be able to connect to your Odoo Enterprise instance using your usual mean of identification. + You can then link your database with your Odoo Enterprise Subscription by entering the code you received + by e-mail in the form input + + +On Linux, using the source code +=============================== + +There are many ways to launch your server when using sources, and you probably +have your own favourite. You may need to adapt sections to your usual workflow. + +* Shutdown your server +* Backup your community database +* Update the ``--addons-path`` parameter of your launch command (``init.d``, custom script, + configuration file, etc.). The Enterprise addons folder should be included" **before** the community modules. + For example: + + .. code-block:: console + + $ --addons-path=~/src/custom_modules,~/src/enterprise,~/src/odoo/addons + +* Update the web module by using + + .. code-block:: console + + $ -d -u web --stop-after-init + + Depending on the size of your database, this may take some time. + +* Restart your server with the updated addons path of point 3. + You should be able to connect to your instance. You can then link your database with your + Odoo Enterprise Subscription by entering the code you received by e-mail in the form input + +On Windows +========== + +* Backup your community database + +* Uninstall Odoo Community (using the Uninstall executable in the installation folder) - + PostgreSQL will remain installed + + .. image:: enterprise/windows_uninstall.png + :class: img-responsive + +* Launch the Odoo Enterprise Installer and follow the steps normally. When choosing + the installation path, you can set the folder of the Community installation + (this folder still contains the PostgreSQL installation). + Uncheck ``Start Odoo`` at the end of the installation + + .. image:: enterprise/windows_setup.png + :class: img-responsive + +* Using a command window, update your Odoo Database using this command (from the Odoo + installation path, in the server subfolder) + + .. code-block:: console + + $ odoo.exe -d -u web --stop-after-init + +* No need to manually launch the server, the service is running. + You should be able to connect to your Odoo Enterprise instance using your usual + mean of identification. You can then link your database with your Odoo Enterprise + Subscription by entering the code you received by e-mail in the form input diff --git a/doc/setup/enterprise/db_manager.png b/doc/setup/enterprise/db_manager.png new file mode 100644 index 00000000000..7805e101946 Binary files /dev/null and b/doc/setup/enterprise/db_manager.png differ diff --git a/doc/setup/enterprise/enterprise_code.png b/doc/setup/enterprise/enterprise_code.png new file mode 100644 index 00000000000..af4af289e91 Binary files /dev/null and b/doc/setup/enterprise/enterprise_code.png differ diff --git a/doc/setup/enterprise/windows_setup.png b/doc/setup/enterprise/windows_setup.png new file mode 100644 index 00000000000..8c790579cb8 Binary files /dev/null and b/doc/setup/enterprise/windows_setup.png differ diff --git a/doc/setup/enterprise/windows_uninstall.png b/doc/setup/enterprise/windows_uninstall.png new file mode 100644 index 00000000000..14e287d0ec9 Binary files /dev/null and b/doc/setup/enterprise/windows_uninstall.png differ diff --git a/openerp/addons/base/res/res_company.py b/openerp/addons/base/res/res_company.py index c5bd670140e..8372acc55d4 100644 --- a/openerp/addons/base/res/res_company.py +++ b/openerp/addons/base/res/res_company.py @@ -198,7 +198,7 @@ class res_company(osv.osv): self.cache_restart(cr) return super(res_company, self).create(cr, uid, vals, context=context) obj_partner = self.pool.get('res.partner') - partner_id = obj_partner.create(cr, uid, {'name': vals['name'], 'is_company':True, 'image': vals.get('logo', False)}, context=context) + partner_id = obj_partner.create(cr, uid, {'name': vals['name'], 'is_company':True, 'image': vals.get('logo', False), 'customer': False}, context=context) vals.update({'partner_id': partner_id}) self.cache_restart(cr) company_id = super(res_company, self).create(cr, uid, vals, context=context) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 60b19502fd0..136dac9ad61 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -1007,7 +1007,7 @@ class change_password_user(osv.TransientModel): _description = 'Change Password Wizard User' _columns = { 'wizard_id': fields.many2one('change.password.wizard', string='Wizard', required=True), - 'user_id': fields.many2one('res.users', string='User', required=True), + 'user_id': fields.many2one('res.users', string='User', required=True, ondelete='cascade'), 'user_login': fields.char('User Login', readonly=True), 'new_passwd': fields.char('New Password'), } diff --git a/openerp/addons/test_new_api/models.py b/openerp/addons/test_new_api/models.py index ddb98cda7c5..e39135c89fc 100644 --- a/openerp/addons/test_new_api/models.py +++ b/openerp/addons/test_new_api/models.py @@ -149,6 +149,9 @@ class Message(models.Model): size = fields.Integer(compute='_compute_size', search='_search_size') double_size = fields.Integer(compute='_compute_double_size') discussion_name = fields.Char(related='discussion.name') + author_partner = fields.Many2one( + 'res.partner', compute='_compute_author_partner', + search='_search_author_partner') @api.one @api.constrains('author', 'discussion') @@ -194,6 +197,15 @@ class Message(models.Model): size = self.size self.double_size = self.double_size + size + @api.one + @api.depends('author', 'author.partner_id') + def _compute_author_partner(self): + self.author_partner = author.partner_id + + @api.model + def _search_author_partner(self, operator, value): + return [('author.partner_id', operator, value)] + class MixedModel(models.Model): _name = 'test_new_api.mixed' diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py index e3e39ea67c3..ba8cb0516c3 100644 --- a/openerp/addons/test_new_api/tests/test_new_fields.py +++ b/openerp/addons/test_new_api/tests/test_new_fields.py @@ -5,6 +5,7 @@ from datetime import date, datetime from openerp.exceptions import AccessError from openerp.tests import common +from openerp.tools import mute_logger class TestNewFields(common.TransactionCase): @@ -419,7 +420,31 @@ class TestNewFields(common.TransactionCase): self.assertEqual(message.name, "[%s] %s" % (discussion.name, '')) self.assertEqual(message.size, len(BODY)) - def test_41_defaults(self): + @mute_logger('openerp.addons.base.ir.ir_model') + def test_41_new_related(self): + """ test the behavior of related fields on new records. """ + discussion = self.env.ref('test_new_api.discussion_0') + access = self.env.ref('test_new_api.access_discussion') + + # make discussions unreadable for demo user + access.write({'perm_read': False}) + + # create an environment for demo user + demo_env = self.env(user=self.env.ref('base.user_demo')) + self.assertEqual(demo_env.user.login, "demo") + + # create a new message as demo user + values = {'discussion': discussion.id} + message = demo_env['test_new_api.message'].new(values) + self.assertEqual(message.discussion, discussion) + + # read the related field discussion_name + self.assertEqual(message.discussion.env, demo_env) + self.assertEqual(message.discussion_name, discussion.name) + with self.assertRaises(AccessError): + message.discussion.name + + def test_50_defaults(self): """ test default values. """ fields = ['discussion', 'body', 'author', 'size'] defaults = self.env['test_new_api.message'].default_get(fields) @@ -428,6 +453,13 @@ class TestNewFields(common.TransactionCase): defaults = self.env['test_new_api.mixed'].default_get(['number']) self.assertEqual(defaults, {'number': 3.14}) + def test_50_search_many2one(self): + """ test search through a path of computed fields""" + messages = self.env['test_new_api.message'].search( + [('author_partner.name', '=', 'Demo User')]) + self.assertEqual(messages, self.env.ref('test_new_api.message_0_1')) + + class TestMagicFields(common.TransactionCase): diff --git a/openerp/fields.py b/openerp/fields.py index 15ab96d29a4..7576a2a4f3c 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -560,8 +560,11 @@ class Field(object): # when related_sudo, bypass access rights checks when reading values others = records.sudo() if self.related_sudo else records for record, other in zip(records, others): - # do not switch to another environment if record is a draft one - other, field = self.traverse_related(other if record.id else record) + if not record.id: + # draft records: copy record's cache to other's cache first + for name, value in record._cache.iteritems(): + other[name] = value + other, field = self.traverse_related(other) record[self.name] = other[field.name] def _inverse_related(self, records): diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 7c961760ec4..d30b6a943cd 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -852,12 +852,12 @@ class expression(object): # as after transforming the column, it will go through this loop once again # ---------------------------------------- - elif len(path) > 1 and column._type == 'many2one' and column._auto_join: + elif len(path) > 1 and column and column._type == 'many2one' and column._auto_join: # res_partner.state_id = res_partner__state_id.id leaf.add_join_context(comodel, path[0], 'id', path[0]) push(create_substitution_leaf(leaf, (path[1], operator, right), comodel)) - elif len(path) > 1 and column._type == 'one2many' and column._auto_join: + elif len(path) > 1 and column and column._type == 'one2many' and column._auto_join: # res_partner.id = res_partner__bank_ids.partner_id leaf.add_join_context(comodel, 'id', column._fields_id, path[0]) domain = column._domain(model) if callable(column._domain) else column._domain @@ -868,16 +868,16 @@ class expression(object): push(create_substitution_leaf(leaf, elem, comodel)) push(create_substitution_leaf(leaf, AND_OPERATOR, comodel)) - elif len(path) > 1 and column._auto_join: + elif len(path) > 1 and column and column._auto_join: raise NotImplementedError('_auto_join attribute not supported on many2many column %s' % left) - elif len(path) > 1 and column._type == 'many2one': + elif len(path) > 1 and column and column._type == 'many2one': right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=dict(context, active_test=False)) leaf.leaf = (path[0], 'in', right_ids) push(leaf) # Making search easier when there is a left operand as column.o2m or column.m2m - elif len(path) > 1 and column._type in ['many2many', 'one2many']: + elif len(path) > 1 and column and column._type in ['many2many', 'one2many']: right_ids = comodel.search(cr, uid, [(path[1], operator, right)], context=context) table_ids = model.search(cr, uid, [(path[0], 'in', right_ids)], context=dict(context, active_test=False)) leaf.leaf = ('id', 'in', table_ids) @@ -894,7 +894,12 @@ class expression(object): domain = [] else: # Let the field generate a domain. - recs = model.browse(cr, uid, [], context) + if len(path) > 1: + right = comodel.search( + cr, uid, [(path[1], operator, right)], + context=context) + operator = 'in' + recs = model.browse(cr, uid, [], context=context) domain = field.determine_domain(recs, operator, right) if not domain: