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 @@
+
+
+
+
+
+
+
+
+
+
+
Journal
+
+
+
+
+
+
Entries Sorted By:
+
Journal Entry Number
+
Date
+
+
+
Target Moves:
+
All Entries
+
All Posted Entries
+
+
+
+
+
+
+ Move
+ Date
+ Account
+ Partner
+ Label
+ Debit
+ Credit
+ Currency
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tax Declaration
+
+ Name
+ Base Amount
+ Tax Amount
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
Partner Ledger
+
+
+
+
+ Date from :
+ Date to :
+
+
+
Target Moves:
+
All Entries
+
All Posted Entries
+
+
+
+
+
+
+ Date
+ JRNL
+ Account
+ Ref
+ Debit
+ Credit
+ Balance
+ Currency
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
+ 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'))]
+
+
+
+
+
+
+
+
+
+
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 @@
Detach
Mark all read
Unstar all
- New message
+ Send mail
Unsubscribe
Settings
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: