summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-06-17 13:20:35 +0200
committerChristophe Simonis <chs@odoo.com>2019-06-17 13:20:35 +0200
commit25e3f270625b1a7f239d34fab94444d0b676d52e (patch)
treea83ee8f72c2aedbe6c28fc89b6de4a655d5d97e6 /addons
parent382989c341935582f6ae47510796ecd8d9f6b710 (diff)
parent48a9f5a6335cfe8b3d465584f0d2f98be2530966 (diff)
[MERGE] forward port branch saas-12.3 up to 48a9f5a6335
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account.py15
-rw-r--r--addons/account/models/account_bank_statement.py124
-rw-r--r--addons/account/models/account_invoice.py119
-rw-r--r--addons/account/models/account_move.py34
-rw-r--r--addons/account/report/account_invoice_report.py5
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js50
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js10
-rw-r--r--addons/account/static/tests/reconciliation_tests.js20
-rw-r--r--addons/account/tests/test_bank_statement_reconciliation.py30
-rw-r--r--addons/account/tests/test_invoice_taxes.py20
-rw-r--r--addons/account/wizard/account_invoice_send.py17
-rw-r--r--addons/account/wizard/account_invoice_send_views.xml3
-rw-r--r--addons/calendar/views/calendar_views.xml6
-rw-r--r--addons/crm/models/crm_lead.py2
-rw-r--r--addons/gamification/data/gamification_karma_rank_demo.xml2
-rw-r--r--addons/google_calendar/models/google_calendar.py2
-rw-r--r--addons/google_spreadsheet/models/google_drive.py2
-rw-r--r--addons/hr/models/res_users.py6
-rw-r--r--addons/hr/views/hr_employee_views.xml1
-rw-r--r--addons/hr/views/res_users.xml2
-rw-r--r--addons/hr_attendance/views/hr_employee_view.xml1
-rw-r--r--addons/hr_expense/models/hr_expense.py4
-rw-r--r--addons/hr_holidays/models/hr_leave.py18
-rw-r--r--addons/hr_holidays/models/hr_leave_allocation.py8
-rw-r--r--addons/hr_holidays/tests/test_out_of_office.py34
-rw-r--r--addons/hr_holidays/views/hr_leave_views.xml1
-rw-r--r--addons/http_routing/models/ir_http.py11
-rw-r--r--addons/l10n_lu/data/account_tax_report_line.xml4
-rw-r--r--addons/l10n_mx/data/account_tag_data.xml100
-rw-r--r--addons/lunch/report/lunch_product_report.py2
-rw-r--r--addons/lunch/static/src/js/lunch_kanban_model.js2
-rw-r--r--addons/mail/models/mail_mail.py2
-rw-r--r--addons/mail/models/mail_message.py10
-rw-r--r--addons/mail/static/src/js/composers/basic_composer.js1
-rw-r--r--addons/membership/models/partner.py9
-rw-r--r--addons/mrp/models/mrp_bom.py1
-rw-r--r--addons/mrp/models/mrp_production.py8
-rw-r--r--addons/mrp/views/mrp_bom_views.xml3
-rw-r--r--addons/mrp/views/stock_move_views.xml2
-rw-r--r--addons/payment/views/payment_templates.xml3
-rw-r--r--addons/payment_adyen/models/payment.py2
-rw-r--r--addons/payment_paypal/models/payment.py2
-rw-r--r--addons/point_of_sale/models/pos_session.py3
-rw-r--r--addons/project/static/src/css/project.css13
-rw-r--r--addons/project/views/project_portal_templates.xml2
-rw-r--r--addons/purchase/models/account_invoice.py6
-rw-r--r--addons/purchase/views/purchase_views.xml2
-rw-r--r--addons/purchase_stock/models/stock_rule.py6
-rw-r--r--addons/repair/views/repair_views.xml4
-rw-r--r--addons/sale/models/sale.py1
-rw-r--r--addons/sale/views/sale_views.xml1
-rw-r--r--addons/sale_timesheet/models/product.py4
-rw-r--r--addons/sale_timesheet/models/project.py6
-rw-r--r--addons/sale_timesheet/models/project_overview.py61
-rw-r--r--addons/sale_timesheet/models/sale_order.py6
-rw-r--r--addons/sale_timesheet/tests/test_sale_service.py4
-rw-r--r--addons/sale_timesheet/views/product_views.xml24
-rw-r--r--addons/sale_timesheet/views/project_task_views.xml38
-rw-r--r--addons/snailmail/models/ir_actions_report.py8
-rw-r--r--addons/snailmail_account/tests/test_pingen_send.py2
-rw-r--r--addons/stock/data/stock_data.xml2
-rw-r--r--addons/stock/models/stock_location.py2
-rw-r--r--addons/stock/models/stock_move.py2
-rw-r--r--addons/stock/models/stock_quant.py2
-rw-r--r--addons/stock/views/stock_picking_views.xml4
-rw-r--r--addons/survey/views/assets.xml1
-rw-r--r--addons/uom/models/uom_uom.py3
-rw-r--r--addons/uom/tests/test_uom.py10
-rw-r--r--addons/web/static/src/js/chrome/action_manager_act_window.js2
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js1
-rw-r--r--addons/web/static/src/js/views/qweb/qweb_view.js5
-rw-r--r--addons/web/static/src/scss/layout_boxed.scss2
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js30
-rw-r--r--addons/website/controllers/main.py7
-rw-r--r--addons/website/data/website_data.xml65
-rw-r--r--addons/website/models/ir_ui_view.py3
-rw-r--r--addons/website/models/website.py2
-rw-r--r--addons/website/tests/test_views.py48
-rw-r--r--addons/website_event/views/event_templates.xml4
-rw-r--r--addons/website_mail/models/mail_message.py5
-rw-r--r--addons/website_profile/views/website_profile.xml33
-rw-r--r--addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js34
-rw-r--r--addons/website_sale_delivery/tests/__init__.py4
-rw-r--r--addons/website_sale_delivery/tests/test_ui.py14
-rw-r--r--addons/website_sale_delivery/views/website_sale_delivery_templates.xml9
-rw-r--r--addons/website_sale_product_configurator/static/src/js/website_sale_options.js17
-rw-r--r--addons/website_slides/controllers/main.py8
-rw-r--r--addons/website_slides/models/slide_slide.py2
-rw-r--r--addons/website_slides/static/src/xml/slide_quiz.xml2
-rw-r--r--addons/website_slides/views/website_slides_templates_course.xml8
-rw-r--r--addons/website_slides/views/website_slides_templates_homepage.xml57
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson.xml8
-rw-r--r--addons/website_slides_forum/views/website_slides_forum_templates.xml4
93 files changed, 873 insertions, 416 deletions
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 6be56cd4795..326e1574014 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -1097,10 +1097,17 @@ class AccountTax(models.Model):
for record in self:
record._check_repartition_lines(record.invoice_repartition_line_ids)
record._check_repartition_lines(record.refund_repartition_line_ids)
- sum_inv_tax = sum(record.mapped('invoice_repartition_line_ids.factor_percent'))
- sum_refund_tax = sum(record.mapped('refund_repartition_line_ids.factor_percent'))
- if float_compare(sum_inv_tax, sum_refund_tax, precision_digits=4) != 0:
- raise ValidationError(_("You should impact the same total percentage of the tax amount for invoices and refunds."))
+
+ if len(record.invoice_repartition_line_ids) != len(record.refund_repartition_line_ids):
+ raise ValidationError(_("Invoice and credit note repartition should have the same number of lines."))
+
+ index = 0
+ while index < len(record.invoice_repartition_line_ids):
+ inv_rep_ln = record.invoice_repartition_line_ids[index]
+ ref_rep_ln = record.refund_repartition_line_ids[index]
+ if inv_rep_ln.repartition_type != ref_rep_ln.repartition_type or inv_rep_ln.factor_percent != ref_rep_ln.factor_percent:
+ raise ValidationError(_("Invoice and credit note repartitions should match (same percentages, in the same order)."))
+ index += 1
@api.one
@api.constrains('children_tax_ids', 'type_tax_use')
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 7537227fdae..4718efa0599 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -446,6 +446,7 @@ 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
@@ -453,19 +454,23 @@ 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 = -sum([x.amount_currency for x in move.line_ids])
+ amount_currency = -amount_sum
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 = -sum([x.amount_currency for x in move.line_ids])
+ amount_currency = -amount_sum
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 = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
+ amount_currency = -amount_sum/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
@@ -474,9 +479,8 @@ 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
- return {
+ aml_dict = {
'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 \
@@ -487,22 +491,72 @@ 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.journal_entry_ids.ids:
- vals = {
+ 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 = {
'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.process_reconciliation(new_aml_dicts=[vals])
+ 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, -aml_dict['debit'] if st_line.amount < 0 else aml_dict['credit'])
+ 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})
def _get_communication(self, payment_method_id):
return self.name or ''
@@ -630,8 +684,6 @@ 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
@@ -654,32 +706,12 @@ 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
- 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
+ self._prepare_move_line_for_currency(aml_dict, date)
# Create write-offs
for aml_dict in new_aml_dicts:
@@ -729,6 +761,36 @@ 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 12670d8e841..4a9cd324718 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -93,11 +93,12 @@ class AccountInvoice(models.Model):
('type', 'in', [TYPE2JOURNAL[ty] for ty in inv_types if ty in TYPE2JOURNAL]),
('company_id', '=', company_id),
]
- journal_with_currency = False
- if self._context.get('default_currency_id'):
- currency_clause = [('currency_id', '=', self._context.get('default_currency_id'))]
- journal_with_currency = self.env['account.journal'].search(domain + currency_clause, limit=1)
- return journal_with_currency or self.env['account.journal'].search(domain, limit=1)
+ company_currency_id = self.env['res.company'].browse(company_id).currency_id.id
+ currency_id = self._context.get('default_currency_id') or company_currency_id
+ currency_clause = [('currency_id', '=', currency_id)]
+ if currency_id == company_currency_id:
+ currency_clause = ['|', ('currency_id', '=', False)] + currency_clause
+ return self.env['account.journal'].search(domain + currency_clause, limit=1)
@api.model
def _default_currency(self):
@@ -1556,80 +1557,22 @@ class AccountInvoice(models.Model):
This function is used in order to fix account.invoice.tax objects generated
for refunds when tax amounts have been modified manually.
- For example, a tax with the following tax repartition...
- - INVOICE:
- 1) 30%
- 2) 70%
- - REFUND
- 3) 40%
- 4) 60%
-
- ... will group line 1) with line 3), and line 2) with lines 3) and 4).
-
- :return: A dictionnary, with invoice repartition line ids as keys, and lists
- of corresponding refund repartition line ids as values.
+ :return: A dictionnary, with invoice repartition line ids as keys, and refund
+ repartition lines as values
"""
rslt = {}
for tax in taxes:
- inv_index = 0
- ref_index = 0
-
- tax_inv_lines = tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
- tax_ref_lines = tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
-
- amount_to_match = tax_inv_lines[inv_index].factor_percent
- while inv_index < len(tax_inv_lines) or ref_index < len(tax_ref_lines):
- inv_rep_line = inv_index < len(tax_inv_lines) and tax_inv_lines[inv_index] or tax_inv_lines[-1]
- ref_rep_line = ref_index < len(tax_ref_lines) and tax_ref_lines[ref_index] or tax_ref_lines[-1]
-
- rslt_list = rslt.get(inv_rep_line.id, [])
- if ref_rep_line.id not in rslt_list:
- rslt_list.append(ref_rep_line.id)
- rslt[inv_rep_line.id] = rslt_list
-
- if amount_to_match > 0:
- amount_to_match -= ref_rep_line.factor_percent
- ref_index += 1
- elif amount_to_match < 0:
- amount_to_match += inv_rep_line.factor_percent
- inv_index +=1
- else:
- inv_index += 1
- if inv_index < len(tax_inv_lines):
- amount_to_match = tax_inv_lines[inv_index].factor_percent
+ index = 0
+ while(index < len(tax.invoice_repartition_line_ids)):
+ # _validate_repartition_lines constraint on taxes ensure invoice and refund repartition are equal, and in the same order
+ inv_rep_ln = tax.invoice_repartition_line_ids[index]
+ ref_rep_ln = tax.refund_repartition_line_ids[index]
+ if inv_rep_ln.repartition_type == 'tax':
+ rslt[inv_rep_ln.id] = ref_rep_ln
+ index += 1
return rslt
- def _group_tax_lines_by_repartition(self):
- self.ensure_one()
- rslt = {}
- for tax_line in self.tax_line_ids:
- rslt[tax_line.tax_repartition_line_id.id] = tax_line
- return rslt
-
- @api.model
- def _fix_refund_tax_lines(self, invoice, refund):
- """ Modifies the tax_line_ids of a draft refund invoice in order to make it
- match the manual tax modifications that were made on its original invoice.
- """
- invoice_tax_lines_map = invoice._group_tax_lines_by_repartition()
- refund_tax_lines_map = refund._group_tax_lines_by_repartition()
- refund_repartition_map = self._create_refund_repartition_mapping(invoice.mapped('invoice_line_ids.invoice_line_tax_ids'))
- computed_tax_values = invoice.get_taxes_values()
-
- for tax_data in computed_tax_values.values():
- rep_line_id = tax_data['tax_repartition_line_id']
-
- matching_invoice_tax = invoice_tax_lines_map.get(rep_line_id)
-
- if matching_invoice_tax:
- ref_rep_line_id = refund_repartition_map[rep_line_id][0]
- refund_tax_line = refund_tax_lines_map.get(ref_rep_line_id)
- manual_difference = matching_invoice_tax.amount - tax_data['amount']
-
- if not invoice.currency_id.is_zero(manual_difference):
- refund_tax_line.amount += manual_difference
-
@api.model
def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None):
""" Prepare the dict of values to create the new credit note from the invoice.
@@ -1678,6 +1621,35 @@ class AccountInvoice(models.Model):
values['date'] = date
if description:
values['name'] = description
+
+ # Treat refund tax lines.
+ # We copy them from the invoice and replace their account_id and
+ # tag_ids based on the refund repartition of the corresponding taxes.
+ tax_rep_ln_mapping = {}
+ for invoice_tax_entry in invoice.tax_line_ids:
+ if not invoice_tax_entry.tax_repartition_line_id.id in tax_rep_ln_mapping:
+ tax_rep_ln_mapping[invoice_tax_entry.tax_id.id] = self._create_refund_repartition_mapping(invoice_tax_entry.tax_id)
+
+ tax_line_vals = []
+ for invoice_tax_entry in invoice.tax_line_ids:
+ ref_rep_ln = tax_rep_ln_mapping[invoice_tax_entry.tax_id.id][invoice_tax_entry.tax_repartition_line_id.id]
+ tax_line_vals.append({
+ 'name': invoice_tax_entry.name,
+ 'tax_id': invoice_tax_entry.tax_id.id,
+ 'tax_repartition_line_id': ref_rep_ln.id,
+ 'account_id': ref_rep_ln.account_id.id or invoice_tax_entry.account_id.id, # If the refund repartition line has no account set, we use the one from the original invoice
+ 'account_analytic_id': invoice_tax_entry.account_analytic_id.id,
+ 'analytic_tag_ids': [(6, 0, invoice_tax_entry.analytic_tag_ids.ids)],
+ 'amount': invoice_tax_entry.amount,
+ 'amount_rounding': invoice_tax_entry.amount_rounding,
+ 'manual': invoice_tax_entry.manual,
+ 'sequence': invoice_tax_entry.sequence,
+ 'base': invoice_tax_entry.base,
+ 'tax_ids': [(6, 0, invoice_tax_entry.tax_ids.ids)],
+ 'tag_ids': [(6, 0, ref_rep_ln.tag_ids.ids)],
+ })
+ values['tax_line_ids'] = [(0, 0, tax_line_val) for tax_line_val in tax_line_vals]
+
return values
@api.multi
@@ -1693,7 +1665,6 @@ class AccountInvoice(models.Model):
message = _("This customer invoice credit note has been created from: <a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a><br>Reason: %s") % (invoice.id, invoice.number, description)
else:
message = _("This vendor bill credit note has been created from: <a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a><br>Reason: %s") % (invoice.id, invoice.number, description)
- self._fix_refund_tax_lines(invoice, refund_invoice)
refund_invoice.message_post(body=message)
new_invoices += refund_invoice
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index e7763356e96..6b9469f7a18 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -88,12 +88,14 @@ 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
@@ -434,6 +436,7 @@ class AccountMove(models.Model):
@api.multi
def _reverse_move(self, date=None, journal_id=None):
self.ensure_one()
+ date = date or fields.Date.today()
with self.env.norecompute():
reversed_move = self.copy(default={
'date': date,
@@ -679,8 +682,8 @@ class AccountMoveLine(models.Model):
tax_audit = fields.Char(string="Tax Audit String", compute="_compute_tax_audit", store=True, help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.")
_sql_constraints = [
- ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'),
- ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
+ ('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry! Credit or debit should be zero.'),
+ ('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry! Credit and debit should be positive.'),
]
@api.model
@@ -815,22 +818,21 @@ 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
- 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)
+ 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)
seen = set()
while todo:
- 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]
+ 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)
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
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index b94bd711c63..2cebf29d6d5 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -96,7 +96,7 @@ class AccountInvoiceReport(models.Model):
sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id,
sub.fiscal_position_id, sub.user_id, sub.company_id, sub.nbr, sub.invoice_id, sub.type, sub.state,
sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
- sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total as amount_total,
+ sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total / COALESCE(cr.rate, 1) as amount_total,
COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id
"""
return select_str
@@ -114,8 +114,7 @@ class AccountInvoiceReport(models.Model):
ai.partner_bank_id,
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_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
- count(*) * invoice_type.sign AS amount_total,
+ SUM(ail.price_total * invoice_type.sign_qty) AS amount_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
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))
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index d4dce5581ca..2846e2d2e2f 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -281,7 +281,6 @@ var StatementModel = BasicModel.extend({
if (last && !this._isValid(last)) {
return Promise.resolve(false);
}
-
prop = this._formatQuickCreate(line);
line.reconciliation_proposition.push(prop);
line.createForm = _.pick(prop, this.quickCreateFields);
@@ -493,13 +492,6 @@ var StatementModel = BasicModel.extend({
this._blurProposition(handle);
var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields));
focus.reconcileModelId = reconcileModelId;
- if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.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.")
- }});
- return Promise.resolve();
- }
line.reconciliation_proposition.push(focus);
if (reconcileModel.has_second_line) {
var second = {};
@@ -619,19 +611,16 @@ var StatementModel = BasicModel.extend({
var self = this;
var line = this.getLine(handle);
var prop = _.last(_.filter(line.reconciliation_proposition, '__focus'));
+ if ('to_check' in values && values.to_check === false) {
+ // check if we have another line with to_check and if yes don't change value of this proposition
+ prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) {
+ return rec_prop.id !== prop.id && rec_prop.to_check;
+ });
+ }
if (!prop) {
prop = this._formatQuickCreate(line);
line.reconciliation_proposition.push(prop);
}
- 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:visible').prop('checked', !values.to_check).change();
- return Promise.resolve();
- }
_.each(values, function (value, fieldName) {
if (fieldName === 'analytic_tag_ids') {
switch (value.operation) {
@@ -683,7 +672,6 @@ var StatementModel = BasicModel.extend({
prop.__tax_to_recompute = true;
}
line.createForm = _.pick(prop, this.quickCreateFields);
-
// If you check/uncheck the force_tax_included box, reset the createForm amount.
if(prop.base_amount)
line.createForm.amount = prop.base_amount;
@@ -744,16 +732,8 @@ var StatementModel = BasicModel.extend({
"new_aml_dicts": _.map(_.filter(props, function (prop) {
return isNaN(prop.id) && prop.display;
}), self._formatToProcessReconciliation.bind(self, line)),
+ "to_check": line.to_check,
};
- line.reconciliation_proposition.some(function(prop) {
- if (prop.to_check) {
- values_dict['to_check'] = true;
- return true;
- }
- });
- if (line.reconciliation_proposition[0].to_check) {
- values_dict['to_check'] = true;
- }
// If the lines are not fully balanced, create an unreconciled amount.
// line.st_line.currency_id is never false here because its equivalent to
@@ -870,7 +850,12 @@ var StatementModel = BasicModel.extend({
var formatOptions = {
currency_id: line.st_line.currency_id,
};
+ line.to_check = false;
_.each(line.reconciliation_proposition, function (prop) {
+ if (prop.to_check) {
+ // If one of the proposition is to_check, set the global to_check flag to true
+ line.to_check = true;
+ }
if (prop.is_tax) {
if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) {
reconciliation_proposition.push(prop);
@@ -1025,6 +1010,7 @@ var StatementModel = BasicModel.extend({
prop.label = prop.name;
prop.account_id = self._formatNameGet(prop.account_id || line.account_id);
prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str;
+ prop.to_check = !!prop.to_check;
});
}
},
@@ -1160,7 +1146,7 @@ var StatementModel = BasicModel.extend({
'link': values.link,
'display': true,
'invalid': true,
- 'to_check': values.to_check,
+ 'to_check': !!values.to_check,
'__tax_to_recompute': true,
'is_tax': values.is_tax,
'__focus': '__focus' in values ? values.__focus : true,
@@ -1406,11 +1392,8 @@ var ManualModel = StatementModel.extend({
return self.loadData(lines);
});
default:
- var partner_ids = context.partner_ids;
- var account_ids = context.account_ids || self.account_ids;
- if (partner_ids && !account_ids) account_ids = [];
- if (!partner_ids && account_ids) partner_ids = [];
- account_ids = null; // TOFIX: REMOVE ME
+ var partner_ids = context.partner_ids || null;
+ var account_ids = context.account_ids || self.account_ids || null;
return self._rpc({
model: 'account.reconciliation.widget',
method: 'get_all_data_for_manual_reconciliation',
@@ -1627,6 +1610,7 @@ var ManualModel = StatementModel.extend({
prop.debit = prop.debit !== 0 ? 0 : tmp_value;
prop.amount = -prop.amount;
prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id);
+ prop.to_check = !!prop.to_check;
});
}
},
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 53cd0b76598..e6be300714d 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -346,7 +346,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
update: function (state) {
var self = this;
// isValid
- var to_check_checked = !!(state.reconciliation_proposition[0] && state.reconciliation_proposition[0].to_check);
+ var to_check_checked = !!(state.to_check);
this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked);
this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked);
this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0);
@@ -476,9 +476,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
}
});
- return true;
+ if (state.to_check) {
+ // Set the to_check field to true if global to_check is set
+ self.$('.create_to_check input').prop('checked', state.to_check).change();
+ }
+ return true;
});
- });
+ });
}
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
},
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index ad34f7b40a8..5d8bb5dc490 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -477,7 +477,7 @@ var auto_reconciliation = {
};
var data_for_manual_reconciliation_widget = {
- '[[],null]': {
+ '[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]': {
'customers': [
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'mode': "customers"},
{'account_id': 7, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'mode': "customers"}
@@ -744,7 +744,7 @@ QUnit.module('account', {
"name": "INV/2017/0003",
"analytic_tag_ids": [[6, null, []]]
}],
- payment_aml_ids: [], new_aml_dicts: []}]],
+ payment_aml_ids: [], new_aml_dicts: [], to_check: false}]],
"Should call process_bank_statement_line with ids");
var def = testUtils.makeTestPromise();
def.abort = function () {};
@@ -782,8 +782,10 @@ QUnit.module('account', {
// line is send back to server.
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
- [[5],[{partner_id: 8, counterpart_aml_dicts: [],
- payment_aml_ids: [], new_aml_dicts: [{
+ [[5],[{partner_id: 8, to_check: false, counterpart_aml_dicts: [],
+ payment_aml_ids: [],
+ to_check: false,
+ new_aml_dicts: [{
account_id: 287,
credit: 1175,
debit: 0,
@@ -834,7 +836,7 @@ QUnit.module('account', {
// We also create a line which is the open balance.
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
- [[5],[{partner_id: 8,
+ [[5],[{partner_id: 8, to_check: false,
counterpart_aml_dicts: [{
counterpart_aml_id: 109,
credit: 650,
@@ -884,6 +886,7 @@ QUnit.module('account', {
}],
payment_aml_ids: [],
new_aml_dicts: [],
+ to_check: false,
}]
], "should call process_bank_statement_line with partial reconcile values");
}
@@ -1408,6 +1411,7 @@ QUnit.module('account', {
payment_aml_ids: [],
partner_id: 8,
+ to_check: false,
new_aml_dicts: []}]
], "should call process_bank_statement_line with partial reconcile values");
}
@@ -1500,6 +1504,7 @@ QUnit.module('account', {
payment_aml_ids: [],
partner_id: 8,
+ to_check: false,
new_aml_dicts: [
{account_id: 282,
credit: 0,
@@ -1609,6 +1614,7 @@ QUnit.module('account', {
payment_aml_ids: [],
partner_id: 8,
+ to_check: false,
new_aml_dicts: []
}]
], "should call process_bank_statement_line with correct counterpart_aml_dicts");
@@ -1662,7 +1668,7 @@ QUnit.module('account', {
assert.expect(5);
// tweak the data to fit our needs
- this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[[],null]']);
+ this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]']);
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [
{account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16",
total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait",
@@ -1772,7 +1778,7 @@ QUnit.module('account', {
QUnit.test('Tax on account receivable', async function(assert){
assert.expect(21);
- this.params.data_for_manual_reconciliation_widget['[[],null]'].accounts = [];
+ this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]'].accounts = [];
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
testUtils.mock.addMockEnvironment(clientAction, {
data: this.params.data,
diff --git a/addons/account/tests/test_bank_statement_reconciliation.py b/addons/account/tests/test_bank_statement_reconciliation.py
index 0e0e2ceadfa..a0a5fe2e65b 100644
--- a/addons/account/tests/test_bank_statement_reconciliation.py
+++ b/addons/account/tests/test_bank_statement_reconciliation.py
@@ -13,6 +13,8 @@ class TestBankStatementReconciliation(AccountingTestCase):
self.bsl_model = self.env['account.bank.statement.line']
self.reconciliation_widget = self.env['account.reconciliation.widget']
self.partner = self.env['res.partner'].create({'name': 'test'})
+ self.currency_usd_id = self.env.ref("base.USD").id
+ self.currency_euro_id = self.env.ref("base.EUR").id
def test_reconciliation_proposition(self):
rcv_mv_line = self.create_invoice(100)
@@ -108,3 +110,31 @@ class TestBankStatementReconciliation(AccountingTestCase):
})
return bank_stmt_line
+
+ def test_confirm_statement_usd(self):
+ company = self.env.ref('base.main_company')
+ self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
+ self.env['res.currency.rate'].search([]).unlink()
+ self.env['res.currency.rate'].create({
+ 'currency_id': self.currency_usd_id,
+ 'rate': 2.0,
+ 'name': '2001-01-01',
+ })
+ bank_journal_usd = self.env['account.journal'].create({
+ 'name': 'Bank US',
+ 'type': 'bank',
+ 'code': 'BNK68',
+ 'currency_id': self.currency_usd_id,
+ })
+ statement = self.bs_model.create({
+ 'journal_id': bank_journal_usd.id,
+ 'balance_end_real': 100,
+ 'line_ids': [(0, 0, {
+ 'name': '_',
+ 'partner_id': self.partner.id,
+ 'amount': 100,
+ 'account_id': bank_journal_usd.default_debit_account_id.id,
+ })],
+ })
+ statement.button_open()
+ statement.button_confirm_bank()
diff --git a/addons/account/tests/test_invoice_taxes.py b/addons/account/tests/test_invoice_taxes.py
index b6759271579..dd2e8b52f15 100644
--- a/addons/account/tests/test_invoice_taxes.py
+++ b/addons/account/tests/test_invoice_taxes.py
@@ -189,7 +189,13 @@ class TestInvoiceTaxes(AccountingTestCase):
}),
(0,0, {
- 'factor_percent': 100,
+ 'factor_percent': 10,
+ 'repartition_type': 'tax',
+ 'tag_ids': [(4, ref_tax_tag.id, 0)],
+ }),
+
+ (0,0, {
+ 'factor_percent': 90,
'repartition_type': 'tax',
'account_id': account_1.id,
'tag_ids': [(4, ref_tax_tag.id, 0)],
@@ -219,13 +225,13 @@ class TestInvoiceTaxes(AccountingTestCase):
refund.action_invoice_open()
refund_move = refund.move_id
- self.assertEqual(len(refund_move.line_ids), 3, "There should be 4 account move lines created for the refund: payable, base and tax line")
+ self.assertEqual(len(refund_move.line_ids), 4, "There should be 4 account move lines created for the refund: payable, base and 2 tax lines")
ref_base_line = refund_move.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.user_type_id.type != 'receivable')
self.assertEqual(len(ref_base_line), 1, "There should be only one base line generated")
self.assertEqual(abs(ref_base_line.balance), 100, "Base amount should be 100")
self.assertEqual(ref_base_line.tag_ids, ref_base_tag, "Base line should have received base tag")
- ref_tax_line = refund_move.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
- self.assertEqual(len(ref_tax_line), 1, "There should be only one tax line")
- self.assertEqual(ref_tax_line.account_id, account_1, "Tax line should have been made on account 1")
- self.assertEqual(abs(ref_tax_line.balance), 42, "Tax line should have been made on account 1")
- self.assertEqual(ref_tax_line.tag_ids, ref_tax_tag, "Tax line should have the right tag")
+ ref_tax_lines = refund_move.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
+ self.assertEqual(len(ref_tax_lines), 2, "There should be two refund tax lines")
+ self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == ref_base_line.account_id).balance), 4.2, "Refund tax line on base account should amount to 4.2 (10% of 42)")
+ self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
+ self.assertEqual(ref_tax_lines.mapped('tag_ids'), ref_tax_tag, "Refund tax lines should have the right tag")
diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py
index 17e539de230..da564fa97b8 100644
--- a/addons/account/wizard/account_invoice_send.py
+++ b/addons/account/wizard/account_invoice_send.py
@@ -11,6 +11,7 @@ class AccountInvoiceSend(models.TransientModel):
_description = 'Account Invoice Send'
is_email = fields.Boolean('Email', default=lambda self: self.env.company.invoice_is_email)
+ invoice_without_email = fields.Text(compute='_compute_invoice_without_email', string='invoice(s) that will not be sent')
is_print = fields.Boolean('Print', default=lambda self: self.env.company.invoice_is_print)
printed = fields.Boolean('Is Printed', default=False)
invoice_ids = fields.Many2many('account.invoice', 'account_invoice_account_invoice_send_rel', string='Invoices')
@@ -45,6 +46,22 @@ class AccountInvoiceSend(models.TransientModel):
self.composer_id.template_id = self.template_id.id
self.composer_id.onchange_template_id_wrapper()
+ @api.onchange('is_email')
+ def _compute_invoice_without_email(self):
+ for wizard in self:
+ if wizard.is_email and len(wizard.invoice_ids) > 1:
+ invoices = self.env['account.invoice'].search([
+ ('id', 'in', self.env.context.get('active_ids')),
+ ('partner_id.email', '=', False)
+ ])
+ if invoices:
+ wizard.invoice_without_email = "%s\n%s" % (
+ _("The following invoice(s) will not be sent by email, because the customers don't have email address."),
+ "\n".join([i.reference for i in invoices])
+ )
+ else:
+ wizard.invoice_without_email = False
+
@api.multi
def _send_email(self):
if self.is_email:
diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml
index b104fa38c01..4f8888e8f54 100644
--- a/addons/account/wizard/account_invoice_send_views.xml
+++ b/addons/account/wizard/account_invoice_send_views.xml
@@ -23,6 +23,9 @@
<field name="is_email" />
<b><label for="is_email"/></b>
</div>
+ <div class="text-left d-inline-block mr8" attrs="{'invisible': ['|', ('is_email','=', False), ('invoice_without_email', '=', False)]}">
+ <field name="invoice_without_email" class="mr4"/>
+ </div>
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
<!-- visible wizard -->
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
diff --git a/addons/calendar/views/calendar_views.xml b/addons/calendar/views/calendar_views.xml
index 9a69e99410b..f7227a99192 100644
--- a/addons/calendar/views/calendar_views.xml
+++ b/addons/calendar/views/calendar_views.xml
@@ -163,7 +163,7 @@
<div class="o_row">
<field name="end_type" attrs="{'required': [('recurrency', '=', True)]}"/>
<field name="count" attrs="{'invisible': [('end_type', '!=', 'count')], 'required': [('recurrency', '=', True)]}"/>
- <field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date')]}"/>
+ <field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date'), ('recurrency', '=', True)]}"/>
</div>
</group>
<group attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}" name="weekdays">
@@ -183,10 +183,10 @@
attrs="{'required': [('month_by', '=', 'date'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'date')]}"/>
<field name="byday" string="The"
- attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
+ attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"/>
<field name="week_list" nolabel="1"
- attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
+ attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
'invisible': [('month_by', '!=', 'day')]}"/>
</div>
</group>
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 8b3b45f79ff..420f0c1f0bd 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -282,7 +282,7 @@ class Lead(models.Model):
return {}
if user_id and self._context.get('team_id'):
team = self.env['crm.team'].browse(self._context['team_id'])
- if user_id in team.member_ids.ids:
+ if user_id in team.member_ids.ids or user_id == team.user_id.id:
return {}
team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
return {'team_id': team_id}
diff --git a/addons/gamification/data/gamification_karma_rank_demo.xml b/addons/gamification/data/gamification_karma_rank_demo.xml
index c2d81ebbcc7..6250ee227f6 100644
--- a/addons/gamification/data/gamification_karma_rank_demo.xml
+++ b/addons/gamification/data/gamification_karma_rank_demo.xml
@@ -39,7 +39,7 @@
<record id="rank_doctor" model="gamification.karma.rank">
<field name="description_motivational" type="html">
<div class="media align-items-center">
- <div class="media-body">Reach the next rank and gain a week-end with a unicorn !</div>
+ <div class="media-body">You have reached the last rank. Congratulations!</div>
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.png"/>
</div>
</field>
diff --git a/addons/google_calendar/models/google_calendar.py b/addons/google_calendar/models/google_calendar.py
index 9155cf9a0fa..bf9010f3023 100644
--- a/addons/google_calendar/models/google_calendar.py
+++ b/addons/google_calendar/models/google_calendar.py
@@ -81,7 +81,7 @@ class SyncEvent(object):
self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
'The event has been deleted from one side, we delete on other side !')
#If event is not deleted !
- elif self.OE.status and (self.GG.status or not is_owner):
+ elif self.OE.status and (self.GG.status or (not is_owner and self.GG.update)):
if abs(self.OE.update - self.GG.update) > timedelta(seconds=1):
if self.OE.update < self.GG.update:
tmpSrc = 'GG'
diff --git a/addons/google_spreadsheet/models/google_drive.py b/addons/google_spreadsheet/models/google_drive.py
index 32402ffe626..f3c04608e72 100644
--- a/addons/google_spreadsheet/models/google_drive.py
+++ b/addons/google_spreadsheet/models/google_drive.py
@@ -35,7 +35,7 @@ class GoogleDrive(models.Model):
if not modifiers.get('invisible') and not modifiers.get('column_invisible'):
display_fields.append(node.get('name'))
fields = " ".join(display_fields)
- domain = domain.replace("'", r"\'").replace('"', "'")
+ domain = domain.replace("'", r"\'").replace('"', "'").replace('True', 'true').replace('False', 'false')
if groupbys:
fields = "%s %s" % (groupbys, fields)
formula = '=oe_read_group("%s";"%s";"%s";"%s")' % (model, fields, groupbys, domain)
diff --git a/addons/hr/models/res_users.py b/addons/hr/models/res_users.py
index 547ef54d7df..a8b201c271b 100644
--- a/addons/hr/models/res_users.py
+++ b/addons/hr/models/res_users.py
@@ -21,7 +21,7 @@ class User(models.Model):
department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False)
address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False)
work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False)
- parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
+ employee_parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False)
address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False)
is_address_home_a_company = fields.Boolean(related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False)
@@ -68,7 +68,7 @@ class User(models.Model):
'child_ids',
'employee_id',
'employee_ids',
- 'parent_id',
+ 'employee_parent_id',
]
hr_writable_fields = [
@@ -96,7 +96,7 @@ class User(models.Model):
'marital',
'mobile_phone',
'notes',
- 'parent_id',
+ 'employee_parent_id',
'passport_id',
'permit_no',
'employee_phone',
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
index b4a9b6bfcc1..2c0b6dd511c 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -67,6 +67,7 @@
</group>
<group>
<field name="department_id"/>
+ <field name="job_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
diff --git a/addons/hr/views/res_users.xml b/addons/hr/views/res_users.xml
index 6838d961adc..2e8c3d1b63f 100644
--- a/addons/hr/views/res_users.xml
+++ b/addons/hr/views/res_users.xml
@@ -73,7 +73,7 @@
<field name="work_location"/>
</group>
<group name="managers" string="Managers">
- <field name="parent_id"/>
+ <field name="employee_parent_id"/>
<field name="coach_id"/>
</group>
</div>
diff --git a/addons/hr_attendance/views/hr_employee_view.xml b/addons/hr_attendance/views/hr_employee_view.xml
index f9403ff847a..6e6b6d7df56 100644
--- a/addons/hr_attendance/views/hr_employee_view.xml
+++ b/addons/hr_attendance/views/hr_employee_view.xml
@@ -162,6 +162,7 @@
<field name="res_model">hr.employee</field>
<field name="view_mode">kanban,tree,activity</field>
<field name="view_id" ref="hr_employees_view_kanban"/>
+ <field name="target">fullscreen</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new employee
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 5c865fc053e..43f272afffc 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -800,12 +800,12 @@ class HrExpenseSheet(models.Model):
return self.employee_id.parent_id.user_id
elif self.employee_id.department_id.manager_id.user_id:
return self.employee_id.department_id.manager_id.user_id
- return self.env.user
+ return self.env['res.users']
def activity_update(self):
for expense_report in self.filtered(lambda hol: hol.state == 'submit'):
self.activity_schedule(
'hr_expense.mail_act_expense_approval',
- user_id=expense_report.sudo()._get_responsible_for_approval().id)
+ user_id=expense_report.sudo()._get_responsible_for_approval().id or self.env.user.id)
self.filtered(lambda hol: hol.state == 'approve').activity_feedback(['hr_expense.mail_act_expense_approval'])
self.filtered(lambda hol: hol.state == 'cancel').activity_unlink(['hr_expense.mail_act_expense_approval'])
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index c4a42b81b64..4a3d2be8666 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -265,6 +265,11 @@ class HolidaysRequest(models.Model):
self.request_unit_hours = False
self.request_unit_custom = False
+ @api.onchange('user_id')
+ def _onchange_user_id(self):
+ if not self.out_of_office_message:
+ self.out_of_office_message = self.user_id.out_of_office_message
+
@api.onchange('request_date_from_period', 'request_hour_from', 'request_hour_to',
'request_date_from', 'request_date_to',
'employee_id')
@@ -575,6 +580,10 @@ class HolidaysRequest(models.Model):
@api.multi
def write(self, values):
+ # Allow an employee to always write his own out of office message
+ if len(self) == 1 and values.keys() == {'out_of_office_message'} and self.employee_id.user_id == self.env.user:
+ return super(HolidaysRequest, self.sudo()).write(values)
+
employee_id = values.get('employee_id', False)
if not self.env.context.get('leave_fast_create') and values.get('state'):
self._check_approval_update(values['state'])
@@ -788,8 +797,7 @@ class HolidaysRequest(models.Model):
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
- if not all(self._ids):
- return
+
for holiday in self:
val_type = holiday.holiday_status_id.validation_type
@@ -829,7 +837,7 @@ class HolidaysRequest(models.Model):
def _get_responsible_for_approval(self):
self.ensure_one()
- responsible = self.env.user
+ responsible = self.env['res.users']
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
if self.holiday_status_id.responsible_id:
@@ -854,13 +862,13 @@ class HolidaysRequest(models.Model):
holiday.activity_schedule(
'hr_holidays.mail_act_leave_approval',
note=note,
- user_id=holiday.sudo()._get_responsible_for_approval().id)
+ user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
elif holiday.state == 'validate1':
holiday.activity_feedback(['hr_holidays.mail_act_leave_approval'])
holiday.activity_schedule(
'hr_holidays.mail_act_leave_second_approval',
note=note,
- user_id=holiday.sudo()._get_responsible_for_approval().id)
+ user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
elif holiday.state == 'validate':
to_do |= holiday
elif holiday.state == 'refuse':
diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py
index c400d47aedf..109a7aab456 100644
--- a/addons/hr_holidays/models/hr_leave_allocation.py
+++ b/addons/hr_holidays/models/hr_leave_allocation.py
@@ -525,7 +525,7 @@ class HolidaysAllocation(models.Model):
def _check_approval_update(self, state):
""" Check if target state is achievable. """
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
- if not current_employee or not all(self._ids):
+ if not current_employee:
return
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
@@ -564,7 +564,7 @@ class HolidaysAllocation(models.Model):
def _get_responsible_for_approval(self):
self.ensure_one()
- responsible = self.env.user
+ responsible = self.env['res.users']
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
if self.holiday_status_id.responsible_id:
@@ -586,13 +586,13 @@ class HolidaysAllocation(models.Model):
allocation.activity_schedule(
'hr_holidays.mail_act_leave_allocation_approval',
note=note,
- user_id=allocation.sudo()._get_responsible_for_approval().id)
+ user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
elif allocation.state == 'validate1':
allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
allocation.activity_schedule(
'hr_holidays.mail_act_leave_allocation_second_approval',
note=note,
- user_id=allocation.sudo()._get_responsible_for_approval().id)
+ user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
elif allocation.state == 'validate':
to_do |= allocation
elif allocation.state == 'refuse':
diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py
index 05e3e480cc0..4c97f465ce1 100644
--- a/addons/hr_holidays/tests/test_out_of_office.py
+++ b/addons/hr_holidays/tests/test_out_of_office.py
@@ -4,26 +4,29 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
-from odoo.tests.common import tagged, users, warmup
+from odoo.tests.common import tagged, users, warmup, Form
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
@tagged('out_of_office')
class TestOutOfOffice(TestHrHolidaysBase):
- def test_leave_ooo(self):
- leave_type = self.env['hr.leave.type'].create({
+ def setUp(self):
+ super().setUp()
+ self.leave_type = self.env['hr.leave.type'].create({
'name': 'Legal Leaves',
'time_type': 'leave',
'allocation_type': 'no',
})
+
+ def test_leave_ooo(self):
self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
leave_date_end = (datetime.today() + relativedelta(days=3))
leave = self.env['hr.leave'].create({
'name': 'Christmas',
'employee_id': self.employee_hruser.id,
- 'holiday_status_id': leave_type.id,
+ 'holiday_status_id': self.leave_type.id,
'date_from': (datetime.today() - relativedelta(days=1)),
'date_to': leave_date_end,
'out_of_office_message': 'contact tde in case of problems',
@@ -51,6 +54,29 @@ class TestOutOfOffice(TestHrHolidaysBase):
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_date_end'], leave_date_end)
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'contact tde in case of problems')
+ def test_leave_ooo_use_default(self):
+ """ Out of office message from Preferences should be used as default value """
+ self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
+
+ leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
+ leave_form.date_from = datetime.today() - relativedelta(days=1)
+ leave_form.date_to = datetime.today()
+ leave_form.holiday_status_id = self.leave_type
+ leave = leave_form.save()
+ self.assertEqual(leave.out_of_office_message, 'contact xdo in case of problems')
+
+ def test_leave_ooo_overwrite_default(self):
+ """ Out of office message default value should be overwrittable """
+ self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
+
+ leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
+ leave_form.date_from = datetime.today() - relativedelta(days=1)
+ leave_form.date_to = datetime.today()
+ leave_form.holiday_status_id = self.leave_type
+ leave_form.out_of_office_message = 'contact tde in case of problems'
+ leave = leave_form.save()
+ self.assertEqual(leave.out_of_office_message, 'contact tde in case of problems')
+
@tagged('out_of_office')
class TestOutOfOfficePerformance(TestHrHolidaysBase):
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index 8ff0691879e..456cfba32d7 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -277,6 +277,7 @@
</div>
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
+ <field name="user_id" invisible="1"/>
<field name="out_of_office_message"/>
</group>
<group name="col_right">
diff --git a/addons/http_routing/models/ir_http.py b/addons/http_routing/models/ir_http.py
index 8208cc5527d..b6d0361498a 100644
--- a/addons/http_routing/models/ir_http.py
+++ b/addons/http_routing/models/ir_http.py
@@ -47,7 +47,7 @@ def _guess_mimetype(ext=False, default='text/html'):
return ext is not False and exts.get(ext, default) or exts
-def slugify_one(s, max_length=None):
+def slugify_one(s, max_length=0):
""" Transform a string to a slug that can be used in a url path.
This method will first try to do the job with python-slugify if present.
Otherwise it will process string by stripping leading and ending spaces,
@@ -65,13 +65,12 @@ def slugify_one(s, max_length=None):
except TypeError:
pass
uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
- slug_str = re.sub('[\W_]', ' ', uni).strip().lower()
- slug_str = re.sub('[-\s]+', '-', slug_str)
+ slug_str = re.sub(r'[\W_]', ' ', uni).strip().lower()
+ slug_str = re.sub(r'[-\s]+', '-', slug_str)
+ return slug_str[:max_length] if max_length > 0 else slug_str
- return slug_str[:max_length]
-
-def slugify(s, max_length=None, path=False):
+def slugify(s, max_length=0, path=False):
if not path:
return slugify_one(s, max_length=max_length)
else:
diff --git a/addons/l10n_lu/data/account_tax_report_line.xml b/addons/l10n_lu/data/account_tax_report_line.xml
index 85c6a112d7b..34e6bb8c706 100644
--- a/addons/l10n_lu/data/account_tax_report_line.xml
+++ b/addons/l10n_lu/data/account_tax_report_line.xml
@@ -1004,7 +1004,7 @@
</record>
<record id="account_tax_report_line_3b_total_input_tax_nd" model="account.tax.report.line">
- <field name="name">III.B. Total input tax non-deductible </field>
+ <field name="name">III.B. Total input tax non-deductible</field>
<field name="sequence">2</field>
<field name="parent_id" ref="account_tax_report_line_3_assessment_deducible_tax"/>
<field name="country_id" ref="base.lu"/>
@@ -1080,7 +1080,7 @@
</record>
<record id="account_tax_report_line_3a_5_taxe_declaree_comme_debiteur_voir_points_ii_e" model="account.tax.report.line">
- <field name="name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F) </field>
+ <field name="name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F)</field>
<field name="tag_name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F)</field>
<field name="sequence">5</field>
<field name="parent_id" ref="account_tax_report_line_3a_total_de_la_taxe_en_amont"/>
diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml
index 1e8b23dacda..a025c4dd4e2 100644
--- a/addons/l10n_mx/data/account_tag_data.xml
+++ b/addons/l10n_mx/data/account_tag_data.xml
@@ -2077,212 +2077,230 @@
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
+ <record id='account_tag_401_04_new' model='account.account.tag'>
+ <field name='name'>401.04 Ventas y/o servicios gravados realizados en zona fronteriza norte</field>
+ <field name='color'>4</field>
+ <field name='applicability'>accounts</field>
+ <field name='nature'>A</field>
+ </record>
+ <record id='account_tag_401_05_new' model='account.account.tag'>
+ <field name='name'>401.05 Ventas y/o servicios gravados realizados en zona fronteriza norte de contado</field>
+ <field name='color'>4</field>
+ <field name='applicability'>accounts</field>
+ <field name='nature'>A</field>
+ </record>
+ <record id='account_tag_401_06_new' model='account.account.tag'>
+ <field name='name'>401.06 Ventas y/o servicios gravados realizados en zona fronteriza norte a crédito</field>
+ <field name='color'>4</field>
+ <field name='applicability'>accounts</field>
+ <field name='nature'>A</field>
+ </record>
<record id='account_tag_401_04' model='account.account.tag'>
- <field name='name'>401.04 Ventas y/o servicios gravados al 0%</field>
+ <field name='name'>401.07 Ventas y/o servicios gravados al 0%</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_05' model='account.account.tag'>
- <field name='name'>401.05 Ventas y/o servicios gravados al 0% de contado</field>
+ <field name='name'>401.08 Ventas y/o servicios gravados al 0% de contado</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_06' model='account.account.tag'>
- <field name='name'>401.06 Ventas y/o servicios gravados al 0% a crédito</field>
+ <field name='name'>401.09 Ventas y/o servicios gravados al 0% a crédito</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_07' model='account.account.tag'>
- <field name='name'>401.07 Ventas y/o servicios exentos</field>
+ <field name='name'>401.10 Ventas y/o servicios exentos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_08' model='account.account.tag'>
- <field name='name'>401.08 Ventas y/o servicios exentos de contado</field>
+ <field name='name'>401.11 Ventas y/o servicios exentos de contado</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_09' model='account.account.tag'>
- <field name='name'>401.09 Ventas y/o servicios exentos a crédito</field>
+ <field name='name'>401.12 Ventas y/o servicios exentos a crédito</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_10' model='account.account.tag'>
- <field name='name'>401.10 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
+ <field name='name'>401.13 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_11' model='account.account.tag'>
- <field name='name'>401.11 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
+ <field name='name'>401.14 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_12' model='account.account.tag'>
- <field name='name'>401.12 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
+ <field name='name'>401.15 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_13' model='account.account.tag'>
- <field name='name'>401.13 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
+ <field name='name'>401.16 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_14' model='account.account.tag'>
- <field name='name'>401.14 Ventas y/o servicios exentos nacionales partes relacionadas</field>
+ <field name='name'>401.17 Ventas y/o servicios exentos nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_15' model='account.account.tag'>
- <field name='name'>401.15 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
+ <field name='name'>401.18 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_16' model='account.account.tag'>
- <field name='name'>401.16 Ingresos por servicios administrativos</field>
+ <field name='name'>401.19 Ingresos por servicios administrativos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_17' model='account.account.tag'>
- <field name='name'>401.17 Ingresos por servicios administrativos nacionales partes relacionadas</field>
+ <field name='name'>401.20 Ingresos por servicios administrativos nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_18' model='account.account.tag'>
- <field name='name'>401.18 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
+ <field name='name'>401.21 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_19' model='account.account.tag'>
- <field name='name'>401.19 Ingresos por servicios profesionales</field>
+ <field name='name'>401.22 Ingresos por servicios profesionales</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_20' model='account.account.tag'>
- <field name='name'>401.20 Ingresos por servicios profesionales nacionales partes relacionadas</field>
+ <field name='name'>401.23 Ingresos por servicios profesionales nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_21' model='account.account.tag'>
- <field name='name'>401.21 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
+ <field name='name'>401.24 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_22' model='account.account.tag'>
- <field name='name'>401.22 Ingresos por arrendamiento</field>
+ <field name='name'>401.25 Ingresos por arrendamiento</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_23' model='account.account.tag'>
- <field name='name'>401.23 Ingresos por arrendamiento nacionales partes relacionadas</field>
+ <field name='name'>401.26 Ingresos por arrendamiento nacionales partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_24' model='account.account.tag'>
- <field name='name'>401.24 Ingresos por arrendamiento extranjeros partes relacionadas</field>
+ <field name='name'>401.27 Ingresos por arrendamiento extranjeros partes relacionadas</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_25' model='account.account.tag'>
- <field name='name'>401.25 Ingresos por exportación</field>
+ <field name='name'>401.28 Ingresos por exportación</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_26' model='account.account.tag'>
- <field name='name'>401.26 Ingresos por comisiones</field>
+ <field name='name'>401.29 Ingresos por comisiones</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_27' model='account.account.tag'>
- <field name='name'>401.27 Ingresos por maquila</field>
+ <field name='name'>401.30 Ingresos por maquila</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_28' model='account.account.tag'>
- <field name='name'>401.28 Ingresos por coordinados</field>
+ <field name='name'>401.31 Ingresos por coordinados</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_29' model='account.account.tag'>
- <field name='name'>401.29 Ingresos por regalías</field>
+ <field name='name'>401.32 Ingresos por regalías</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_30' model='account.account.tag'>
- <field name='name'>401.30 Ingresos por asistencia técnica</field>
+ <field name='name'>401.33 Ingresos por asistencia técnica</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_31' model='account.account.tag'>
- <field name='name'>401.31 Ingresos por donativos</field>
+ <field name='name'>401.34 Ingresos por donativos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_32' model='account.account.tag'>
- <field name='name'>401.32 Ingresos por intereses (actividad propia)</field>
+ <field name='name'>401.35 Ingresos por intereses (actividad propia)</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_33' model='account.account.tag'>
- <field name='name'>401.33 Ingresos de copropiedad</field>
+ <field name='name'>401.36 Ingresos de copropiedad</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_34' model='account.account.tag'>
- <field name='name'>401.34 Ingresos por fideicomisos</field>
+ <field name='name'>401.37 Ingresos por fideicomisos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_35' model='account.account.tag'>
- <field name='name'>401.35 Ingresos por factoraje financiero</field>
+ <field name='name'>401.38 Ingresos por factoraje financiero</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_36' model='account.account.tag'>
- <field name='name'>401.36 Ingresos por arrendamiento financiero</field>
+ <field name='name'>401.39 Ingresos por arrendamiento financiero</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_37' model='account.account.tag'>
- <field name='name'>401.37 Ingresos de extranjeros con establecimiento en el país</field>
+ <field name='name'>401.40 Ingresos de extranjeros con establecimiento en el país</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_401_38' model='account.account.tag'>
- <field name='name'>401.38 Otros ingresos propios</field>
+ <field name='name'>401.41 Otros ingresos propios</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
@@ -2293,20 +2311,26 @@
<field name='applicability'>accounts</field>
<field name='nature'>D</field>
</record>
+ <record id='account_tag_402_02_new' model='account.account.tag'>
+ <field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios en zona fronteriza norte</field>
+ <field name='color'>4</field>
+ <field name='applicability'>accounts</field>
+ <field name='nature'>D</field>
+ </record>
<record id='account_tag_402_02' model='account.account.tag'>
- <field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
+ <field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_402_03' model='account.account.tag'>
- <field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
+ <field name='name'>402.04 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_402_04' model='account.account.tag'>
- <field name='name'>402.04 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
+ <field name='name'>402.05 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>D</field>
diff --git a/addons/lunch/report/lunch_product_report.py b/addons/lunch/report/lunch_product_report.py
index 2615cb827ee..3a30fc426d3 100644
--- a/addons/lunch/report/lunch_product_report.py
+++ b/addons/lunch/report/lunch_product_report.py
@@ -69,7 +69,7 @@ class LunchProductReport(models.Model):
self._cr.execute("""
CREATE or REPLACE view %s AS (
SELECT
- row_number() over () AS id,
+ row_number() over (ORDER BY user_id,product.id) AS id,
product.id AS product_id,
product.name,
product.category_id,
diff --git a/addons/lunch/static/src/js/lunch_kanban_model.js b/addons/lunch/static/src/js/lunch_kanban_model.js
index ddd11f9a9be..c2386c7ace8 100644
--- a/addons/lunch/static/src/js/lunch_kanban_model.js
+++ b/addons/lunch/static/src/js/lunch_kanban_model.js
@@ -40,7 +40,7 @@ var LunchKanbanModel = KanbanModel.extend({
});
},
reload: function (id, options) {
- var domain = options.domain || this.localData[id].domain;
+ var domain = options && options.domain || this.localData[id].domain;
this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
options = _.extend(options, {domain: domain});
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 3b575b08405..f97d0a52cde 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -172,6 +172,8 @@ class MailMail(models.Model):
messages._notify_failure_update() # notify user that we have a failure
(notifications - failed).sudo().write({
'email_status': 'sent',
+ 'failure_type': '',
+ 'failure_reason': '',
})
if not failure_type or failure_type == 'RECIPIENT': # if we have another error, we want to keep the mail.
mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 84607013996..2f55eef230a 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -750,8 +750,9 @@ class Message(models.Model):
WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
if self._cr.fetchall():
raise AccessError(
- _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
- (self._description, operation))
+ _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
+ + ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
+ )
# Read mail_message.ids to have their values
message_values = dict((message_id, {}) for message_id in self.ids)
@@ -956,8 +957,9 @@ class Message(models.Model):
if not self.browse(messages_to_check).exists():
return
raise AccessError(
- _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
- (self._description, operation))
+ _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
+ + ' - ({} {}, {} {})'.format(_('Records:'), list(other_ids)[:6], _('User:'), self._uid)
+ )
@api.model
def _get_record_name(self, values):
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
index c84296d55d1..e84d7588b1b 100644
--- a/addons/mail/static/src/js/composers/basic_composer.js
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -538,6 +538,7 @@ var BasicComposer = Widget.extend({
files: ev.currentTarget.files,
submitForm: true
});
+ ev.target.value = "";
},
/**
* @private
diff --git a/addons/membership/models/partner.py b/addons/membership/models/partner.py
index 3a13a1e1d33..907efb32b88 100644
--- a/addons/membership/models/partner.py
+++ b/addons/membership/models/partner.py
@@ -44,12 +44,19 @@ class Partner(models.Model):
'member_lines.account_invoice_line.invoice_id.partner_id',
'free_member',
'member_lines.date_to', 'member_lines.date_from',
- 'associate_member.membership_state')
+ 'associate_member')
def _compute_membership_state(self):
values = self._membership_state()
for partner in self:
partner.membership_state = values[partner.id]
+ # Do not depend directly on "associate_member.membership_state" or we might end up in an
+ # infinite loop. Since we still need this dependency somehow, we explicitly search for the
+ # "parent members" and trigger a recompute.
+ parent_members = self.search([('associate_member', 'in', self.ids)]) - self
+ if parent_members:
+ parent_members._recompute_todo(self._fields['membership_state'])
+
@api.depends('member_lines.account_invoice_line.invoice_id.state',
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
'member_lines.account_invoice_line.invoice_id.payment_ids',
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index 6ace858bc78..d862d5260a7 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -249,6 +249,7 @@ class MrpBomLine(models.Model):
'mrp.bom', 'Parent BoM',
index=True, ondelete='cascade', required=True)
parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id')
+ valid_product_attribute_value_ids = fields.Many2many('product.attribute.value', related='bom_id.product_tmpl_id.valid_product_attribute_value_ids')
attribute_value_ids = fields.Many2many(
'product.attribute.value', string='Apply on Variants',
help="BOM Product Variants needed form apply this line.")
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 0e2ee4727d5..dc27c437219 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -602,7 +602,7 @@ class MrpProduction(models.Model):
move_raw.write({
'group_id': production.procurement_group_id.id,
'unit_factor': move_raw.product_uom_qty / production.product_qty,
- 'reference': self.name, # set reference when MO name is different than 'New'
+ 'reference': production.name, # set reference when MO name is different than 'New'
})
production._generate_finished_moves()
production.move_raw_ids._adjust_procure_method()
@@ -861,8 +861,10 @@ class MrpProduction(models.Model):
raise UserError(_('Work order %s is still running') % wo.name)
self._check_lots()
self.post_inventory()
- moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel'))
- moves_to_cancel._action_cancel()
+ # Moves without quantity done are not posted => set them as done instead of canceling. In
+ # case the user edits the MO later on and sets some consumed quantity on those, we do not
+ # want the move lines to be canceled.
+ (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({'state': 'done'})
self.write({'date_finished': fields.Datetime.now()})
return True
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 0d68a231435..7236116e0b6 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -65,8 +65,9 @@
<field name="product_qty"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="parent_product_tmpl_id" invisible="1" />
+ <field name="valid_product_attribute_value_ids" invisible="1"/>
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
- <field name="attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
+ <field name="attribute_value_ids" widget="many2many_tags" domain="[('id', 'in', valid_product_attribute_value_ids)]" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
<field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/>
</tree>
</field>
diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml
index aff0e661120..f1b7c8228ef 100644
--- a/addons/mrp/views/stock_move_views.xml
+++ b/addons/mrp/views/stock_move_views.xml
@@ -49,7 +49,7 @@
<field name="finished_lots_exist" invisible="1"/>
</group>
</group>
- <field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_locked', '=', True)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
+ <field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': ['|', ('is_locked', '=', True), ('state', '=', 'cancel')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
<tree editable="bottom" decoration-success="product_uom_qty==qty_done" decoration-danger="(product_uom_qty &gt; 0) and (qty_done&gt;product_uom_qty)">
<field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
<field name="lot_produced_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index fdf2425d498..6124a77bc87 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -93,6 +93,9 @@
<input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/>
Save my payment data
</label>
+ <t t-if="acq.save_token == 'always'">
+ <input type="checkbox" name="o_payment_form_save_token" checked="'checked'" class="o_hidden" data-remove-me=""/>
+ </t>
</div>
</t>
<t t-else="acq.payment_flow == 's2s'">
diff --git a/addons/payment_adyen/models/payment.py b/addons/payment_adyen/models/payment.py
index 818de84dc20..7cb1020870e 100644
--- a/addons/payment_adyen/models/payment.py
+++ b/addons/payment_adyen/models/payment.py
@@ -170,7 +170,7 @@ class AcquirerAdyen(models.Model):
'sessionValidity': tmp_date.isoformat('T')[:19] + "Z",
'resURL': urls.url_join(base_url, AdyenController._return_url),
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False,
- 'shopperEmail': values.get('partner_email', ''),
+ 'shopperEmail': values.get('partner_email') or values.get('billing_partner_email') or '',
})
values['merchantSig'] = self._adyen_generate_merchant_sig_sha256('in', values)
diff --git a/addons/payment_paypal/models/payment.py b/addons/payment_paypal/models/payment.py
index fb12cabdc50..f83129749a9 100644
--- a/addons/payment_paypal/models/payment.py
+++ b/addons/payment_paypal/models/payment.py
@@ -86,7 +86,7 @@ class AcquirerPaypal(models.Model):
else:
percentage = self.fees_int_var
fixed = self.fees_int_fixed
- fees = (percentage / 100.0 * amount + fixed) / (1 - percentage / 100.0)
+ fees = (percentage / 100.0 * amount) + fixed / (1 - percentage / 100.0)
return fees
@api.multi
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index a101a6da380..17d9f0cca98 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -224,7 +224,8 @@ class PosSession(models.Model):
st_values = {
'journal_id': journal.id,
'user_id': self.env.user.id,
- 'name': pos_name
+ 'name': pos_name,
+ 'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id)
}
statements.append(ABS.with_context(ctx).sudo(uid).create(st_values).id)
diff --git a/addons/project/static/src/css/project.css b/addons/project/static/src/css/project.css
index 9522588f358..e3c9656063c 100644
--- a/addons/project/static/src/css/project.css
+++ b/addons/project/static/src/css/project.css
@@ -38,3 +38,16 @@
margin-top: 6px;
margin-right: 8px;
}
+
+.o_form_project_tasks.o_form_editable .oe_title {
+ max-width: initial;
+}
+
+.o_form_project_tasks.o_form_editable .oe_title .o_task_name {
+ /*
+ * should be (coming from addons/web/static/src/scss/form_view.scss):
+ * max-width: map-get($container-max-widths, md) - (2 * $o-horizontal-padding);
+ */
+ max-width: 688px;
+ margin-right: auto;
+}
diff --git a/addons/project/views/project_portal_templates.xml b/addons/project/views/project_portal_templates.xml
index bc1f45fca7c..b76a27479b9 100644
--- a/addons/project/views/project_portal_templates.xml
+++ b/addons/project/views/project_portal_templates.xml
@@ -238,7 +238,7 @@
<div class="row">
<div t-attf-class="col {{'col-lg-6' if not task.description else 'col-lg-12'}}">
<ul class="list-group">
- <a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}" target="_blank" data-no-post-process="">
+ <a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank" data-no-post-process="">
<div class='oe_attachment_embedded o_image o_image_small mr-2 mr-lg-3' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
<div class='oe_attachment_name text-truncate'><t t-esc='attachment.name'/></div>
</a>
diff --git a/addons/purchase/models/account_invoice.py b/addons/purchase/models/account_invoice.py
index 74811a27436..130c40a7d61 100644
--- a/addons/purchase/models/account_invoice.py
+++ b/addons/purchase/models/account_invoice.py
@@ -95,9 +95,9 @@ class AccountInvoice(models.Model):
self.partner_id = self.purchase_id.partner_id.id
vendor_ref = self.purchase_id.partner_ref
- if vendor_ref:
- self.reference = ", ".join([self.reference, vendor_ref]) if (
- self.reference and vendor_ref not in self.reference) else vendor_ref
+ if vendor_ref and (not self.reference or (
+ vendor_ref + ", " not in self.reference and not self.reference.endswith(vendor_ref))):
+ self.reference = ", ".join([self.reference, vendor_ref]) if self.reference else vendor_ref
if not self.invoice_line_ids:
#as there's no invoice line yet, we keep the currency of the PO
diff --git a/addons/purchase/views/purchase_views.xml b/addons/purchase/views/purchase_views.xml
index 19b654a7c6f..08d4522df07 100644
--- a/addons/purchase/views/purchase_views.xml
+++ b/addons/purchase/views/purchase_views.xml
@@ -386,7 +386,7 @@
<field name="amount_total" sum="Total amount" widget="monetary"/>
<field name="currency_id" invisible="1"/>
<field name="state"/>
- <field name="invoice_status" attrs="{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))]}"/>
+ <field name="invoice_status" invisible="1"/>
</tree>
</field>
</record>
diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py
index 885e8990ad7..77d571b9171 100644
--- a/addons/purchase_stock/models/stock_rule.py
+++ b/addons/purchase_stock/models/stock_rule.py
@@ -188,11 +188,15 @@ class StockRule(models.Model):
price_unit = seller.currency_id._convert(
price_unit, line.order_id.currency_id, line.order_id.company_id, fields.Date.today())
- return {
+ res = {
'product_qty': line.product_qty + procurement_uom_po_qty,
'price_unit': price_unit,
'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
}
+ orderpoint_id = values.get('orderpoint_id')
+ if orderpoint_id:
+ res['orderpoint_id'] = orderpoint_id.id
+ return res
@api.model
def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, company_id, values, po):
diff --git a/addons/repair/views/repair_views.xml b/addons/repair/views/repair_views.xml
index f91daadd95c..57e00ba5ede 100644
--- a/addons/repair/views/repair_views.xml
+++ b/addons/repair/views/repair_views.xml
@@ -78,6 +78,7 @@
<form string="Operations">
<group>
<group>
+ <field name="type"/>
<field name="product_id"/>
<field name="name"/>
<field name="product_uom_category_id" invisible="1"/>
@@ -86,7 +87,6 @@
<field name="product_uom_qty"/>
<field name="product_uom" groups="uom.group_uom"/>
</div>
- <field name="type" invisible="1"/>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
<field name="invoiced" invisible="1"/>
@@ -135,7 +135,7 @@
<field name="fees_lines">
<form string="Fees">
<group>
- <field name="product_id"/>
+ <field name="product_id" domain="[('type','=','service')]" required="True"/>
<field name="name"/>
<field name="product_uom_category_id" invisible="1"/>
<label for="product_uom_qty"/>
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index d6afcd519bf..7d463e9b01f 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -1247,6 +1247,7 @@ class SaleOrderLine(models.Model):
:param additional_domain: domain to restrict AAL to include in computation (required since timesheet is an AAL with a project ...)
"""
result = {}
+
# avoid recomputation if no SO lines concerned
if not self:
return result
diff --git a/addons/sale/views/sale_views.xml b/addons/sale/views/sale_views.xml
index a351d854f1c..69359a982cc 100644
--- a/addons/sale/views/sale_views.xml
+++ b/addons/sale/views/sale_views.xml
@@ -444,6 +444,7 @@
optional="hide"
groups="analytic.group_analytic_tags"
widget="many2many_tags"
+ options="{'color_field': 'color'}"
/>
<field
name="product_uom_qty"
diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py
index 75c09e1db4a..5d1d240ea9a 100644
--- a/addons/sale_timesheet/models/product.py
+++ b/addons/sale_timesheet/models/product.py
@@ -92,10 +92,10 @@ class ProductTemplate(models.Model):
@api.onchange('type')
def _onchange_type(self):
res = super(ProductTemplate, self)._onchange_type()
- if self.type == 'service':
+ if self.type == 'service' and not self.invoice_policy:
self.invoice_policy = 'order'
self.service_type = 'timesheet'
- elif self.type == 'consu' and self.service_policy == 'ordered_timesheet':
+ elif self.type == 'consu' and not self.invoice_policy and self.service_policy == 'ordered_timesheet':
self.invoice_policy = 'order'
return res
diff --git a/addons/sale_timesheet/models/project.py b/addons/sale_timesheet/models/project.py
index adec4d21015..7ee2d8b9969 100644
--- a/addons/sale_timesheet/models/project.py
+++ b/addons/sale_timesheet/models/project.py
@@ -129,8 +129,8 @@ class ProjectTask(models.Model):
def _get_default_partner(self):
partner = False
if 'default_project_id' in self.env.context: # partner from SO line is prior on one from project
- project = self.env['project.project'].browse(self.env.context['default_project_id'])
- partner = project.sale_line_id.order_partner_id
+ project_sudo = self.env['project.project'].browse(self.env.context['default_project_id']).sudo()
+ partner = project_sudo.sale_line_id.order_partner_id
if not partner:
partner = super(ProjectTask, self)._get_default_partner()
return partner
@@ -213,7 +213,7 @@ class ProjectTask(models.Model):
@api.multi
@api.constrains('sale_line_id')
def _check_sale_line_type(self):
- for task in self:
+ for task in self.sudo():
if task.sale_line_id:
if not task.sale_line_id.is_service or task.sale_line_id.is_expense:
raise ValidationError(_('You cannot link the order item %s - %s to this task because it is a re-invoiced expense.' % (task.sale_line_id.order_id.id, task.sale_line_id.product_id.name)))
diff --git a/addons/sale_timesheet/models/project_overview.py b/addons/sale_timesheet/models/project_overview.py
index bd6d277a9df..d1a78b9318c 100644
--- a/addons/sale_timesheet/models/project_overview.py
+++ b/addons/sale_timesheet/models/project_overview.py
@@ -92,12 +92,16 @@ class Project(models.Model):
#
# Time Repartition (per employee per billable types)
#
- user_ids = self.env['project.task'].sudo().search_read([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'])
+ user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id'])
user_ids = [user_id['user_id'][0] for user_id in user_ids]
employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids'])
# flatten the list of list
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
- employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id')
+
+ aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
+ employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
+
+ employees = self.env['hr.employee'].sudo().browse(employee_ids)
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
# repartition data, without timesheet on cancelled so
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
@@ -348,8 +352,14 @@ class Project(models.Model):
def _plan_prepare_actions(self, values):
actions = []
if len(self) == 1:
+ task_order_line_ids = []
+ # retrieve all the sale order line that we will need later below
+ if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
+ task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id'])
+ task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids]
+
if self.env.user.has_group('sales_team.group_sale_salesman'):
- if not self.sale_line_id and not self.tasks.mapped('sale_line_id'):
+ if not self.sale_line_id and not task_order_line_ids:
actions.append({
'label': _("Create a Sales Order"),
'type': 'action',
@@ -358,14 +368,19 @@ class Project(models.Model):
})
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account
- sale_orders = self.tasks.mapped('sale_line_id.order_id').filtered(lambda so: so.invoice_status == 'to invoice')
- if to_invoice_amount and sale_orders:
- if len(sale_orders) == 1:
+
+ sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id'])
+ sale_order_ids = [s['order_id'][0] for s in sale_order_ids]
+ sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id'])
+ sale_order_ids = list(map(lambda x: x['id'], sale_order_ids))
+
+ if to_invoice_amount and sale_order_ids:
+ if len(sale_order_ids) == 1:
actions.append({
'label': _("Create Invoice"),
'type': 'action',
'action_id': 'sale.action_view_sale_advance_payment_inv',
- 'context': json.dumps({'active_ids': sale_orders.ids, 'active_model': 'project.project'}),
+ 'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}),
})
else:
actions.append({
@@ -401,9 +416,13 @@ class Project(models.Model):
# if only one project, add it in the context as default value
tasks_domain = [('project_id', 'in', self.ids)]
tasks_context = self.env.context
- tasks_projects = self.env['project.task'].sudo().search(tasks_domain).mapped('project_id')
- if len(tasks_projects) == 1:
- tasks_context = {**tasks_context, 'default_project_id': tasks_projects.id}
+
+ # filter out all the projects that have no tasks
+ task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id'])
+ task_projects_ids = [p['project_id'][0] for p in task_projects_ids]
+
+ if len(task_projects_ids) == 1:
+ tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]}
stat_buttons.append({
'name': _('Tasks'),
'count': sum(self.mapped('task_count')),
@@ -416,8 +435,13 @@ class Project(models.Model):
})
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
- sale_orders = self.mapped('sale_line_id.order_id') | self.mapped(
- 'tasks.sale_order_id')
+ # read all the sale orders linked to the projects' tasks
+ task_so_ids = self.env['project.task'].search_read([
+ ('project_id', 'in', self.ids), ('sale_order_id', '!=', False)
+ ], ['sale_order_id'])
+ task_so_ids = [o['sale_order_id'][0] for o in task_so_ids]
+
+ sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids)
if sale_orders:
stat_buttons.append({
'name': _('Sales Orders'),
@@ -429,15 +453,20 @@ class Project(models.Model):
context={'create': False, 'edit': False, 'delete': False}
)
})
- invoices = sale_orders.mapped('invoice_ids').filtered(lambda inv: inv.type == 'out_invoice')
- if invoices:
+
+ invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids'])
+ invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids]))
+ invoice_ids = self.env['account.invoice'].search_read([('id', 'in', invoice_ids), ('type', '=', 'out_invoice')], ['id'])
+ invoice_ids = list(map(lambda x: x['id'], invoice_ids))
+
+ if invoice_ids:
stat_buttons.append({
'name': _('Invoices'),
- 'count': len(invoices),
+ 'count': len(invoice_ids),
'icon': 'fa fa-pencil-square-o',
'action': _to_action_data(
action=self.env.ref('account.action_invoice_tree1'),
- domain=[('id', 'in', invoices.ids), ('type', '=', 'out_invoice')],
+ domain=[('id', 'in', invoice_ids), ('type', '=', 'out_invoice')],
context={'create': False, 'delete': False}
)
})
diff --git a/addons/sale_timesheet/models/sale_order.py b/addons/sale_timesheet/models/sale_order.py
index 82654d8b81d..dd903ba7330 100644
--- a/addons/sale_timesheet/models/sale_order.py
+++ b/addons/sale_timesheet/models/sale_order.py
@@ -365,6 +365,12 @@ class SaleOrderLine(models.Model):
map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project
else:
map_so_project[so_line.order_id.id] = project
+ elif not project:
+ # Attach subsequent SO lines to the created project
+ so_line.project_id = (
+ map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id))
+ or map_so_project.get(so_line.order_id.id)
+ )
if so_line.product_id.service_tracking == 'task_in_project':
if not project:
if so_line.product_id.project_template_id:
diff --git a/addons/sale_timesheet/tests/test_sale_service.py b/addons/sale_timesheet/tests/test_sale_service.py
index eb1532d8744..e2591ba0268 100644
--- a/addons/sale_timesheet/tests/test_sale_service.py
+++ b/addons/sale_timesheet/tests/test_sale_service.py
@@ -363,8 +363,8 @@ class TestSaleService(TestCommonSaleTimesheetNoChart):
# check each line has or no generate something
self.assertTrue(so_line1.project_id, "Line1 should have create a project based on template A")
self.assertTrue(so_line2.project_id, "Line2 should have create an empty project")
- self.assertFalse(so_line3.project_id, "Line3 should not have create a project, since line1 already create a project based on template A")
- self.assertFalse(so_line4.project_id, "Line4 should not have create a project, since line1 already create an empty project")
+ self.assertEqual(so_line3.project_id, so_line1.project_id, "Line3 should reuse project of line1")
+ self.assertEqual(so_line4.project_id, so_line2.project_id, "Line4 should reuse project of line2")
self.assertTrue(so_line4.task_id, "Line4 should have create a new task, even if no project created.")
self.assertTrue(so_line5.project_id, "Line5 should have create a project based on template B")
diff --git a/addons/sale_timesheet/views/product_views.xml b/addons/sale_timesheet/views/product_views.xml
index 92c7e0e21cc..2831a293b17 100644
--- a/addons/sale_timesheet/views/product_views.xml
+++ b/addons/sale_timesheet/views/product_views.xml
@@ -18,12 +18,28 @@
</field>
</record>
+ <record id="product_template_view_search_sale_timesheet" model="ir.ui.view">
+ <field name="name">product.template.search.timesheet</field>
+ <field name="model">product.template</field>
+ <field name="inherit_id" ref="product.product_template_search_view"/>
+ <field name="mode">primary</field>
+ <field name="arch" type="xml">
+ <xpath expr="//filter[@name='consumable']" position="after">
+ <separator/>
+ <filter string="Time-based services" name="product_time_based" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')]"/>
+ <filter string="Fixed price services" name="product_service_fixed" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'order'), ('service_type', '=', 'timesheet')]"/>
+ <filter string="Milestone services" name="product_service_milestone" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'manual')]"/>
+ </xpath>
+ </field>
+ </record>
+
<record id="product_template_action_time_based" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
- <field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet'}</field>
+ <field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
+ <field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet', 'default_service_policy': 'delivered_timesheet'}</field>
</record>
<record id="product_template_action_fixed" model="ir.actions.act_window">
@@ -31,7 +47,8 @@
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
- <field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet'}</field>
+ <field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
+ <field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet', 'default_service_policy': 'ordered_timesheet'}</field>
</record>
<record id="product_template_action_milestone" model="ir.actions.act_window">
@@ -39,7 +56,8 @@
<field name="res_model">product.template</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
- <field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual'}</field>
+ <field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
+ <field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual', 'default_service_policy': 'delivered_manual'}</field>
</record>
</odoo>
diff --git a/addons/sale_timesheet/views/project_task_views.xml b/addons/sale_timesheet/views/project_task_views.xml
index d02defc8558..a1eb0003c66 100644
--- a/addons/sale_timesheet/views/project_task_views.xml
+++ b/addons/sale_timesheet/views/project_task_views.xml
@@ -61,20 +61,42 @@
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
<field name="inherit_id" ref="project.view_task_form2"/>
<field name="arch" type="xml">
- <xpath expr="//button[@name='toggle_active']" position="before">
- <button type="object" name="action_view_so"
- class="oe_stat_button" icon="fa-dollar"
- attrs="{'invisible': [('sale_order_id', '=', False)]}"
- string="Sales Order"/>
- <field name="sale_order_id" invisible="1"/>
- </xpath>
<field name="partner_id" position="after">
- <field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_create": True}' context="{'create': False, 'edit': False, 'delete': False}"/>
+ <field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
<field name="billable_type" invisible="1"/>
</field>
</field>
</record>
+ <record id="project_task_view_form_inherit_sale_line_editable" model="ir.ui.view">
+ <field name="name">project.task.form.inherit.sale.line.editable.salesman</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="view_sale_service_inherit_form2"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='sale_line_id']" position="attributes">
+ <attribute name="options">{"no_create": True}</attribute>
+ <attribute name="readonly">0</attribute>
+ </xpath>
+ </field>
+ <field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
+ </record>
+
+ <record id="project_task_view_form_sale_order" model="ir.ui.view">
+ <field name="name">project.task.form.inherit.sale.order</field>
+ <field name="model">project.task</field>
+ <field name="inherit_id" ref="project.view_task_form2"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@name='toggle_active']" position="before">
+ <button type="object" name="action_view_so"
+ class="oe_stat_button" icon="fa-dollar"
+ attrs="{'invisible': [('sale_order_id', '=', False)]}"
+ string="Sales Order"/>
+ <field name="sale_order_id" invisible="1"/>
+ </xpath>
+ </field>
+ <field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
+ </record>
+
<record id="project_task_view_search" model="ir.ui.view">
<field name="name">project.task.search.inherit</field>
<field name="model">project.task</field>
diff --git a/addons/snailmail/models/ir_actions_report.py b/addons/snailmail/models/ir_actions_report.py
index 2a1ad2f9496..b441d55c0fb 100644
--- a/addons/snailmail/models/ir_actions_report.py
+++ b/addons/snailmail/models/ir_actions_report.py
@@ -6,6 +6,14 @@ from odoo import models, fields, api, _
class IrActionsReport(models.Model):
_inherit = 'ir.actions.report'
+ @api.multi
+ def retrieve_attachment(self, record):
+ # Override this method in order to force to re-render the pdf in case of
+ # using snailmail
+ if self.env.context.get('snailmail_layout'):
+ return False
+ return super(IrActionsReport, self).retrieve_attachment(record)
+
@api.model
def get_paperformat(self):
# force the right format (euro/A4) when sending letters, only if we are not using the l10n_DE layout
diff --git a/addons/snailmail_account/tests/test_pingen_send.py b/addons/snailmail_account/tests/test_pingen_send.py
index ce73f388274..2cbda8ac9df 100644
--- a/addons/snailmail_account/tests/test_pingen_send.py
+++ b/addons/snailmail_account/tests/test_pingen_send.py
@@ -25,7 +25,7 @@ class TestPingenSend(AccountingTestCase):
self.data = {
'data': json.dumps({
'speed': 1,
- 'color': 2,
+ 'color': 1,
'duplex': 0,
'send': True,
})
diff --git a/addons/stock/data/stock_data.xml b/addons/stock/data/stock_data.xml
index 34693a1c29f..199c6207c7b 100644
--- a/addons/stock/data/stock_data.xml
+++ b/addons/stock/data/stock_data.xml
@@ -14,6 +14,8 @@
<!--
Resource: stock.location
-->
+ </data>
+ <data noupdate="1">
<record id="stock_location_locations" model="stock.location">
<field name="name">Physical Locations</field>
<field name="usage">view</field>
diff --git a/addons/stock/models/stock_location.py b/addons/stock/models/stock_location.py
index e1a1968c2bd..50e0e087ce8 100644
--- a/addons/stock/models/stock_location.py
+++ b/addons/stock/models/stock_location.py
@@ -200,7 +200,7 @@ class Route(models.Model):
@api.onchange('warehouse_selectable')
def _onchange_warehouse_selectable(self):
if not self.warehouse_selectable:
- self.warehouse_ids = []
+ self.warehouse_ids = [(5, 0, 0)]
def toggle_active(self):
for route in self:
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 8ef7bf53fab..254d34f0c6d 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1137,7 +1137,7 @@ class StockMove(models.Model):
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
if len(result_package.quant_ids.mapped('location_id')) > 1:
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
- picking = moves_todo and moves_todo[0].picking_id or False
+ picking = moves_todo.mapped('picking_id')
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
moves_todo.mapped('move_dest_ids')._action_assign()
diff --git a/addons/stock/models/stock_quant.py b/addons/stock/models/stock_quant.py
index ad720afe45c..db3ded04691 100644
--- a/addons/stock/models/stock_quant.py
+++ b/addons/stock/models/stock_quant.py
@@ -647,5 +647,5 @@ class QuantPackage(models.Model):
for quant in self._get_contained_quants():
if quant.product_id not in res:
res[quant.product_id] = 0
- res[quant.product_id] += quant.qty
+ res[quant.product_id] += quant.quantity
return res
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 8f87c56dc37..a5c34bd3678 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -380,7 +380,7 @@
<separator/>
<filter name="late" string="Late" domain="[('scheduled_date','&lt;', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Pickings that are late on scheduled time"/>
<separator/>
- <filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False)]" help="Remaining parts of picking partially processed"/>
+ <filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Remaining parts of picking partially processed"/>
<separator/>
<filter string="My Activities" name="activities_my"
domain="[('activity_ids.user_id', '=', uid)]"/>
@@ -566,7 +566,6 @@ action = action_values
'default_picking_type_id': active_id,
'contact_display': 'partner_address',
'search_default_late': 1,
- 'search_default_confirmed': 1,
}
</field>
<field name="search_view_id" ref="view_picking_internal_search"/>
@@ -589,7 +588,6 @@ action = action_values
'default_picking_type_id': active_id,
'contact_display': 'partner_address',
'search_default_backorder': 1,
- 'search_default_confirmed': 1,
}
</field>
<field name="search_view_id" ref="view_picking_internal_search"/>
diff --git a/addons/survey/views/assets.xml b/addons/survey/views/assets.xml
index 381531bfd41..59dd9448fd6 100644
--- a/addons/survey/views/assets.xml
+++ b/addons/survey/views/assets.xml
@@ -13,6 +13,7 @@
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
+ <link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
</template>
<template id="survey_report_assets_pdf" inherit_id="web.report_assets_pdf">
diff --git a/addons/uom/models/uom_uom.py b/addons/uom/models/uom_uom.py
index 77465058468..6907a9fb890 100644
--- a/addons/uom/models/uom_uom.py
+++ b/addons/uom/models/uom_uom.py
@@ -84,9 +84,8 @@ class UoM(models.Model):
self._cr.execute("""
SELECT C.id AS category_id, count(U.id) AS uom_count
FROM uom_category C
- LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference'
+ LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't'
WHERE C.id IN %s
- AND U.active = 't'
GROUP BY C.id
""", (tuple(category_ids),))
for uom_data in self._cr.dictfetchall():
diff --git a/addons/uom/tests/test_uom.py b/addons/uom/tests/test_uom.py
index 5979fa57d36..26cbb5ef66a 100644
--- a/addons/uom/tests/test_uom.py
+++ b/addons/uom/tests/test_uom.py
@@ -68,6 +68,16 @@ class TestUom(TransactionCase):
'name': 'Custom UoM category',
})
+ # at first we can not create a non reference in custom category
+ with self.assertRaises(ValidationError):
+ self.env['uom.uom'].create({
+ 'name': 'Bigger UoM of my category',
+ 'factor_inv': 42,
+ 'uom_type': 'bigger',
+ 'rounding': 0.5,
+ 'category_id': category.id
+ })
+
# create the reference
self.env['uom.uom'].create({
'name': 'Reference UoM of my category',
diff --git a/addons/web/static/src/js/chrome/action_manager_act_window.js b/addons/web/static/src/js/chrome/action_manager_act_window.js
index ee3ce15bdd2..c19364fa7a9 100644
--- a/addons/web/static/src/js/chrome/action_manager_act_window.js
+++ b/addons/web/static/src/js/chrome/action_manager_act_window.js
@@ -372,7 +372,7 @@ ActionManager.include({
Widget: View,
});
} else if (config.isDebug('assets')) {
- console.error("View type '" + viewType + "' is not present in the view registry.");
+ console.log("View type '" + viewType + "' is not present in the view registry.");
}
});
return views;
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index beb7116169a..14f03a83222 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1526,6 +1526,7 @@ var AbstractFieldBinary = AbstractField.extend({
*/
_clearFile: function (){
var self = this;
+ this.$('.o_input_file').val('');
this.set_filename('');
if (!this.isDestroyed()) {
this._setValue(false).then(function() {
diff --git a/addons/web/static/src/js/views/qweb/qweb_view.js b/addons/web/static/src/js/views/qweb/qweb_view.js
index 9e79ecf19d2..45c7340d5c1 100644
--- a/addons/web/static/src/js/views/qweb/qweb_view.js
+++ b/addons/web/static/src/js/views/qweb/qweb_view.js
@@ -65,8 +65,9 @@ var Model = AbstractModel.extend({
/**
* reload
*/
- reload: function (_id, _params) {
- // FIXME: update this._state or something?
+ reload: function (_id, params) {
+ _.extend(this._state, _.pick(params, ['domain', 'context']));
+
return this._fetch();
}
});
diff --git a/addons/web/static/src/scss/layout_boxed.scss b/addons/web/static/src/scss/layout_boxed.scss
index 1916725f812..8b2d555365e 100644
--- a/addons/web/static/src/scss/layout_boxed.scss
+++ b/addons/web/static/src/scss/layout_boxed.scss
@@ -1,5 +1,5 @@
.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
- font-family: 'Lato' 'Noto';
+ font-family: 'Lato', 'Noto';
color: gray('700');
font-size: 15px;
}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 5c399547458..4e5deb56780 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1998,6 +1998,36 @@ QUnit.module('basic_fields', {
list.destroy();
});
+ QUnit.test("binary fields input value is empty whean clearing after uploading", async function (assert) {
+ assert.expect(2);
+
+ var form = await createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" filename="foo"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ await testUtils.form.clickEdit(form);
+
+ // // We need to convert the input type since we can't programmatically set the value of a file input
+ form.$('.o_input_file').attr('type', 'text').val('coucou.txt');
+
+ assert.strictEqual(form.$('.o_input_file').val(), 'coucou.txt',
+ "input value should be changed to \"coucou.txt\"");
+
+ await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button'));
+
+ assert.strictEqual(form.$('.o_input_file').val(), '',
+ "input value should be empty");
+
+ form.destroy();
+ });
+
QUnit.test('field text in editable list view', async function (assert) {
assert.expect(1);
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 34838401607..3d6c72fa314 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -99,7 +99,7 @@ class Website(Home):
if request.env['res.users'].browse(request.uid).has_group('base.group_user'):
redirect = b'/web?' + request.httprequest.query_string
else:
- redirect = '/'
+ redirect = '/my'
return http.redirect_with_hash(redirect)
return response
@@ -471,3 +471,8 @@ class WebsiteBinary(http.Controller):
if unique:
kw['unique'] = unique
return Binary().content_image(**kw)
+
+ @http.route(['/favicon.ico'], type='http', auth='public', website=True)
+ def favicon(self, **kw):
+ # when opening a pdf in chrome, chrome tries to open the default favicon url
+ return self.content_image(model='website', id=str(request.website.id), field='favicon', **kw)
diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml
index 1d50c7bf704..92d79ffd03d 100644
--- a/addons/website/data/website_data.xml
+++ b/addons/website/data/website_data.xml
@@ -1,27 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
- <data noupdate="1">
- <!-- Default Menu to store module menus for new website -->
- <record id="main_menu" model="website.menu">
- <field name="name">Default Main Menu</field>
- <field name="url">/default-main-menu</field>
- </record>
- <record id="menu_home" model="website.menu">
- <field name="name">Home</field>
- <field name="url">/</field>
- <field name="parent_id" ref="website.main_menu"/>
- <field name="sequence" type="int">10</field>
- </record>
- <record id="menu_contactus" model="website.menu">
- <field name="name">Contact us</field>
- <field name="url">/contactus</field>
- <field name="parent_id" ref="website.main_menu"/>
- <field name="sequence" type="int">60</field>
- </record>
-
+ <data>
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
-
- <!-- Homepage -->
<record id="homepage" model="ir.ui.view">
<field name="name">Home</field>
<field name="type">qweb</field>
@@ -35,13 +15,7 @@
</t>
</field>
</record>
- <record id="homepage_page" model="website.page">
- <field name="website_published">True</field>
- <field name="url">/</field>
- <field name="view_id" ref="homepage"/>
- </record>
- <!-- Contact Us page -->
<record id="contactus" model="ir.ui.view">
<field name="name">Contact Us</field>
<field name="type">qweb</field>
@@ -93,13 +67,7 @@
</t>
</field>
</record>
- <record id="contactus_page" model="website.page">
- <field name="url">/contactus</field>
- <field name="website_published">True</field>
- <field name="view_id" ref="contactus"/>
- </record>
- <!-- About Us page -->
<record id="aboutus" model="ir.ui.view">
<field name="name">About us</field>
<field name="type">qweb</field>
@@ -145,12 +113,43 @@
</t>
</field>
</record>
+ </data>
+
+ <data noupdate="1">
+ <record id="homepage_page" model="website.page">
+ <field name="website_published">True</field>
+ <field name="url">/</field>
+ <field name="view_id" ref="homepage"/>
+ </record>
+ <record id="contactus_page" model="website.page">
+ <field name="url">/contactus</field>
+ <field name="website_published">True</field>
+ <field name="view_id" ref="contactus"/>
+ </record>
<record id="aboutus_page" model="website.page">
<field name="website_published">True</field>
<field name="url">/aboutus</field>
<field name="view_id" ref="aboutus"/>
</record>
+ <!-- Default Menu to store module menus for new website -->
+ <record id="main_menu" model="website.menu">
+ <field name="name">Default Main Menu</field>
+ <field name="url">/default-main-menu</field>
+ </record>
+ <record id="menu_home" model="website.menu">
+ <field name="name">Home</field>
+ <field name="url">/</field>
+ <field name="parent_id" ref="website.main_menu"/>
+ <field name="sequence" type="int">10</field>
+ </record>
+ <record id="menu_contactus" model="website.menu">
+ <field name="name">Contact us</field>
+ <field name="url">/contactus</field>
+ <field name="parent_id" ref="website.main_menu"/>
+ <field name="sequence" type="int">60</field>
+ </record>
+
<record id="default_website" model="website">
<field name="name">My Website</field>
<field name="domain"></field>
diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py
index 892916b2a96..1091e43c73f 100644
--- a/addons/website/models/ir_ui_view.py
+++ b/addons/website/models/ir_ui_view.py
@@ -82,7 +82,8 @@ class View(models.Model):
# original tree. Indeed, the order of children 'id' fields
# must remain the same so that the inheritance is applied
# in the same order in the copied tree.
- inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
+ child = inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
+ inherit_child.inherit_children_ids.write({'inherit_id': child.id})
inherit_child.unlink()
else:
# Trigger COW on inheriting views
diff --git a/addons/website/models/website.py b/addons/website/models/website.py
index 5202798fb97..8f32c91242f 100644
--- a/addons/website/models/website.py
+++ b/addons/website/models/website.py
@@ -154,7 +154,7 @@ class Website(models.Model):
self._get_languages.clear_cache(self)
if 'company_id' in values and 'user_id' not in values:
- public_user_to_change_websites = self.filtered(lambda w: w.user_id.company_id.id != values['company_id'])
+ public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id'])
if public_user_to_change_websites:
company = self.env['res.company'].browse(values['company_id'])
super(Website, public_user_to_change_websites).write(dict(values, user_id=company._get_public_user().id))
diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py
index bb847ba9f6a..0d24a2517ad 100644
--- a/addons/website/tests/test_views.py
+++ b/addons/website/tests/test_views.py
@@ -797,6 +797,54 @@ class TestCowViewSaving(common.TransactionCase):
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
+ def test_multiple_inherit_level(self):
+ """ Test multi-level inheritance:
+ Base
+ |
+ ---> Extension (Website-specific)
+ |
+ ---> Extension 2 (Website-specific)
+ """
+ View = self.env['ir.ui.view']
+
+ self.inherit_view.website_id = 1
+ inherit_view_2 = View.create({
+ 'name': 'Extension 2',
+ 'mode': 'extension',
+ 'inherit_id': self.inherit_view.id,
+ 'arch': '<div position="inside">, extended content 2</div>',
+ 'key': 'website.extension_view_2',
+ 'website_id': 1,
+ })
+
+ total_views = View.search_count([])
+
+ # id | name | content | website_id | inherit | key
+ # --------------------------------------------------------------------------------------------
+ # 1 | Base | base content | / | / | website.base_view
+ # 2 | Extension | , extended content | 1 | 1 | website.extension_view
+ # 3 | Extension 2 | , extended content 2 | 1 | 2 | website.extension_view_2
+
+ self.base_view.with_context(website_id=1).write({'arch': '<div>modified content</div>'})
+
+ # 2 views are created, one is deleted
+ self.assertEqual(View.search_count([]), total_views + 1)
+ self.assertFalse(self.inherit_view.exists())
+ self.assertTrue(inherit_view_2.exists())
+
+ # Verify the inheritance
+ base_specific = View.search([('key', '=', self.base_view.key), ('website_id', '=', 1)]).with_context(load_all_views=True)
+ extend_specific = View.search([('key', '=', 'website.extension_view'), ('website_id', '=', 1)])
+ self.assertEqual(extend_specific.inherit_id, base_specific)
+ self.assertEqual(inherit_view_2.inherit_id, extend_specific)
+
+ # id | name | content | website_id | inherit | key
+ # --------------------------------------------------------------------------------------------
+ # 1 | Base | base content | / | / | website.base_view
+ # 4 | Base | modified content | 1 | / | website.base_view
+ # 5 | Extension | , extended content | 1 | 4 | website.extension_view
+ # 3 | Extension 2 | , extended content 2 | 1 | 5 | website.extension_view_2
+
class Crawler(HttpCase):
def setUp(self):
diff --git a/addons/website_event/views/event_templates.xml b/addons/website_event/views/event_templates.xml
index 36228dfb580..287e1a35b76 100644
--- a/addons/website_event/views/event_templates.xml
+++ b/addons/website_event/views/event_templates.xml
@@ -218,6 +218,10 @@
</div>
</div>
<t t-raw="0"/>
+ <div class="alert alert-info alert-dismissable mt16 css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status">
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">&amp;times;</button>
+ Following content will appear on all events
+ </div>
<div class="oe_structure" id="oe_structure_website_event_layout_1"/>
</div>
</t>
diff --git a/addons/website_mail/models/mail_message.py b/addons/website_mail/models/mail_message.py
index 683990e1e67..911571787cf 100644
--- a/addons/website_mail/models/mail_message.py
+++ b/addons/website_mail/models/mail_message.py
@@ -59,7 +59,10 @@ class MailMessage(models.Model):
if self.user_has_groups('base.group_public'):
self.env.cr.execute('SELECT id FROM "%s" WHERE website_published IS FALSE AND id = ANY (%%s)' % (self._table), (self.ids,))
if self.env.cr.fetchall():
- raise AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation))
+ raise AccessError(
+ _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
+ + ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
+ )
return super(MailMessage, self).check_access_rule(operation=operation)
@api.multi
diff --git a/addons/website_profile/views/website_profile.xml b/addons/website_profile/views/website_profile.xml
index d03428d9287..8ea869d5455 100644
--- a/addons/website_profile/views/website_profile.xml
+++ b/addons/website_profile/views/website_profile.xml
@@ -258,8 +258,8 @@
<button class="btn btn-sm d-md-none bg-white border" type="button" data-toggle="collapse" data-target="#o_wprofile_sidebar_collapse" aria-expanded="false" aria-controls="o_wprofile_sidebar_collapse">More info</button>
</div>
<div class="collapse d-md-block" id="o_wprofile_sidebar_collapse">
- <div t-if="user.next_rank_id" class="mb-3">
- <small class="font-weight-bold mt-1">Next rank:</small>
+ <div t-if="user.rank_id" class="mb-3">
+ <small t-if="user.next_rank_id" class="font-weight-bold mt-1">Next rank:</small>
<t t-call="website_profile.profile_next_rank_card">
<t t-set="img_max_width">40%</t>
</t>
@@ -313,13 +313,18 @@
</template>
<template id="profile_next_rank_card" name="Profile Next Rank Card">
- <div t-if="user.next_rank_id" class="o_wprofile_progress_circle">
+ <div class="o_wprofile_progress_circle">
<svg viewBox="0 0 36 36" class="o_pc_circular_chart">
- <t t-if="(user.next_rank_id.karma_min - user.rank_id.karma_min) > 0">
- <t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))"/>
+ <t t-if="user.next_rank_id">
+ <t t-if="(user.next_rank_id.karma_min - user.rank_id.karma_min) > 0">
+ <t t-set="user_points" t-value="int(100*(user.karma - user.rank_id.karma_min)/(user.next_rank_id.karma_min - user.rank_id.karma_min))"/>
+ </t>
+ <t t-else="">
+ <t t-set="user_points" t-value="0"/>
+ </t>
</t>
<t t-else="">
- <t t-set="user_points" t-value="0"/>
+ <t t-set="user_points" t-value="100"/>
</t>
<path class="o_pc_circle_bg" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" />
<path class="o_pc_circle" t-attf-stroke-dasharray="#{user_points}, 100" d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" stroke="url(#gradient)" mask="url(#mask)"/>
@@ -333,12 +338,20 @@
</svg>
<div class="o_pc_overlay d-flex flex-column align-items-center justify-content-center">
<img class="img-fluid"
- t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id}/image_medium"
- t-att-alt="user.next_rank_id.name + ' badge'"
+ t-attf-src="/web/image/gamification.karma.rank/#{user.next_rank_id.id if user.next_rank_id else user.rank_id.id}/image_medium"
+ t-att-alt="(user.next_rank_id.name if user.next_rank_id else user.rank_id.name) + ' badge'"
t-att-style="'max-width: ' + (img_max_width if img_max_width else '50%;')"/>
- <h4 class=" mb-0">"<span t-field="user.next_rank_id.name"/>"</h4>
+ <h4 class=" mb-0">
+ "
+ <span t-if="user.next_rank_id" t-field="user.next_rank_id.name"/>
+ <span t-else="" t-field="user.rank_id.name"/>
+ "
+ </h4>
<small>
- <span class="font-weight-bold text-primary" t-field="user.karma"/>/<span class="font-weight-bold" t-field="user.next_rank_id.karma_min"/> xp
+ <span class="font-weight-bold text-primary" t-field="user.karma"/>/
+ <span t-if="user.next_rank_id" class="font-weight-bold" t-field="user.next_rank_id.karma_min"/>
+ <span t-else="" class="font-weight-bold" t-field="user.rank_id.karma_min"/>
+ xp
</small>
</div>
</div>
diff --git a/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js b/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js
new file mode 100644
index 00000000000..a1093ca98ee
--- /dev/null
+++ b/addons/website_sale_delivery/static/src/js/website_free_delivery_tour.js
@@ -0,0 +1,34 @@
+odoo.define('website_sale_delivery.tour', function (require) {
+'use strict';
+
+var tour = require("web_tour.tour");
+
+tour.register('check_free_delivery', {
+ test: true,
+ url: '/shop?search=conference chair',
+},
+ [
+ {
+ content: "select conference chair",
+ trigger: '.oe_product_cart:first a:contains("Conference Chair")',
+ },
+ {
+ content: "click on add to cart",
+ extra_trigger: 'label:contains(Steel) input:propChecked',
+ trigger: '#product_detail form[action^="/shop/cart/update"] .btn-primary',
+ },
+ {
+ content: "click in modal on 'Proceed to checkout' button",
+ trigger: 'button:contains("Proceed to Checkout")',
+ },
+ {
+ content: "go to checkout",
+ extra_trigger: '#cart_products input.js_quantity:propValue(1)',
+ trigger: 'a[href*="/shop/checkout"]',
+ },
+ {
+ content: "Check Free Delivery value to be zero",
+ trigger: "#delivery_carrier span:contains('0.0')"
+ },
+ ]);
+});
diff --git a/addons/website_sale_delivery/tests/__init__.py b/addons/website_sale_delivery/tests/__init__.py
new file mode 100644
index 00000000000..2235dde4188
--- /dev/null
+++ b/addons/website_sale_delivery/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_ui
diff --git a/addons/website_sale_delivery/tests/test_ui.py b/addons/website_sale_delivery/tests/test_ui.py
new file mode 100644
index 00000000000..1c61c00b9fb
--- /dev/null
+++ b/addons/website_sale_delivery/tests/test_ui.py
@@ -0,0 +1,14 @@
+import odoo.tests
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+@odoo.tests.tagged('post_install', '-at_install')
+class TestUi(odoo.tests.HttpCase):
+
+ def test_01_free_delivery_when_exceed_threshold(self):
+ self.env.ref("delivery.free_delivery_carrier").write({
+ 'fixed_price': 2,
+ 'free_over': True,
+ 'amount': 10,
+ })
+ self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('check_free_delivery')", "odoo.__DEBUG__.services['web_tour.tour'].tours.check_free_delivery.ready", login="admin")
diff --git a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
index 711c11c936f..201fe46d843 100644
--- a/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
+++ b/addons/website_sale_delivery/views/website_sale_delivery_templates.xml
@@ -19,6 +19,7 @@
<xpath expr="." position="inside">
<script type="text/javascript" src="/website_sale_delivery/static/src/js/website_sale_delivery.js"></script>
<link rel="stylesheet" type="text/scss" href="/website_sale_delivery/static/src/scss/website_sale_delivery.scss"/>
+ <script type="text/javascript" src="/website_sale_delivery/static/src/js/website_free_delivery_tour.js"></script>
</xpath>
</template>
@@ -28,11 +29,9 @@
<t t-set='badge_class' t-value="(delivery_nb != 1 and 'float-right ' or '') + 'badge badge-secondary'" />
<t t-if="delivery.delivery_type == 'fixed'">
<span t-if="delivery.fixed_price > 0.0" t-att-class="badge_class">
- <t t-set="monetary_options" t-value='{"widget": "monetary",
- "from_currency": delivery.product_id.company_id.currency_id,
- "display_currency": website_sale_order.currency_id}'/>
- <t t-if="delivery.free_over" t-esc="delivery.rate_shipment(website_sale_order)['price']" t-options='monetary_options'/>
- <t t-else="" t-esc="delivery.fixed_price" t-options='monetary_options'/>
+ <t t-esc="delivery.rate_shipment(website_sale_order)['price'] if delivery.free_over else delivery.fixed_price"
+ t-options='{"widget": "monetary", "from_currency": delivery.product_id.company_id.currency_id,
+ "display_currency": website_sale_order.currency_id}'/>
</span>
<span t-else="" t-att-class="badge_class">Free</span>
</t>
diff --git a/addons/website_sale_product_configurator/static/src/js/website_sale_options.js b/addons/website_sale_product_configurator/static/src/js/website_sale_options.js
index 18558954c0c..1e368e0d9a8 100644
--- a/addons/website_sale_product_configurator/static/src/js/website_sale_options.js
+++ b/addons/website_sale_product_configurator/static/src/js/website_sale_options.js
@@ -43,11 +43,20 @@ publicWidget.registry.WebsiteSale.include({
* @param {integer} quantity
*/
_onOptionsUpdateQuantity: function (quantity) {
- this.$form
+ var $qtyInput = this.$form
.find('.js_main_product input[name="add_qty"]')
- .first()
- .val(quantity)
- .trigger('change');
+ .first();
+
+ if ($qtyInput.length) {
+ $qtyInput.val(quantity).trigger('change');
+ } else {
+ // This handles the case when the "Select Quantity" customize show
+ // is disabled, and therefore the above selector does not find an
+ // element.
+ // To avoid duplicating all RPC, only trigger the variant change if
+ // it is not already done from the above trigger.
+ this.optionalProductsModal.triggerVariantChange(this.optionalProductsModal.$el);
+ }
},
/**
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index c7e36539ce7..2eada6aee13 100644
--- a/addons/website_slides/controllers/main.py
+++ b/addons/website_slides/controllers/main.py
@@ -268,6 +268,7 @@ class WebsiteSlides(WebsiteProfile):
'channels_newest': channels_newest,
'achievements': achievements,
'users': users,
+ 'top3_users': self._get_top3_users(),
'challenges': challenges,
'challenges_done': challenges_done,
})
@@ -311,6 +312,7 @@ class WebsiteSlides(WebsiteProfile):
'search_my': my,
'search_tags': search_tags,
'search_channel_tag_id': post.get('channel_tag_id'),
+ 'top3_users': self._get_top3_users(),
})
return request.render('website_slides.courses_all', values)
@@ -318,6 +320,11 @@ class WebsiteSlides(WebsiteProfile):
def _prepare_additional_channel_values(self, values, **kwargs):
return values
+ def _get_top3_users(self):
+ return request.env['res.users'].sudo().search_read([
+ ('karma', '>', 0),
+ ('image', '!=', False)], ['id'], limit=3, order='karma desc')
+
@http.route([
'/slides/<model("slide.channel"):channel>',
'/slides/<model("slide.channel"):channel>/page/<int:page>',
@@ -374,6 +381,7 @@ class WebsiteSlides(WebsiteProfile):
values = {
'channel': channel,
+ 'active_tab': kw.get('active_tab', 'home'),
# search
'search_category': category,
'search_tag': tag,
diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py
index 2d2404c65c9..22029420ec5 100644
--- a/addons/website_slides/models/slide_slide.py
+++ b/addons/website_slides/models/slide_slide.py
@@ -668,6 +668,6 @@ class Slide(models.Model):
res = super(Slide, self)._default_website_meta()
res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name
res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.description
- res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = "/web/image/slide.slide/%s/image_small" % (self.id)
+ res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = "/web/image/slide.slide/%s/image" % (self.id)
res['default_meta_description'] = self.description
return res
diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml
index 0f1f3626c7e..645ecae94cd 100644
--- a/addons/website_slides/static/src/xml/slide_quiz.xml
+++ b/addons/website_slides/static/src/xml/slide_quiz.xml
@@ -8,7 +8,7 @@
</div>
<div t-foreach="widget.quiz.questions" t-as="question"
- t-attf-class="o_wslides_js_lesson_quiz_question my-3 #{widget.readonly ? 'disabled' : ''} #{widget.slide.completed ? 'completed-disabled' : ''}"
+ t-attf-class="o_wslides_js_lesson_quiz_question mt-3 mb-4 #{widget.readonly ? 'disabled' : ''} #{widget.slide.completed ? 'completed-disabled' : ''}"
t-att-data-question-id="question.id" t-att-data-title="question.question">
<div class="h4">
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/>
diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml
index 8fc2281ec4f..8aeb4df9749 100644
--- a/addons/website_slides/views/website_slides_templates_course.xml
+++ b/addons/website_slides/views/website_slides_templates_course.xml
@@ -163,20 +163,20 @@
<div class="col-12 col-md-8 col-lg-9">
<ul class="nav nav-tabs o_wslides_nav_tabs flex-nowrap" role="tablist" id="profile_extra_info_tablist">
<li class="nav-item o_wslides_course_header_nav_home">
- <a t-att-class="'nav-link %s' % ('' if search_slide_type else 'active')" id="home-tab" data-toggle="pill" href="#home" role="tab" aria-controls="home" aria-selected="true">
+ <a t-att-class="'nav-link %s' % ('active' if active_tab == 'home' else '')" id="home-tab" data-toggle="pill" href="#home" role="tab" aria-controls="home" t-att-aria-selected="'true' if active_tab == 'home' else 'false'">
<i class="fa fa-home"/> Course
</a>
</li>
<li t-if="channel.allow_comment" class="nav-item">
- <a class="nav-link" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" aria-selected="false">Review</a>
+ <a t-att-class="'nav-link %s' % ('active' if active_tab == 'review' else '')" id="review-tab" data-toggle="pill" href="#review" role="tab" aria-controls="review" t-att-aria-selected="'true' if active_tab == 'review' else 'false'">Review</a>
</li>
</ul>
<div class="tab-content py-4 o_wslides_tabs_content mb-4" id="courseMainTabContent">
- <div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
+ <div t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'home' else '')" id="home" role="tabpanel" aria-labelledby="home-tab">
<t t-if="channel.channel_type == 'training'" t-call="website_slides.course_slides_list"/>
</div>
- <div t-if="channel.allow_comment" class="tab-pane fade" id="review" role="tabpanel" aria-labelledby="review-tab">
+ <div t-if="channel.allow_comment" t-att-class="'tab-pane fade %s' % ('show active' if active_tab == 'review' else '')" id="review" role="tabpanel" aria-labelledby="review-tab">
<t t-call="portal.message_thread">
<t t-set="object" t-value="channel"/>
<t t-set="hash" t-value="message_post_hash"/>
diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml
index 59db5c942b0..1cf83c02b13 100644
--- a/addons/website_slides/views/website_slides_templates_homepage.xml
+++ b/addons/website_slides/views/website_slides_templates_homepage.xml
@@ -1,11 +1,34 @@
<?xml version="1.0" ?>
<odoo><data>
+<template id='courses_footer'>
+ <!-- 'users': 3 top users with picture (skip users without picture) -->
+ <section class="s_banner bg-900" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
+ <div class="container py-5">
+ <div class="row align-items-center">
+ <div t-attf-class="col-12 #{'col-md' if top3_users else 'text-center'}">
+ <h2 class="my-4 text-white">Meet The Heros Who Transformed Their Company!</h2>
+ <a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
+ </div>
+ <div class="col-12 col-md-5" t-if="top3_users">
+ <div class="ml-4">
+ <t t-foreach="top3_users" t-as="user">
+ <img class="rounded-circle img-fluid shadow ml-n4"
+ style="border: 6px solid white"
+ t-attf-src="/web/image/res.users/#{user['id']}/image_medium"/>
+ </t>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+</template>
+
<!-- Channels home template -->
<template id='courses_home' name="Odoo Courses Homepage">
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
- <div class="wrap o_wslides_wrap">
+ <div id="wrap" class="wrap o_wslides_wrap">
<section class="s_banner overflow-hidden bg-900" style="background-image: url(&quot;/website_slides/static/src/img/banner_default.svg&quot;); background-size: cover; background-position: 55% 65%">
<div class="container align-items-center d-flex mb-5 mt-lg-5 pt-lg-4 pb-lg-1">
<div>
@@ -146,18 +169,7 @@
</div>
</div>
</div>
- <section class="s_banner" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
- <div class="container py-5">
- <div class="row align-items-center">
- <div class="col-12 col-md">
- <h2 class="my-4 text-white">Meet the people who transformed their company</h2>
- </div>
- <div class="col-12 col-md-4">
- <a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
- </div>
- </div>
- </div>
- </section>
+ <t t-call="website_slides.courses_footer"></t>
</div>
</t>
</template>
@@ -264,18 +276,7 @@
</div>
</div>
- <section class="s_banner bg-900" style="background-image: url(/website_slides/static/src/img/banner_heroes_default.svg); background-size: cover; background-position: right bottom">
- <div class="container py-5">
- <div class="row align-items-center">
- <div class="col-12 col-md">
- <h2 class="my-4">Meet the people who transformed their company</h2>
- </div>
- <div class="col-12 col-md-4">
- <a href="/profile/users" class="btn btn-primary btn-lg">Go meet them</a>
- </div>
- </div>
- </div>
- </section>
+ <t t-call="website_slides.courses_footer"></t>
</div>
</t>
</template>
@@ -395,13 +396,13 @@
<img t-attf-src="/web/image/gamification.karma.rank/#{user.rank_id.id}/image_small" height="16" alt="" class="mr-1"/>
<a href="/profile/ranks_badges" t-field="user.rank_id"/>
</div>
- <t t-if="user.next_rank_id">
- <div class="font-weight-bold text-muted mt-1">Next rank:</div>
+ <t t-if="user.rank_id">
+ <div t-if="user.next_rank_id" class="font-weight-bold text-muted mt-1">Next rank:</div>
<t t-call="website_profile.profile_next_rank_card">
<t t-set="bg_class">bg-200</t>
<t t-set="img_max_width">50%</t>
</t>
- <div t-field="user.next_rank_id.description_motivational"/>
+ <div t-field="user.rank_id.description_motivational"/>
</t>
</div>
</template>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 6676cc4932f..0853c8dccb9 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -6,7 +6,7 @@
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
<t t-set="main_object" t-value="slide"/>
- <div class="wrap o_wslides_wrap">
+ <div id="wrap" class="wrap o_wslides_wrap">
<div class="o_wslides_lesson_header o_wslides_gradient position-relative text-white pb-0 pt-2 pt-md-5">
<t t-call="website_slides.course_nav">
<t t-set="channel" t-value="slide.channel_id"/>
@@ -185,12 +185,12 @@
<div class="row align-items-center my-3">
<div class="col-12 col-md order-2 order-md-1 d-flex">
<div class="d-flex align-items-center">
- <h4 class="my-0">
+ <h1 class="h5 my-0">
<t t-call="website_slides.slide_icon">
<t t-set="icon_class">mr-1</t>
</t>
<span t-field="slide.name"/>
- </h4>
+ </h1>
<span t-if="slide.question_ids"
t-att-class="'ml-2 badge %s' % ('badge-success' if channel_progress[slide.id].get('completed') else 'badge-info')">
<span t-if="channel_progress[slide.id].get('completed')">
@@ -427,7 +427,7 @@
t-att-data-has-next="1 if next_slide else 0"
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
<div t-foreach="slide_questions" t-as="question"
- t-att-class="'o_wslides_js_lesson_quiz_question mt-3 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
+ t-att-class="'o_wslides_js_lesson_quiz_question mt-3 mb-4 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
<div class="h4">
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
</div>
diff --git a/addons/website_slides_forum/views/website_slides_forum_templates.xml b/addons/website_slides_forum/views/website_slides_forum_templates.xml
index 16102ecee1f..ed0e278479a 100644
--- a/addons/website_slides_forum/views/website_slides_forum_templates.xml
+++ b/addons/website_slides_forum/views/website_slides_forum_templates.xml
@@ -9,13 +9,13 @@
<li class="nav-item">
<a t-att-href="'/slides/%s%s' % (slug(forum.slide_channel_id), '/%s' % slug(category) if category else '')"
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">
- <i class="fa fa-home"/> Home</a>
+ <i class="fa fa-home"/> Course</a>
</li>
<li class="nav-item">
<a href="#" t-att-class="'nav-link active o_wprofile_navlink'" style="border-left: 0px">Forum</a>
</li>
<li class="nav-item">
- <a t-att-href="'/slides/%s' % (slug(forum.slide_channel_id))"
+ <a t-att-href="'/slides/%s?active_tab=review' % (slug(forum.slide_channel_id))"
t-att-class="'nav-link o_wprofile_navlink'" style="border-left: 0px">Review</a>
</li>
</ul>