diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000000..98eb36db26e
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,40 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported |
+| ------- | ------------------ |
+| 12.0 | :white_check_mark: |
+| 11.0 | :white_check_mark: |
+| 10.0 | :white_check_mark: |
+| <=9.0 | :x: |
+
+## Reporting a Vulnerability
+
+Please share privately the details of your security vulnerability by contacting our Security Team:
+[Contact Info](https://www.odoo.com/security-report)
+
+Make sure to include as much information as possible, with the detailed steps to reproduce the problem,
+the versions that are affected, the expected results and actual results, and any other information that
+might help us react faster and more efficiently.
+
+We tend to prefer _text-based descriptions_ accompanied with a proof-of-concept script/exploit, rather
+than screenshots and videos.
+
+Our [Responsible Disclosure](https://www.odoo.com/security-report) page gives an overview of the
+process, including:
+
+ - Our Incident Response Procedure (what will happen after you report an issue)
+ - Our Rules (what you can and cannot do while researching security issues)
+ - Guidelines with **DO REPORT** and **DO NOT REPORT** issues
+ (what kind of issues will be accepted/rejected)
+
+
+## Important note
+
+We receive a majority of security reports that have little to no impact on the security of Odoo or
+the Odoo Cloud, and we ultimately have to reject them. To avoid a disappointing experience when
+contacting us, please try to put together a proof-of-concept attack and take a critical look at
+what's really at risk.
+If the proposed attack scenario turns out unrealistic, your report will probably be rejected.
+Also be sure to review our list of [non-qualifying issues](https://www.odoo.com/security-report#what).
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 155064820bc..487477dee6a 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -446,7 +446,6 @@ class AccountBankStatementLine(models.Model):
""" Prepare the dict of values to balance the move.
:param recordset move: the account.move to link the move line
- :param dict move: a dict of vals of a account.move which will be created later
:param float amount: the amount of transaction that wasn't already reconciled
"""
company_currency = self.journal_id.company_id.currency_id
@@ -454,23 +453,19 @@ class AccountBankStatementLine(models.Model):
st_line_currency = self.currency_id or statement_currency
amount_currency = False
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
- if isinstance(move, dict):
- amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
- else:
- amount_sum = sum(x.amount_currency for x in move.line_ids)
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
if st_line_currency != company_currency and st_line_currency == statement_currency:
# company in currency A, statement in currency B and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
- amount_currency = -amount_sum
+ amount_currency = -sum([x.amount_currency for x in move.line_ids])
elif st_line_currency != company_currency and statement_currency == company_currency:
# company in currency A, statement in currency A and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
- amount_currency = -amount_sum
+ amount_currency = -sum([x.amount_currency for x in move.line_ids])
elif st_line_currency != company_currency and st_line_currency != statement_currency:
# company in currency A, statement in currency B and transaction in currency C
# counterpart line must have currency B and use rate between B and C to compute correct amount
- amount_currency = -amount_sum/st_line_currency_rate
+ amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
elif st_line_currency == company_currency and statement_currency != company_currency:
# company in currency A, statement in currency B and transaction in currency A
# counterpart line must have currency B and amount is computed using the rate between A and B
@@ -479,8 +474,9 @@ class AccountBankStatementLine(models.Model):
# last case is company in currency A, statement in currency A and transaction in currency A
# and in this case counterpart line does not need any second currency nor amount_currency
- aml_dict = {
+ return {
'name': self.name,
+ 'move_id': move.id,
'partner_id': self.partner_id and self.partner_id.id or False,
'account_id': amount >= 0 \
and self.statement_id.journal_id.default_credit_account_id.id \
@@ -491,71 +487,22 @@ class AccountBankStatementLine(models.Model):
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
'amount_currency': amount_currency,
}
- if isinstance(move, self.env['account.move'].__class__):
- aml_dict['move_id'] = move.id
- return aml_dict
- @api.multi
def fast_counterpart_creation(self):
"""This function is called when confirming a bank statement and will allow to automatically process lines without
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
entry using that account to counterpart the bank account
"""
- payment_list = []
- move_list = []
- account_type_receivable = self.env.ref('account.data_account_type_receivable')
- already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
- managed_st_line = []
for st_line in self:
# Technical functionality to automatically reconcile by creating a new move line
- if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
- managed_st_line.append(st_line.id)
- # Create payment vals
- total = st_line.amount
- payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
- currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
- partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
- payment_list.append({
- 'payment_method_id': payment_methods and payment_methods[0].id or False,
- 'payment_type': total > 0 and 'inbound' or 'outbound',
- 'partner_id': st_line.partner_id.id,
- 'partner_type': partner_type,
- 'journal_id': st_line.statement_id.journal_id.id,
- 'payment_date': st_line.date,
- 'state': 'reconciled',
- 'currency_id': currency.id,
- 'amount': abs(total),
- 'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
- 'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
- })
-
- # Create move and move line vals
- move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
- aml_dict = {
+ if st_line.account_id and not st_line.journal_entry_ids.ids:
+ vals = {
'name': st_line.name,
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
'credit': st_line.amount > 0 and st_line.amount or 0.0,
'account_id': st_line.account_id.id,
- 'partner_id': st_line.partner_id.id,
- 'statement_line_id': st_line.id,
}
- st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
- move_vals['line_ids'] = [(0, 0, aml_dict)]
- balance_line = self._prepare_reconciliation_move_line(move_vals, st_line.amount)
- move_vals['line_ids'].append((0, 0, balance_line))
- move_list.append(move_vals)
-
- # Creates
- payment_ids = self.env['account.payment'].create(payment_list)
- for payment_id, move_vals in zip(payment_ids, move_list):
- for line in move_vals['line_ids']:
- line[2]['payment_id'] = payment_id.id
- move_ids = self.env['account.move'].create(move_list)
- move_ids.post()
-
- for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
- st_line.write({'move_name': move.name})
- payment.write({'payment_reference': move.name})
+ st_line.process_reconciliation(new_aml_dicts=[vals])
def _get_communication(self, payment_method_id):
return self.name or ''
@@ -683,6 +630,8 @@ class AccountBankStatementLine(models.Model):
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
# case we reconcile the existing and the new move lines together, or being a write-off.
if counterpart_aml_dicts or new_aml_dicts:
+ st_line_currency = self.currency_id or statement_currency
+ st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
# Create the move
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
@@ -705,12 +654,32 @@ class AccountBankStatementLine(models.Model):
# Complete dicts to create both counterpart move lines and write-offs
to_create = (counterpart_aml_dicts + new_aml_dicts)
+ company = self.company_id
date = self.date or fields.Date.today()
for aml_dict in to_create:
aml_dict['move_id'] = move.id
aml_dict['partner_id'] = self.partner_id.id
aml_dict['statement_line_id'] = self.id
- self._prepare_move_line_for_currency(aml_dict, date)
+ if st_line_currency.id != company_currency.id:
+ aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
+ aml_dict['currency_id'] = st_line_currency.id
+ if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
+ # Statement is in company currency but the transaction is in foreign currency
+ aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
+ aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
+ elif self.currency_id and st_line_currency_rate:
+ # Statement is in foreign currency and the transaction is in another one
+ aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
+ aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
+ else:
+ # Statement is in foreign currency and no extra currency is given for the transaction
+ aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
+ aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
+ elif statement_currency.id != company_currency.id:
+ # Statement is in foreign currency but the transaction is in company currency
+ prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
+ aml_dict['amount_currency'] = prorata_factor * self.amount
+ aml_dict['currency_id'] = statement_currency.id
# Create write-offs
for aml_dict in new_aml_dicts:
@@ -760,36 +729,6 @@ class AccountBankStatementLine(models.Model):
counterpart_moves.assert_balanced()
return counterpart_moves
- @api.multi
- def _prepare_move_line_for_currency(self, aml_dict, date):
- self.ensure_one()
- company_currency = self.journal_id.company_id.currency_id
- statement_currency = self.journal_id.currency_id or company_currency
- st_line_currency = self.currency_id or statement_currency
- st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
- company = self.company_id
-
- if st_line_currency.id != company_currency.id:
- aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
- aml_dict['currency_id'] = st_line_currency.id
- if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
- # Statement is in company currency but the transaction is in foreign currency
- aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
- aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
- elif self.currency_id and st_line_currency_rate:
- # Statement is in foreign currency and the transaction is in another one
- aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
- aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
- else:
- # Statement is in foreign currency and no extra currency is given for the transaction
- aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
- aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
- elif statement_currency.id != company_currency.id:
- # Statement is in foreign currency but the transaction is in company currency
- prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
- aml_dict['amount_currency'] = prorata_factor * self.amount
- aml_dict['currency_id'] = statement_currency.id
-
def _check_invoice_state(self, invoice):
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
invoice.write({'state': 'paid'})
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 14fe69ffcb5..925c43e2ce0 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -875,7 +875,7 @@ class AccountInvoice(models.Model):
domain = {}
company_id = self.company_id.id
p = self.partner_id if not company_id else self.partner_id.with_context(force_company=company_id)
- type = self.type
+ type = self.type or self.env.context.get('type', 'out_invoice')
if p:
rec_account = p.property_account_receivable_id
pay_account = p.property_account_payable_id
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index f127893c4f0..853e5ab9e1c 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from babel.dates import format_datetime, format_date
from odoo import models, api, _, fields
+from odoo.osv import expression
from odoo.release import version
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, safe_eval
from odoo.tools.misc import formatLang, format_date as odoo_format_date
@@ -427,10 +428,16 @@ class account_journal(models.Model):
action_name = 'action_view_bank_statement_tree'
elif self.type == 'sale':
action_name = 'action_invoice_tree1'
- self = self.with_context(use_domain=[('journal_id', '=', self.id)])
+ use_domain = expression.AND(
+ [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]]
+ )
+ self = self.with_context(use_domain=use_domain)
elif self.type == 'purchase':
action_name = 'action_vendor_bill_template'
- self = self.with_context(use_domain=[('journal_id', '=', self.id)])
+ use_domain = expression.AND(
+ [self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]]
+ )
+ self = self.with_context(use_domain=use_domain)
else:
action_name = 'action_move_journal_line'
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 873733528d6..e5f4920cc3c 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -87,14 +87,12 @@ class AccountMove(models.Model):
if line.account_id.user_type_id.type in ('receivable', 'payable'):
amount = abs(line.debit - line.credit)
total_amount += amount
+ for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
+ total_reconciled += partial_line.amount
precision_currency = move.currency_id or move.company_id.currency_id
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
move.matched_percentage = 1.0
else:
- for line in move.line_ids:
- if line.account_id.user_type_id.type in ('receivable', 'payable'):
- for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
- total_reconciled += partial_line.amount
move.matched_percentage = total_reconciled / total_amount
@api.multi
@@ -580,7 +578,7 @@ class AccountMoveLine(models.Model):
else:
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
rate = line.currency_id.with_context(date=date).rate
- amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
+ amount_residual_currency += sign_partial_line * partial_line.amount * rate
#computing the `reconciled` field.
reconciled = False
@@ -829,21 +827,22 @@ class AccountMoveLine(models.Model):
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
"""
# Get first all aml involved
- todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
- amls = set(self.ids)
+ part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
+ amls = self
+ todo = set(part_recs)
seen = set()
while todo:
- aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
- amls |= set(aml_ids)
- seen |= set([rec['id'] for rec in todo])
- todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
-
- partial_rec_ids = list(seen)
+ partial_rec = todo.pop()
+ seen.add(partial_rec)
+ for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
+ if aml not in amls:
+ amls += aml
+ for x in aml.matched_debit_ids | aml.matched_credit_ids:
+ if x not in seen:
+ todo.add(x)
+ partial_rec_ids = [x.id for x in seen]
if not amls:
return
- else:
- amls = self.browse(list(amls))
-
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
multiple_currency = False
@@ -1006,7 +1005,10 @@ class AccountMoveLine(models.Model):
debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
# Compute on which field reconciliation should be based upon:
- field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ if self[0].account_id.currency_id and self[0].account_id.currency_id != self[0].account_id.company_id.currency_id:
+ field = 'amount_residual_currency'
+ else:
+ field = 'amount_residual'
#if all lines share the same currency, use amount_residual_currency to avoid currency rounding error
if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]):
field = 'amount_residual_currency'
diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py
index db3fb9437a1..05edf105763 100644
--- a/addons/account/models/reconciliation_widget.py
+++ b/addons/account/models/reconciliation_widget.py
@@ -83,28 +83,43 @@ class AccountReconciliation(models.AbstractModel):
@api.model
def _get_bank_statement_line_partners(self, st_lines):
+ params = []
+
+ # Add the res.partner.ban's IR rules. In case partners are not shared between companies,
+ # identical bank accounts may exist in a company we don't have access to.
+ ir_rules_query = self.env['res.partner.bank']._where_calc([])
+ self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read')
+ from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
+ if where_clause:
+ where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank')
+ params += where_clause_params
+ else:
+ where_bank = ''
+
+ # Add the res.partner's IR rules. In case partners are not shared between companies,
+ # identical partners may exist in a company we don't have access to.
+ ir_rules_query = self.env['res.partner']._where_calc([])
+ self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
+ from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
+ if where_clause:
+ where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3')
+ params += where_clause_params
+ else:
+ where_partner = ''
+
query = '''
SELECT
st_line.id AS id,
COALESCE(p1.id,p2.id,p3.id) AS partner_id
FROM account_bank_statement_line st_line
- LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number
- LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id
- LEFT JOIN res_partner p2 ON bank.partner_id=p2.id
- LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name
- WHERE st_line.id IN %s
'''
- params = [tuple(st_lines.ids)]
+ query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank)
+ query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n'
+ query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n'
+ query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner)
+ query += 'WHERE st_line.id IN %s'
- # Add the res.partner's IR rules to the WHERE clause. In case partners are not shared
- # between companies, identical partners may exist in a company we don't have access to.
- ir_rules_query = self.env['res.partner']._where_calc([])
- self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
- from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
- where_p3 = (" AND %s" % where_clause).replace('res_partner', 'p3') if where_clause else ''
- if where_p3:
- query += where_p3
- params += where_clause_params
+ params += [tuple(st_lines.ids)]
self._cr.execute(query, params)
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index 7861644525a..abc17743e61 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -112,13 +112,13 @@ class AccountInvoiceReport(models.Model):
1 AS nbr,
ai.id AS invoice_id, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
ai.partner_bank_id,
- SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty,
+ SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty,
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
- (ai.amount_total * invoice_type.sign) / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
+ ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
count(*) * invoice_type.sign AS amount_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
- WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
- THEN SUM(ail.quantity / u.factor * u2.factor)
+ WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric
+ THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1))
ELSE 1::numeric
END AS price_average,
ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index 67d5de9de08..5f27b0970f6 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -346,9 +346,12 @@ var StatementAction = AbstractAction.extend({
'context': self.model.getContext(),
});
_.each(result.handles, function (handle) {
- self._getWidget(handle).destroy();
- var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
- self.widgets.splice(index, 1);
+ var widget = self._getWidget(handle);
+ if (widget) {
+ widget.destroy();
+ var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
+ self.widgets.splice(index, 1);
+ }
});
// Get number of widget and if less than constant and if there are more to laod, load until constant
if (self.widgets.length < self.model.defaultDisplayQty
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index c28790796d2..d4dce5581ca 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -623,13 +623,13 @@ var StatementModel = BasicModel.extend({
prop = this._formatQuickCreate(line);
line.reconciliation_proposition.push(prop);
}
- if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
+ if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
new CrashManager().show_warning({data: {
exception_type: _t("Incorrect Operation"),
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
}});
// FIXME: model should not be tied to the DOM !
- $('.create_to_check input').click();
+ $('.create_to_check input:visible').prop('checked', !values.to_check).change();
return Promise.resolve();
}
_.each(values, function (value, fieldName) {
diff --git a/addons/account/tests/test_account_customer_invoice.py b/addons/account/tests/test_account_customer_invoice.py
index b3821a989c2..5c45aca69fe 100644
--- a/addons/account/tests/test_account_customer_invoice.py
+++ b/addons/account/tests/test_account_customer_invoice.py
@@ -306,30 +306,30 @@ class TestAccountCustomerInvoice(AccountTestUsers):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 2)
- self.assertEquals(dashboard_data['sum_draft'], '$ 68.42')
+ self.assertIn('68.42', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 0)
- self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00')
+ self.assertIn('0.00', dashboard_data['sum_waiting'])
# Check Both
invoice.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 1)
- self.assertEquals(dashboard_data['sum_draft'], '$ -13.30')
+ self.assertIn('-13.30', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 1)
- self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72')
+ self.assertIn('81.72', dashboard_data['sum_waiting'])
# Check waiting payment
refund.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
- self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
+ self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 2)
- self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42')
+ self.assertIn('68.42', dashboard_data['sum_waiting'])
# Check partial
receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable')
@@ -353,12 +353,12 @@ class TestAccountCustomerInvoice(AccountTestUsers):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
- self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
+ self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 2)
- self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42')
+ self.assertIn('78.42', dashboard_data['sum_waiting'])
with patch('odoo.fields.Date.today', patched_today):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_late'], 2)
- self.assertEquals(dashboard_data['sum_late'], '$ 78.42')
+ self.assertIn('78.42', dashboard_data['sum_late'])
diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py
index 01eb2c80c9c..dcc8f0e470c 100644
--- a/addons/account/tests/test_reconciliation.py
+++ b/addons/account/tests/test_reconciliation.py
@@ -91,6 +91,11 @@ class TestReconciliation(AccountingTestCase):
'code': 'CABA',
'type': 'general',
})
+ self.general_journal = self.env['account.journal'].create({
+ 'name': 'general',
+ 'code': 'GENE',
+ 'type': 'general',
+ })
# Tax Cash Basis
self.tax_cash_basis = self.env['account.tax'].create({
@@ -2062,3 +2067,68 @@ class TestReconciliationExec(TestReconciliation):
pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
self.assertTrue(pay_receivable_line1.reconciled)
self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id)
+
+ def test_reconciliation_with_currency(self):
+ #reconciliation on an account having a foreign currency being
+ #the same as the company one
+ account_rcv = self.account_rcv
+ account_rcv.currency_id = self.currency_euro_id
+ aml_obj = self.env['account.move.line'].with_context(
+ check_move_validity=False)
+ general_move1 = self.env['account.move'].create({
+ 'name': 'general1',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'debit1',
+ 'account_id': account_rcv.id,
+ 'debit': 11,
+ 'move_id': general_move1.id,
+ })
+ aml_obj.create({
+ 'name': 'credit1',
+ 'account_id': self.account_rsa.id,
+ 'credit': 11,
+ 'move_id': general_move1.id,
+ })
+ general_move1.post()
+ general_move2 = self.env['account.move'].create({
+ 'name': 'general2',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'credit2',
+ 'account_id': account_rcv.id,
+ 'credit': 10,
+ 'move_id': general_move2.id,
+ })
+ aml_obj.create({
+ 'name': 'debit2',
+ 'account_id': self.account_rsa.id,
+ 'debit': 10,
+ 'move_id': general_move2.id,
+ })
+ general_move2.post()
+ general_move3 = self.env['account.move'].create({
+ 'name': 'general3',
+ 'journal_id': self.general_journal.id,
+ })
+ aml_obj.create({
+ 'name': 'credit3',
+ 'account_id': account_rcv.id,
+ 'credit': 1,
+ 'move_id': general_move3.id,
+ })
+ aml_obj.create({
+ 'name': 'debit3',
+ 'account_id': self.account_rsa.id,
+ 'debit': 1,
+ 'move_id': general_move3.id,
+ })
+ general_move3.post()
+ to_reconcile = ((general_move1 + general_move2 + general_move3)
+ .mapped('line_ids')
+ .filtered(lambda l: l.account_id.id == account_rcv.id))
+ to_reconcile.reconcile()
+ for aml in to_reconcile:
+ self.assertEqual(aml.amount_residual, 0.0)
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index 772a29affff..a3e175bdf20 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -99,7 +99,8 @@
-
+
+
-
+
diff --git a/addons/hr_attendance_presence/views/hr_employee.xml b/addons/hr_attendance_presence/views/hr_employee.xml
index c49871522a2..a645e594b20 100644
--- a/addons/hr_attendance_presence/views/hr_employee.xml
+++ b/addons/hr_attendance_presence/views/hr_employee.xml
@@ -4,7 +4,7 @@
hr.employeehr.employee
-
+ 20
diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py
index cf7ec0d6549..29e9896ac7e 100644
--- a/addons/hr_contract/models/hr_contract.py
+++ b/addons/hr_contract/models/hr_contract.py
@@ -96,12 +96,12 @@ class Contract(models.Model):
@api.constrains('employee_id', 'state', 'date_start', 'date_end')
def _check_current_contract(self):
- """ Two contracts in state [incoming | pending | open | close] cannot overlap """
- for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']):
+ """ Two contracts in state [incoming | pending | open] cannot overlap """
+ for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']):
domain = [
('id', '!=', contract.id),
('employee_id', '=', contract.employee_id.id),
- ('state', 'in', ['incoming', 'pending', 'open', 'close']),
+ ('state', 'in', ['incoming', 'pending', 'open']),
]
if not contract.date_end:
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 9ea4c4753b0..4e5207a385a 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -151,14 +151,14 @@ class HrExpense(models.Model):
@api.model
def get_empty_list_help(self, help_message):
- if help_message and "oe_view_nocontent_smiling_face" not in help_message:
+ if help_message and "o_view_nocontent_smiling_face" not in help_message:
use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
if alias_record and alias_record.alias_domain and alias_record.alias_name:
link = "%(email)s" % {
'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
}
- return '
%s
%s
' % (
+ return '
%s
%s
' % (
_('Add a new expense,'),
_('or send receipts by email to %s.') % (link),)
return super(HrExpense, self).get_empty_list_help(help_message)
@@ -211,6 +211,25 @@ class HrExpense(models.Model):
# Business
# ----------------------------------------
+ @api.multi
+ def _prepare_move_values(self):
+ """
+ This function prepares move values related to an expense
+ """
+ self.ensure_one()
+ journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id
+ account_date = self.sheet_id.accounting_date or self.date
+ move_values = {
+ 'journal_id': journal.id,
+ 'company_id': self.env.user.company_id.id,
+ 'date': account_date,
+ 'ref': self.sheet_id.name,
+ # force the name to the default value, to avoid an eventual 'default_name' in the context
+ # to set it to '' which cause no number to be given to the account.move when posted.
+ 'name': '/',
+ }
+ return move_values
+
@api.multi
def _get_account_move_by_sheet(self):
""" Return a mapping between the expense sheet of current expense and its account move
@@ -219,18 +238,8 @@ class HrExpense(models.Model):
move_grouped_by_sheet = {}
for expense in self:
# create the move that will contain the accounting entries
- account_date = expense.sheet_id.accounting_date or expense.date
if expense.sheet_id.id not in move_grouped_by_sheet:
- journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
- move = self.env['account.move'].create({
- 'journal_id': journal.id,
- 'company_id': self.env.user.company_id.id,
- 'date': account_date,
- 'ref': expense.sheet_id.name,
- # force the name to the default value, to avoid an eventual 'default_name' in the context
- # to set it to '' which cause no number to be given to the account.move when posted.
- 'name': '/',
- })
+ move = self.env['account.move'].create(expense._prepare_move_values())
move_grouped_by_sheet[expense.sheet_id.id] = move
else:
move = move_grouped_by_sheet[expense.sheet_id.id]
diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py
index 6a0dae5d5a2..2a8a5506217 100644
--- a/addons/hr_holidays/models/hr.py
+++ b/addons/hr_holidays/models/hr.py
@@ -206,8 +206,8 @@ class Employee(models.Model):
hr_vals['manager_id'] = values['parent_id']
if values.get('department_id') is not None:
hr_vals['department_id'] = values['department_id']
- holidays = self.env['hr.leave'].search(['|',('state', 'in', ['draft', 'confirm']),('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
+ holidays = self.env['hr.leave'].sudo().search(['|',('state', 'in', ['draft', 'confirm']),('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
holidays.write(hr_vals)
- allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
+ allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations.write(hr_vals)
return res
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index 3ae7d554205..01c19561b34 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -13,6 +13,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare
+from odoo.tools.float_utils import float_round
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
@@ -147,6 +148,8 @@ class HolidaysRequest(models.Model):
number_of_hours_display = fields.Float(
'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True,
help='Number of hours of the time off request according to your working schedule. Used for interface.')
+ duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display',
+ help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details
# details
meeting_id = fields.Many2one('calendar.event', string='Meeting')
parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
@@ -356,11 +359,17 @@ class HolidaysRequest(models.Model):
self.mode_company_id = False
self.department_id = False
+ @api.multi
+ def _sync_employee_details(self):
+ for holiday in self:
+ holiday.manager_id = holiday.employee_id.parent_id.id
+ if holiday.employee_id:
+ holiday.department_id = holiday.employee_id.department_id
+
@api.onchange('employee_id')
def _onchange_employee_id(self):
- self.manager_id = self.employee_id.parent_id.id
- if self.employee_id:
- self.department_id = self.employee_id.department_id
+ self._sync_employee_details()
+ self.holiday_status_id = False
@api.onchange('date_from', 'date_to', 'employee_id')
def _onchange_leave_dates(self):
@@ -386,6 +395,16 @@ class HolidaysRequest(models.Model):
else:
holiday.number_of_hours_display = 0
+ @api.multi
+ @api.depends('number_of_hours_display', 'number_of_days_display')
+ def _compute_duration_display(self):
+ for leave in self:
+ leave.duration_display = '%g %s' % (
+ (float_round(leave.number_of_hours_display, precision_digits=2)
+ if leave.leave_type_request_unit == 'hour'
+ else float_round(leave.number_of_days_display, precision_digits=2)),
+ _('hour(s)') if leave.leave_type_request_unit == 'hour' else _('day(s)'))
+
@api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
@@ -414,7 +433,7 @@ class HolidaysRequest(models.Model):
def _check_date(self):
for holiday in self:
domain = [
- ('date_from', '<=', holiday.date_to),
+ ('date_from', '<', holiday.date_to),
('date_to', '>', holiday.date_from),
('employee_id', '=', holiday.employee_id.id),
('id', '!=', holiday.id),
@@ -457,7 +476,10 @@ class HolidaysRequest(models.Model):
res = []
for leave in self:
if self.env.context.get('short_name'):
- res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
+ if leave.leave_type_request_unit == 'hour':
+ res.append((leave.id, _("%s : %.2f hour(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display)))
+ else:
+ res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
else:
if leave.holiday_type == 'company':
target = leave.mode_company_id.name
@@ -467,11 +489,18 @@ class HolidaysRequest(models.Model):
target = leave.category_id.name
else:
target = leave.employee_id.name
- res.append(
- (leave.id,
- _("%s on %s :%.2f day(s)") %
- (target, leave.holiday_status_id.name, leave.number_of_days))
- )
+ if leave.leave_type_request_unit == 'hour':
+ res.append(
+ (leave.id,
+ _("%s on %s : %.2f hour(s)") %
+ (target, leave.holiday_status_id.name, leave.number_of_hours_display))
+ )
+ else:
+ res.append(
+ (leave.id,
+ _("%s on %s : %.2f day(s)") %
+ (target, leave.holiday_status_id.name, leave.number_of_days))
+ )
return res
@api.multi
@@ -512,7 +541,7 @@ class HolidaysRequest(models.Model):
if holiday.validation_type == 'hr':
holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids)
if employee_id:
- holiday._onchange_employee_id()
+ holiday._sync_employee_details()
if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values):
holiday._onchange_leave_dates()
if leave_type.validation_type == 'no_validation':
@@ -551,7 +580,7 @@ class HolidaysRequest(models.Model):
for holiday in self:
if employee_id:
holiday.add_follower(employee_id)
- holiday._onchange_employee_id()
+ self._sync_employee_details()
if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values):
holiday._onchange_leave_dates()
return result
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index 1a1bacbb83b..50ab5127021 100644
--- a/addons/hr_holidays/models/hr_leave_allocation.py
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -207,7 +207,10 @@ class HolidaysAllocation(models.Model):
@api.depends('number_of_days', 'employee_id')
def _compute_number_of_hours_display(self):
for allocation in self:
- allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
+ if allocation.parent_id and allocation.parent_id.type_request_unit == "hour":
+ allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY
+ else:
+ allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@api.multi
@api.depends('number_of_hours_display', 'number_of_days_display')
diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py
index 9f32d96e6c6..f15f9cfb9e5 100644
--- a/addons/hr_holidays/tests/test_leave_requests.py
+++ b/addons/hr_holidays/tests/test_leave_requests.py
@@ -158,9 +158,9 @@ class TestLeaveRequests(TestHrHolidaysBase):
self.employee_hrmanager.write({'department_id': self.hr_dept.id})
self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)]))
leave_form = Form(self.env['hr.leave'].sudo(self.user_hrmanager))
- leave_form.holiday_status_id = self.holidays_type_1
leave_form.holiday_type = 'department'
leave_form.department_id = self.hr_dept
+ leave_form.holiday_status_id = self.holidays_type_1
leave = leave_form.save()
leave.action_approve()
member_ids = self.hr_dept.member_ids.ids
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 5ff1739f107..e137648e758 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -313,7 +313,7 @@
-
+
diff --git a/addons/hr_maintenance/views/hr_views.xml b/addons/hr_maintenance/views/hr_views.xml
index 070328c9464..826ddcf2d44 100644
--- a/addons/hr_maintenance/views/hr_views.xml
+++ b/addons/hr_maintenance/views/hr_views.xml
@@ -7,7 +7,7 @@
-
+
Гадаад борлуулалт - Худалдан авалт
diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml
index 244c9b65ff3..1e8b23dacda 100644
--- a/addons/l10n_mx/data/account_tag_data.xml
+++ b/addons/l10n_mx/data/account_tag_data.xml
@@ -479,25 +479,25 @@
119.01 IVA pendiente de pago4accounts
- A
+ D119.02 IVA de importación pendiente de pago4accounts
- A
+ D119.03 IEPS pendiente de pago4accounts
- A
+ D119.04 IEPS pendiente de pago en importación4accounts
- A
+ D120.01 Anticipo a proveedores nacional
@@ -917,7 +917,7 @@
180.01 Crédito mercantil4accounts
- A
+ D181.01 Gastos de instalación
@@ -2423,7 +2423,7 @@
503 Devoluciones, descuentos o bonificaciones sobre compras1accounts
- D
+ A503.01 Devoluciones, descuentos o bonificaciones sobre compras
@@ -2585,13 +2585,13 @@
505.01 Costo por venta de activo fijo4accounts
- A
+ D505.02 Costo por baja de activo fijo4accounts
- A
+ D601.01 Sueldos y salarios
@@ -4781,7 +4781,7 @@
607.01 Participación de los trabajadores en las utilidades4accounts
- A
+ D608.01 Participación en resultados de subsidiarias
@@ -4799,7 +4799,7 @@
610.01 Participación de los trabajadores en las utilidades diferida4accounts
- A
+ D611.01 Impuesto Sobre la renta
@@ -4817,7 +4817,7 @@
612.01 Gastos no deducibles para CUFIN4accounts
- A
+ D613.01 Depreciación de edificios
diff --git a/addons/l10n_za/data/account.account.tag.csv b/addons/l10n_za/data/account.account.tag.csv
index e07dd5d5bb9..9b8832a53eb 100644
--- a/addons/l10n_za/data/account.account.tag.csv
+++ b/addons/l10n_za/data/account.account.tag.csv
@@ -1,17 +1,17 @@
-id,name,applicability
-tag_ST1,ST1,taxes
-tag_ST1A,ST1A,taxes
-tag_ST2,ST2,taxes
-tag_ST2A,ST2A,taxes
-tag_ST3,ST3,taxes
-tag_ST5,ST5,taxes
-tag_ST7,ST7,taxes
-tag_ST10,ST10,taxes
-tag_ST12,ST12,taxes
-tag_PT14,PT14,taxes
-tag_PT14A,PT14A,taxes
-tag_PT15,PT15,taxes
-tag_PT15A,PT15A,taxes
-tag_PT16,PT16,taxes
-tag_PT17,PT17,taxes
-tag_PT18,PT18,taxes
+id,name,applicability
+tag_ST1,ST1,taxes
+tag_ST1A,ST1A,taxes
+tag_ST2,ST2,taxes
+tag_ST2A,ST2A,taxes
+tag_ST3,ST3,taxes
+tag_ST5,ST5,taxes
+tag_ST7,ST7,taxes
+tag_ST10,ST10,taxes
+tag_ST12,ST12,taxes
+tag_PT14,PT14,taxes
+tag_PT14A,PT14A,taxes
+tag_PT15,PT15,taxes
+tag_PT15A,PT15A,taxes
+tag_PT16,PT16,taxes
+tag_PT17,PT17,taxes
+tag_PT18,PT18,taxes
\ No newline at end of file
diff --git a/addons/l10n_za/data/account.account.template.csv b/addons/l10n_za/data/account.account.template.csv
index b47ab5beed0..404eb24a86e 100644
--- a/addons/l10n_za/data/account.account.template.csv
+++ b/addons/l10n_za/data/account.account.template.csv
@@ -1,111 +1,111 @@
-id,code,name,user_type_id/id,reconcile,chart_template_id/id
-100020,100020,Stock Valuation Account,account.data_account_type_current_assets,True,default_chart_template
-100030,100030,Stock Work In Progress,account.data_account_type_current_assets,False,default_chart_template
-100040,100040,Stock Finished Goods,account.data_account_type_current_assets,False,default_chart_template
-100050,100050,Stock Delivered Control Account,account.data_account_type_current_assets,True,default_chart_template
-100060,100060,Purchase Tax Control Account,account.data_account_type_current_assets,False,default_chart_template
-100070,100070,Other Current Assets,account.data_account_type_current_assets,False,default_chart_template
-110010,110010,Debtors Control Account,account.data_account_type_receivable,True,default_chart_template
-110020,110020,Sundry Debtors,account.data_account_type_receivable,True,default_chart_template
-124010,124010,Credit Card Merchant Account,account.data_account_type_liquidity,False,default_chart_template
-126010,126010,Cash In Hand,account.data_account_type_liquidity,False,default_chart_template
-130010,130010,Prepayments,account.data_account_type_prepayments,False,default_chart_template
-140010,140010,Software,account.data_account_type_fixed_assets,False,default_chart_template
-140020,140020,Patents & Trademarks,account.data_account_type_fixed_assets,False,default_chart_template
-140030,140030,Fixtures & Fittings,account.data_account_type_fixed_assets,False,default_chart_template
-140040,140040,Land & Buildings,account.data_account_type_fixed_assets,False,default_chart_template
-140050,140050,Motor Vehicles,account.data_account_type_fixed_assets,False,default_chart_template
-140060,140060,Office Equipment (incl computer equipment),account.data_account_type_fixed_assets,False,default_chart_template
-140070,140070,Plant & Machinery,account.data_account_type_fixed_assets,False,default_chart_template
-150010,150010,Non-current assets,account.data_account_type_non_current_assets,False,default_chart_template
-200010,200010,Stock Received Control Account,account.data_account_type_current_liabilities,True,default_chart_template
-200020,200020,Sundry Creditors,account.data_account_type_current_liabilities,False,default_chart_template
-200030,200030,Other Creditors,account.data_account_type_current_liabilities,False,default_chart_template
-200040,200040,Accruals,account.data_account_type_current_liabilities,False,default_chart_template
-200050,200050,Bad debt provision,account.data_account_type_current_liabilities,False,default_chart_template
-200060,200060,Sales Tax Control Account,account.data_account_type_current_liabilities,False,default_chart_template
-200070,200070,Manual Adjustments & VAT,account.data_account_type_current_liabilities,False,default_chart_template
-200080,200080,Loans,account.data_account_type_current_liabilities,False,default_chart_template
-200090,200090,Hire Purchase,account.data_account_type_current_liabilities,False,default_chart_template
-200100,200100,Mortgages,account.data_account_type_current_liabilities,False,default_chart_template
-210010,210010,Company Credit Card,account.data_account_type_credit_card,False,default_chart_template
-220010,220010,Creditors Control Account,account.data_account_type_payable,True,default_chart_template
-220020,220020,SARS - VAT Account,account.data_account_type_payable,True,default_chart_template
-220030,220030,P.A.Y.E. & UIF,account.data_account_type_payable,True,default_chart_template
-220040,220040,Net Wages,account.data_account_type_payable,True,default_chart_template
-220050,220050,Pension Fund,account.data_account_type_payable,True,default_chart_template
-220060,220060,Corporation Tax,account.data_account_type_payable,True,default_chart_template
-300010,300010,Called up share capital,account.data_account_type_equity,False,default_chart_template
-300020,300020,Share premium account,account.data_account_type_equity,False,default_chart_template
-300030,300030,Revaluation reserve,account.data_account_type_equity,False,default_chart_template
-300040,300040,Other reserves,account.data_account_type_equity,False,default_chart_template
-300050,300050,Capital,account.data_account_type_equity,False,default_chart_template
-300060,300060,Dividends,account.data_account_type_equity,False,default_chart_template
-300070,300070,Drawings,account.data_account_type_equity,False,default_chart_template
-400010,400010,Undistributed Profits/Losses,account.data_unaffected_earnings,False,default_chart_template
-500010,500010,Sales category 1,account.data_account_type_revenue,False,default_chart_template
-500020,500020,Sales category 2,account.data_account_type_revenue,False,default_chart_template
-500030,500030,Sales category 3,account.data_account_type_revenue,False,default_chart_template
-500040,500040,Sales category 4,account.data_account_type_revenue,False,default_chart_template
-500050,500050,Bank Interest received,account.data_account_type_revenue,False,default_chart_template
-500060,500060,Investment Interest received,account.data_account_type_revenue,False,default_chart_template
-500070,500070,Profits/Losses on disposals of assets,account.data_account_type_revenue,False,default_chart_template
-500080,500080,Rental Income,account.data_account_type_revenue,False,default_chart_template
-510010,510010,Other Income,account.data_account_type_other_income,False,default_chart_template
-600010,600010,Cost of sales 1,account.data_account_type_direct_costs,False,default_chart_template
-600020,600020,Cost of sales 2,account.data_account_type_direct_costs,False,default_chart_template
-600030,600030,Cost of sales 3,account.data_account_type_direct_costs,False,default_chart_template
-600040,600040,Cost of sales 4,account.data_account_type_direct_costs,False,default_chart_template
-610010,610010,Marketing,account.data_account_type_expenses,False,default_chart_template
-610020,610020,Exhibitions and events,account.data_account_type_expenses,False,default_chart_template
-610030,610030,PR,account.data_account_type_expenses,False,default_chart_template
-610040,610040,Distribution vehicles,account.data_account_type_expenses,False,default_chart_template
-610050,610050,Distribution salaries and wages,account.data_account_type_expenses,False,default_chart_template
-610060,610060,Shipping,account.data_account_type_expenses,False,default_chart_template
-610070,610070,Directors pension,account.data_account_type_expenses,False,default_chart_template
-610080,610080,Directors remuneration,account.data_account_type_expenses,False,default_chart_template
-610090,610090,Gross Salaries,account.data_account_type_expenses,False,default_chart_template
-610100,610100,Employers SDL & UIF,account.data_account_type_expenses,False,default_chart_template
-610110,610110,Subcontractors payments,account.data_account_type_expenses,False,default_chart_template
-610120,610120,Rent and rates,account.data_account_type_expenses,False,default_chart_template
-610130,610130,Light / heat and power,account.data_account_type_expenses,False,default_chart_template
-610140,610140,Repairs and maintenance,account.data_account_type_expenses,False,default_chart_template
-610150,610150,Car hire,account.data_account_type_expenses,False,default_chart_template
-610160,610160,Car fuel,account.data_account_type_expenses,False,default_chart_template
-610170,610170,Car maintenance,account.data_account_type_expenses,False,default_chart_template
-610180,610180,Telephone,account.data_account_type_expenses,False,default_chart_template
-610190,610190,Internet & hosting,account.data_account_type_expenses,False,default_chart_template
-610200,610200,Mobiles,account.data_account_type_expenses,False,default_chart_template
-610210,610210,Stationery,account.data_account_type_expenses,False,default_chart_template
-610220,610220,Office consumables,account.data_account_type_expenses,False,default_chart_template
-610230,610230,Postage and Carriage,account.data_account_type_expenses,False,default_chart_template
-610240,610240,Books,account.data_account_type_expenses,False,default_chart_template
-610250,610250,Network costs,account.data_account_type_expenses,False,default_chart_template
-610260,610260,Software expenses,account.data_account_type_expenses,False,default_chart_template
-610270,610270,Other computer costs,account.data_account_type_expenses,False,default_chart_template
-610280,610280,Recruitment fees,account.data_account_type_expenses,False,default_chart_template
-610290,610290,Other admin expenses,account.data_account_type_expenses,False,default_chart_template
-610300,610300,Accounting,account.data_account_type_expenses,False,default_chart_template
-610310,610310,Auditing,account.data_account_type_expenses,False,default_chart_template
-610320,610320,Consultancy,account.data_account_type_expenses,False,default_chart_template
-610330,610330,Legal and professional charges,account.data_account_type_expenses,False,default_chart_template
-610340,610340,Exchange gains/losses,account.data_account_type_expenses,False,default_chart_template
-610350,610350,Other sundry expenses,account.data_account_type_expenses,False,default_chart_template
-610360,610360,Bad debts,account.data_account_type_expenses,False,default_chart_template
-610370,610370,Interest paid,account.data_account_type_expenses,False,default_chart_template
-610380,610380,Bank Charges,account.data_account_type_expenses,False,default_chart_template
-610390,610390,Donations,account.data_account_type_expenses,False,default_chart_template
-610400,610400,Entertaining,account.data_account_type_expenses,False,default_chart_template
-610410,610410,Insurance,account.data_account_type_expenses,False,default_chart_template
-610420,610420,Travel and subsistence,account.data_account_type_expenses,False,default_chart_template
-610430,610430,Corporation tax expense,account.data_account_type_expenses,False,default_chart_template
-610440,610440,Foreign Exchange Gains/Losses,account.data_account_type_expenses,False,default_chart_template
-610450,610450,Price Differences Control Account,account.data_account_type_expenses,False,default_chart_template
-610460,610460,Cash Register Gains/Losses,account.data_account_type_expenses,False,default_chart_template
-620010,620010,Software Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620020,620020,Patents & Trademarks Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620030,620030,Fixtures and fittings Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620040,620040,Land and buildings Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620050,620050,Motor vehicles Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620060,620060,Office equipment (inc computer equipment) Depreciation,account.data_account_type_depreciation,False,default_chart_template
-620070,620070,Plant and machinery Depreciation,account.data_account_type_depreciation,False,default_chart_template
+id,code,name,user_type_id/id,reconcile,chart_template_id/id
+100020,100020,Stock Valuation Account,account.data_account_type_current_assets,True,default_chart_template
+100030,100030,Stock Work In Progress,account.data_account_type_current_assets,False,default_chart_template
+100040,100040,Stock Finished Goods,account.data_account_type_current_assets,False,default_chart_template
+100050,100050,Stock Delivered Control Account,account.data_account_type_current_assets,True,default_chart_template
+100060,100060,Purchase Tax Control Account,account.data_account_type_current_assets,False,default_chart_template
+100070,100070,Other Current Assets,account.data_account_type_current_assets,False,default_chart_template
+110010,110010,Debtors Control Account,account.data_account_type_receivable,True,default_chart_template
+110020,110020,Sundry Debtors,account.data_account_type_receivable,True,default_chart_template
+124010,124010,Credit Card Merchant Account,account.data_account_type_liquidity,False,default_chart_template
+126010,126010,Cash In Hand,account.data_account_type_liquidity,False,default_chart_template
+130010,130010,Prepayments,account.data_account_type_prepayments,False,default_chart_template
+140010,140010,Software,account.data_account_type_fixed_assets,False,default_chart_template
+140020,140020,Patents & Trademarks,account.data_account_type_fixed_assets,False,default_chart_template
+140030,140030,Fixtures & Fittings,account.data_account_type_fixed_assets,False,default_chart_template
+140040,140040,Land & Buildings,account.data_account_type_fixed_assets,False,default_chart_template
+140050,140050,Motor Vehicles,account.data_account_type_fixed_assets,False,default_chart_template
+140060,140060,Office Equipment (incl computer equipment),account.data_account_type_fixed_assets,False,default_chart_template
+140070,140070,Plant & Machinery,account.data_account_type_fixed_assets,False,default_chart_template
+150010,150010,Non-current assets,account.data_account_type_non_current_assets,False,default_chart_template
+200010,200010,Stock Received Control Account,account.data_account_type_current_liabilities,True,default_chart_template
+200020,200020,Sundry Creditors,account.data_account_type_current_liabilities,False,default_chart_template
+200030,200030,Other Creditors,account.data_account_type_current_liabilities,False,default_chart_template
+200040,200040,Accruals,account.data_account_type_current_liabilities,False,default_chart_template
+200050,200050,Bad debt provision,account.data_account_type_current_liabilities,False,default_chart_template
+200060,200060,Sales Tax Control Account,account.data_account_type_current_liabilities,False,default_chart_template
+200070,200070,Manual Adjustments & VAT,account.data_account_type_current_liabilities,False,default_chart_template
+200080,200080,Loans,account.data_account_type_current_liabilities,False,default_chart_template
+200090,200090,Hire Purchase,account.data_account_type_current_liabilities,False,default_chart_template
+200100,200100,Mortgages,account.data_account_type_current_liabilities,False,default_chart_template
+210010,210010,Company Credit Card,account.data_account_type_credit_card,False,default_chart_template
+220010,220010,Creditors Control Account,account.data_account_type_payable,True,default_chart_template
+220020,220020,SARS - VAT Account,account.data_account_type_payable,True,default_chart_template
+220030,220030,P.A.Y.E. & UIF,account.data_account_type_payable,True,default_chart_template
+220040,220040,Net Wages,account.data_account_type_payable,True,default_chart_template
+220050,220050,Pension Fund,account.data_account_type_payable,True,default_chart_template
+220060,220060,Corporation Tax,account.data_account_type_payable,True,default_chart_template
+300010,300010,Called up share capital,account.data_account_type_equity,False,default_chart_template
+300020,300020,Share premium account,account.data_account_type_equity,False,default_chart_template
+300030,300030,Revaluation reserve,account.data_account_type_equity,False,default_chart_template
+300040,300040,Other reserves,account.data_account_type_equity,False,default_chart_template
+300050,300050,Capital,account.data_account_type_equity,False,default_chart_template
+300060,300060,Dividends,account.data_account_type_equity,False,default_chart_template
+300070,300070,Drawings,account.data_account_type_equity,False,default_chart_template
+400010,400010,Undistributed Profits/Losses,account.data_unaffected_earnings,False,default_chart_template
+500010,500010,Sales category 1,account.data_account_type_revenue,False,default_chart_template
+500020,500020,Sales category 2,account.data_account_type_revenue,False,default_chart_template
+500030,500030,Sales category 3,account.data_account_type_revenue,False,default_chart_template
+500040,500040,Sales category 4,account.data_account_type_revenue,False,default_chart_template
+500050,500050,Bank Interest received,account.data_account_type_revenue,False,default_chart_template
+500060,500060,Investment Interest received,account.data_account_type_revenue,False,default_chart_template
+500070,500070,Profits/Losses on disposals of assets,account.data_account_type_revenue,False,default_chart_template
+500080,500080,Rental Income,account.data_account_type_revenue,False,default_chart_template
+510010,510010,Other Income,account.data_account_type_other_income,False,default_chart_template
+600010,600010,Cost of sales 1,account.data_account_type_direct_costs,False,default_chart_template
+600020,600020,Cost of sales 2,account.data_account_type_direct_costs,False,default_chart_template
+600030,600030,Cost of sales 3,account.data_account_type_direct_costs,False,default_chart_template
+600040,600040,Cost of sales 4,account.data_account_type_direct_costs,False,default_chart_template
+610010,610010,Marketing,account.data_account_type_expenses,False,default_chart_template
+610020,610020,Exhibitions and events,account.data_account_type_expenses,False,default_chart_template
+610030,610030,PR,account.data_account_type_expenses,False,default_chart_template
+610040,610040,Distribution vehicles,account.data_account_type_expenses,False,default_chart_template
+610050,610050,Distribution salaries and wages,account.data_account_type_expenses,False,default_chart_template
+610060,610060,Shipping,account.data_account_type_expenses,False,default_chart_template
+610070,610070,Directors pension,account.data_account_type_expenses,False,default_chart_template
+610080,610080,Directors remuneration,account.data_account_type_expenses,False,default_chart_template
+610090,610090,Gross Salaries,account.data_account_type_expenses,False,default_chart_template
+610100,610100,Employers SDL & UIF,account.data_account_type_expenses,False,default_chart_template
+610110,610110,Subcontractors payments,account.data_account_type_expenses,False,default_chart_template
+610120,610120,Rent and rates,account.data_account_type_expenses,False,default_chart_template
+610130,610130,Light / heat and power,account.data_account_type_expenses,False,default_chart_template
+610140,610140,Repairs and maintenance,account.data_account_type_expenses,False,default_chart_template
+610150,610150,Car hire,account.data_account_type_expenses,False,default_chart_template
+610160,610160,Car fuel,account.data_account_type_expenses,False,default_chart_template
+610170,610170,Car maintenance,account.data_account_type_expenses,False,default_chart_template
+610180,610180,Telephone,account.data_account_type_expenses,False,default_chart_template
+610190,610190,Internet & hosting,account.data_account_type_expenses,False,default_chart_template
+610200,610200,Mobiles,account.data_account_type_expenses,False,default_chart_template
+610210,610210,Stationery,account.data_account_type_expenses,False,default_chart_template
+610220,610220,Office consumables,account.data_account_type_expenses,False,default_chart_template
+610230,610230,Postage and Carriage,account.data_account_type_expenses,False,default_chart_template
+610240,610240,Books,account.data_account_type_expenses,False,default_chart_template
+610250,610250,Network costs,account.data_account_type_expenses,False,default_chart_template
+610260,610260,Software expenses,account.data_account_type_expenses,False,default_chart_template
+610270,610270,Other computer costs,account.data_account_type_expenses,False,default_chart_template
+610280,610280,Recruitment fees,account.data_account_type_expenses,False,default_chart_template
+610290,610290,Other admin expenses,account.data_account_type_expenses,False,default_chart_template
+610300,610300,Accounting,account.data_account_type_expenses,False,default_chart_template
+610310,610310,Auditing,account.data_account_type_expenses,False,default_chart_template
+610320,610320,Consultancy,account.data_account_type_expenses,False,default_chart_template
+610330,610330,Legal and professional charges,account.data_account_type_expenses,False,default_chart_template
+610340,610340,Exchange gains/losses,account.data_account_type_expenses,False,default_chart_template
+610350,610350,Other sundry expenses,account.data_account_type_expenses,False,default_chart_template
+610360,610360,Bad debts,account.data_account_type_expenses,False,default_chart_template
+610370,610370,Interest paid,account.data_account_type_expenses,False,default_chart_template
+610380,610380,Bank Charges,account.data_account_type_expenses,False,default_chart_template
+610390,610390,Donations,account.data_account_type_expenses,False,default_chart_template
+610400,610400,Entertaining,account.data_account_type_expenses,False,default_chart_template
+610410,610410,Insurance,account.data_account_type_expenses,False,default_chart_template
+610420,610420,Travel and subsistence,account.data_account_type_expenses,False,default_chart_template
+610430,610430,Corporation tax expense,account.data_account_type_expenses,False,default_chart_template
+610440,610440,Foreign Exchange Gains/Losses,account.data_account_type_expenses,False,default_chart_template
+610450,610450,Price Differences Control Account,account.data_account_type_expenses,False,default_chart_template
+610460,610460,Cash Register Gains/Losses,account.data_account_type_expenses,False,default_chart_template
+620010,620010,Software Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620020,620020,Patents & Trademarks Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620030,620030,Fixtures and fittings Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620040,620040,Land and buildings Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620050,620050,Motor vehicles Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620060,620060,Office equipment (inc computer equipment) Depreciation,account.data_account_type_depreciation,False,default_chart_template
+620070,620070,Plant and machinery Depreciation,account.data_account_type_depreciation,False,default_chart_template
\ No newline at end of file
diff --git a/addons/lunch/controllers/main.py b/addons/lunch/controllers/main.py
index ac645c2edc4..2097f86659a 100644
--- a/addons/lunch/controllers/main.py
+++ b/addons/lunch/controllers/main.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import http, fields
+from odoo import _, http, fields
+from odoo.exceptions import AccessError
from odoo.http import request
from odoo.osv import expression
from odoo.tools import float_round, float_repr
@@ -10,6 +11,7 @@ from odoo.tools import float_round, float_repr
class LunchController(http.Controller):
@http.route('/lunch/infos', type='json', auth='user')
def infos(self, user_id=None):
+ self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
infos = self._make_infos(user, order=False)
@@ -35,6 +37,7 @@ class LunchController(http.Controller):
@http.route('/lunch/trash', type='json', auth='user')
def trash(self, user_id=None):
+ self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user.id)
@@ -43,6 +46,7 @@ class LunchController(http.Controller):
@http.route('/lunch/pay', type='json', auth='user')
def pay(self, user_id=None):
+ self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user.id)
@@ -60,13 +64,15 @@ class LunchController(http.Controller):
@http.route('/lunch/user_location_set', type='json', auth='user')
def set_user_location(self, location_id, user_id=None):
+ self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
- user.last_lunch_location_id = request.env['lunch.location'].browse(location_id)
+ user.sudo().last_lunch_location_id = request.env['lunch.location'].browse(location_id)
return True
@http.route('/lunch/user_location_get', type='json', auth='user')
def get_user_location(self, user_id=None):
+ self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
return user.last_lunch_location_id.id
@@ -87,7 +93,9 @@ class LunchController(http.Controller):
'currency': {'symbol': currency.symbol, 'position': currency.position},
})
- user_location = user.last_lunch_location_id if user.last_lunch_location_id else request.env['lunch.location'].search([], limit=1)
+ if not user.last_lunch_location_id:
+ user.last_lunch_location_id = request.env['lunch.location'].search([], limit=1)
+ user_location = user.last_lunch_location_id
alert_domain = expression.AND([
[('available_today', '=', True)],
@@ -101,6 +109,10 @@ class LunchController(http.Controller):
return res
+ def _check_user_impersonification(self, user_id=None):
+ if (user_id and request.env.uid != user_id and not request.env.user.has_group('lunch.group_lunch_manager')):
+ raise AccessError(_('You are trying to impersonate another user, but this can only be done by a lunch manager'))
+
def _get_current_lines(self, user_id):
return request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today()), ('state', '!=', 'cancelled')])
diff --git a/addons/lunch/models/lunch_order.py b/addons/lunch/models/lunch_order.py
index ee3f01c6d44..67d5dc182e9 100644
--- a/addons/lunch/models/lunch_order.py
+++ b/addons/lunch/models/lunch_order.py
@@ -42,6 +42,10 @@ class LunchOrder(models.Model):
display_toppings = fields.Text('Toppings', compute='_compute_display_toppings', store=True)
+ def init(self):
+ self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
+ % self._table)
+
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
def _compute_total_price(self):
for line in self:
@@ -69,8 +73,8 @@ class LunchOrder(models.Model):
'To add some money to your wallet, please contact your lunch manager.'))
def action_order(self):
- self._check_wallet()
self.write({'state': 'ordered'})
+ self._check_wallet()
def action_confirm(self):
self.write({'state': 'confirmed'})
diff --git a/addons/lunch/report/lunch_product_report.py b/addons/lunch/report/lunch_product_report.py
index 7c9fcfcb000..2615cb827ee 100644
--- a/addons/lunch/report/lunch_product_report.py
+++ b/addons/lunch/report/lunch_product_report.py
@@ -81,13 +81,12 @@ class LunchProductReport(models.Model):
users.id AS user_id,
fav.user_id IS NOT NULL AS is_favorite,
product.new_until >= current_date AS is_new,
- max(l_order.date) AS last_order_date
+ orders.last_order_date
FROM lunch_product product
- RIGHT JOIN res_users users ON users.company_id = product.company_id -- multi company
- LEFT JOIN lunch_product_favorite_user_rel fav ON fav.product_id = product.id AND fav.user_id = users.id
- JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
- JOIN lunch_order l_order ON l_order.product_id = product.id AND l_order.user_id = users.id AND l_order.date <= current_date
+ INNER JOIN res_users users ON users.company_id = product.company_id -- multi company
+ INNER JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
+ LEFT JOIN LATERAL (select max(date) AS last_order_date FROM lunch_order where user_id=users.id and product_id=product.id) AS orders ON TRUE
+ LEFT JOIN LATERAL (select user_id FROM lunch_product_favorite_user_rel where user_id=users.id and product_id=product.id) AS fav ON TRUE
WHERE users.active AND product.active AND groups.gid = %%s --only take into account active products and users
- GROUP BY product.id, product.name, product.category_id, product.description, product.price, product.supplier_id, product.company_id, product.active, users.id, is_favorite, is_new
);
""" % self._table, (self.env.ref('base.group_user').id,))
diff --git a/addons/lunch/static/src/js/lunch_kanban_controller.js b/addons/lunch/static/src/js/lunch_kanban_controller.js
index 80a39adae96..f1acf81b301 100644
--- a/addons/lunch/static/src/js/lunch_kanban_controller.js
+++ b/addons/lunch/static/src/js/lunch_kanban_controller.js
@@ -77,13 +77,14 @@ var LunchKanbanController = KanbanController.extend({
*/
_renderLunchKanbanWidget: function () {
var self = this;
- if (this.widget) {
- this.widget.destroy();
- }
+ var oldWidget = this.widget;
this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2);
this.widget = new LunchKanbanWidget(this, _.extend(this.widgetData, {edit: this.editMode}));
return this.widget.appendTo(document.createDocumentFragment()).then(function () {
self.$('.o_lunch_kanban').prepend(self.widget.$el);
+ if (oldWidget) {
+ oldWidget.destroy();
+ }
});
},
_showPaymentDialog: function (title) {
diff --git a/addons/lunch/static/tests/lunch_kanban_tests.js b/addons/lunch/static/tests/lunch_kanban_tests.js
index 32cac593dcc..8863499f120 100644
--- a/addons/lunch/static/tests/lunch_kanban_tests.js
+++ b/addons/lunch/static/tests/lunch_kanban_tests.js
@@ -128,6 +128,52 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
+ QUnit.test('no flickering at reload', async function (assert) {
+ assert.expect(2);
+
+ const self = this;
+ let infosProm = testUtils.makeTestPromise();
+ const kanban = await createLunchKanbanView({
+ View: LunchKanbanView,
+ model: 'product',
+ data: this.data,
+ arch: `
+
+
+
+
+
+
+
+ `,
+ mockRPC: function (route, args) {
+ if (route === '/lunch/user_location_get') {
+ return Promise.resolve(self.data['lunch.location'].records[0].id);
+ }
+ if (route === '/lunch/infos') {
+ return Promise.resolve(self.regularInfos);
+ }
+ var result = this._super.apply(this, arguments);
+ if (args.method === 'xmlid_to_res_id') {
+ // delay the rendering of the lunch widget
+ return infosProm.then(_.constant(result));
+ }
+ return result;
+ },
+ });
+
+ kanban.reload();
+
+ assert.strictEqual(kanban.$('.o_lunch_widget').length, 1,
+ "old widget should still be present");
+
+ await infosProm.resolve();
+
+ assert.strictEqual(kanban.$('.o_lunch_widget').length, 1);
+
+ kanban.destroy();
+ });
+
QUnit.module('LunchKanbanWidget', function () {
QUnit.test('empty cart', async function (assert) {
@@ -814,7 +860,6 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
-
});
});
diff --git a/addons/lunch/views/res_config_settings.xml b/addons/lunch/views/res_config_settings.xml
index bd99a708b4c..fc73ba99151 100644
--- a/addons/lunch/views/res_config_settings.xml
+++ b/addons/lunch/views/res_config_settings.xml
@@ -14,6 +14,7 @@
title="None">
Lunch Overdraft
+
Maximum overdraft that your employees can reach
diff --git a/addons/lunch/wizard/lunch_line_wizard.py b/addons/lunch/wizard/lunch_line_wizard.py
index 2006144e582..d05cfd06de1 100644
--- a/addons/lunch/wizard/lunch_line_wizard.py
+++ b/addons/lunch/wizard/lunch_line_wizard.py
@@ -86,7 +86,7 @@ class LunchOrderWizard(models.TransientModel):
sum((wizard.topping_ids_1 | wizard.topping_ids_2 | wizard.topping_ids_3).mapped('price')))
def _get_matching_lines(self):
- domain = [('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())]
+ domain = [('user_id', '=', self.user_id.id), ('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())]
lines = self.env['lunch.order'].search(domain)
return lines.filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3) == self.topping_ids_1)
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index f5bfd7c11cf..bf0b0141604 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -147,7 +147,7 @@ class MailActivity(models.Model):
('today', 'Today'),
('planned', 'Planned')], 'State',
compute='_compute_state')
- recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True)
+ recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
has_recommended_activities = fields.Boolean(
'Next activities available',
@@ -298,7 +298,7 @@ class MailActivity(models.Model):
target_record = self.env[activity.res_model].browse(activity.res_id)
if hasattr(target_record, 'company_id') and (
target_record.company_id != target_user.company_id and (
- len(target_user.company_ids) > 1)):
+ len(target_user.sudo().company_ids) > 1)):
return # in that case we skip the check, assuming it would fail because of the company
model.browse(activity.res_id).check_access_rule('read')
except exceptions.AccessError:
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index e2ebb16c69e..39510206641 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -4,6 +4,7 @@
import logging
import re
+from binascii import Error as binascii_error
from operator import itemgetter
from email.utils import formataddr
from openerp.http import request
@@ -933,7 +934,7 @@ class Message(models.Model):
def _invalidate_documents(self):
""" Invalidate the cache of the documents followed by ``self``. """
for record in self:
- if record.model and record.res_id and 'message_ids' in self.env[record.model]:
+ if record.model and record.res_id and issubclass(self.pool[record.model], self.pool['mail.thread']):
self.env[record.model].invalidate_cache(fnames=[
'message_ids',
'message_unread',
@@ -968,16 +969,21 @@ class Message(models.Model):
key = match.group(2)
if not data_to_url.get(key):
name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
- attachment = Attachments.create({
- 'name': name,
- 'datas': match.group(2),
- 'datas_fname': name,
- 'res_model': values.get('model'),
- 'res_id': values.get('res_id'),
- })
- attachment.generate_access_token()
- values['attachment_ids'].append((4, attachment.id))
- data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
+ try:
+ attachment = Attachments.create({
+ 'name': name,
+ 'datas': match.group(2),
+ 'datas_fname': name,
+ 'res_model': values.get('model'),
+ 'res_id': values.get('res_id'),
+ })
+ except binascii_error:
+ _logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.")
+ return match.group(3) # group(3) is the url ending single/double quote matched by the regexp
+ else:
+ attachment.generate_access_token()
+ values['attachment_ids'].append((4, attachment.id))
+ data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index 296cf092efb..a11be5335a4 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -139,7 +139,7 @@ var Chatter = Widget.extend({
if (this.fields.activity) {
this.fields.activity.$el.detach();
}
- if (this.fields.thread) {
+ if (this.fields.thread && this.fields.thread.$el ) {
this.fields.thread.$el.detach();
}
@@ -404,7 +404,7 @@ var Chatter = Widget.extend({
self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
}
}
- if (self.fields.thread) {
+ if (self.fields.thread && self.fields.thread.$el) {
self.fields.thread.$el.appendTo(self.$el);
}
}).then(always).guardedCatch(always);
diff --git a/addons/mail/static/src/js/models/messages/message.js b/addons/mail/static/src/js/models/messages/message.js
index 77e4120da29..a118807ac47 100644
--- a/addons/mail/static/src/js/models/messages/message.js
+++ b/addons/mail/static/src/js/models/messages/message.js
@@ -578,7 +578,8 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
* @return {boolean}
*/
_isOdoobotAuthor: function () {
- return this._serverAuthorID === this.call('mail_service', 'getOdoobotID');
+ return this._serverAuthorID &&
+ this._serverAuthorID[0] === this.call('mail_service', 'getOdoobotID')[0];
},
/**
* State whether the message is transient or not
diff --git a/addons/mail/static/src/js/services/mail_manager.js b/addons/mail/static/src/js/services/mail_manager.js
index 27a7183ec88..dedeacca013 100644
--- a/addons/mail/static/src/js/services/mail_manager.js
+++ b/addons/mail/static/src/js/services/mail_manager.js
@@ -46,7 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers
var MailManager = AbstractService.extend({
dependencies: ['ajax', 'bus_service', 'local_storage'],
- _ODOOBOT_ID: "ODOOBOT", // default authorID for transient messages
+ _ODOOBOT_ID: ["ODOOBOT", "ODOOBOT"], // authorID for transient messages
/**
* @override
@@ -258,7 +258,7 @@ var MailManager = AbstractService.extend({
/**
* Get the OdooBot ID, which is the default authorID for transient messages
*
- * @returns {string}
+ * @returns {Array}
*/
getOdoobotID: function () {
return this._ODOOBOT_ID;
diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js
index 75754f375d8..caf0c3c4e12 100644
--- a/addons/mail/static/src/js/services/mail_notification_manager.js
+++ b/addons/mail/static/src/js/services/mail_notification_manager.js
@@ -471,12 +471,11 @@ MailManager.include({
*
* @private
* @param {Object} data
- * @param {string} data.author_id
*/
_handlePartnerTransientMessageNotification: function (data) {
var lastMessage = _.last(this._messages);
data.id = (lastMessage ? lastMessage.getID() : 0) + 0.01;
- data.author_id = data.author_id || this.getOdoobotID();
+ data.author_id = this.getOdoobotID();
this.addMessage(data);
},
/**
diff --git a/addons/mail/static/src/js/services/mail_status_manager.js b/addons/mail/static/src/js/services/mail_status_manager.js
index 4a2df2af94f..7891461c9af 100644
--- a/addons/mail/static/src/js/services/mail_status_manager.js
+++ b/addons/mail/static/src/js/services/mail_status_manager.js
@@ -30,6 +30,9 @@ MailManager.include({
getImStatus: function (data) {
var self = this;
var partnerID = data.partnerID;
+ if (partnerID === this.getOdoobotID()[0]) {
+ return 'bot';
+ }
if (!this._imStatus[partnerID]) {
// Add to list to call it in next bus update or _fetchMissingImStatus
this._imStatus[partnerID] = undefined;
@@ -84,9 +87,10 @@ MailManager.include({
return Promise.resolve();
}
return this._rpc({
- model: 'res.partner',
- method: 'read',
- args: [partnerIDs, ['id', 'im_status']],
+ route: '/longpolling/im_status',
+ params: { partner_ids: partnerIDs },
+ }, {
+ shadow: true,
}).then( function (results) {
self.updateImStatus(results);
});
diff --git a/addons/mail/static/tests/mail_status_manager_tests.js b/addons/mail/static/tests/mail_status_manager_tests.js
index be769e8d0b0..f21cefdec9e 100644
--- a/addons/mail/static/tests/mail_status_manager_tests.js
+++ b/addons/mail/static/tests/mail_status_manager_tests.js
@@ -39,7 +39,7 @@ QUnit.test('simple set im_status', function (assert) {
});
QUnit.test('multi get_im_status', async function (assert) {
- assert.expect(9);
+ assert.expect(8);
var readCount = 0;
var parent = testUtils.createParent({
//data: this.data,
@@ -48,9 +48,8 @@ QUnit.test('multi get_im_status', async function (assert) {
if (route === '/mail/init_messaging') {
return this._super.apply(this, arguments);
}
- if (args.method === 'read' && args.model === 'res.partner') {
- assert.deepEqual(args.args[0], [2,3]);
- assert.deepEqual(args.args[1], ['id', 'im_status']);
+ if (route === '/longpolling/im_status') {
+ assert.deepEqual(args.partner_ids, [2,3]);
readCount++;
return Promise.resolve([
{id: 2, im_status: 'away'},
@@ -85,7 +84,7 @@ QUnit.test('multi get_im_status', async function (assert) {
});
QUnit.test('update loop', async function (assert) {
- assert.expect(12);
+ assert.expect(10);
var readCount = 0;
var parent = testUtils.createParent({
services: this.services,
@@ -93,9 +92,8 @@ QUnit.test('update loop', async function (assert) {
if (route === '/mail/init_messaging') {
return this._super.apply(this, arguments);
}
- if (args.method === 'read' && args.model === 'res.partner') {
- assert.deepEqual(args.args[0], [1, 2]);
- assert.deepEqual(args.args[1], ['id', 'im_status']);
+ if (route === '/longpolling/im_status') {
+ assert.deepEqual(args.partner_ids, [1, 2]);
readCount++;
return Promise.resolve([
{"id": 1, "im_status": "online"},
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index 46ec87060cf..95ae6372c41 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -387,6 +387,7 @@ class MailComposer(models.TransientModel):
values = self.generate_email_for_composer(template_id, [res_id])[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
+ attachment_ids = []
Attachment = self.env['ir.attachment']
for attach_fname, attach_datas in values.pop('attachments', []):
data_attach = {
@@ -397,7 +398,9 @@ class MailComposer(models.TransientModel):
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
- values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id)
+ attachment_ids.append(Attachment.create(data_attach).id)
+ if values.get('attachment_ids', []) or attachment_ids:
+ values['attachment_ids'] = [(5,)] + values.get('attachment_ids', []) + attachment_ids
else:
default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'])
values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values)
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 2c48732e933..a00538f04a2 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -457,7 +457,7 @@ class MassMailing(models.Model):
return res
active = fields.Boolean(default=True)
- subject = fields.Char('Subject', help='Subject of emails to send', required=True)
+ subject = fields.Char('Subject', help='Subject of emails to send', required=True, translate=True)
email_from = fields.Char(string='From', required=True,
default=lambda self: self.env['mail.message']._get_default_from())
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py
index 8389ce269ef..7f2459d4a07 100644
--- a/addons/mass_mailing/wizard/test_mailing.py
+++ b/addons/mass_mailing/wizard/test_mailing.py
@@ -27,7 +27,7 @@ class TestMassMailing(models.TransientModel):
'email_from': mailing.email_from,
'reply_to': mailing.reply_to,
'email_to': test_mail,
- 'subject': mailing.name,
+ 'subject': mailing.subject,
'body_html': mass_mail_layout.render({'body': body}, engine='ir.qweb', minimal_qcontext=True),
'notification': True,
'mailing_id': mailing.id,
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 12002b3b25a..858b6705d1c 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -487,6 +487,8 @@ class MrpProduction(models.Model):
}
def _generate_finished_moves(self):
+ if self.product_id in self.bom_id.byproduct_ids.mapped('product_id'):
+ raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name)
moves_values = [self._get_finished_move_value(self.product_id.id, self.product_qty, self.product_uom_id.id)]
for byproduct in self.bom_id.byproduct_ids:
product_uom_factor = self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id)
diff --git a/addons/mrp/models/stock_warehouse.py b/addons/mrp/models/stock_warehouse.py
index 1cc46635b73..0e26c2bb3ec 100644
--- a/addons/mrp/models/stock_warehouse.py
+++ b/addons/mrp/models/stock_warehouse.py
@@ -249,7 +249,7 @@ class StockWarehouse(models.Model):
@api.multi
def _get_all_routes(self):
- routes = super(StockWarehouse, self).get_all_routes_for_wh()
+ routes = super(StockWarehouse, self)._get_all_routes()
routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id')
return routes
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index 55a4d08e4b9..8b4585b9393 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
+import psycopg2
from odoo import http, _
from odoo.http import request
@@ -40,7 +41,7 @@ class PaymentProcessing(http.Controller):
# I prefer to let the controller chose when to access to payment.transaction using sudo
return request.session.get("__payment_tx_ids__", [])
- @http.route(['/payment/process'], type="http", auth="public", website=True)
+ @http.route(['/payment/process'], type="http", auth="public", website=True, sitemap=False)
def payment_status_page(self, **kwargs):
# When the customer is redirect to this website page,
# we retrieve the payment transaction list from his session
@@ -93,11 +94,15 @@ class PaymentProcessing(http.Controller):
tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False)
try:
tx_to_process._post_process_after_done()
+ except psycopg2.OperationalError as e:
+ request.env.cr.rollback()
+ result['success'] = False
+ result['error'] = "tx_process_retry"
except Exception as e:
request.env.cr.rollback()
result['success'] = False
result['error'] = str(e)
- _logger.error("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
+ _logger.exception("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
return result
@@ -122,7 +127,7 @@ class WebsitePayment(http.Controller):
}
return request.render("payment.pay_methods", values)
- @http.route(['/website_payment/pay'], type='http', auth='public', website=True)
+ @http.route(['/website_payment/pay'], type='http', auth='public', website=True, sitemap=False)
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
env = request.env
user = env.user.sudo()
@@ -262,7 +267,7 @@ class WebsitePayment(http.Controller):
except Exception as e:
return request.redirect('/payment/process')
- @http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
+ @http.route(['/website_payment/confirm'], type='http', auth='public', website=True, sitemap=False)
def confirm(self, **kw):
tx_id = int(kw.get('tx_id', 0))
if tx_id:
diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py
index 0f2ce5d222a..6847cb76461 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -899,7 +899,7 @@ class PaymentTransaction(models.Model):
# custom create
custom_method_name = '%s_create' % acquirer.provider
- if hasattr(acquirer, custom_method_name):
+ if hasattr(self, custom_method_name):
values.update(getattr(self, custom_method_name)(values))
if not values.get('reference'):
diff --git a/addons/payment/static/src/js/payment_processing.js b/addons/payment/static/src/js/payment_processing.js
index 0fcfbc7ce81..73af1a344bb 100644
--- a/addons/payment/static/src/js/payment_processing.js
+++ b/addons/payment/static/src/js/payment_processing.js
@@ -54,6 +54,8 @@ odoo.define('payment.processing', function (require) {
}
else {
switch(data.error) {
+ case "tx_process_retry":
+ break;
case "no_tx_found":
self.displayContent("payment.no_tx_found", {});
break;
@@ -63,18 +65,12 @@ odoo.define('payment.processing', function (require) {
}
}
self.startPolling();
- }).then(function() {
- if ($.blockUI) {
- $.unblockUI();
- }
+
}).guardedCatch(function() {
- if ($.blockUI) {
- $.unblockUI();
- }
self.displayContent("payment.rpc_error", {});
self.startPolling();
});
- },
+ },g
processPolledData: function (transactions) {
var render_values = {
'tx_draft': [],
@@ -120,6 +116,7 @@ odoo.define('payment.processing', function (require) {
},
displayContent: function (xmlid, render_values) {
var html = Qweb.render(xmlid, render_values);
+ $.unblockUI();
this.$el.find('.o_payment_processing_content').html(html);
},
displayLoading: function () {
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
index 79d0a7589c5..922d9df1bf1 100644
--- a/addons/payment_authorize/models/authorize_request.py
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -226,6 +226,12 @@ class AuthorizeAPI():
etree.SubElement(customer, "email").text = partner.email or ''
response = self._authorize_request(root)
res = dict()
+ if not response.find('customerProfileId'):
+ _logger.warning(
+ 'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s'
+ % (transaction_id, partner)
+ )
+ return res
res['profile_id'] = response.find('customerProfileId').text
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index 5a6ed0bf8e1..946f031484d 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -228,17 +228,18 @@ class TxAuthorize(models.Model):
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
transaction = AuthorizeAPI(self.acquirer_id)
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
- token_id = self.env['payment.token'].create({
- 'authorize_profile': res.get('profile_id'),
- 'name': res.get('name'),
- 'acquirer_ref': res.get('payment_profile_id'),
- 'acquirer_id': self.acquirer_id.id,
- 'partner_id': self.partner_id.id,
- })
- self.payment_token_id = token_id
+ if res:
+ token_id = self.env['payment.token'].create({
+ 'authorize_profile': res.get('profile_id'),
+ 'name': res.get('name'),
+ 'acquirer_ref': res.get('payment_profile_id'),
+ 'acquirer_id': self.acquirer_id.id,
+ 'partner_id': self.partner_id.id,
+ })
+ self.payment_token_id = token_id
- if self.payment_token_id:
- self.payment_token_id.verified = True
+ if self.payment_token_id:
+ self.payment_token_id.verified = True
return True
elif status_code == self._authorize_pending_tx_status:
self.write({'acquirer_reference': data.get('x_trans_id')})
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index bf13410b137..8ee35a1f0ab 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -891,7 +891,7 @@ class PosOrder(models.Model):
# a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line
qty = 1.0
if stock_production_lot.product_id.tracking == 'lot':
- qty = pos_pack_lot.pos_order_line_id.qty
+ qty = abs(pos_pack_lot.pos_order_line_id.qty)
qty_done += qty
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
else:
diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js
index 0184b07a984..0a269721abd 100644
--- a/addons/point_of_sale/static/src/js/gui.js
+++ b/addons/point_of_sale/static/src/js/gui.js
@@ -405,7 +405,7 @@ var Gui = core.Class.extend({
}
} else if (input === '-') {
if (options.firstinput) {
- newbuf = '-';
+ newbuf = '-0';
} else if ( newbuf[0] === '-' ) {
newbuf = newbuf.substring(1,newbuf.length);
} else {
@@ -420,6 +420,9 @@ var Gui = core.Class.extend({
newbuf += input;
}
}
+ if (newbuf === "-") {
+ newbuf = "";
+ }
// End of input buffer at 12 characters.
if (newbuf.length > buffer.length && newbuf.length > 12) {
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 10025258bde..2a1e1490633 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1450,7 +1450,7 @@ exports.Orderline = Backbone.Model.extend({
var lots_required = 1;
if (this.product.tracking == 'serial') {
- lots_required = this.quantity;
+ lots_required = Math.abs(this.quantity);
}
return lots_required;
@@ -1998,8 +1998,11 @@ var PacklotlineCollection = Backbone.Collection.extend({
set_quantity_by_lot: function() {
if (this.order_line.product.tracking == 'serial') {
- var valid_lots = this.get_valid_lots();
- this.order_line.set_quantity(valid_lots.length);
+ var valid_lots_quantity = this.get_valid_lots().length;
+ if (this.order_line.quantity < 0){
+ valid_lots_quantity = -valid_lots_quantity;
+ }
+ this.order_line.set_quantity(valid_lots_quantity);
}
}
});
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index 26d315631a8..17c47b154ad 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -990,7 +990,7 @@
-
+
diff --git a/addons/point_of_sale/views/point_of_sale_dashboard.xml b/addons/point_of_sale/views/point_of_sale_dashboard.xml
index c45bc2e19cf..b630f2e4e12 100644
--- a/addons/point_of_sale/views/point_of_sale_dashboard.xml
+++ b/addons/point_of_sale/views/point_of_sale_dashboard.xml
@@ -47,7 +47,6 @@
-
diff --git a/addons/portal/controllers/mail.py b/addons/portal/controllers/mail.py
index 8d1378218a1..fbc1a1a96cc 100644
--- a/addons/portal/controllers/mail.py
+++ b/addons/portal/controllers/mail.py
@@ -137,7 +137,7 @@ class PortalChatter(http.Controller):
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
- domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False), ('website_published', '=', True)], domain])
+ domain = expression.AND([Message._non_employee_message_domain(), domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py
index 4a5d1f804d9..afe97f1b8ca 100644
--- a/addons/portal/models/mail_message.py
+++ b/addons/portal/models/mail_message.py
@@ -21,3 +21,7 @@ class MailMessage(models.Model):
message_tree = dict((m.id, m) for m in self.sudo())
self._message_read_dict_postprocess(message_values, message_tree)
return message_values
+
+ @api.model
+ def _non_employee_message_domain(self):
+ return ['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)]
diff --git a/addons/pos_sale/report/sale_report.py b/addons/pos_sale/report/sale_report.py
index 735aaf56169..a340d0b7c4d 100644
--- a/addons/pos_sale/report/sale_report.py
+++ b/addons/pos_sale/report/sale_report.py
@@ -14,11 +14,6 @@ class SaleReport(models.Model):
('invoiced', 'Invoiced')], string='Status', readonly=True)
def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
- with_clause += '''pol_tax AS (SELECT line_tax.pos_order_line_id AS pol_id,sum(tax.amount/100) AS amount
- FROM account_tax_pos_order_line_rel line_tax
- JOIN account_tax tax ON line_tax.account_tax_id = tax.id
- GROUP BY line_tax.pos_order_line_id)'''
-
res = super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
select_ = '''
@@ -29,10 +24,10 @@ class SaleReport(models.Model):
sum(l.qty * u.factor) AS qty_delivered,
CASE WHEN pos.state = 'invoiced' THEN sum(qty) ELSE 0 END AS qty_invoiced,
CASE WHEN pos.state != 'invoiced' THEN sum(qty) ELSE 0 END AS qty_to_invoice,
- (COALESCE(COALESCE((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ) * pol_tax.amount), (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ))), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total,
- (COALESCE((l.qty * u.factor * l.price_unit * ((1 - COALESCE(l.discount / 100, 0)) )), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal,
- (CASE WHEN pos.state != 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)))) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice,
- (CASE WHEN pos.state = 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced,
+ SUM(l.price_subtotal_incl) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total,
+ SUM(l.price_subtotal) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal,
+ (CASE WHEN pos.state != 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice,
+ (CASE WHEN pos.state = 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced,
count(*) AS nbr,
pos.name AS name,
pos.date_order AS date,
@@ -70,7 +65,6 @@ class SaleReport(models.Model):
pos_order_line l
join pos_order pos on (l.order_id=pos.id)
left join res_partner partner ON (pos.partner_id = partner.id OR pos.partner_id = NULL)
- left join pol_tax pol_tax on pol_tax.pol_id=l.id
left join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
LEFT JOIN uom_uom u ON (u.id=t.uom_id)
@@ -98,7 +92,6 @@ class SaleReport(models.Model):
partner.country_id,
partner.industry_id,
partner.commercial_partner_id,
- pol_tax.amount,
u.factor,
config.crm_team_id
'''
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 99a314662dd..0e3fe051a80 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -196,6 +196,7 @@ class ProductProduct(models.Model):
('active', '=', True),
]) <= 1
):
+ record.image_raw_original = False
record.product_tmpl_id.image_original = record.image_original
else:
record.image_raw_original = record.image_original
@@ -391,6 +392,16 @@ class ProductProduct(models.Model):
product._set_standard_price(vals.get('standard_price') or 0.0)
# `_get_variant_id_for_combination` depends on existing variants
self.clear_caches()
+ self.env['product.template'].invalidate_cache(
+ fnames=[
+ 'valid_archived_variant_ids',
+ 'valid_existing_variant_ids',
+ 'product_variant_ids',
+ 'product_variant_id',
+ 'product_variant_count'
+ ],
+ ids=products.mapped('product_tmpl_id').ids
+ )
return products
@api.multi
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 3b1ce35701e..8684c59a59b 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -135,8 +135,10 @@ class ProductTemplate(models.Model):
compute="_compute_valid_attributes", string='Valid Product Attribute Values Without No Variant Attributes', help="Technical compute")
valid_product_attribute_wnva_ids = fields.Many2many('product.attribute',
compute="_compute_valid_attributes", string='Valid Product Attributes Without No Variant Attributes', help="Technical compute")
+ # valid_archived_variant_ids deprecated
valid_archived_variant_ids = fields.Many2many('product.product',
- compute="_compute_valid_attributes", string='Valid Archived Variants', help="Technical compute")
+ compute="_compute_valid_archived_variant_ids", string='Valid Archived Variants', help="Technical compute")
+ # valid_existing_variant_ids deprecated
valid_existing_variant_ids = fields.Many2many('product.product',
compute="_compute_valid_existing_variant_ids", string='Valid Existing Variants', help="Technical compute")
@@ -606,8 +608,6 @@ class ProductTemplate(models.Model):
For what is considered an archived variant, see `_has_valid_attributes`.
"""
- archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)])
-
# prefetch
self.mapped('attribute_line_ids.value_ids.id')
self.mapped('attribute_line_ids.attribute_id.create_variant')
@@ -622,6 +622,12 @@ class ProductTemplate(models.Model):
record.valid_product_attribute_ids = record.valid_product_template_attribute_line_ids.mapped('attribute_id')
record.valid_product_attribute_wnva_ids = record.valid_product_template_attribute_line_wnva_ids.mapped('attribute_id')
+ @api.multi
+ def _compute_valid_archived_variant_ids(self):
+ """This compute is done outside of `_compute_valid_attributes` because
+ it is often not needed, and it can be very bad on performance."""
+ archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)])
+ for record in self:
valid_value_ids = record.valid_product_attribute_value_wnva_ids
valid_attribute_ids = record.valid_product_attribute_wnva_ids
@@ -699,14 +705,12 @@ class ProductTemplate(models.Model):
:return: dict of exclusions
- exclusions: from this product itself
+ - parent_combination: ids of the given parent_combination
- parent_exclusions: from the parent_combination
- - archived_combinations: variants that are archived
- - existing_combinations: variants that are existing (as opposed to
- deleted, because deleted need to be considered impossible if
- there are no dynamic attributes).
- - has_dynamic_attributes: whether there is a dynamic attribute
- - no_variant_product_template_attribute_value_ids: values that are
- no_variant
+ - archived_combinations: deprecated
+ - existing_combinations: deprecated
+ - has_dynamic_attributes: deprecated
+ - no_variant_product_template_attribute_value_ids: deprecated
- parent_product_name: the name of the parent product if any, used in the interface
to explain why some combinations are not available.
(e.g: Not available with Customizable Desk (Legs: Steel))
@@ -715,13 +719,15 @@ class ProductTemplate(models.Model):
(e.g: Not available with Color: Black)
"""
self.ensure_one()
+ parent_combination = parent_combination or self.env['product.template.attribute.value']
return {
'exclusions': self._complete_inverse_exclusions(self._get_own_attribute_exclusions()),
'parent_exclusions': self._get_parent_attribute_exclusions(parent_combination),
- 'archived_combinations': self._get_archived_combinations(),
+ 'parent_combination': parent_combination.ids,
+ 'archived_combinations': [],
'has_dynamic_attributes': self.has_dynamic_attributes(),
- 'existing_combinations': self._get_existing_combinations(),
- 'no_variant_product_template_attribute_value_ids': self._get_no_variant_product_template_attribute_values(),
+ 'existing_combinations': [],
+ 'no_variant_product_template_attribute_value_ids': [],
'parent_product_name': parent_name,
'mapped_attribute_names': self._get_mapped_attribute_names(parent_combination),
}
@@ -789,29 +795,21 @@ class ProductTemplate(models.Model):
@api.multi
def _get_archived_combinations(self):
- """Get archived combinations.
-
- Array, each element is an array with ids of an archived combination.
- """
+ """Deprecated"""
self.ensure_one()
return [archived_variant.product_template_attribute_value_ids.ids
for archived_variant in self.valid_archived_variant_ids]
@api.multi
def _get_existing_combinations(self):
- """Get existing combinations.
-
- Needed because when not using dynamic attributes, the combination is
- not ok if it doesn't exist (= if the variant has been deleted).
-
- Array, each element is an array with ids of an existing combination.
- """
+ """Deprecated"""
self.ensure_one()
return [variant.product_template_attribute_value_ids.ids
for variant in self.valid_existing_variant_ids]
@api.multi
def _get_no_variant_product_template_attribute_values(self):
+ """Deprecated"""
self.ensure_one()
product_template_attribute_values = self._get_valid_product_template_attribute_lines().mapped('product_template_value_ids')
return product_template_attribute_values.filtered(
@@ -876,9 +874,16 @@ class ProductTemplate(models.Model):
# combination has different values than the ones configured on the template
return False
- if not self.has_dynamic_attributes() and not self._get_variant_for_combination(combination):
- # the variant has been deleted
- return False
+ variant = self._get_variant_for_combination(combination)
+
+ if self.has_dynamic_attributes():
+ if variant and not variant.active:
+ # dynamic and the variant has been archived
+ return False
+ else:
+ if not variant or not variant.active:
+ # not dynamic, the variant has been archived or deleted
+ return False
exclusions = self._get_own_attribute_exclusions()
if exclusions:
@@ -898,11 +903,6 @@ class ProductTemplate(models.Model):
if exclusion in combination.ids:
return False
- filtered_combination = combination._without_no_variant_attributes()
- archived_combinations = self._get_archived_combinations()
- if archived_combinations and filtered_combination.ids in archived_combinations:
- return False
-
return True
@api.multi
@@ -936,7 +936,7 @@ class ProductTemplate(models.Model):
for pav in attribute_values:
domain = expression.AND([[('attribute_value_ids', 'in', pav.id)], domain])
- res = self.env['product.product'].with_context(active_test=False).search(domain)
+ res = self.env['product.product'].with_context(active_test=False).search(domain, order='active DESC')
# The domain above is checking for the `product.attribute.value`, but we
# need to make sure it's the same `product.template.attribute.value`.
diff --git a/addons/product/tests/test_product_attribute_value_config.py b/addons/product/tests/test_product_attribute_value_config.py
index e966adbdbdc..497f6fbeba3 100644
--- a/addons/product/tests/test_product_attribute_value_config.py
+++ b/addons/product/tests/test_product_attribute_value_config.py
@@ -312,6 +312,21 @@ class TestProductAttributeValueConfig(TestProductAttributeValueSetup):
variant.unlink()
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
+ # CASE: if multiple variants exist for the same combination and at least
+ # one of them is not archived, the combination is possible
+ values = self.ssd_256 + self.ram_8 + self.hdd_1
+ self.env['product.product'].create({
+ 'product_tmpl_id': self.computer.id,
+ 'attribute_value_ids': [(6, 0, values.ids)],
+ 'active': False,
+ })
+ self.env['product.product'].create({
+ 'product_tmpl_id': self.computer.id,
+ 'attribute_value_ids': [(6, 0, values.ids)],
+ 'active': True,
+ })
+ self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
+
def test_get_first_possible_combination(self):
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512)
diff --git a/addons/product/tests/test_variants.py b/addons/product/tests/test_variants.py
index 9381868b623..ed7f1f777c5 100644
--- a/addons/product/tests/test_variants.py
+++ b/addons/product/tests/test_variants.py
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import base64
+import io
+from PIL import Image
+
from . import common
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
@@ -461,3 +465,72 @@ class TestVariantsManyAttributes(common.TestAttributesCommon):
self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10)
self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100)
self.assertEqual(len(toto.product_variant_ids), 0)
+
+
+class TestVariantsImages(common.TestProductCommon):
+
+ def setUp(self):
+ res = super(TestVariantsImages, self).setUp()
+
+ self.colors = {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'}
+ self.images = {}
+
+ product_attribute = self.env['product.attribute'].create({'name': 'Color'})
+
+ self.template = self.env['product.template'].create({
+ 'name': 'template',
+ })
+
+ for color in self.colors:
+ attr = self.env['product.attribute.value'].create({
+ 'name': color,
+ 'attribute_id': product_attribute.id,
+ })
+
+ f = io.BytesIO()
+ Image.new('RGB', (800, 500), self.colors[color]).save(f, 'PNG')
+ f.seek(0)
+ self.images.update({color: base64.b64encode(f.read())})
+
+ self.env['product.product'].create({
+ 'image_raw_original': self.images[color],
+ 'attribute_value_ids': [(6, 0, [attr.id])],
+ 'product_tmpl_id': self.template.id,
+ })
+ # the first one has no image, no color
+ self.variants = self.template.product_variant_ids.sorted('id')
+
+ return res
+
+ def test_variant_images(self):
+ """Check that on variant, the image used is the image_raw_original if set,
+ and defaults to the template image otherwise.
+ """
+ f = io.BytesIO()
+ Image.new('RGB', (800, 500), '#000000').save(f, 'PNG')
+ f.seek(0)
+ image_black = base64.b64encode(f.read())
+
+ images = self.variants.mapped('image_original')
+ self.assertEqual(len(set(images)), 4)
+
+ variant_no_image = self.variants[0]
+ self.assertFalse(variant_no_image.image)
+ self.template.image = image_black
+
+ # the first has no image variant, all the others do
+ self.assertFalse(variant_no_image.image_raw_original)
+ self.assertTrue(all(images[1:]))
+
+ # template image is the same as this one, since it has no image variant
+ self.assertEqual(variant_no_image.image, self.template.image)
+ # having changed the template image should not have changed these
+ self.assertEqual(images[1:], self.variants.mapped('image')[1:])
+
+ def test_update_images_with_archived_variants(self):
+ """Update images after variants have been archived"""
+ self.variants[1:].write({'active': False})
+ self.variants[0].image = self.images['red']
+ self.assertEqual(self.template.image, self.images['red'])
+ self.assertEqual(self.variants[0].image_raw_original, False)
+ self.assertEqual(self.variants[0].image, self.images['red'])
diff --git a/addons/product_email_template/models/product.py b/addons/product_email_template/models/product.py
index 9ef844e8f46..e043717e6e8 100644
--- a/addons/product_email_template/models/product.py
+++ b/addons/product_email_template/models/product.py
@@ -11,4 +11,6 @@ class ProductTemplate(models.Model):
_inherit = "product.template"
email_template_id = fields.Many2one('mail.template', string='Product Email Template',
- help='Send a product-specific email once the invoice is paid.')
+ help='When validating an invoice, an email will be sent to the customer '
+ 'based on this template. The customer will receive an email for each '
+ 'product linked to an email template.')
diff --git a/addons/product_email_template/views/product_views.xml b/addons/product_email_template/views/product_views.xml
index 393ab6d3ec9..9afb300f64f 100644
--- a/addons/product_email_template/views/product_views.xml
+++ b/addons/product_email_template/views/product_views.xml
@@ -10,7 +10,7 @@
0
- 0:
+ if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
self.discount = discount
def _is_delivery(self):
diff --git a/addons/sale/report/report_all_channels_sales.py b/addons/sale/report/report_all_channels_sales.py
index 1638c087d05..473ab1f1362 100644
--- a/addons/sale/report/report_all_channels_sales.py
+++ b/addons/sale/report/report_all_channels_sales.py
@@ -52,7 +52,7 @@ class PosSaleReport(models.Model):
LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id)
LEFT JOIN uom_uom u on (u.id=sol.product_uom)
LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id)
- WHERE so.state != 'cancel'
+ WHERE so.state in ('sale','done')
"""
return so_str
diff --git a/addons/sale/static/src/js/variant_mixin.js b/addons/sale/static/src/js/variant_mixin.js
index 6ce51f2998d..f216fce656e 100644
--- a/addons/sale/static/src/js/variant_mixin.js
+++ b/addons/sale/static/src/js/variant_mixin.js
@@ -1,6 +1,7 @@
odoo.define('sale.VariantMixin', function (require) {
'use strict';
+var concurrency = require('web.concurrency');
var core = require('web.core');
var utils = require('web.utils');
var ajax = require('web.ajax');
@@ -8,9 +9,8 @@ var _t = core._t;
var VariantMixin = {
events: {
- 'click .css_attribute_color input': '_onChangeColorAttribute',
+ 'change .css_attribute_color input': '_onChangeColorAttribute',
'change .main_product:not(.in_cart) input.js_quantity': 'onChangeAddQuantity',
- 'click button.js_add_cart_json': 'onClickAddCartJSON',
'change [data-attribute_exclusions]': 'onChangeVariant'
},
@@ -36,6 +36,20 @@ var VariantMixin = {
* @param {$.Element} [params.$container] force the used container
*/
onChangeVariant: function (ev, params) {
+ var $parent = $(ev.target).closest('.js_product');
+ if (!$parent.data('uniqueId')) {
+ $parent.data('uniqueId', _.uniqueId());
+ }
+ this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev);
+ },
+ /**
+ * @see onChangeVariant
+ *
+ * @private
+ * @param {Event} ev
+ * @returns {Deferred}
+ */
+ _getCombinationInfo: function (ev) {
var self = this;
if ($(ev.target).hasClass('variant_custom_value')) {
@@ -57,16 +71,21 @@ var VariantMixin = {
var qty = $component.find('input[name="add_qty"]').val();
var $parent = $(ev.target).closest('.js_product');
+
var combination = this.getSelectedVariantValues($parent);
+ var parentCombination = $parent.find('ul[data-attribute_exclusions]').data('attribute_exclusions').parent_combination;
+
+ var productTemplateId = parseInt($parent.find('.product_template_id').val());
self._checkExclusions($parent, combination);
ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', {
- product_template_id: parseInt($parent.find('.product_template_id').val()),
- product_id: this._getProductId($parent),
- combination: combination,
- add_qty: parseInt(qty),
- pricelist_id: this.pricelistId || false,
+ 'product_template_id': productTemplateId,
+ 'product_id': this._getProductId($parent),
+ 'combination': combination,
+ 'add_qty': parseInt(qty),
+ 'pricelist_id': this.pricelistId || false,
+ 'parent_combination': parentCombination,
}).then(function (combinationData) {
self._onChangeCombination(ev, $parent, combinationData);
});
@@ -138,10 +157,13 @@ var VariantMixin = {
var $input = $link.closest('.input-group').find("input");
var min = parseFloat($input.data("min") || 0);
var max = parseFloat($input.data("max") || Infinity);
- var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10);
+ var previousQty = parseFloat($input.val() || 0, 10);
+ var quantity = ($link.has(".fa-minus").length ? -1 : 1) + previousQty;
var newQty = quantity > min ? (quantity < max ? quantity : max) : min;
- $input.val(newQty).trigger('change');
+ if (newQty !== previousQty) {
+ $input.val(newQty).trigger('change');
+ }
return false;
},
@@ -332,23 +354,6 @@ var VariantMixin = {
* @param {Array} combination the selected combination of product attribute values
*/
_checkExclusions: function ($parent, combination) {
-
- function areCombinationsEqual(c1, c2) {
- return c1.length === c2.length && _.every(c1, function (ptav) {
- return c2.indexOf(ptav) > -1;
- });
- }
-
- function isCombinationInList(c1, list) {
- return _.some(list, function (c2) {
- return areCombinationsEqual(c1, c2);
- });
- }
-
- function isPtavInCombination(ptav, combination) {
- return combination.indexOf(ptav) > -1;
- }
-
var self = this;
var combinationData = $parent
.find('ul[data-attribute_exclusions]')
@@ -360,17 +365,6 @@ var VariantMixin = {
.attr('title', '')
.data('excluded-by', '');
- var disable = false;
-
- // compatibility 12.0
- var filteredCombination = combination;
- if (combinationData.no_variant_product_template_attribute_value_ids !== undefined) {
- var no_variants = combinationData.no_variant_product_template_attribute_value_ids;
- filteredCombination = _.filter(combination, function (ptav) {
- return !isPtavInCombination(ptav, no_variants);
- });
- }
-
// exclusion rules: array of ptav
// for each of them, contains array with the other ptav they exclude
if (combinationData.exclusions) {
@@ -379,11 +373,6 @@ var VariantMixin = {
if (combinationData.exclusions.hasOwnProperty(current_ptav)) {
// for each exclusion of the current attribute:
_.each(combinationData.exclusions[current_ptav], function (excluded_ptav) {
- // disable if it excludes any other attribute already in the combination
- if (isPtavInCombination(excluded_ptav, combination)) {
- disable = true;
- }
-
// disable the excluded input (even when not already selected)
// to give a visual feedback before click
self._disableInput(
@@ -401,9 +390,6 @@ var VariantMixin = {
_.each(combinationData.parent_exclusions, function (exclusions, excluded_by){
// check that the selected combination is in the parent exclusions
_.each(exclusions, function (ptav) {
- if (isPtavInCombination(ptav, combination)) {
- disable = true;
- }
// disable the excluded input (even when not already selected)
// to give a visual feedback before click
@@ -416,29 +402,7 @@ var VariantMixin = {
);
});
});
-
- // archived variants
- if (isCombinationInList(filteredCombination, combinationData.archived_combinations)) {
- disable = true;
- }
-
- // if not using dynamic attributes, exclude variants that are deleted
- if (filteredCombination.length && // compatibility 12.0 list view of variants
- combinationData.has_dynamic_attributes === false &&
- combinationData.existing_combinations !== undefined &&
- !isCombinationInList(filteredCombination, combinationData.existing_combinations)
- ) {
- disable = true;
- }
-
- $parent.toggleClass('css_not_available', disable);
- $parent.find("#add_to_cart").toggleClass('disabled', disable);
- $parent
- .parents('.modal')
- .find('.o_sale_product_configurator_add, .o_sale_product_configurator_edit')
- .toggleClass('disabled', disable);
},
-
/**
* Extracted to a method to be extendable by other modules
*
@@ -447,7 +411,6 @@ var VariantMixin = {
_getProductId: function ($parent) {
return parseInt($parent.find('.product_id').val());
},
-
/**
* Will disable the input/option that refers to the passed attributeValueId.
* This is used for showing the user that some combinations are not available.
@@ -489,7 +452,6 @@ var VariantMixin = {
$target.data('excluded-by', JSON.stringify(excludedByData));
}
},
-
/**
* @see onChangeVariant
*
@@ -506,6 +468,10 @@ var VariantMixin = {
$price.text(self._priceToStr(combination.price));
$default_price.text(self._priceToStr(combination.list_price));
+ var isCombinationPossible = combination.is_combination_possible;
+ this._toggleDisable($parent, isCombinationPossible);
+
+
// compatibility_check to remove in master
// needed for fix in 12.0 in the case of git pull and no server restart
var compatibility_check = combination.list_price - combination.price >= 0.01;
@@ -545,7 +511,8 @@ var VariantMixin = {
combination.display_image,
combination.product_id,
combination.product_template_id,
- combination.carousel
+ combination.carousel,
+ isCombinationPossible
);
}
@@ -586,7 +553,46 @@ var VariantMixin = {
formatted[0] = utils.insert_thousand_seps(formatted[0]);
return formatted.join(l10n.decimal_point);
},
-
+ /**
+ * Returns a throttled `_getCombinationInfo` with a leading and a trailing
+ * call, which is memoized per `uniqueId`, and for which previous results
+ * are dropped.
+ *
+ * The uniqueId is needed because on the configurator modal there might be
+ * multiple elements triggering the rpc at the same time, and we need each
+ * individual product rpc to be executed, but only once per individual
+ * product.
+ *
+ * The leading execution is to keep good reactivity on the first call, for
+ * a better user experience. The trailing is because ultimately only the
+ * information about the last selected combination is useful. All
+ * intermediary rpc can be ignored and are therefore best not done at all.
+ *
+ * The DropMisordered is to make sure slower rpc are ignored if the result
+ * of a newer rpc has already been received.
+ *
+ * @private
+ * @param {string} uniqueId
+ * @returns {function}
+ */
+ _throttledGetCombinationInfo: _.memoize(function (uniqueId) {
+ var dropMisordered = new concurrency.DropMisordered();
+ var _getCombinationInfo = _.throttle(this._getCombinationInfo.bind(this), 500);
+ return function (ev) {
+ return dropMisordered.add(_getCombinationInfo(ev));
+ };
+ }),
+ /**
+ * Toggles the disabled class depending on the $parent element
+ * and the possibility of the current combination.
+ *
+ * @private
+ * @param {$.Element} $parent
+ * @param {boolean} isCombinationPossible
+ */
+ _toggleDisable: function ($parent, isCombinationPossible) {
+ $parent.toggleClass('css_not_available', !isCombinationPossible);
+ },
/**
* Updates the product image.
* This will use the productId if available or will fallback to the productTemplateId.
diff --git a/addons/sale/views/res_config_settings_views.xml b/addons/sale/views/res_config_settings_views.xml
index 86abec7541c..7c825ff4973 100644
--- a/addons/sale/views/res_config_settings_views.xml
+++ b/addons/sale/views/res_config_settings_views.xml
@@ -61,7 +61,7 @@
- Send a product-specific email once the invoice is paid
+ Send a product-specific email once the invoice is validated