diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index d7613478ce6..872bd8f13d0 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -67,12 +67,11 @@ class AccountAccount(models.Model):
@api.constrains('user_type_id')
def _check_user_type_id(self):
data_unaffected_earnings = self.env.ref('account.data_unaffected_earnings')
- for company in self.mapped('company_id'):
- account_unaffected_earnings = self.search([
- ('company_id', '=', company.id),
- ('user_type_id', '=', data_unaffected_earnings.id),
- ])
- if len(account_unaffected_earnings) >= 2:
+ result = self.read_group([('user_type_id', '=', data_unaffected_earnings.id)], ['company_id'], ['company_id'])
+ for res in result:
+ if res.get('company_id_count', 0) >= 2:
+ account_unaffected_earnings = self.search([('company_id', '=', res['company_id'][0]),
+ ('user_type_id', '=', data_unaffected_earnings.id)])
raise ValidationError(_('You cannot have more than one account with "Current Year Earnings" as type. (accounts: %s)') % [a.code for a in account_unaffected_earnings])
name = fields.Char(required=True, index=True)
@@ -228,7 +227,6 @@ class AccountAccount(models.Model):
self.group_id = group
@api.multi
- @api.depends('name', 'code')
def name_get(self):
result = []
for account in self:
@@ -801,7 +799,6 @@ class AccountJournal(models.Model):
}).id
@api.multi
- @api.depends('name', 'currency_id', 'company_id', 'company_id.currency_id')
def name_get(self):
res = []
for journal in self:
@@ -946,7 +943,6 @@ class AccountTax(models.Model):
default = dict(default or {}, name=_("%s (Copy)") % self.name)
return super(AccountTax, self).copy(default=default)
- @api.depends('name', 'type_tax_use')
def name_get(self):
if not self._context.get('append_type_to_tax_name'):
return super(AccountTax, self).name_get()
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index b8a478cfef8..d0632970bed 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -550,6 +550,8 @@ class AccountBankStatementLine(models.Model):
:returns: The journal entries with which the transaction was matched. If there was at least an entry in counterpart_aml_dicts or new_aml_dicts, this list contains
the move created by the reconciliation, containing entries for the statement.line (1), the counterpart move lines (0..*) and the new move lines (0..*).
"""
+ payable_account_type = self.env.ref('account.data_account_type_payable')
+ receivable_account_type = self.env.ref('account.data_account_type_receivable')
counterpart_aml_dicts = counterpart_aml_dicts or []
payment_aml_rec = payment_aml_rec or self.env['account.move.line']
new_aml_dicts = new_aml_dicts or []
@@ -570,10 +572,16 @@ class AccountBankStatementLine(models.Model):
raise UserError(_('A selected move line was already reconciled.'))
if isinstance(aml_dict['move_line'], int):
aml_dict['move_line'] = aml_obj.browse(aml_dict['move_line'])
+
+ account_types = self.env['account.account.type']
for aml_dict in (counterpart_aml_dicts + new_aml_dicts):
if aml_dict.get('tax_ids') and isinstance(aml_dict['tax_ids'][0], int):
# Transform the value in the format required for One2many and Many2many fields
aml_dict['tax_ids'] = [(4, id, None) for id in aml_dict['tax_ids']]
+
+ user_type_id = self.env['account.account'].browse(aml_dict.get('account_id')).user_type_id
+ if user_type_id in [payable_account_type, receivable_account_type] and user_type_id not in account_types:
+ account_types |= user_type_id
if any(line.journal_entry_ids for line in self):
raise UserError(_('A selected statement line was already reconciled with an account move.'))
@@ -609,6 +617,8 @@ class AccountBankStatementLine(models.Model):
payment = self.env['account.payment']
if abs(total)>0.00001:
payment_vals = self._prepare_payment_vals(total)
+ if self.partner_id and len(account_types) == 1:
+ payment_vals['partner_type'] = 'customer' if account_types == receivable_account_type else 'supplier'
payment = payment.create(payment_vals)
# Complete dicts to create both counterpart move lines and write-offs
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 8db6ced6104..91f06252680 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -147,8 +147,13 @@ class AccountInvoice(models.Model):
amount_to_show = currency._convert(abs(line.amount_residual), self.currency_id, self.company_id, line.date or fields.Date.today())
if float_is_zero(amount_to_show, precision_rounding=self.currency_id.rounding):
continue
+ if line.ref :
+ title = '%s : %s' % (line.move_id.name, line.ref)
+ else:
+ title = line.move_id.name
info['content'].append({
'journal_name': line.ref or line.move_id.name,
+ 'title': title,
'amount': amount_to_show,
'currency': currency_id.symbol,
'id': line.id,
@@ -1402,6 +1407,20 @@ class AccountInvoice(models.Model):
return result
@api.model
+ def _refund_tax_lines_account_change(self, lines, taxes_to_change):
+ # Let's change the account on tax lines when
+ # @param {list} lines: a list of orm commands
+ # @param {dict} taxes_to_change
+ # key: tax ID, value: refund account
+
+ if not taxes_to_change:
+ return lines
+
+ for line in lines:
+ if isinstance(line[2], dict) and line[2]['tax_id'] in taxes_to_change:
+ line[2]['account_id'] = taxes_to_change[line[2]['tax_id']]
+ return lines
+
def _get_refund_common_fields(self):
return ['partner_id', 'payment_term_id', 'account_id', 'currency_id', 'journal_id']
@@ -1447,7 +1466,12 @@ class AccountInvoice(models.Model):
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
tax_lines = invoice.tax_line_ids
- values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines)
+ taxes_to_change = {
+ line.tax_id.id: line.tax_id.refund_account_id.id
+ for line in tax_lines.filtered(lambda l: l.tax_id.refund_account_id != l.tax_id.account_id)
+ }
+ cleaned_tax_lines = self._refund_cleanup_lines(tax_lines)
+ values['tax_line_ids'] = self._refund_tax_lines_account_change(cleaned_tax_lines, taxes_to_change)
if journal_id:
journal = self.env['account.journal'].browse(journal_id)
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index f4753ef4796..ee8087a33b0 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -185,13 +185,14 @@ class account_journal(models.Model):
self.env.cr.execute(query, query_args)
query_results_drafts = self.env.cr.dictfetchall()
- today = datetime.today()
- query = """SELECT amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';"""
+ today = fields.Date.today()
+ query = """SELECT residual_signed as amount_total, currency_id AS currency, type, date_invoice, company_id FROM account_invoice WHERE journal_id = %s AND date <= %s AND state = 'open';"""
self.env.cr.execute(query, (self.id, today))
late_query_results = self.env.cr.dictfetchall()
- (number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency)
- (number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency)
- (number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency)
+ curr_cache = {}
+ (number_waiting, sum_waiting) = self._count_results_and_sum_amounts(query_results_to_pay, currency, curr_cache=curr_cache)
+ (number_draft, sum_draft) = self._count_results_and_sum_amounts(query_results_drafts, currency, curr_cache=curr_cache)
+ (number_late, sum_late) = self._count_results_and_sum_amounts(late_query_results, currency, curr_cache=curr_cache)
difference = currency.round(last_balance-account_sum) + 0.0
return {
@@ -226,23 +227,45 @@ class account_journal(models.Model):
gather the bills in draft state data, and the arguments
dictionary to use to run it as its second.
"""
- return ("""SELECT state, amount_total, currency_id AS currency, type, date_invoice, company_id
- FROM account_invoice
+ # there is no account_move_lines for draft invoices, so no relevant residual_signed value
+ return ("""SELECT state,
+ (CASE WHEN inv.type in ('out_invoice', 'in_invoice')
+ THEN inv.amount_total
+ ELSE (-1 * inv.amount_total)
+ END) AS amount_total,
+ inv.currency_id AS currency,
+ inv.type,
+ inv.date_invoice,
+ inv.company_id
+ FROM account_invoice inv
WHERE journal_id = %(journal_id)s AND state = 'draft';""", {'journal_id':self.id})
- def _count_results_and_sum_amounts(self, results_dict, target_currency):
+ def _count_results_and_sum_amounts(self, results_dict, target_currency, curr_cache=None):
""" Loops on a query result to count the total number of invoices and sum
their amount_total field (expressed in the given target currency).
+ amount_total must be signed !
"""
rslt_count = 0
rslt_sum = 0.0
+ # Create a cache with currency rates to avoid unnecessary SQL requests. Do not copy
+ # curr_cache on purpose, so the dictionary is modified and can be re-used for subsequent
+ # calls of the method.
+ curr_cache = {} if curr_cache is None else curr_cache
for result in results_dict:
cur = self.env['res.currency'].browse(result.get('currency'))
company = self.env['res.company'].browse(result.get('company_id')) or self.env.user.company_id
rslt_count += 1
- type_factor = result.get('type') in ('in_refund', 'out_refund') and -1 or 1
- rslt_sum += type_factor * cur._convert(
- result.get('amount_total'), target_currency, company, result.get('date_invoice') or fields.Date.today())
+ date = result.get('date_invoice') or fields.Date.today()
+
+ amount = result.get('amount_total', 0)
+ if cur != target_currency:
+ key = (cur, target_currency, company, date)
+ # Using setdefault will call _get_conversion_rate, so we explicitly check the
+ # existence of the key in the cache instead.
+ if key not in curr_cache:
+ curr_cache[key] = self.env['res.currency']._get_conversion_rate(*key)
+ amount *= curr_cache[key]
+ rslt_sum += target_currency.round(amount)
return (rslt_count, rslt_sum)
@api.multi
@@ -360,7 +383,7 @@ class account_journal(models.Model):
action['view_id'] = False
if self.type == 'purchase':
new_help = self.env['account.invoice'].with_context(ctx).complete_empty_list_help()
- action.update({'help': action.get('help', '') + new_help})
+ action.update({'help': (action.get('help') or '') + new_help})
return action
@api.multi
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index b19485674a4..baf69b2e646 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -410,9 +410,6 @@ class AccountMove(models.Model):
journal_id=journal_id,
auto=auto)
reversed_moves |= reversed_move
- #unreconcile all lines reversed
- aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
- aml.remove_move_reconcile()
#reconcile together the reconcilable (or the liquidity aml) and their newly created counterpart
for account in set([x.account_id for x in aml]):
to_rec = aml.filtered(lambda y: y.account_id == account)
@@ -429,6 +426,18 @@ class AccountMove(models.Model):
def open_reconcile_view(self):
return self.line_ids.open_reconcile_view()
+ # FIXME: Clarify me and change me in master
+ @api.multi
+ def action_duplicate(self):
+ self.ensure_one()
+ action = self.env.ref('account.action_move_journal_line').read()[0]
+ action['target'] = 'inline'
+ action['context'] = dict(self.env.context)
+ action['context']['view_no_maturity'] = False
+ action['views'] = [(self.env.ref('account.view_move_form').id, 'form')]
+ action['res_id'] = self.copy().id
+ return action
+
@api.model
def _run_reverses_entries(self):
''' This method is called from a cron job. '''
@@ -618,7 +627,7 @@ class AccountMoveLine(models.Model):
index=True, store=True, copy=False) # related is required
blocked = fields.Boolean(string='No Follow-up', default=False,
help="You can check this box to mark this journal item as a litigation with the associated partner")
- date_maturity = fields.Date(string='Due date', index=True, required=True,
+ date_maturity = fields.Date(string='Due date', index=True, required=True, copy=False,
help="This field is used for payable and receivable journal entries. You can put the limit date for the payment of this line.")
date = fields.Date(related='move_id.date', string='Date', index=True, store=True, copy=False, readonly=False) # related is required
analytic_line_ids = fields.One2many('account.analytic.line', 'move_id', string='Analytic lines', oldname="analytic_lines")
@@ -877,22 +886,13 @@ class AccountMoveLine(models.Model):
ret = self._reconcile_lines(debit_moves, credit_moves, field)
return ret
- @api.multi
- def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
- # Empty self can happen if the user tries to reconcile entries which are already reconciled.
- # The calling method might have filtered out reconciled lines.
- if not self:
- return True
-
+ def _check_reconcile_validity(self):
#Perform all checks on lines
company_ids = set()
all_accounts = []
- partners = set()
for line in self:
company_ids.add(line.company_id.id)
all_accounts.append(line.account_id)
- if (line.account_id.internal_type in ('receivable', 'payable')):
- partners.add(line.partner_id.id)
if line.reconciled:
raise UserError(_('You are trying to reconcile some entries that are already reconciled.'))
if len(company_ids) > 1:
@@ -902,6 +902,14 @@ class AccountMoveLine(models.Model):
if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
raise UserError(_('Account %s (%s) does not allow reconciliation. First change the configuration of this account to allow it.') % (all_accounts[0].name, all_accounts[0].code))
+ @api.multi
+ def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
+ # Empty self can happen if the user tries to reconcile entries which are already reconciled.
+ # The calling method might have filtered out reconciled lines.
+ if not self:
+ return True
+
+ self._check_reconcile_validity()
#reconcile everything that can be
remaining_moves = self.auto_reconcile_lines()
@@ -919,7 +927,7 @@ class AccountMoveLine(models.Model):
#add writeoff line to reconcile algorithm and finish the reconciliation
remaining_moves = (remaining_moves + writeoff_to_reconcile).auto_reconcile_lines()
# Check if reconciliation is total or needs an exchange rate entry to be created
- (self+writeoff_to_reconcile).check_full_reconcile()
+ (self + writeoff_to_reconcile).check_full_reconcile()
return True
def _create_writeoff(self, writeoff_vals):
@@ -963,8 +971,9 @@ class AccountMoveLine(models.Model):
raise UserError(_("Either pass both debit and credit or none."))
if 'date' not in vals:
vals['date'] = self._context.get('date_p') or fields.Date.today()
- if vals['date'] < date:
- date = vals['date']
+ vals['date'] = fields.Date.to_date(vals['date'])
+ if vals['date'] and vals['date'] < date:
+ date = vals['date']
if 'name' not in vals:
vals['name'] = self._context.get('comment') or _('Write-Off')
if 'analytic_account_id' not in vals:
diff --git a/addons/account/models/account_payment.py b/addons/account/models/account_payment.py
index c83d2b31ed3..bb2e7ff4f03 100644
--- a/addons/account/models/account_payment.py
+++ b/addons/account/models/account_payment.py
@@ -324,7 +324,7 @@ class account_register_payments(models.TransientModel):
pmt_communication = self.show_communication_field and self.communication \
or self.group_invoices and ' '.join([inv.reference or inv.number for inv in invoices]) \
or invoices[0].reference # in this case, invoices contains only one element, since group_invoices is False
- return {
+ values = {
'journal_id': self.journal_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_date': self.payment_date,
@@ -337,8 +337,13 @@ class account_register_payments(models.TransientModel):
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
'partner_bank_account_id': bank_account.id,
'multi': False,
+ 'payment_difference_handling': self.payment_difference_handling,
+ 'writeoff_account_id': self.writeoff_account_id.id,
+ 'writeoff_label': self.writeoff_label,
}
+ return values
+
@api.multi
def get_payments_vals(self):
'''Compute the values for payments.
@@ -429,6 +434,8 @@ class account_payment(models.Model):
if move_line.account_id.reconcile:
move_line_id = move_line.id
break;
+ if not self.partner_id:
+ raise UserError(_("Payments without a customer can't be matched"))
action_context = {'company_ids': [self.company_id.id], 'partner_ids': [self.partner_id.commercial_partner_id.id]}
if self.partner_type == 'customer':
action_context.update({'mode': 'customers'})
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 8d7a48424fe..717b99ad26d 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -173,7 +173,7 @@ class AccountChartTemplate(models.Model):
# (won't work well for multi-company)
company = self.env.user.company_id
# If we don't have any chart of account on this company, install this chart of account
- if not company.chart_template_id:
+ if not self.existing_accounting(company):
self.load_for_current_company(15.0, 15.0)
def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
diff --git a/addons/account/models/company.py b/addons/account/models/company.py
index 5ea0db1cd95..f2efe950361 100644
--- a/addons/account/models/company.py
+++ b/addons/account/models/company.py
@@ -381,8 +381,13 @@ Best Regards,'''))
('user_type_id', '=', unaffected_earnings_type.id)])
if account:
return account[0]
+ # Do not assume '999999' doesn't exist since the user might have created such an account
+ # manually.
+ code = 999999
+ while self.env['account.account'].search([('code', '=', str(code)), ('company_id', '=', self.id)]):
+ code -= 1
return self.env['account.account'].create({
- 'code': '999999',
+ 'code': str(code),
'name': _('Undistributed Profits/Losses'),
'user_type_id': unaffected_earnings_type.id,
'company_id': self.id,
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index f2485b379e6..85ec5f9bb66 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -204,7 +204,7 @@ class ResPartner(models.Model):
@api.multi
def _credit_debit_get(self):
- tables, where_clause, where_params = self.env['account.move.line']._query_get()
+ tables, where_clause, where_params = self.env['account.move.line'].with_context(company_id=self.env.user.company_id.id)._query_get()
where_params = [tuple(self.ids)] + where_params
if where_clause:
where_clause = 'AND ' + where_clause
diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py
index 2fb6aad94b8..df92a5b45f1 100644
--- a/addons/account/models/reconciliation_widget.py
+++ b/addons/account/models/reconciliation_widget.py
@@ -224,6 +224,27 @@ class AccountReconciliation(models.AbstractModel):
""" Returns the data required for the invoices & payments matching of partners/accounts.
If an argument is None, fetch all related reconciliations. Use [] to fetch nothing.
"""
+ MoveLine = self.env['account.move.line']
+ aml_ids = self._context.get('active_ids') and self._context.get('active_model') == 'account.move.line' and tuple(self._context.get('active_ids'))
+ if aml_ids:
+ aml = MoveLine.browse(aml_ids)
+ aml._check_reconcile_validity()
+ account = aml[0].account_id
+ currency = account.currency_id or account.company_id.currency_id
+ return {
+ 'accounts': [{
+ 'reconciliation_proposition': self._prepare_move_lines(aml, target_currency=currency),
+ 'company_id': account.company_id.id,
+ 'currency_id': currency.id,
+ 'mode': 'accounts',
+ 'account_id': account.id,
+ 'account_name': account.name,
+ 'account_code': account.code,
+ }],
+ 'customers': [],
+ 'suppliers': [],
+ }
+
return {
'customers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'receivable'),
'suppliers': self.get_data_for_manual_reconciliation('partner', partner_ids, 'payable'),
@@ -344,7 +365,6 @@ class AccountReconciliation(models.AbstractModel):
# be reconciled.
return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']]
-
@api.model
def process_move_lines(self, data):
""" Used to validate a batch of reconciliations in a single call
diff --git a/addons/account/report/account_aged_partner_balance.py b/addons/account/report/account_aged_partner_balance.py
index bc660b494b3..08d0b976263 100644
--- a/addons/account/report/account_aged_partner_balance.py
+++ b/addons/account/report/account_aged_partner_balance.py
@@ -142,6 +142,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
if not self.env.user.company_id.currency_id.is_zero(line_amount):
partners_amount[partner_id] += line_amount
+ lines.setdefault(partner_id, [])
lines[partner_id].append({
'line': line,
'amount': line_amount,
@@ -179,6 +180,7 @@ class ReportAgedPartnerBalance(models.AbstractModel):
line_amount -= partial_line.company_id.currency_id._convert(partial_line.amount, user_currency, user_company, date_from)
if not self.env.user.company_id.currency_id.is_zero(line_amount):
undue_amounts[partner_id] += line_amount
+ lines.setdefault(partner_id, [])
lines[partner_id].append({
'line': line,
'amount': line_amount,
diff --git a/addons/account/static/src/js/account_payment_field.js b/addons/account/static/src/js/account_payment_field.js
index a20a681b0e6..c4dec36dda0 100644
--- a/addons/account/static/src/js/account_payment_field.js
+++ b/addons/account/static/src/js/account_payment_field.js
@@ -162,4 +162,8 @@ var ShowPaymentLineWidget = AbstractField.extend({
field_registry.add('payment', ShowPaymentLineWidget);
+return {
+ ShowPaymentLineWidget: ShowPaymentLineWidget
+};
+
});
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 5e756f28247..06387df0579 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -858,6 +858,7 @@ var StatementModel = BasicModel.extend({
'tax_id': [tax.id, null],
'amount': tax.amount,
'label': tax.name,
+ 'date': prop.date,
'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id,
'analytic': tax.analytic,
'is_tax': true,
@@ -1072,6 +1073,7 @@ var StatementModel = BasicModel.extend({
*/
_formatQuickCreate: function (line, values) {
values = values || {};
+ var today = new moment().utc().format();
var account = this._formatNameGet(values.account_id);
var formatOptions = {
currency_id: line.st_line.currency_id,
@@ -1088,6 +1090,7 @@ var StatementModel = BasicModel.extend({
'tax_id': this._formatNameGet(values.tax_id),
'debit': 0,
'credit': 0,
+ 'date': values.date ? values.date : field_utils.parse.date(today, {}, {isUTC: true}),
'base_amount': values.amount_type !== "percentage" ?
(amount) : line.balance.amount * values.amount / 100,
'percent': values.amount_type === "percentage" ? values.amount : null,
@@ -1238,7 +1241,7 @@ var StatementModel = BasicModel.extend({
* datas allowing manual reconciliation
*/
var ManualModel = StatementModel.extend({
- quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids'],
+ quickCreateFields: ['account_id', 'journal_id', 'amount', 'analytic_account_id', 'label', 'tax_id', 'force_tax_included', 'analytic_tag_ids', 'date'],
//--------------------------------------------------------------------------
// Public
@@ -1636,6 +1639,12 @@ var ManualModel = StatementModel.extend({
})
.then(this._formatMoveLine.bind(this, handle));
},
+
+ _formatToProcessReconciliation: function (line, prop) {
+ var result = this._super(line, prop);
+ result['date'] = prop.date;
+ return result;
+ },
});
return {
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index f0641eae409..23c680ddcda 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -542,10 +542,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}, {
type: 'float',
name: 'amount',
+ }, {
+ type: 'char', //TODO is it a bug or a feature when type date exists ?
+ name: 'date',
}], {
account_id: {string: _t("Account")},
label: {string: _t("Label")},
- amount: {string: _t("Account")}
+ amount: {string: _t("Account")},
}).then(function (recordID) {
self.handleCreateRecord = recordID;
var record = self.model.get(self.handleCreateRecord);
@@ -573,6 +576,9 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
self.fields.amount = new basic_fields.FieldFloat(self,
'amount', record, {mode: 'edit'});
+
+ self.fields.date = new basic_fields.FieldDate(self,
+ 'date', record, {mode: 'edit'});
var $create = $(qweb.render("reconciliation.line.create", {'state': state}));
self.fields.account_id.appendTo($create.find('.create_account_id .o_td_field'))
@@ -586,6 +592,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.then(addRequiredStyle.bind(self, self.fields.label));
self.fields.amount.appendTo($create.find('.create_amount .o_td_field'))
.then(addRequiredStyle.bind(self, self.fields.amount));
+ self.fields.date.appendTo($create.find('.create_date .o_td_field'))
self.$('.create').append($create);
function addRequiredStyle(widget) {
@@ -893,6 +900,7 @@ var ManualLineRenderer = LineRenderer.extend({
_renderCreate: function (state) {
this._super(state);
this.$('.create .create_journal_id').show();
+ this.$('.create .create_date').removeClass('d-none')
this.$('.create .create_journal_id .o_input').addClass('o_required_modifier');
},
diff --git a/addons/account/static/src/xml/account_payment.xml b/addons/account/static/src/xml/account_payment.xml
index 4e72bd67d06..55b327183fc 100644
--- a/addons/account/static/src/xml/account_payment.xml
+++ b/addons/account/static/src/xml/account_payment.xml
@@ -16,8 +16,8 @@
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index e133f006f7c..364aa6cd96e 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -583,21 +583,21 @@ class PosOrder(models.Model):
@api.onchange('statement_ids', 'lines')
def _onchange_amount_all(self):
- amounts = {order_id: {'paid': 0, 'return': 0, 'untaxed': 0} for order_id in self.ids}
+ amounts = {order_id: {'paid': 0, 'return': 0, 'taxed': 0, 'taxes': 0} for order_id in self.ids}
for order in self.env['account.bank.statement.line'].read_group([('pos_statement_id', 'in', self.ids)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['paid'] = order['amount']
for order in self.env['account.bank.statement.line'].read_group(['&', ('pos_statement_id', 'in', self.ids), ('amount', '<', 0)], ['pos_statement_id', 'amount'], ['pos_statement_id']):
amounts[order['pos_statement_id'][0]]['return'] = order['amount']
- for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal'], ['order_id']):
- amounts[order['order_id'][0]]['untaxed'] = order['price_subtotal']
+ for order in self.env['pos.order.line'].read_group([('order_id', 'in', self.ids)], ['order_id', 'price_subtotal', 'price_subtotal_incl'], ['order_id']):
+ amounts[order['order_id'][0]]['taxed'] = order['price_subtotal_incl']
+ amounts[order['order_id'][0]]['taxes'] = order['price_subtotal_incl'] - order['price_subtotal']
for order in self:
currency = order.pricelist_id.currency_id
order.amount_paid = amounts[order.id]['paid']
order.amount_return = amounts[order.id]['return']
- order.amount_tax = currency.round(sum(self._amount_line_tax(line, order.fiscal_position_id) for line in order.lines))
- amount_untaxed = currency.round(amounts[order.id]['untaxed'])
- order.amount_total = order.amount_tax + amount_untaxed
+ order.amount_tax = currency.round(amounts[order.id]['taxes'])
+ order.amount_total = currency.round(amounts[order.id]['taxed'])
@api.onchange('partner_id')
def _onchange_partner_id(self):
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index a1c1c11340c..912858825fb 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -876,12 +876,20 @@ exports.PosModel = Backbone.Model.extend({
transfer.pipe(function(order_server_id){
// generate the pdf and download it
- self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
- active_ids:order_server_id,
- }}).done(function () {
- invoiced.resolve();
- done.resolve();
- });
+ if (order_server_id.length) {
+ self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
+ active_ids:order_server_id,
+ }}).done(function () {
+ invoiced.resolve();
+ done.resolve();
+ });
+ } else {
+ // The order has been pushed separately in batch when
+ // the connection came back.
+ // The user has to go to the backend to print the invoice
+ invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
+ done.reject();
+ }
});
return done;
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index b82bcb38eb5..4420434229a 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -2062,6 +2062,17 @@ var PaymentScreenWidget = ScreenWidget.extend({
self.gui.show_screen('clientlist');
},
});
+ } else if (error.message === 'Backend Invoice') {
+ self.gui.show_popup('confirm',{
+ 'title': _t('Please print the invoice from the backend'),
+ 'body': _t('The order has been synchronized earlier. Please make the invoice from the backend for the order: ') + error.data.order.name,
+ confirm: function () {
+ this.gui.show_screen('receipt');
+ },
+ cancel: function () {
+ this.gui.show_screen('receipt');
+ },
+ });
} else if (error.code < 0) { // XmlHttpRequest Errors
self.gui.show_popup('error',{
'title': _t('The order could not be sent'),
diff --git a/addons/portal/static/src/scss/portal.scss b/addons/portal/static/src/scss/portal.scss
index 26beabb5865..4bbb741a653 100644
--- a/addons/portal/static/src/scss/portal.scss
+++ b/addons/portal/static/src/scss/portal.scss
@@ -162,6 +162,19 @@ hr {
.container-fluid .container-fluid {
@extend %o-double-container-no-padding;
}
+#wrap {
+ > .container, > .container-fluid {
+ // BS3 used to do this on all containers so that margins and floats are
+ // cleared inside containers. As lots of current odoo layouts may rely
+ // on this for some alignments, this is restored (at least for a while)
+ // here only for main containers of the frontend.
+ &::before, &::after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+ }
+}
[class^="col-lg-"] {
min-height: 24px;
}
diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml
index 02f118e2bae..34e9cf3a9ef 100644
--- a/addons/portal/views/portal_templates.xml
+++ b/addons/portal/views/portal_templates.xml
@@ -439,7 +439,7 @@
-
+
diff --git a/addons/pos_cache/models/pos_cache.py b/addons/pos_cache/models/pos_cache.py
index 86dbfdfb39f..e7b779581d9 100644
--- a/addons/pos_cache/models/pos_cache.py
+++ b/addons/pos_cache/models/pos_cache.py
@@ -9,6 +9,7 @@ from odoo import models, fields, api
class pos_cache(models.Model):
_name = 'pos.cache'
+ _description = 'Point of Sale Cache'
cache = fields.Binary(attachment=True)
product_domain = fields.Text(required=True)
diff --git a/addons/product/report/product_product_templates.xml b/addons/product/report/product_product_templates.xml
index 2a612f556bc..f8213d924b0 100644
--- a/addons/product/report/product_product_templates.xml
+++ b/addons/product/report/product_product_templates.xml
@@ -1,75 +1,80 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Price:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Price:
+
+
+
+
+
+
+
+
+
+
+
+ No barcode available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No barcode available
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons/rating/models/mail_message.py b/addons/rating/models/mail_message.py
index f6b3553b7f4..aade3b9ade7 100644
--- a/addons/rating/models/mail_message.py
+++ b/addons/rating/models/mail_message.py
@@ -8,7 +8,7 @@ class MailMessage(models.Model):
_inherit = 'mail.message'
rating_ids = fields.One2many('rating.rating', 'message_id', string='Related ratings')
- rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False)
+ rating_value = fields.Float("Rating Value", compute='_compute_rating_value', store=False, search='_search_rating_value')
@api.multi
@api.depends('rating_ids', 'rating_ids.rating')
@@ -17,3 +17,7 @@ class MailMessage(models.Model):
mapping = dict((r.message_id.id, r.rating) for r in ratings)
for message in self:
message.rating_value = mapping.get(message.id, 0.0)
+
+ @api.multi
+ def _search_rating_value(self, operator, operand):
+ return [('rating_ids.rating', operator, operand)]
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index b16c121799b..fd0e8260fc5 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -196,7 +196,8 @@ class ResourceCalendar(models.Model):
hour_count = 0.0
for attendance in attendances:
hour_count += attendance.hour_to - attendance.hour_from
- self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2)
+ if attendances:
+ self.hours_per_day = float_round(hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2)
# --------------------------------------------------
# Computation API
diff --git a/addons/sale/data/mail_data.xml b/addons/sale/data/mail_data.xml
index 5edabafb06b..a2abc42ff23 100644
--- a/addons/sale/data/mail_data.xml
+++ b/addons/sale/data/mail_data.xml
@@ -103,7 +103,7 @@
- ${(object.name or '').replace('/','_')}${object.state == 'draft' and '_draft' or ''}
+ ${(object.name or '').replace('/','_')}${object.partner_id.lang}
diff --git a/addons/sale/models/analytic.py b/addons/sale/models/analytic.py
index 8fb938582a0..a60dd11b023 100644
--- a/addons/sale/models/analytic.py
+++ b/addons/sale/models/analytic.py
@@ -19,7 +19,7 @@ class AccountAnalyticLine(models.Model):
@api.model
def create(self, values):
result = super(AccountAnalyticLine, self).create(values)
- if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy != 'no' and result.amount <= 0: # allow to force a False value for so_line
+ if 'so_line' not in values and not result.so_line and result.product_id and result.product_id.expense_policy not in [False, 'no'] and result.amount <= 0: # allow to force a False value for so_line
result.sudo()._sale_determine_order_line()
return result
@@ -28,7 +28,8 @@ class AccountAnalyticLine(models.Model):
result = super(AccountAnalyticLine, self).write(values)
if 'so_line' not in values: # allow to force a False value for so_line
# only take the AAL from expense or vendor bill, meaning having a negative amount
- self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no' and aal.amount <= 0)._sale_determine_order_line()
+ self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no'] and aal.amount <= 0)._sale_determine_order_line()
+ return result
# ----------------------------------------------------------
# Vendor Bill / Expense : determine the Sale Order to reinvoice
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index f4b4ceb6826..da8ee7fa25c 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -47,8 +47,8 @@ class SaleOrder(models.Model):
amount_untaxed += line.price_subtotal
amount_tax += line.price_tax
order.update({
- 'amount_untaxed': order.pricelist_id.currency_id.round(amount_untaxed),
- 'amount_tax': order.pricelist_id.currency_id.round(amount_tax),
+ 'amount_untaxed': amount_untaxed,
+ 'amount_tax': amount_tax,
'amount_total': amount_untaxed + amount_tax,
})
@@ -66,12 +66,14 @@ class SaleOrder(models.Model):
for possible refunds created directly from existing invoices. This is necessary since such a
refund is not directly linked to the SO.
"""
+ # Ignore the status of the deposit product
+ deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
+ line_invoice_status_all = [(d['order_id'][0], d['invoice_status']) for d in self.env['sale.order.line'].read_group([('order_id', 'in', self.ids), ('product_id', '!=', deposit_product_id.id)], ['order_id', 'invoice_status'], ['order_id', 'invoice_status'], lazy=False)]
for order in self:
invoice_ids = order.order_line.mapped('invoice_lines.invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund'])
- # Ignore the status of the deposit product
- deposit_product_id = self.env['sale.advance.payment.inv']._default_product_id()
- line_invoice_status = [line.invoice_status for line in order.order_line if line.product_id != deposit_product_id]
+
+ line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id]
if order.state not in ('sale', 'done'):
invoice_status = 'no'
@@ -652,7 +654,7 @@ class SaleOrder(models.Model):
# create an analytic account if at least an expense product
for order in self:
- if any([expense_policy != 'no' for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
+ if any([expense_policy not in [False, 'no'] for expense_policy in order.order_line.mapped('product_id.expense_policy')]):
if not order.analytic_account_id:
order._create_analytic_account()
@@ -1016,7 +1018,7 @@ class SaleOrderLine(models.Model):
msg = _("Extra line with %s ") % (line.product_id.display_name,)
line.order_id.message_post(body=msg)
# create an analytic account if at least an expense product
- if line.product_id.expense_policy != 'no' and not self.order_id.analytic_account_id:
+ if line.product_id.expense_policy not in [False, 'no'] and not self.order_id.analytic_account_id:
self.order_id._create_analytic_account()
return line
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index ade6a2e3f52..d0f81c38a8a 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -6,7 +6,6 @@
name="Sales"
web_icon="sale,static/description/icon.png"
active="False"
- groups="sales_team.group_sale_salesman,sales_team.group_sale_salesman_all_leads,sales_team.group_sale_manager"
sequence="7"/>
@@ -314,6 +314,7 @@
+
@@ -322,6 +323,7 @@
name="product_uom_qty" class="oe_inline"/>
+
@@ -480,6 +484,7 @@
+
diff --git a/addons/sale_expense/models/analytic.py b/addons/sale_expense/models/analytic.py
index 9f037bfcae1..d387091adc1 100644
--- a/addons/sale_expense/models/analytic.py
+++ b/addons/sale_expense/models/analytic.py
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _sale_determine_order(self):
mapping = super(AccountAnalyticLine, self)._sale_determine_order()
- for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'):
+ for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']):
if analytic_line.move_id.expense_id.sale_order_id:
mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id
return mapping
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 9f7613ccf51..75e7953e903 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -51,7 +51,7 @@ class SaleOrder(models.Model):
def _action_confirm(self):
""" On SO confirmation, some lines should generate a task or a project. """
result = super(SaleOrder, self)._action_confirm()
- self.mapped('order_line').sudo()._timesheet_service_generation()
+ self.mapped('order_line').sudo().with_context(default_company_id=self.company_id.id)._timesheet_service_generation()
return result
@api.multi
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index b92954e3199..25215366e04 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -143,7 +143,7 @@ class StockMoveLine(models.Model):
"""
res = {}
if self.qty_done and self.product_id.tracking == 'serial':
- if float_compare(self.qty_done, 1.0, precision_rounding=self.move_id.product_id.uom_id.rounding) != 0:
+ if float_compare(self.qty_done, 1.0, precision_rounding=self.product_id.uom_id.rounding) != 0:
message = _('You can only process 1.0 %s of products with unique serial number.') % self.product_id.uom_id.name
res['warning'] = {'title': _('Warning'), 'message': message}
return res
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index 16f6b505acb..7c8eeefb57a 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -131,7 +131,8 @@ class StockScrap(models.Model):
self.package_id,
self.owner_id,
strict=True).mapped('quantity'))
- if float_compare(available_qty, self.scrap_qty, precision_digits=precision) >= 0:
+ scrap_qty = self.product_uom_id._compute_quantity(self.scrap_qty, self.product_id.uom_id)
+ if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0:
return self.do_scrap()
else:
return {
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 2e2d171eddf..8e9ebbd980c 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -971,7 +971,7 @@ class Orderpoint(models.Model):
# These days will be substracted when creating the PO
days += self.product_id._select_seller(
quantity=product_qty,
- date=start_date,
+ date=fields.Date.context_today(self,start_date),
uom_id=self.product_uom).delay or 0.0
date_planned = start_date + relativedelta.relativedelta(days=days)
return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
diff --git a/addons/stock/report/report_stockpicking_operations.xml b/addons/stock/report/report_stockpicking_operations.xml
index 63bfbd8cc14..09715938ca3 100644
--- a/addons/stock/report/report_stockpicking_operations.xml
+++ b/addons/stock/report/report_stockpicking_operations.xml
@@ -155,7 +155,7 @@
-
+
All products could not be reserved. Click on the "Check Availability" button to try to reserve products
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index 2037ec4f34e..0939389dda9 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -3569,6 +3569,24 @@ class StockMove(TransactionCase):
self.assertEqual(scrap.state, 'done')
self.assertEqual(move1.reserved_availability, 0.25)
+ def test_scrap_6(self):
+ """ Check that scrap correctly handle UoM. """
+ self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 1)
+ scrap = self.env['stock.scrap'].create({
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_dozen.id,
+ 'scrap_qty': 1,
+ })
+ warning_message = scrap.action_validate()
+ self.assertEqual(warning_message.get('res_model', 'Wrong Model'), 'stock.warn.insufficient.qty.scrap')
+ insufficient_qty_wizard = self.env['stock.warn.insufficient.qty.scrap'].create({
+ 'product_id': self.product1.id,
+ 'location_id': self.stock_location.id,
+ 'scrap_id': scrap.id
+ })
+ insufficient_qty_wizard.action_done()
+ self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).quantity, -11)
+
def test_in_date_1(self):
""" Check that moving a tracked quant keeps the incoming date.
"""
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 3a75e9744a0..8629dbff92c 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -220,7 +220,7 @@
-
+
diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py
index 8e4bde1a2e2..3ba8f85bf26 100644
--- a/addons/stock_account/models/product.py
+++ b/addons/stock_account/models/product.py
@@ -138,23 +138,38 @@ class ProductProduct(models.Model):
StockMove = self.env['stock.move']
to_date = self.env.context.get('to_date')
- self.env['account.move.line'].check_access_rights('read')
- fifo_automated_values = {}
- query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
- FROM account_move_line AS aml
- WHERE aml.product_id IS NOT NULL AND aml.company_id=%%s %s
- GROUP BY aml.product_id, aml.account_id"""
- params = (self.env.user.company_id.id,)
- if to_date:
- query = query % ('AND aml.date <= %s',)
- params = params + (to_date,)
- else:
- query = query % ('',)
- self.env.cr.execute(query, params=params)
+ real_time_product_ids = [product.id for product in self if product.product_tmpl_id.valuation == 'real_time']
+ if real_time_product_ids:
+ self.env['account.move.line'].check_access_rights('read')
+ fifo_automated_values = {}
+ query = """SELECT aml.product_id, aml.account_id, sum(aml.debit) - sum(aml.credit), sum(quantity), array_agg(aml.id)
+ FROM account_move_line AS aml
+ WHERE aml.product_id IN %%s AND aml.company_id=%%s %s
+ GROUP BY aml.product_id, aml.account_id"""
+ params = (tuple(real_time_product_ids), self.env.user.company_id.id)
+ if to_date:
+ query = query % ('AND aml.date <= %s',)
+ params = params + (to_date,)
+ else:
+ query = query % ('',)
+ self.env.cr.execute(query, params=params)
- res = self.env.cr.fetchall()
- for row in res:
- fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
+ res = self.env.cr.fetchall()
+ for row in res:
+ fifo_automated_values[(row[0], row[1])] = (row[2], row[3], list(row[4]))
+
+ product_values = {product: 0 for product in self}
+ product_move_ids = {product: [] for product in self}
+ if to_date:
+ domain = [('product_id', 'in', self.ids), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
+ for move in StockMove.search(domain).with_context(prefetch_fields=False):
+ product_values[move.product_id] += move.value
+ product_move_ids[move.product_id].append(move.id)
+ else:
+ domain = [('product_id', 'in', self.ids)] + StockMove._get_all_base_domain()
+ for move in StockMove.search(domain).with_context(prefetch_fields=False):
+ product_values[move.product_id] += move.remaining_value
+ product_move_ids[move.product_id].append(move.id)
for product in self:
if product.cost_method in ['standard', 'average']:
@@ -170,11 +185,9 @@ class ProductProduct(models.Model):
elif product.cost_method == 'fifo':
if to_date:
if product.product_tmpl_id.valuation == 'manual_periodic':
- domain = [('product_id', '=', product.id), ('date', '<=', to_date)] + StockMove._get_all_base_domain()
- moves = StockMove.search(domain)
- product.stock_value = sum(moves.mapped('value'))
+ product.stock_value = product_values[product]
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
- product.stock_fifo_manual_move_ids = StockMove.browse(moves.ids)
+ product.stock_fifo_manual_move_ids = StockMove.browse(product_move_ids[product])
elif product.product_tmpl_id.valuation == 'real_time':
valuation_account_id = product.categ_id.property_stock_valuation_account_id.id
value, quantity, aml_ids = fifo_automated_values.get((product.id, valuation_account_id)) or (0, 0, [])
@@ -182,7 +195,7 @@ class ProductProduct(models.Model):
product.qty_at_date = quantity
product.stock_fifo_real_time_aml_ids = self.env['account.move.line'].browse(aml_ids)
else:
- product.stock_value, moves = product._sum_remaining_values()
+ product.stock_value, moves = product_values[product], StockMove.browse(product_move_ids[product])
product.qty_at_date = product.with_context(company_owned=True, owner_id=False).qty_available
if product.product_tmpl_id.valuation == 'manual_periodic':
product.stock_fifo_manual_move_ids = moves
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 3f362f29989..11d1bcb40a4 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -387,7 +387,7 @@ class StockMove(models.Model):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
rounding = move.product_id.uom_id.rounding
- qty_done = 0.0
+ qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
new_std_price = move._get_price_unit()
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
@@ -396,7 +396,6 @@ class StockMove(models.Model):
else:
# Get the standard price
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
- qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
qty = forced_qty or qty_done
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
@@ -483,10 +482,8 @@ class StockMove(models.Model):
fifo_valued_products = self.env['product.product']
fifo_valued_categories = self.env['product.category'].search([('property_cost_method', '=', 'fifo')])
fifo_valued_products |= self.env['product.product'].search([('categ_id', 'child_of', fifo_valued_categories.ids)])
- moves_to_vacuum = self.env['stock.move']
- for product in fifo_valued_products:
- moves_to_vacuum |= self.search(
- [('product_id', '=', product.id), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
+ moves_to_vacuum = self.search(
+ [('product_id', 'in', fifo_valued_products.ids), ('remaining_qty', '<', 0)] + self._get_all_base_domain())
moves_to_vacuum._fifo_vacuum()
@api.multi
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index 3720b453635..0c7be5df182 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertAlmostEqual(self.product1.stock_value, 0.0)
+ def test_average_perpetual_6(self):
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 1 units at 5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 5,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1.0
+
+ # Receive both at the same time
+ (move1 | move2)._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 7.5)
+
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py
index aadfa7eecdc..4d3bb1380e9 100644
--- a/addons/stock_landed_costs/models/stock_landed_cost.py
+++ b/addons/stock_landed_costs/models/stock_landed_cost.py
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
return False
accounts = self.product_id.product_tmpl_id.get_product_accounts()
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
+ # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
+ if self.move_id._is_dropshipped():
+ debit_account_id = accounts.get('expense') and accounts['expense'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
diff --git a/addons/survey/models/survey_user.py b/addons/survey/models/survey_user.py
index 32250825bd3..6495a9a1ffb 100644
--- a/addons/survey/models/survey_user.py
+++ b/addons/survey/models/survey_user.py
@@ -77,6 +77,7 @@ class SurveyUserInput(models.Model):
""" Send again the invitation """
self.ensure_one()
local_context = {
+ 'survey_resent_user_input': self.id,
'survey_resent_token': True,
'default_partner_ids': self.partner_id and [self.partner_id.id] or [],
'default_multi_email': self.email or "",
diff --git a/addons/survey/wizard/survey_invite.py b/addons/survey/wizard/survey_invite.py
index 7aa47259262..4cf1768b0e3 100644
--- a/addons/survey/wizard/survey_invite.py
+++ b/addons/survey/wizard/survey_invite.py
@@ -205,6 +205,10 @@ class SurveyInvite(models.TransientModel):
Mail.create(values).send()
def create_token(wizard, partner_id, email):
+ if context.get("survey_resent_user_input"):
+ survey_user_input = SurveyUserInput.browse(context.get("survey_resent_user_input"))
+ if survey_user_input.state in ('new', 'skip'):
+ return survey_user_input.token
if context.get("survey_resent_token"):
survey_user_input = SurveyUserInput.search([('survey_id', '=', wizard.survey_id.id),
('state', 'in', ['new', 'skip']), '|', ('partner_id', '=', partner_id),
diff --git a/addons/test_mail/tests/test_mail_channel.py b/addons/test_mail/tests/test_mail_channel.py
index 74e49ce7de4..79ab747fed5 100644
--- a/addons/test_mail/tests/test_mail_channel.py
+++ b/addons/test_mail/tests/test_mail_channel.py
@@ -155,7 +155,15 @@ class TestChannelFeatures(common.BaseFunctionalTest, common.MockEmails):
""" Posting a message on a mailing list should send one email to all recipients """
self.env['ir.config_parameter'].set_param('mail.catchall.domain', 'schlouby.fr')
self.test_channel.write({'email_send': True})
- self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner)
+
+ # Subscribe an user without email. We shouldn't try to send email to them.
+ nomail = self.env['res.users'].create({
+ "login": "nomail",
+ "name": "No Mail",
+ "email": False,
+ "notification_type": "email",
+ })
+ self._join_channel(self.test_channel, self.user_employee.partner_id | self.test_partner | nomail.partner_id)
self.test_channel.message_post(body="Test", message_type='comment', subtype='mt_comment')
self.assertEqual(len(self._mails), 1)
diff --git a/addons/test_mass_mailing/tests/test_performance.py b/addons/test_mass_mailing/tests/test_performance.py
index 54f9df307b3..f69f2e74966 100644
--- a/addons/test_mass_mailing/tests/test_performance.py
+++ b/addons/test_mass_mailing/tests/test_performance.py
@@ -49,7 +49,7 @@ class TestMassMailPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
- with self.assertQueryCount(__system__=2435, marketing=3091):
+ with self.assertQueryCount(__system__=2480, marketing=3136):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
@@ -86,7 +86,7 @@ class TestMassMailBlPerformance(TestMassMailPerformanceBase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
- with self.assertQueryCount(__system__=2807, marketing=3559):
+ with self.assertQueryCount(__system__=2867, marketing=3619):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 57c86a2c6d6..3eaed012f25 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1063,21 +1063,19 @@ class Binary(http.Controller):
if status != 200 and download:
return request.env['ir.http']._response_by_status(status, headers, content)
+ if not content:
+ content = base64.b64encode(self.placeholder(image='placeholder.png'))
+ headers = self.force_contenttype(headers, contenttype='image/png')
+ if not (width or height):
+ suffix = field.split('_')[-1]
+ if suffix in ('small', 'medium', 'big'):
+ content = getattr(odoo.tools, 'image_resize_image_%s' % suffix)(content)
+
content = limited_image_resize(
content, width=width, height=height, crop=crop, upper_limit=upper_limit, avoid_if_small=avoid_if_small)
- if content:
- image_base64 = base64.b64decode(content)
- else:
- suffix = field.split('_')[-1]
- if suffix in ('small', 'medium', 'big'):
- encoded_placeholder = base64.b64encode(self.placeholder(image='placeholder.png'))
- image_base64 = base64.b64decode(getattr(odoo.tools, 'image_resize_image_%s' % suffix)(encoded_placeholder))
- else:
- image_base64 = self.placeholder(image='placeholder.png') # could return (contenttype, content) in master
- headers = self.force_contenttype(headers, contenttype='image/png')
-
+ image_base64 = base64.b64decode(content)
headers.append(('Content-Length', len(image_base64)))
response = request.make_response(image_base64, headers)
response.status_code = status
diff --git a/addons/web/models/models.py b/addons/web/models/models.py
index fe01ffe6938..768885f701f 100644
--- a/addons/web/models/models.py
+++ b/addons/web/models/models.py
@@ -63,7 +63,8 @@ class Base(models.AbstractModel):
locale=locale)
if field_type == 'selection':
- group_by_value = selection_labels[group_by_value]
+ group_by_value = selection_labels[group_by_value] \
+ if group_by_value in selection_labels else False
if type(group_by_value) == tuple:
group_by_value = group_by_value[1] # FIXME should use technical value (0)
diff --git a/addons/web/static/lib/tempusdominus/tempusdominus.js b/addons/web/static/lib/tempusdominus/tempusdominus.js
index a5dac48ac3a..ffca0c8e632 100644
--- a/addons/web/static/lib/tempusdominus/tempusdominus.js
+++ b/addons/web/static/lib/tempusdominus/tempusdominus.js
@@ -2093,6 +2093,9 @@ var TempusDominusBootstrap4 = function ($) {
}
currentDate = this._viewDate.clone().startOf('M').startOf('w').startOf('d');
+ // !! ODOO FIX START !!
+ var now = this.getMoment();
+ // !! ODOO FIX END !!
for (i = 0; i < 42; i++) {
//always display 42 days (should show 6 weeks)
@@ -2125,7 +2128,9 @@ var TempusDominusBootstrap4 = function ($) {
if (!this._isValid(currentDate, 'd')) {
clsName += ' disabled';
}
- if (currentDate.isSame(this.getMoment(), 'd')) {
+ // !! ODOO FIX START !!
+ if (currentDate.date() === now.date() && currentDate.month() === now.month() && currentDate.year() === now.year()) {
+ // !! ODOO FIX END !!
clsName += ' today';
}
if (currentDate.day() === 0 || currentDate.day() === 6) {
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 6d53d283f8a..14eb10c663c 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({
*/
_bindAutoComplete: function () {
var self = this;
+ // avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
+ if (this.$input.attr('placeholder')) {
+ this.$input.attr('placeholder', function (index, val) {
+ return val.split('').join('\ufeff');
+ });
+ }
this.$input.autocomplete({
source: function (req, resp) {
_.each(self._autocompleteSources, function (source) {
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 9706251ed07..646e883fb7e 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -432,6 +432,7 @@ var BasicModel = AbstractModel.extend({
var isNew = this.isNew(id);
var rollback = 'rollback' in options ? options.rollback : isNew;
var initialOffset = element.offset;
+ element._domains = {};
this._visitChildren(element, function (elem) {
if (rollback && elem._savePoint) {
if (elem._savePoint instanceof Array) {
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index c2c90ea7e62..03c59885fbe 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -362,6 +362,11 @@ return AbstractRenderer.extend({
this.$calendar = this.$(".o_calendar_widget");
+ // This seems like a workaround but apparently passing the locale
+ // in the options is not enough. We should initialize it beforehand
+ var locale = moment.locale();
+ $.fullCalendar.locale(locale);
+
//Documentation here : http://arshaw.com/fullcalendar/docs/
var fc_options = $.extend({}, this.state.fc_options, {
eventDrop: function (event) {
@@ -414,6 +419,7 @@ return AbstractRenderer.extend({
height: 'parent',
unselectAuto: false,
isRTL: _t.database.parameters.direction === "rtl",
+ locale: locale, // reset locale when fullcalendar has already been instanciated before now
});
this.$calendar.fullCalendar(fc_options);
diff --git a/addons/web/static/src/js/views/control_panel/search/search_filters.js b/addons/web/static/src/js/views/control_panel/search/search_filters.js
index 6ff84dce9c7..4703995ac7c 100644
--- a/addons/web/static/src/js/views/control_panel/search/search_filters.js
+++ b/addons/web/static/src/js/views/control_panel/search/search_filters.js
@@ -193,8 +193,10 @@ var DateTime = Field.extend({
operators: [
{value: "=", text: _lt("is equal to")},
{value: "!=", text: _lt("is not equal to")},
- {value: ">=", text: _lt("is after")},
- {value: "<=", text: _lt("is before")},
+ {value: ">", text: _lt("is after")},
+ {value: "<", text: _lt("is before")},
+ {value: ">=", text: _lt("is after or equal to")},
+ {value: "<=", text: _lt("is before or equal to")},
{value: "between", text: _lt("is between")},
{value: "∃", text: _lt("is set")},
{value: "∄", text: _lt("is not set")}
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 5feefce716f..d85a538d5ec 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -227,17 +227,16 @@ var FormController = BasicController.extend({
// are displayed with an alert
var fields = self.renderer.state.fields;
var data = self.renderer.state.data;
- var alertFields = [];
+ var alertFields = {};
for (var k = 0; k < changedFields.length; k++) {
var field = fields[changedFields[k]];
var fieldData = data[changedFields[k]];
if (field.translate && fieldData) {
- alertFields.push(field);
+ alertFields[changedFields[k]] = field;
}
}
- if (alertFields.length) {
- self.renderer.alertFields = alertFields;
- self.renderer.displayTranslationAlert();
+ if (!_.isEmpty(alertFields)) {
+ self.renderer.updateAlertFields(alertFields);
}
}
return changedFields;
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 52bbc044d1f..2d6615b4b00 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -14,6 +14,7 @@ var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
events: _.extend({}, BasicRenderer.prototype.events, {
'click .o_notification_box .oe_field_translate': '_onTranslate',
+ 'click .o_notification_box .close': '_onTranslateNotificationClose',
'click .oe_title, .o_inner_group': '_onClick',
}),
custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
@@ -31,6 +32,7 @@ var FormRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
this.idsForLabels = {};
this.lastActivatedFieldIndex = -1;
+ this.alertFields = {};
},
/**
* @override
@@ -104,23 +106,32 @@ var FormRenderer = BasicRenderer.extend({
});
return fieldNames;
},
+ /*
+ * Updates translation alert fields for the current state and display updated fields
+ *
+ * @param {Object} alertFields
+ */
+ updateAlertFields: function (alertFields) {
+ this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields);
+ this.displayTranslationAlert();
+ },
/**
* Show a warning message if the user modified a translated field. For each
* field, the notification provides a link to edit the field's translations.
- *
- * @param {Object[]} alertFields field list
*/
displayTranslationAlert: function () {
this.$('.o_notification_box').remove();
- var $notification = $(qweb.render('notification-box', {type: 'info'}))
- .append(qweb.render('translation-alert', {
- fields: this.alertFields,
- lang: _t.database.parameters.name
- }));
- if (this.$('.o_form_statusbar').length) {
- this.$('.o_form_statusbar').after($notification);
- } else {
- this.$el.prepend($notification);
+ if (this.alertFields[this.state.res_id]) {
+ var $notification = $(qweb.render('notification-box', {type: 'info'}))
+ .append(qweb.render('translation-alert', {
+ fields: this.alertFields[this.state.res_id],
+ lang: _t.database.parameters.name
+ }));
+ if (this.$('.o_form_statusbar').length) {
+ this.$('.o_form_statusbar').after($notification);
+ } else {
+ this.$el.prepend($notification);
+ }
}
},
/**
@@ -334,6 +345,7 @@ var FormRenderer = BasicRenderer.extend({
});
self.trigger_up('swipe_right');
},
+ excludedElements: ".o_notebook .nav.nav-tabs",
});
},
/**
@@ -915,6 +927,9 @@ var FormRenderer = BasicRenderer.extend({
return $.when.apply($, defs).then(function () {
self._updateView($form.contents());
+ if (self.state.res_id in self.alertFields) {
+ self.displayTranslationAlert();
+ }
}, function () {
$form.remove();
}).then(function(){
@@ -1030,6 +1045,15 @@ var FormRenderer = BasicRenderer.extend({
ev.preventDefault();
this.trigger_up('translate', {fieldName: ev.target.name, id: this.state.id});
},
+ /**
+ * remove alert fields of record from alertFields object
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onTranslateNotificationClose: function(ev) {
+ delete this.alertFields[this.state.res_id];
+ },
});
return FormRenderer;
diff --git a/addons/web/static/src/js/views/graph/graph_renderer.js b/addons/web/static/src/js/views/graph/graph_renderer.js
index 856882a303b..09706695bbc 100644
--- a/addons/web/static/src/js/views/graph/graph_renderer.js
+++ b/addons/web/static/src/js/views/graph/graph_renderer.js
@@ -198,10 +198,12 @@ return AbstractRenderer.extend({
}
}
- // For Bar chart View, we keep only groups where count > 0
- data[0].values = _.filter(data[0].values, function (elem, index) {
- return self.state.data[index].count > 0;
- });
+ // For one level Bar chart View, we keep only groups where count > 0
+ if (this.state.groupedBy.length <= 1) {
+ data[0].values = _.filter(data[0].values, function (elem, index) {
+ return self.state.data[index].count > 0;
+ });
+ }
var $svgContainer = $('', {class: 'o_graph_svg_container'});
this.$el.append($svgContainer);
diff --git a/addons/web/static/src/js/widgets/data_export.js b/addons/web/static/src/js/widgets/data_export.js
index d05bf2d33fc..347769b4042 100644
--- a/addons/web/static/src/js/widgets/data_export.js
+++ b/addons/web/static/src/js/widgets/data_export.js
@@ -311,7 +311,7 @@ var DataExport = Dialog.extend({
});
self.$export_format_inputs = $fmts.find('input');
- self.$export_format_inputs.first().prop('checked', true);
+ self.$export_format_inputs.filter(':enabled').first().prop('checked', true);
}
},
show_exports_list: function() {
diff --git a/addons/web/static/src/js/widgets/date_picker.js b/addons/web/static/src/js/widgets/date_picker.js
index b677abe377a..367ab23178d 100644
--- a/addons/web/static/src/js/widgets/date_picker.js
+++ b/addons/web/static/src/js/widgets/date_picker.js
@@ -25,6 +25,7 @@ var DateWidget = Widget.extend({
this.name = parent.name;
this.options = _.extend({
+ locale: moment.locale(),
format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
minDate: moment({ y: 1900 }),
maxDate: moment().add(200, "y"),
diff --git a/addons/web/static/src/scss/base_settings.scss b/addons/web/static/src/scss/base_settings.scss
index 82137b00948..a7ff1d848a7 100644
--- a/addons/web/static/src/scss/base_settings.scss
+++ b/addons/web/static/src/scss/base_settings.scss
@@ -114,9 +114,11 @@
font-size: 13px;
line-height: 40px;
cursor: pointer;
+ white-space: nowrap;
.icon {
width: 23px;
+ min-width: 23px;
margin-right: 10px;
}
diff --git a/addons/web/static/src/scss/form_view.scss b/addons/web/static/src/scss/form_view.scss
index 31032a75cc2..697ec4dcd12 100644
--- a/addons/web/static/src/scss/form_view.scss
+++ b/addons/web/static/src/scss/form_view.scss
@@ -743,6 +743,7 @@
}
.o_field_widget {
width: 50%;
+ flex: 0 0 auto;
}
button.btn-link:first-child {
padding-left: 0;
diff --git a/addons/web/static/src/scss/kanban_view.scss b/addons/web/static/src/scss/kanban_view.scss
index c771d6a37ab..c4c0e9d011d 100644
--- a/addons/web/static/src/scss/kanban_view.scss
+++ b/addons/web/static/src/scss/kanban_view.scss
@@ -564,6 +564,8 @@
@include o-kanban-record-color;
.oe_kanban_color_help {
- @include o-position-absolute(0, 5px, 0, -1px);
+ @include o-position-absolute(0, auto, 0, -1px);
+ width: $o-kanban-color-border-width;
+ z-index: 1; // show the title over kanban color
}
}
diff --git a/addons/web/static/src/scss/navbar_mobile.scss b/addons/web/static/src/scss/navbar_mobile.scss
index 6ef917b8517..f4593436163 100644
--- a/addons/web/static/src/scss/navbar_mobile.scss
+++ b/addons/web/static/src/scss/navbar_mobile.scss
@@ -23,7 +23,6 @@
position: relative;
height: $o-navbar-height;
- overflow: hidden;
> ul {
> li {
@@ -39,6 +38,7 @@
&.o_menu_sections {
width: 100%;
+ display: none;
.dropdown-menu.show {
position: static;
@@ -82,6 +82,9 @@
body.o_mobile_menu_opened > .o_main_navbar {
height: 100%;
overflow: auto;
+ .o_menu_sections {
+ display: block;
+ }
}
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 55d744d3a60..5e2f27ccc66 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1488,7 +1488,7 @@