[MERGE] forward port branch saas-12.2 up to 243a4ae5cb

This commit is contained in:
Christophe Simonis
2019-05-13 11:02:27 +02:00
80 changed files with 726 additions and 272 deletions
+92 -31
View File
@@ -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,71 @@ 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, st_line.amount)
move_vals['line_ids'].append((0, 0, balance_line))
move_list.append(move_vals)
# Creates
payment_ids = self.env['account.payment'].create(payment_list)
for payment_id, move_vals in zip(payment_ids, move_list):
for line in move_vals['line_ids']:
line[2]['payment_id'] = payment_id.id
move_ids = self.env['account.move'].create(move_list)
move_ids.post()
for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
st_line.write({'move_name': move.name})
payment.write({'payment_reference': move.name})
def _get_communication(self, payment_method_id):
return self.name or ''
@@ -630,8 +683,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 +705,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 +760,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'})
+15 -14
View File
@@ -87,12 +87,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
@@ -832,22 +834,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
@@ -7,5 +7,8 @@
<record id="base.mx" model="res.country">
<field eval="'%(street_name)s %(street_number)s/%(street_number2)s'" name="street_format" />
</record>
<record id="base.hr" model="res.country">
<field eval="'%(street_name)s %(street_number)s %(street_number2)s'" name="street_format" />
</record>
</data>
</odoo>
@@ -6,7 +6,7 @@ import re
from odoo import api, fields, models, _
from odoo.exceptions import UserError
STREET_FIELDS = ('street_name', 'street_number', 'street_number2')
STREET_FIELDS = ['street_name', 'street_number', 'street_number2']
class ResCountry(models.Model):
@@ -33,6 +33,10 @@ class Partner(models.Model):
street_number2 = fields.Char('Door', compute='_split_street', help="Door Number",
inverse='_set_street', store=True)
def _formatting_address_fields(self):
"""Returns the list of address fields usable to format addresses."""
return super(Partner, self)._formatting_address_fields() + self.get_street_fields()
def get_street_fields(self):
"""Returns the fields that can be used in a street format.
Overwrite this function if you want to add your own fields."""
+14
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import codecs
import collections
import unicodedata
@@ -32,6 +33,12 @@ DEFAULT_IMAGE_REGEX = r"(?:http|https)://.*(?:png|jpe?g|tiff?|gif|bmp)"
DEFAULT_IMAGE_CHUNK_SIZE = 32768
IMAGE_FIELDS = ["icon", "image", "logo", "picture"]
_logger = logging.getLogger(__name__)
BOM_MAP = {
'utf-16le': codecs.BOM_UTF16_LE,
'utf-16be': codecs.BOM_UTF16_BE,
'utf-32le': codecs.BOM_UTF32_LE,
'utf-32be': codecs.BOM_UTF32_BE,
}
try:
import xlrd
@@ -323,6 +330,13 @@ class Import(models.TransientModel):
encoding = options.get('encoding')
if not encoding:
encoding = options['encoding'] = chardet.detect(csv_data)['encoding'].lower()
# some versions of chardet (e.g. 2.3.0 but not 3.x) will return
# utf-(16|32)(le|be), which for python means "ignore / don't strip
# BOM". We don't want that, so rectify the encoding to non-marked
# IFF the guessed encoding is LE/BE and csv_data starts with a BOM
bom = BOM_MAP.get(encoding)
if bom and csv_data.startswith(bom):
encoding = options['encoding'] = encoding[:-2]
if encoding != 'utf-8':
csv_data = csv_data.decode(encoding).encode('utf-8')
+12 -3
View File
@@ -26,18 +26,27 @@ class TestEncoding(ImportCase):
options.setdefault('separator', '\t')
test_text = "text\tnumber\tdate\tdatetime\n%s\t1.23.45,67\t\t\n" % text
for encoding in ['utf-8', 'utf-16', 'utf-32', *encodings]:
if isinstance(encoding, tuple):
encoding, es = encoding
else:
es = [encoding]
preview = self._make_import(
test_text.encode(encoding)).parse_preview(dict(options))
self.assertIsNone(preview.get('error'))
guessed = preview['options']['encoding']
self.assertIsNotNone(guessed, encoding)
self.assertEqual(codecs.lookup(guessed).name, codecs.lookup(encoding).name)
self.assertIsNotNone(guessed)
self.assertIn(
codecs.lookup(guessed).name, [
codecs.lookup(e).name
for e in es
]
)
def test_autodetect_encoding(self):
""" Check that import preview can detect & return encoding
"""
self._check_text("Iñtërnâtiônàlizætiøn", ['iso-8859-1'])
self._check_text("Iñtërnâtiônàlizætiøn", [('iso-8859-1', ['iso-8859-1', 'iso-8859-2'])])
self._check_text("やぶら小路の藪柑子。海砂利水魚の、食う寝る処に住む処、パイポパイポ パイポのシューリンガン。", ['eucjp', 'shift_jis', 'iso2022_jp'])
@@ -807,6 +807,12 @@ QUnit.test('save two searches to dashboard', async function (assert) {
await testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
await testUtils.dom.click(actionManager.$('.o_favorites_menu_button'));
$('.o_search_options .dropdown-menu.o_favorites_menu').one('click', function (ev) {
// This handler is on the webClient
// But since the test suite doesn't have one
// We manually set it here
ev.stopPropagation();
});
await testUtils.dom.click(actionManager.$('.o_add_to_board'));
await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
// Remove it
@@ -819,6 +825,12 @@ QUnit.test('save two searches to dashboard', async function (assert) {
await testUtils.dom.click(actionManager.$('.o_apply_filter'));
// Add it to dashboard
await testUtils.dom.click(actionManager.$('.o_favorites_menu_button'));
$('.o_search_options .dropdown-menu.o_favorites_menu').one('click', function (ev) {
// This handler is on the webClient
// But since the test suite doesn't have one
// We manually set it here
ev.stopPropagation();
});
await testUtils.dom.click(actionManager.$('.o_add_to_board'));
await testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
+3 -2
View File
@@ -406,8 +406,9 @@ class AlarmManager(models.AbstractModel):
all_meetings = self.get_next_potential_limit_alarm('notification', partner_id=partner.id)
time_limit = 3600 * 24 # return alarms of the next 24 hours
for meeting in self.env['calendar.event'].search([('id', 'in', list(all_meetings))]): # cannot browse
max_delta = all_meetings[meeting.id]['max_duration']
for event_id in all_meetings:
max_delta = all_meetings[event_id]['max_duration']
meeting = self.env['calendar.event'].browse(event_id)
if meeting.recurrency:
b_found = False
last_found = False
+1 -2
View File
@@ -26,7 +26,6 @@ class Message(models.Model):
def _find_allowed_model_wise(self, doc_model, doc_dict):
if doc_model == 'calendar.event':
order = self._context.get('order', self.env[doc_model]._order)
records = self.env[doc_model].search([('id', 'in', list(doc_dict))])
for virtual_id in records.get_recurrent_ids([], order=order):
for virtual_id in self.env[doc_model].with_context(active_test=False).search([('id', 'in', list(doc_dict))], order=order).ids:
doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
return super(Message, self)._find_allowed_model_wise(doc_model, doc_dict)
+2 -2
View File
@@ -42,7 +42,7 @@ class StockPicking(models.Model):
@api.one
@api.depends('move_line_ids')
@api.depends('move_line_ids', 'move_line_ids.result_package_id')
def _compute_packages(self):
self.ensure_one()
packs = set()
@@ -52,7 +52,7 @@ class StockPicking(models.Model):
self.package_ids = list(packs)
@api.one
@api.depends('move_line_ids')
@api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done')
def _compute_bulk_weight(self):
weight = 0.0
for move_line in self.move_line_ids:
+1 -1
View File
@@ -116,7 +116,7 @@
<table style="width: 100%; max-width: 600px; margin-top: 5px; border: 1px solid #e7e7e7;">
<tr>
<td style="border-collapse: collapse; background-color: #ffffff; line-height: 21px; padding: 0px 20px;"><br/>
<div style="color: #3d466e; line-height: 23px;">${ctx['tip_description']|safe}</div>
<div style="color: #3d466e; line-height: 23px;">${tips | safe}</div>
</td>
</tr>
</table>
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from pprint import pformat
import json
import logging
@@ -183,13 +184,16 @@ class GoogleService(models.TransientModel):
except:
pass
except requests.HTTPError as error:
# https://developers.google.com/calendar/v3/errors
if error.response.status_code in (204, 404):
status = error.response.status_code
response = ""
else:
_logger.exception("Bad google request : %s !", error.response.content)
req = json.loads(error.request.body)
res = error.response.json()
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res))
if error.response.status_code in (400, 401, 410):
raise error
raise UserError(_("Error while requesting Google Services: %s" % res['error']['message']))
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
return (status, response, ask_time)
+7 -10
View File
@@ -29,16 +29,13 @@ class Employee(models.Model):
"""
Returns the contracts of the employee between date_from and date_to
"""
# a contract is valid if it ends between the given dates
clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)]
# OR if it starts between the given dates
clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)]
# OR if it starts before the date_from and finish after the date_end (or never finish)
clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)]
clause_final = expression.AND([
[('employee_id', 'in', self.ids), ('state', 'in', states)],
expression.OR([clause_1, clause_2, clause_3])])
return self.env['hr.contract'].search(clause_final)
return self.env['hr.contract'].search([
'&', '&', '&',
('employee_id', 'in', self.ids),
('state', 'in', states),
('date_start', '<=', date_to),
'|', ('date_end', '=', False), ('date_end', '>=', date_from)
])
@api.model
def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']):
@@ -12,6 +12,7 @@ from odoo import api, fields, models
from odoo.addons.resource.models.resource import HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError
from odoo.tools.translate import _
from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
@@ -72,6 +73,8 @@ class HolidaysAllocation(models.Model):
number_of_hours_display = fields.Float(
'Duration (hours)', compute='_compute_number_of_hours_display',
help="UX field allowing to see and modify the allocation duration, computed in hours.")
duration_display = fields.Char('Allocated (Days/Hours)', compute='_compute_duration_display',
help="Field allowing to see the allocation duration in days or hours depending on the type_request_unit")
# details
parent_id = fields.Many2one('hr.leave.allocation', string='Parent')
linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests')
@@ -206,6 +209,16 @@ class HolidaysAllocation(models.Model):
for allocation in self:
allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@api.multi
@api.depends('number_of_hours_display', 'number_of_days_display')
def _compute_duration_display(self):
for allocation in self:
allocation.duration_display = '%g %s' % (
(float_round(allocation.number_of_hours_display, precision_digits=2)
if allocation.type_request_unit == 'hour'
else float_round(allocation.number_of_days_display, precision_digits=2)),
_('hours') if allocation.type_request_unit == 'hour' else _('days'))
@api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
+19 -7
View File
@@ -172,10 +172,16 @@ class HolidaysType(models.Model):
for request in requests:
status_dict = result[request.holiday_status_id.id]
status_dict['virtual_remaining_leaves'] -= request.number_of_days
status_dict['virtual_remaining_leaves'] -= (request.number_of_hours_display
if request.leave_type_request_unit == 'hour'
else request.number_of_days)
if request.state == 'validate':
status_dict['leaves_taken'] += request.number_of_days
status_dict['remaining_leaves'] -= request.number_of_days
status_dict['leaves_taken'] += (request.number_of_hours_display
if request.leave_type_request_unit == 'hour'
else request.number_of_days)
status_dict['remaining_leaves'] -= (request.number_of_hours_display
if request.leave_type_request_unit == 'hour'
else request.number_of_days)
for allocation in allocations:
status_dict = result[allocation.holiday_status_id.id]
@@ -183,9 +189,15 @@ class HolidaysType(models.Model):
# note: add only validated allocation even for the virtual
# count; otherwise pending then refused allocation allow
# the employee to create more leaves than possible
status_dict['virtual_remaining_leaves'] += allocation.number_of_days
status_dict['max_leaves'] += allocation.number_of_days
status_dict['remaining_leaves'] += allocation.number_of_days
status_dict['virtual_remaining_leaves'] += (allocation.number_of_hours_display
if allocation.type_request_unit == 'hour'
else allocation.number_of_days)
status_dict['max_leaves'] += (allocation.number_of_hours_display
if allocation.type_request_unit == 'hour'
else allocation.number_of_days)
status_dict['remaining_leaves'] += (allocation.number_of_hours_display
if allocation.type_request_unit == 'hour'
else allocation.number_of_days)
return result
@@ -273,7 +285,7 @@ class HolidaysType(models.Model):
'count': _('%g remaining out of %g') % (
float_round(record.virtual_remaining_leaves, precision_digits=2) or 0.0,
float_round(record.max_leaves, precision_digits=2) or 0.0,
)
) + (_(' hours') if record.request_unit == 'hour' else _(' days'))
}
res.append((record.id, name))
return res
@@ -175,7 +175,7 @@
<field name="department_id"/>
<field name="holiday_status_id"/>
<field name="name"/>
<field name="number_of_days" string="Allocated Days" sum="Total allocated days"/>
<field name="duration_display"/>
<field name="message_needaction" invisible="1"/>
<field name="state"/>
<button string="Approve" name="action_approve" type="object"
+10 -5
View File
@@ -92,7 +92,7 @@ class Blackbox(Thread):
def _send_and_wait_for_ack(self, packet, serial):
ack = 0
MAX_RETRIES = 1
MAX_RETRIES = 1 # no more than 9
while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
serial.write(packet)
@@ -104,7 +104,7 @@ class Blackbox(Thread):
# message so it's safe to do it. Also it would be a pain
# to have to throw this all the way back to js just so it
# can increment the retry counter and then try again.
packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:]
packet[4] += 1
if ack:
ack = ord(ack)
@@ -141,11 +141,11 @@ class Blackbox(Thread):
if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc):
got_response = True
ser.write(chr(0x06))
ser.write(chr(0x06).encode())
else:
_logger.warning("received ACK but not a valid response, sending NACK...")
sent_nacks += 1
ser.write(chr(0x15))
ser.write(chr(0x15).encode())
if not got_response:
_logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
@@ -173,4 +173,9 @@ if isfile("/home/pi/registered_blackbox_be"):
@http.route('/hw_proxy/request_serial/', type='json', auth='none', cors='*')
def request_serial(self):
return subprocess.check_output("ifconfig eth0 | grep 'ether.*' | sed 's/://g' | awk '{print $2}'", shell=True).rstrip()[-7:]
try:
with open('/sys/class/net/eth0/address', 'rb') as f:
return f.read().rstrip().replace(b':', b'')[-7:]
except IOError as e:
_logger.warning("eth0 network interface MAC address could not be found")
return b''
+1 -1
View File
@@ -1099,7 +1099,7 @@ msgstr "Zu zahlende MwSt. bei 3,7% (LSt)"
#. module: l10n_ch
#: model:account.tax.template,name:l10n_ch.vat_77
msgid "TVA due a 7.7% (TN)"
msgstr "Zu zahlende MwSt. bei 8,0% (NRZ)"
msgstr "Zu zahlende MwSt. bei 7,7% (NRZ)"
#. module: l10n_ch
#: model:account.tax.template,name:l10n_ch.vat_77_purchase_return
+4 -3
View File
@@ -70,7 +70,7 @@ class ServerActions(models.Model):
@api.model
def run_action_followers_multi(self, action, eval_context=None):
Model = self.env[action.model_id.model]
Model = self.env[action.model_name]
if self.partner_ids or self.channel_ids and hasattr(Model, 'message_subscribe'):
records = Model.browse(self._context.get('active_ids', self._context.get('active_id')))
records.message_subscribe(self.partner_ids.ids, self.channel_ids.ids)
@@ -83,7 +83,7 @@ class ServerActions(models.Model):
When need to know it to skip these steps.
Except if the computed field is supposed to trigger the action
"""
records = self.env[action.model_id.model].browse(
records = self.env[action.model_name].browse(
self._context.get('active_ids', self._context.get('active_id')))
old_values = action._context.get('old_values')
if old_values:
@@ -112,6 +112,7 @@ class ServerActions(models.Model):
# with wrong values in subsequent operations
cleaned_ctx = dict(self.env.context)
cleaned_ctx.pop('default_type', None)
cleaned_ctx.pop('default_parent_id', None)
action.template_id.with_context(cleaned_ctx).send_mail(self._context.get('active_id'), force_send=False, raise_exception=False)
return False
@@ -120,7 +121,7 @@ class ServerActions(models.Model):
if not action.activity_type_id or not self._context.get('active_id') or self._is_recompute(action):
return False
records = self.env[action.model_id.model].browse(self._context.get('active_ids', self._context.get('active_id')))
records = self.env[action.model_name].browse(self._context.get('active_ids', self._context.get('active_id')))
vals = {
'summary': action.activity_summary or '',
+2 -1
View File
@@ -617,7 +617,8 @@ class Channel(models.Model):
# direct_partner should be removed from channel info since we can find it from members and channel_type
# we keep it know to avoid change tests and javascript
direct_partner = channel_partners.filtered(lambda pc: pc.partner_id.id != self.env.user.partner_id.id)
info['direct_partner'] = [partner_infos[direct_partner[0].partner_id.id]]
if direct_partner:
info['direct_partner'] = [partner_infos[direct_partner[0].partner_id.id]]
# add needaction and unread counter, since the user is logged
info['message_needaction_counter'] = channel.message_needaction_counter
info['message_unread_counter'] = channel.message_unread_counter
+1 -1
View File
@@ -366,7 +366,7 @@ var Chatter = Widget.extend({
this._fetchAttachments().then(this._openAttachmentBox.bind(this));
}
if (this.fields.thread) {
this.trigger_up('reload', { fieldNames: ['message_attachment_count'] });
this.trigger_up('reload', { fieldNames: ['message_attachment_count'], keepChanges: true });
}
},
/**
@@ -31,7 +31,6 @@
}
.o_followers_follow_button {
width: 85px;
padding-right: 0;
color: $body-color;
font-weight: bold;
@@ -194,7 +194,7 @@ class ReportBomStructure(models.AbstractModel):
if line._skip_bom_line(product):
continue
if line.child_bom_id:
qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id)
qty = line.product_uom_id._compute_quantity(line.product_qty * factor, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty
sub_price = self._get_price(line.child_bom_id, qty, line.product_id)
price += sub_price
else:
+86
View File
@@ -628,3 +628,89 @@ class TestBoM(TestMrpCommon):
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_car.id, searchQty=1, searchVariant=red_car_without_gps.id)
# Same math than before but without GPS
self.assertEqual(report_values['lines']['total'], 210)
def test_bom_report_recursive_bom(self):
""" Test report with recursive BoM and different quantities.
BoM 1:
product = Finished (units)
quantity = 100 units
- Semi-Finished 5 kg
BoM 2:
product = Semi-Finished (kg)
quantity = 11 kg
- Assembly 2 dozens
BoM 3:
product = Assembly (dozens)
quantity = 5 dozens
- Raw Material 4 litres (product.product 5$/litre)
Check the Price for 80 units of Finished -> 2.92$:
"""
# Create a products templates
uom_unit = self.env.ref('uom.product_uom_unit')
uom_kg = self.env.ref('uom.product_uom_kgm')
uom_dozen = self.env.ref('uom.product_uom_dozen')
uom_litre = self.env.ref('uom.product_uom_litre')
finished = self.env['product.product'].create({
'name': 'Finished',
'type': 'product',
'uom_id': uom_unit.id,
'uom_po_id': uom_unit.id,
})
semi_finished = self.env['product.product'].create({
'name': 'Semi-Finished',
'type': 'product',
'uom_id': uom_kg.id,
'uom_po_id': uom_kg.id,
})
assembly = self.env['product.product'].create({
'name': 'Assembly',
'type': 'product',
'uom_id': uom_dozen.id,
'uom_po_id': uom_dozen.id,
})
raw_material = self.env['product.product'].create({
'name': 'Raw Material',
'type': 'product',
'uom_id': uom_litre.id,
'uom_po_id': uom_litre.id,
'standard_price': 5,
})
#Create bom
bom_finished = Form(self.env['mrp.bom'])
bom_finished.product_tmpl_id = finished.product_tmpl_id
bom_finished.product_qty = 100
with bom_finished.bom_line_ids.new() as line:
line.product_id = semi_finished
line.product_uom_id = uom_kg
line.product_qty = 5
bom_finished = bom_finished.save()
bom_semi_finished = Form(self.env['mrp.bom'])
bom_semi_finished.product_tmpl_id = semi_finished.product_tmpl_id
bom_semi_finished.product_qty = 11
with bom_semi_finished.bom_line_ids.new() as line:
line.product_id = assembly
line.product_uom_id = uom_dozen
line.product_qty = 2
bom_semi_finished = bom_semi_finished.save()
bom_assembly = Form(self.env['mrp.bom'])
bom_assembly.product_tmpl_id = assembly.product_tmpl_id
bom_assembly.product_qty = 5
with bom_assembly.bom_line_ids.new() as line:
line.product_id = raw_material
line.product_uom_id = uom_litre
line.product_qty = 4
bom_assembly = bom_assembly.save()
report_values = self.env['report.mrp.report_bom_structure']._get_report_data(bom_id=bom_finished.id, searchQty=80)
self.assertAlmostEqual(report_values['lines']['total'], 2.92)
+16 -10
View File
@@ -122,6 +122,18 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
},
disableButton: function (button) {
$(button).attr('disabled', true);
$(button).children('.fa-lock').removeClass('fa-lock');
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
},
enableButton: function (button) {
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-lock');
$(button).find('span.o_loader').remove();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -184,10 +196,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
return;
}
$(button).attr('disabled', true);
$(button).children('.fa-plus-circle').removeClass('fa-plus-circle');
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i>&nbsp;</span>');
this.disableButton(button);
var verify_validity = this.$el.find('input[name="verify_validity"]');
if (verify_validity.length>0) {
@@ -225,14 +234,10 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
}
// here we remove the 'processing' icon from the 'add a new payment' button
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle');
$(button).find('span.o_loader').remove();
self.enableButton(button);
}).guardedCatch(function (error) {
// if the rpc fails, pretty obvious
$(button).attr('disabled', false);
$(button).children('.fa').addClass('fa-plus-circle');
$(button).find('span.o_loader').remove();
self.enableButton(button);
self.displayError(
_t('Server Error'),
@@ -300,6 +305,7 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
}
else { // if the user is using an old payment then we just submit the form
this.disableButton(button);
form.submit();
return new Promise(function () {});
}
+1 -5
View File
@@ -237,11 +237,7 @@ class PosOrder(models.Model):
# Oldlin trick
invoice_line = InvoiceLine.sudo().new(inv_line)
invoice_line._onchange_product_id()
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids
fiscal_position_id = line.order_id.fiscal_position_id
if fiscal_position_id:
invoice_line.invoice_line_tax_ids = fiscal_position_id.map_tax(invoice_line.invoice_line_tax_ids, line.product_id, line.order_id.partner_id)
invoice_line.invoice_line_tax_ids = invoice_line.invoice_line_tax_ids.ids
invoice_line.invoice_line_tax_ids = [(6, False, line.tax_ids_after_fiscal_position.filtered(lambda t: t.company_id.id == line.order_id.company_id.id).ids)]
# We convert a new id object back to a dictionary to write to
# bridge between old and new api
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
@@ -880,6 +880,9 @@ exports.PosModel = Backbone.Model.extend({
}}).then(function () {
resolveInvoiced();
resolveDone();
}).guardedCatch(function (error) {
rejectInvoiced({code:401, message:'Backend Invoice', data:{order: order}});
rejectDone();
});
} else {
// The order has been pushed separately in batch when
@@ -671,3 +671,58 @@ class TestPointOfSaleFlow(TestPointOfSaleCommon):
self.assertAlmostEqual(a, b)
for a, b in zip(sorted(amount_currency_lines), [-855.0, -777.27, -81.82, -45.0, 1752.75]):
self.assertAlmostEqual(a, b)
def test_order_to_invoice_no_tax(self):
# I create a new PoS order with 2 units of PC1 at 450 EUR (Tax Incl) and 3 units of PCSC349 at 300 EUR. (Tax Excl)
self.pos_order_pos1 = self.PosOrder.create({
'company_id': self.company_id,
'partner_id': self.partner1.id,
'pricelist_id': self.partner1.property_product_pricelist.id,
'lines': [(0, 0, {
'name': "OL/0001",
'product_id': self.product3.id,
'price_unit': 450,
'discount': 5.0,
'qty': 2.0,
'price_subtotal': 855,
'price_subtotal_incl': 855,
}), (0, 0, {
'name': "OL/0002",
'product_id': self.product4.id,
'price_unit': 300,
'discount': 5.0,
'qty': 3.0,
'price_subtotal': 855,
'price_subtotal_incl': 855,
})],
'amount_tax': 855 * 2,
'amount_total': 855 * 2,
'amount_paid': 0.0,
'amount_return': 0.0,
})
# I click on the "Make Payment" wizard to pay the PoS order
context_make_payment = {"active_ids": [self.pos_order_pos1.id], "active_id": self.pos_order_pos1.id}
self.pos_make_payment = self.PosMakePayment.with_context(context_make_payment).create({
'amount': 855 * 2,
})
# I click on the validate button to register the payment.
context_payment = {'active_id': self.pos_order_pos1.id}
self.pos_make_payment.with_context(context_payment).check()
# I check that the order is marked as paid and there is no invoice
# attached to it
self.assertEqual(self.pos_order_pos1.state, 'paid', "Order should be in paid state.")
self.assertFalse(self.pos_order_pos1.invoice_id, 'Invoice should not be attached to order.')
# I generate an invoice from the order
res = self.pos_order_pos1.action_pos_order_invoice()
self.assertIn('res_id', res, "No invoice created")
# I test that the total of the attached invoice is correct
invoice = self.env['account.invoice'].browse(res['res_id'])
self.assertAlmostEqual(
invoice.amount_total, self.pos_order_pos1.amount_total, places=2, msg="Invoice not correct")
for iline in invoice.invoice_line_ids:
self.assertFalse(iline.invoice_line_tax_ids)
-1
View File
@@ -4,7 +4,6 @@
<!-- We want to activate pay and sign by default for easier demoing. -->
<record id="base.main_company" model="res.company">
<field name="portal_confirmation_sign" eval="True"/>
<field name="portal_confirmation_pay" eval="True"/>
</record>
+1 -1
View File
@@ -7,7 +7,7 @@ from odoo import api, fields, models, _
class ResCompany(models.Model):
_inherit = "res.company"
portal_confirmation_sign = fields.Boolean(string='Online Signature')
portal_confirmation_sign = fields.Boolean(string='Online Signature', default=True)
portal_confirmation_pay = fields.Boolean(string='Online Payment')
quotation_validity_days = fields.Integer(default=30, string="Default Quotation Validity (Days)")
+1 -1
View File
@@ -42,7 +42,7 @@ class ResConfigSettings(models.TransientModel):
oldname='default_deposit_product_id',
help='Default product used for payment advances')
auto_done_setting = fields.Boolean("Lock Confirmed Sales", config_parameter='sale.auto_done_setting')
module_website_sale_digital = fields.Boolean("Sell digital products - provide downloadable content on your customer portal")
module_website_sale_digital = fields.Boolean("Digital Content")
auth_signup_uninvited = fields.Selection([
('b2b', 'On invitation'),
+15 -4
View File
@@ -500,6 +500,9 @@ class SaleOrder(models.Model):
invoices_origin = {}
invoices_name = {}
# Keep track of the sequences of the lines
# To keep lines under their section
inv_line_sequence = 0
for order in self:
group_key = order.id if grouped else (order.partner_invoice_id.id, order.currency_id.id)
@@ -508,6 +511,7 @@ class SaleOrder(models.Model):
# Create lines in batch to avoid performance problems
line_vals_list = []
# sequence is the natural order of order_lines
for line in order.order_line:
if line.display_type == 'line_section':
pending_section = line
@@ -529,14 +533,21 @@ class SaleOrder(models.Model):
if line.qty_to_invoice > 0 or (line.qty_to_invoice < 0 and final):
if pending_section:
line_vals_list.extend(pending_section.invoice_line_create_vals(
section_invoice = pending_section.invoice_line_create_vals(
invoices[group_key].id,
pending_section.qty_to_invoice
))
)
inv_line_sequence += 1
section_invoice[0]['sequence'] = inv_line_sequence
line_vals_list.extend(section_invoice)
pending_section = None
line_vals_list.extend(line.invoice_line_create_vals(
inv_line_sequence += 1
inv_line = line.invoice_line_create_vals(
invoices[group_key].id, line.qty_to_invoice
))
)
inv_line[0]['sequence'] = inv_line_sequence
line_vals_list.extend(inv_line)
if references.get(invoices.get(group_key)):
if order not in references[invoices[group_key]]:
@@ -71,7 +71,7 @@ publicWidget.registry.SalePortalSidebar = PortalSidebar.extend({
if (!text) {
break;
}
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastLI = $("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo($bsSidenav);
lastUL = false;
break;
case "h3":
@@ -84,7 +84,7 @@ publicWidget.registry.SalePortalSidebar = PortalSidebar.extend({
if (!lastUL) {
lastUL = $("<ul class='nav flex-column'>").appendTo(lastLI);
}
$("<li class='nav-item'>").append($('<a class="nav-link" href="#' + id + '"/>').text(text)).appendTo(lastUL);
$("<li class='nav-item'>").append($('<a class="nav-link" style="max-width: 200px;" href="#' + id + '"/>').text(text)).appendTo(lastUL);
}
break;
}
+2 -1
View File
@@ -443,7 +443,8 @@ class SaleOrderLine(models.Model):
def _update_line_quantity(self, values):
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
if self.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(self.mapped('qty_delivered')), precision_digits=precision) == -1:
line_products = self.filtered(lambda l: l.product_id.type in ['product', 'consu'])
if line_products.mapped('qty_delivered') and float_compare(values['product_uom_qty'], max(line_products.mapped('qty_delivered')), precision_digits=precision) == -1:
raise UserError(_('You cannot decrease the ordered quantity below the delivered quantity.\n'
'Create a return first.'))
super(SaleOrderLine, self)._update_line_quantity(values)
@@ -193,7 +193,7 @@ class Project(models.Model):
rows_sale_line[sale_line_row_key][-1] = 0
rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted]
rows_sale_order_done_sold = dict.fromkeys(set(map_sol_so.values()) | set([None]), dict(sold=0.0, done=0.0)) # SO id -> {'sold':0.0, 'done': 0.0}
rows_sale_order_done_sold = {key : dict(sold=0.0, done=0.0) for key in set(map_sol_so.values()) | set([None])} # SO id -> {'sold':0.0, 'done': 0.0}
for row_key, row_sale_line in rows_sale_line.items():
sale_order_id = row_key[0]
# sale order row
+39 -26
View File
@@ -4,6 +4,7 @@
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
from odoo.osv import expression
from odoo.tools.float_utils import float_round
from datetime import datetime
import operator as py_operator
@@ -179,33 +180,45 @@ class Product(models.Model):
[('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id)],
[('location_id.company_id', '=', company_id), ('location_dest_id.company_id', '=', False),
])
location_ids = []
if self.env.context.get('location', False):
if isinstance(self.env.context['location'], int):
location_ids = [self.env.context['location']]
elif isinstance(self.env.context['location'], str):
domain = [('complete_name', 'ilike', self.env.context['location'])]
if self.env.context.get('force_company', False):
domain += [('company_id', '=', self.env.context['force_company'])]
location_ids = self.env['stock.location'].search(domain).ids
else:
location_ids = self.env.context['location']
else:
if self.env.context.get('warehouse', False):
if isinstance(self.env.context['warehouse'], int):
wids = [self.env.context['warehouse']]
elif isinstance(self.env.context['warehouse'], str):
domain = [('name', 'ilike', self.env.context['warehouse'])]
if self.env.context.get('force_company', False):
domain += [('company_id', '=', self.env.context['force_company'])]
wids = Warehouse.search(domain).ids
else:
wids = self.env.context['warehouse']
else:
wids = Warehouse.search([]).ids
for w in Warehouse.browse(wids):
location_ids.append(w.view_location_id.id)
def _search_ids(model, values, force_company_id):
ids = set()
domain = []
for item in values:
if isinstance(item, int):
ids.add(item)
else:
domain = expression.OR([[('name', 'ilike', item)], domain])
if force_company_id:
domain = expression.AND([[('company_id', '=', force_company_id)], domain])
if domain:
ids |= set(self.env[model].search(domain).ids)
return ids
# We may receive a location or warehouse from the context, either by explicit
# python code or by the use of dummy fields in the search view.
# Normalize them into a list.
location = self.env.context.get('location')
if location and not isinstance(location, list):
location = [location]
warehouse = self.env.context.get('warehouse')
if warehouse and not isinstance(warehouse, list):
warehouse = [warehouse]
force_company = self.env.context.get('force_company', False)
# filter by location and/or warehouse
if warehouse:
w_ids = set(Warehouse.browse(_search_ids('stock.warehouse', warehouse, force_company)).mapped('view_location_id').ids)
if location:
l_ids = _search_ids('stock.location', location, force_company)
location_ids = w_ids & l_ids
else:
location_ids = w_ids
else:
if location:
location_ids = _search_ids('stock.location', location, force_company)
else:
location_ids = set(Warehouse.search([]).mapped('view_location_id').ids)
return self._get_domain_locations_new(location_ids, company_id=self.env.context.get('force_company', False), compute_child=self.env.context.get('compute_child', True))
def _get_domain_locations_new(self, location_ids, company_id=False, compute_child=True):
+11 -6
View File
@@ -4,10 +4,7 @@
<!-- survey assets -->
<template id="survey_assets" name="Survey Results assets">
<script src="/web/static/lib/Chart/Chart.js"></script>
<script type="text/javascript" src="/web/static/src/js/public/public_root.js"/>
<script type="text/javascript" src="/web/static/src/js/public/public_root_instance.js"/>
<script type="text/javascript" src="/web/static/src/js/public/public_widget.js"/>
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
<script type="text/javascript" src="/web/static/src/js/fields/field_utils.js"></script>
<script type="text/javascript" src="/survey/static/src/js/survey_result.js" />
@@ -18,6 +15,16 @@
<script type="text/javascript" src="/survey/static/src/js/tour_test_certification_failure.js"></script>
<script type="text/javascript" src="/survey/static/src/js/tour_test_survey_prefill.js"></script>
<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>
</template>
<template id="survey_web_assets" name="Survey Web Assets">
<script type="text/javascript" src="/web/static/src/js/public/public_root.js"/>
<script type="text/javascript" src="/web/static/src/js/public/public_root_instance.js"/>
<script type="text/javascript" src="/web/static/src/js/public/public_widget.js"/>
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
<t t-call="web._assets_helpers">
<t t-call="web._assets_frontend_helpers"/>
</t>
@@ -25,8 +32,6 @@
<link rel="stylesheet" type="text/less" href="/web/static/src/scss/fonts.scss" />
<!-- Some helper classes (like mt- mb-) are required for survey templates -->
<link rel="stylesheet" type="text/less" href="/web_editor/static/src/scss/web_editor.common.scss"/>
<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>
+1
View File
@@ -142,6 +142,7 @@
};
</script>
<t t-call-assets="web.assets_common"/>
<t t-call-assets="survey.survey_web_assets"/>
<t t-call-assets="survey.survey_assets"/>
</t>
<body>
+3 -4
View File
@@ -84,8 +84,7 @@ class IrTranslation(models.Model):
'project': project,
'lang': lang_code,
'module': translation.module,
'src': werkzeug.url_quote_plus(
"text:'" +
translation.source[:50].replace("'", "\\'") +
"'"),
'src': "text:'" + werkzeug.url_quote_plus(
translation.source[:50].replace("\n", "").replace("'", "")
) + "'",
}
@@ -31,7 +31,9 @@ var qweb = core.qweb;
var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMixin, {
dependencies: ['notification'],
events: _.extend(KeyboardNavigationMixin.events, {}),
events: _.extend({}, KeyboardNavigationMixin.events, {
'click .o_search_options .dropdown-menu': '_onClickDropDownMenu',
}),
custom_events: {
clear_uncommitted_changes: function (e) {
this.clear_uncommitted_changes().then(e.data.callback);
@@ -333,6 +335,16 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
// Handlers
//--------------------------------------------------------------------------
/**
* When clicking inside a dropdown to modify search options
* prevents the bootstrap dropdown to close on itself
*
* @private
* @param {Event} ev
*/
_onClickDropDownMenu: function (ev) {
ev.stopPropagation();
},
/**
* Whenever the connection is lost, we need to notify the user.
*
@@ -495,7 +495,6 @@ var AbstractController = mvc.Controller.extend(ActionMixin, {
additional_context: _.extend({}, data.context)
});
}
return false;
},
/**
* FIXME: this logic should be rethought
@@ -204,7 +204,9 @@ var CalendarController = AbstractController.extend({
* @param {OdooEvent} event
*/
_onDropRecord: function (event) {
this._updateRecord(event.data);
this._updateRecord(_.extend({}, event.data, {
'drop': true,
}));
},
/**
* @private
@@ -105,7 +105,9 @@ return AbstractModel.extend({
}
if (this.mapping.date_delay) {
data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600;
if (this.data.scale !== 'month' || (this.data.scale === 'month' && !event.drop)) {
data[this.mapping.date_delay] = (end.diff(start) <= 0 ? end.endOf('day').diff(start) : end.diff(start)) / 1000 / 3600;
}
}
return data;
@@ -18,7 +18,6 @@ var ControlPanelRenderer = Renderer.extend({
get_action_info: '_onGetActionInfo',
},
events: _.extend({}, Renderer.prototype.events, {
'click.bs.dropdown .o_search_options .dropdown-menu': '_onDropdownClicked',
'click .o_searchview_more': '_onMore',
}),
@@ -367,15 +366,6 @@ var ControlPanelRenderer = Renderer.extend({
// Handlers
//--------------------------------------------------------------------------
/**
* Prevent the search dropdowns from closing when clicking inside them.
*
* @private
* @param {MouseEvent} ev
*/
_onDropdownClicked: function (ev) {
ev.stopPropagation();
},
/**
* Toggle the search menus visibility.
*
@@ -0,0 +1,2 @@
// remove Noto Color Emoji
$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
@@ -1,5 +1,5 @@
.o_background_footer, .o_background_header, .o_report_layout_background {
font-family: 'Lato';
font-family: 'Lato', 'Noto';
color: gray('700');
}
.o_background_header {
+1 -1
View File
@@ -1,5 +1,5 @@
.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
font-family: 'Lato';
font-family: 'Lato' 'Noto';
color: gray('700');
font-size: 12px;
}
+6 -5
View File
@@ -1369,13 +1369,14 @@
<div role="menuitem" class="dropdown-item-text o_favorite_name">
<input type="text" class="o_input"></input>
</div>
<t t-set="favId" t-value="_.uniqueId('_checkbox_')" />
<div class="custom-control custom-checkbox">
<input type="checkbox" id="o_favorite_use_by_default" class="custom-control-input"/>
<label for="o_favorite_use_by_default" class="custom-control-label">Use by default</label>
<input type="checkbox" t-attf-id="o_favorite_use_by_default#{favId}" class="custom-control-input"/>
<label t-attf-for="o_favorite_use_by_default#{favId}" class="custom-control-label">Use by default</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" id="o_favorite_share_all_users" class="custom-control-input"/>
<label for="o_favorite_share_all_users" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
<input type="checkbox" t-attf-id="o_favorite_share_all_users#{favId}" class="custom-control-input"/>
<label t-attf-for="o_favorite_share_all_users#{favId}" class="custom-control-label">Share with all users <span class="fa fa-users" role="img" aria-label="Users" title="Users"/></label>
</div>
<div class="dropdown-item-text o_save_favorite">
<button type="button" class="btn btn-primary">Save</button>
@@ -1559,7 +1560,7 @@
<t t-name="SwitchCompanyMenu">
<li class="o_switch_company_menu">
<a role="button" class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle" data-toggle="dropdown" data-display="static" aria-expanded="false" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span t-attf-class="#{widget.isMobile ? 'fa fa-building-o' : 'oe_topbar_name'}"/>
</a>
<div class="dropdown-menu dropdown-menu-right" role="menu"/>
+2 -2
View File
@@ -15,7 +15,7 @@
-->
<t t-name="AppsMenu">
<li class="dropdown">
<a class="full" data-toggle="dropdown" href="#">
<a class="full" data-toggle="dropdown" data-display="static" href="#">
<i class="fa fa-th-large"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -77,7 +77,7 @@
</t>
<t t-else="">
<li>
<a href="#" class="dropdown-toggle o-no-caret o_menu_header_lvl_1" t-att-data-menu-xmlid="second_level_menu.xmlid" data-toggle="dropdown" role="button" aria-expanded="false">
<a href="#" class="dropdown-toggle o-no-caret o_menu_header_lvl_1" t-att-data-menu-xmlid="second_level_menu.xmlid" data-toggle="dropdown" data-display="static" role="button" aria-expanded="false">
<t t-esc="second_level_menu.name"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -40,13 +40,33 @@ QUnit.module('Views', {
QUnit.test("The banner should be fetched from the route", function (assert) {
var done = assert.async();
assert.expect(5);
assert.expect(6);
var banner_html =
'<div>' +
'<link type="text/css" href="' + test_css_url + '" rel="stylesheet">' +
'<div class="hello_banner"/>' +
'</div>';
var banner_html =`
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-footer">
<a type="action" class="btn btn-primary" data-dismiss="modal"
data-toggle="collapse" href=".o_onboarding_container">
Remove
</a>
</div>
</div>
</div>
</div>
<div class="o_onboarding_container collapse show">
<div class="o_onboarding_wrap">
<a href="#" data-toggle="modal" data-target=".o_onboarding_modal"
class="float-right o_onboarding_btn_close">
<i class="fa fa-times" title="Close the onboarding panel" />
</a>
</div>
<div>
<link type="text/css" href="` + test_css_url + `" rel="stylesheet">
<div class="hello_banner">Here is the banner</div>
</div>
</div>`;
createView({
View: TestView,
@@ -60,7 +80,7 @@ QUnit.module('Views', {
}
return this._super(route, args);
},
}).then(function (view) {
}).then(async function (view) {
var $banner = view.$('.hello_banner');
assert.strictEqual($banner.length, 1,
"The view should contain the response from the controller.");
@@ -73,6 +93,12 @@ QUnit.module('Views', {
var $banner_link = $('link[href$="' + test_css_url + '"]', $banner);
assert.strictEqual($banner_link.length, 0,
"The stylesheet should have been removed from the banner.");
await testUtils.dom.click(view.$('.o_onboarding_btn_close')); // click on close to remove banner
await testUtils.dom.click(view.$('.o_technical_modal .btn-primary:contains("Remove")')); // click on button remove from techinal modal
assert.strictEqual(view.$('.o_onboarding_container.show').length, 0,
"Banner should be removed from the view");
view.destroy();
done();
});
@@ -42,6 +42,7 @@ QUnit.module('Views', {
stop_date: {string: "stop date", type: "date"},
start: {string: "start datetime", type: "datetime"},
stop: {string: "stop datetime", type: "datetime"},
delay: {string: "delay", type: "float"},
allday: {string: "allday", type: "boolean"},
partner_ids: {string: "attendees", type: "one2many", relation: 'partner', default: [[6, 0, [1]]]},
type: {string: "type", type: "integer"},
@@ -2383,6 +2384,50 @@ QUnit.module('Views', {
await testUtils.nextTick();
assert.verifySteps(["2017-01-07 00:00:00"]);
assert.strictEqual(calendar.$('.o_field_start_date').text().trim(), "01/07/2017");
calendar.destroy();
});
QUnit.test('drag and drop on month mode with date_start and date_delay', async function (assert) {
assert.expect(1);
var calendar = await createCalendarView({
View: CalendarView,
model: 'event',
data: this.data,
arch:
'<calendar date_start="start" date_delay="delay" mode="month">'+
'<field name="name"/>'+
'<field name="start"/>'+
'<field name="delay"/>'+
'</calendar>',
archs: archs,
viewOptions: {
initialDate: initialDate,
},
mockRPC: function (route, args) {
if (args.method === "write") {
// delay should not be written at drag and drop
assert.equal(args.args[1].delay, undefined)
}
return this._super(route, args);
},
});
// Create event (on 20 december)
var $cell = calendar.$('.fc-day-grid .fc-row:eq(3) .fc-day:eq(2)');
testUtils.triggerMouseEvent($cell, "mousedown");
testUtils.triggerMouseEvent($cell, "mouseup");
await testUtils.nextTick();
var $input = $('.modal-body input:first');
await testUtils.fields.editInput($input, "An event");
await testUtils.dom.click($('.modal button.btn:contains(Create)'));
await testUtils.nextTick();
// Move event to another day (on 27 november)
await testUtils.dragAndDrop(
calendar.$('.fc-event').first(),
calendar.$('.fc-day-top').first()
);
calendar.destroy();
});
@@ -1280,7 +1280,7 @@ QUnit.module('Search View', {
});
QUnit.test('Customizing filter does not close the filter dropdown', async function (assert) {
assert.expect(3);
assert.expect(5);
var self = this;
_.each(this.data.partner.records.slice(), function (rec) {
@@ -1304,6 +1304,13 @@ QUnit.module('Search View', {
'partner,false,search': '<search><field name="date_field"/></search>',
},
res_id: 1,
intercepts: {
create_filter: function (ev) {
var data = ev.data;
assert.strictEqual(data.filter.name, 'Fire on the bayou');
assert.strictEqual(data.filter.is_default, true);
},
},
});
await testUtils.fields.many2one.clickOpenDropdown('bar');
@@ -1327,9 +1334,22 @@ QUnit.module('Search View', {
$input.click();
await testUtils.nextTick();
});
assert.isVisible($filterDropdown);
// Favorites Menu
var $modal = $('.modal');
await testUtils.dom.click($modal.find('.o_favorites_menu_button'));
await testUtils.dom.click($modal.find('.o_add_favorite'));
$modal.find('.o_search_options .dropdown-menu').one('click', function (ev) {
// This handler is on the webClient
// But since the test suite doesn't have one
// We manually set it here
ev.stopPropagation();
});
await testUtils.fields.editInput($modal.find('div.o_favorite_name input'), 'Fire on the bayou');
await testUtils.dom.click($modal.find('.o_add_favorite ~ div label:contains(Use by default)'));
await testUtils.dom.click($modal.find('.o_save_favorite button'));
form.destroy();
});
});
+2 -1
View File
@@ -5,7 +5,8 @@
<template id="report_assets_common">
<t t-call="web._assets_helpers">
<!-- FIXME web_editor ? -->
<link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/bootstrap_overridden.scss"/>
<link rel="stylesheet" type="text/scss" href="/web/static/src/scss/bootstrap_overridden_report.scss"/>
<link rel="stylesheet" type="text/scss" href="/web_editor/static/src/scss/bootstrap_overridden.scss"/>
</t>
<t t-call="web._assets_bootstrap"/>
@@ -19,6 +19,7 @@ var DropzonePlugin = Plugins.dropzone.extend({
attachDragAndDropEvent: function () {
this._super.apply(this, arguments);
this.$dropzone.off('drop');
this.$dropzone.on('dragenter', this._onDragenter.bind(this));
this.$dropzone.on('drop', this._onDrop.bind(this));
},
@@ -71,7 +72,7 @@ var DropzonePlugin = Plugins.dropzone.extend({
if (range.so >= dom.nodeLength(range.sc)) {
$(range.sc).append(spinner);
} else {
$(range.sc).before(range.sc.childNodes[range.so]);
$(range.sc.childNodes[range.so]).before(spinner);
}
} else {
range.sc.splitText(range.so);
@@ -151,12 +152,27 @@ var DropzonePlugin = Plugins.dropzone.extend({
route: '/web_editor/add_image_base64',
params: {
res_model: options.res_model,
res_id: options.res_id,
res_id: options.res_id || 0,
image_base64: imageBase64.split(';base64,').pop(),
filename: fileName,
},
});
},
/**
* @private
* @param {JQueryEvent} e
*/
_onDragenter: function (e) {
var range = this.context.invoke('editor.createRange');
if (!this.editable.contains(range.sc)) {
var node = this.editable;
if (node && node.firstChild) {
node = node.firstChild;
}
range = this.context.invoke('editor.setRange', node, 0, node, 0);
range.select();
}
},
/**
* @private
* @param {JQueryEvent} e
@@ -164,6 +180,8 @@ var DropzonePlugin = Plugins.dropzone.extend({
_onDrop: function (e) {
e.preventDefault();
this.$editor.removeClass('dragover');
if (this.options.disableDragAndDrop) {
return;
}
@@ -43,6 +43,8 @@ var MediaPlugin = AbstractPlugin.extend({
'dblclick .note-editable': '_onDblclick',
},
mousePosition: {},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -55,5 +55,3 @@ $gray-700: gray('700') !default;
$gray-800: gray('800') !default;
$gray-900: gray('900') !default;
$black: gray('black') !default;
$font-family-sans-serif: "Noto", "Lucida Grande", Helvetica, Verdana, Arial, sans-serif;
@@ -87,7 +87,7 @@ var RunningTourActionHelper = core.Class.extend({
$options.prop("selected", false).removeProp("selected");
var $selectedOption = $options.filter(function () { return $(this).val() === text; });
if ($selectedOption.length === 0) {
$selectedOption = $options.filter(function () { return $(this).text() === text; });
$selectedOption = $options.filter(function () { return $(this).text().trim() === text; });
}
$selectedOption.prop("selected", true);
this._click(values);
@@ -45,8 +45,8 @@ var Dashboard = AbstractAction.extend({
this._super(parent, context);
this.date_range = 'week'; // possible values : 'week', 'month', year'
this.date_from = moment().subtract(1, 'week');
this.date_to = moment();
this.date_from = moment.utc().subtract(1, 'week');
this.date_to = moment.utc();
this.dashboards_templates = ['website.dashboard_header', 'website.dashboard_content'];
this.graphs = [];
@@ -303,13 +303,13 @@ var Dashboard = AbstractAction.extend({
on_date_range_button: function(date_range) {
if (date_range === 'week') {
this.date_range = 'week';
this.date_from = moment().subtract(1, 'weeks');
this.date_from = moment.utc().subtract(1, 'weeks');
} else if (date_range === 'month') {
this.date_range = 'month';
this.date_from = moment().subtract(1, 'months');
this.date_from = moment.utc().subtract(1, 'months');
} else if (date_range === 'year') {
this.date_range = 'year';
this.date_from = moment().subtract(1, 'years');
this.date_from = moment.utc().subtract(1, 'years');
} else {
console.log('Unknown date range. Choose between [week, month, year]');
return;
+1
View File
@@ -973,6 +973,7 @@ class WebsiteSale(http.Controller):
return request.redirect('/shop')
if order and not order.amount_total and not tx:
order.with_context(send_email=True).action_confirm()
return request.redirect(order.get_portal_url())
# clean context and session, then redirect to the confirmation page
+1 -1
View File
@@ -325,7 +325,7 @@ class SaleOrder(models.Model):
'view_id': composer_form_view_id,
'target': 'new',
'context': {
'default_composition_mode': 'mass_mail',
'default_composition_mode': 'mass_mail' if len(self.ids) > 1 else 'comment',
'default_res_id': self.ids[0],
'default_model': 'sale.order',
'default_use_template': bool(template_id),
@@ -27,8 +27,14 @@
<label class="label-optional" t-field="delivery.name"/>
<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-field="delivery.fixed_price" t-options='{"widget": "monetary", "from_currency": delivery.product_id.company_id.currency_id, "display_currency": website_sale_order.currency_id}'/>
<span t-else="" t-att-class="badge_class">Free</span>
<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'/>
</span>
<span t-else="" t-att-class="badge_class">Free</span>
</t>
<t t-else="">
<span t-attf-class="#{badge_class} o_wsale_delivery_badge_price">Select to compute delivery rate</span>
@@ -1,8 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, time
from odoo import http
from odoo.addons.website_sale.controllers.backend import WebsiteSaleBackend
from odoo.fields import Date
from odoo.http import request
@@ -10,8 +13,10 @@ class WebsiteSaleLinkTrackerBackend(WebsiteSaleBackend):
@http.route()
def fetch_dashboard_data(self, website_id, date_from, date_to):
datetime_from = datetime.combine(Date.from_string(date_from), time.min)
datetime_to = datetime.combine(Date.from_string(date_to), time.max)
results = super(WebsiteSaleLinkTrackerBackend, self).fetch_dashboard_data(website_id, date_from, date_to)
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(date_from, date_to)
results['dashboards']['sales']['utm_graph'] = self.fetch_utm_data(datetime_from, datetime_to)
return results
def fetch_utm_data(self, date_from, date_to):
+20 -19
View File
@@ -119,9 +119,25 @@ class WebsiteSlides(WebsiteProfile):
return values
def _get_slide_quiz_info(self, slide, quiz_done=False):
def _get_slide_quiz_partner_info(self, slide, quiz_done=False):
return slide._compute_quiz_info(request.env.user.partner_id, quiz_done=quiz_done)[slide.id]
def _get_slide_quiz_data(self, slide):
slide_completed = slide.user_membership_id.sudo().completed
values = {
'slide_questions': [{
'id': question.id,
'question': question.question,
'answers': [{
'id': answer.id,
'text_value': answer.text_value,
'is_correct': answer.is_correct if slide_completed else None
} for answer in question.sudo().answer_ids],
} for question in slide.question_ids]
}
values.update(self._get_slide_quiz_partner_info(slide))
return values
# CHANNEL UTILITIES
# --------------------------------------------------
@@ -489,7 +505,7 @@ class WebsiteSlides(WebsiteProfile):
values = self._get_slide_detail(slide)
# quiz-specific: update with karma and quiz information
if slide.question_ids:
values.update(self._get_slide_quiz_info(slide))
values.update(self._get_slide_quiz_data(slide))
# sidebar: update with user channel progress
values['channel_progress'] = self._get_channel_progress(slide.channel_id, include_quiz=True)
@@ -623,22 +639,7 @@ class WebsiteSlides(WebsiteProfile):
if fetch_res.get('error'):
return fetch_res
slide = fetch_res['slide']
quiz_info = self._get_slide_quiz_info(slide)
slide_completed = slide.user_membership_id.sudo().completed
return {
'questions': [{
'id': question.id,
'question': question.question,
'answers': [{
'id': answer.id,
'text_value': answer.text_value,
'is_correct': answer.is_correct if slide_completed else None
} for answer in question.answer_ids],
} for question in slide.question_ids],
'quizAttemptsCount': quiz_info['quiz_attempts_count'],
'quizKarmaGain': quiz_info['quiz_karma_gain'],
'quizKarmaWon': quiz_info['quiz_karma_won'],
}
return self._get_slide_quiz_data(slide)
@http.route('/slides/slide/quiz/submit', type="json", auth="public", website=True)
def slide_quiz_submit(self, slide_id, answer_ids):
@@ -662,7 +663,7 @@ class WebsiteSlides(WebsiteProfile):
user_good_answers = user_answers - user_bad_answers
self._set_viewed_slide(slide, quiz_attempts_inc=True)
quiz_info = self._get_slide_quiz_info(slide, quiz_done=True)
quiz_info = self._get_slide_quiz_partner_info(slide, quiz_done=True)
rank_progress = {}
if not user_bad_answers:
@@ -5,7 +5,7 @@ access_slide_slide_partner_all,slide.slide.partner.all,model_slide_slide_partner
access_slide_slide_partner_system,slide.slide.partner.system,model_slide_slide_partner,website.group_website_publisher,1,1,1,1
access_slide_question_all,slide.question.all,model_slide_question,,1,0,0,0
access_slide_question_publisher,slide.question.publisher,model_slide_question,website.group_website_publisher,1,1,1,1
access_slide_answer_all,slide.answer.all,model_slide_answer,,1,0,0,0
access_slide_answer_all,slide.answer.all,model_slide_answer,,0,0,0,0
access_slide_answer_publisher,slide.answer.publisher,model_slide_answer,website.group_website_publisher,1,1,1,1
access_slide_tag_all,slide.tag.all,model_slide_tag,,1,0,0,0
access_slide_tag_publisher,slide.tag.publisher,model_slide_tag,website.group_website_publisher,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_slide_slide_partner_system slide.slide.partner.system model_slide_slide_partner website.group_website_publisher 1 1 1 1
6 access_slide_question_all slide.question.all model_slide_question 1 0 0 0
7 access_slide_question_publisher slide.question.publisher model_slide_question website.group_website_publisher 1 1 1 1
8 access_slide_answer_all slide.answer.all model_slide_answer 1 0 0 0 0
9 access_slide_answer_publisher slide.answer.publisher model_slide_answer website.group_website_publisher 1 1 1 1
10 access_slide_tag_all slide.tag.all model_slide_tag 1 0 0 0
11 access_slide_tag_publisher slide.tag.publisher model_slide_tag website.group_website_publisher 1 1 1 1
@@ -106,7 +106,12 @@ odoo.define('website_slides.quiz', function (require) {
'slide_id': self.slide.id,
}
}).then(function (quiz_data) {
self.quiz = quiz_data;
self.quiz = {
questions: quiz_data.slide_questions,
quizAttemptsCount: quiz_data.quiz_attempts_count,
quizKarmaGain: quiz_data.quiz_karma_gain,
quizKarmaWon: quiz_data.quiz_karma_won
};
});
},
@@ -60,7 +60,7 @@
<b t-else="" class="my-0 h5">Done !</b>
<span class="my-0 h5" style="line-height: 1">
<span role="button" title="Succeed and gain karma" aria-label="Succeed and gain karma" class="badge badge-pill badge-warning text-white font-weight-bold ml-3 px-2">
+ <t t-esc="widget.quiz.quizKarmaGain"/> XP
+ <t t-if="! widget.slide.completed" t-esc="widget.quiz.quizKarmaGain"/><t t-else="" t-esc="widget.quiz.quizKarmaWon"/> XP
</span>
</span>
</div>
@@ -426,27 +426,27 @@
t-att-data-quiz-karma-won="quiz_karma_won"
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.question_ids" 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">
<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']">
<div class="h4">
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question.question"/>
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
</div>
<div class="list-group">
<t t-foreach="question.answer_ids" t-as="answer">
<a t-att-data-answer-id="answer.id" href="#"
t-att-data-text="answer.text_value"
t-att-class="'o_wslides_quiz_answer list-group-item list-group-item-action d-flex align-items-center %s' % ('list-group-item-success' if slide_completed and answer.is_correct else '')">
<t t-foreach="question['answers']" t-as="answer">
<a t-att-data-answer-id="answer['id']" href="#"
t-att-data-text="answer['text_value']"
t-att-class="'o_wslides_quiz_answer list-group-item list-group-item-action d-flex align-items-center %s' % ('list-group-item-success' if slide_completed and answer['is_correct'] else '')">
<label class="my-0 d-flex align-items-center justify-content-center mr-2">
<input type="radio"
t-att-name="question.id"
t-att-value="answer.id"
t-att-name="question['id']"
t-att-value="answer['id']"
class="d-none"
t-att-disabled="True if not slide.channel_id.is_member or slide_completed else ''"/>
<i t-att-class="'fa fa-circle text-400 %s' % ('d-none' if slide_completed and answer.is_correct else '')"/>
<i t-att-class="'fa fa-circle text-400 %s' % ('d-none' if slide_completed and answer['is_correct'] else '')"/>
<i class="fa fa-times-circle text-danger d-none"></i>
<i t-att-class="'fa fa-check-circle text-success %s' % ('d-none' if not (slide_completed and answer.is_correct) else '')"></i>
<i t-att-class="'fa fa-check-circle text-success %s' % ('d-none' if not (slide_completed and answer['is_correct']) else '')"></i>
</label>
<span t-esc="answer.text_value"/>
<span t-esc="answer['text_value']"/>
</a>
</t>
</div>
+2 -2
View File
@@ -20,5 +20,5 @@ class exercise(nodes.Admonition, nodes.Element): pass
class Exercise(admonitions.BaseAdmonition):
node_class = exercise
from sphinx.locale import admonitionlabels, l_
admonitionlabels['exercise'] = l_('Exercise')
from sphinx.locale import admonitionlabels
admonitionlabels['exercise'] = 'Exercise'
+7 -1
View File
@@ -21,6 +21,10 @@ def setup(app):
location="odoo extension")
app.config.html_translator_class = 'odoo_ext.translator.BootstrapTranslator'
add_js_file = getattr(app, 'add_js_file', None) or app.add_javascript
for f in ['jquery.min.js', 'bootstrap.js', 'doc.js', 'jquery.noconflict.js']:
add_js_file(f)
switcher.setup(app)
app.add_config_value('odoo_cover_default', None, 'env')
app.add_config_value('odoo_cover_external', {}, 'env')
@@ -28,7 +32,9 @@ def setup(app):
app.connect('html-page-context', update_meta)
def update_meta(app, pagename, templatename, context, doctree):
meta = context.setdefault('meta', {})
meta = context.get('meta')
if meta is None:
meta = context['meta'] = {}
meta.setdefault('banner', app.config.odoo_cover_default)
def navbarify(node, navbar=None):
-7
View File
@@ -1,12 +1,5 @@
{% extends "basic/layout.html" %}
{% set script_files = script_files + [
'_static/jquery.min.js',
'_static/bootstrap.js',
'_static/doc.js',
'_static/jquery.noconflict.js',
] %}
{% set classes = [] %}
{% if pagename == master_doc %}
{% set classes = classes + ['index'] %}
+7 -2
View File
@@ -10,7 +10,7 @@ except ImportError:
from urllib import url2pathname # pylint: disable=deprecated-module
from docutils import nodes
from sphinx import addnodes, util
from sphinx import addnodes, util, builders
from sphinx.locale import admonitionlabels
@@ -41,7 +41,12 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
'<meta name="viewport" content="width=device-width, initial-scale=1">'
]
def __init__(self, builder, document):
def __init__(self, document, builder):
# order of parameter swapped between Sphinx 1.x and 2.x, check if
# we're running 1.x and swap back
if not isinstance(builder, builders.Builder):
builder, document = document, builder
super(BootstrapTranslator, self).__init__(document)
self.builder = builder
self.body = []
+3 -3
View File
@@ -17,7 +17,7 @@ Rafael Blasco rafael.blasco@tecnativa.com https://github.com/rafaelbn
Sergio Teruel sergio.teruel@tecnativa.com https://github.com/sergio-teruel
Carlos Dauden carlos.dauden@tecnativa.com https://github.com/carlosdauden
Jairo Llopis jairo.llopis@tecnativa.com https://github.com/yajo
Vicent Cubells vicent.cubells@tecnativa.com https://github.com/cubells
Luis Montalba luis.montalba@tecnativa.com https://github.com/luismontalba
David Vidal david.vidal@tecnativa.com https://github.com/chienandalu
Cristina Martín cristina.martin@tecnativa.com https://github.com/cristinamartinrod
Cristina Martín cristina.martin@tecnativa.com https://github.com/cristinamartinrod (up to 2019-04-30)
Ernesto Tejeda ernesto.tejeda@tecnativa.com https://github.com/ernestotejeda
Alexandre Díaz alexandre.diaz@tecnativa.com https://github.com/Tardo
+4 -4
View File
@@ -1102,10 +1102,10 @@ description. Try to follow the preferred structure for your commit messages
End the message with references, such as task or bug numbers, PR numbers, and
OPW tickets, following the suggested format:
Related to task #taskId
Fixes #12345 (link and close issue on Github)
Closes #7865 (link and close PR on Github)
OPW-112233
task-123 (related to task)
Fixes #123 (close related issue on Github)
Closes #123 (close related PR on Github)
opw-123 (related to ticket)
Tag and module name
-------------------
+1 -1
View File
@@ -99,7 +99,7 @@ Extra conditional branching directives ``t-elif`` and ``t-else`` are also
available::
<div>
<p t-if="user.birthday == today()">Happy bithday!</p>
<p t-if="user.birthday == today()">Happy birthday!</p>
<p t-elif="user.login == 'root'">Welcome master!</p>
<p t-else="">Welcome!</p>
</div>
+2 -2
View File
@@ -423,7 +423,7 @@ should be empty if everything went fine.
URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
curl -sS "${PROCESS_URL}?${URL_PARAMS}"
.. _upgrade-api-status-method:
.. _upgrade-api-skip-tests:
Asking to skip the tests
@@ -433,7 +433,7 @@ This action asks the Upgrade Platform to skip the tests for your request.
If you don't want Odoo to test and validate the migration, you can bypass the testing stage and directly get the migrated dump.
The ``skip_test`` method
----------------------
------------------------
.. py:function:: https://upgrade.odoo.com/database/v1/skip_test
+1 -1
View File
@@ -11,7 +11,7 @@
"base.lang_zh_CN","Chinese (Simplified) / 简体中文","zh_CN","zh_CN","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H时%M分%S秒","True","7"
"base.lang_zh_HK","Chinese (HK)","zh_HK","zh_HK","Left-to-Right","[3,0]",".",",","%Y年%m月%d日 %A","%I時%M分%S秒","True","7"
"base.lang_zh_TW","Chinese (Traditional) / 正體字","zh_TW","zh_TW","Left-to-Right","[3,0]",".",",","%Y年%m月%d日","%H時%M分%S秒","True","7"
"base.lang_hr","Croatian / hrvatski jezik","hr_HR","hr","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1"
"base.lang_hr","Croatian / hrvatski jezik","hr_HR","hr","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True","1"
"base.lang_cs_CZ","Czech / Čeština","cs_CZ","cs_CZ","Left-to-Right","[3,0]",","," ","%d.%m.%Y","%H:%M:%S","True","1"
"base.lang_da_DK","Danish / Dansk","da_DK","da_DK","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True","1"
"base.lang_nl_BE","Dutch (BE) / Nederlands (BE)","nl_BE","nl_BE","Left-to-Right","[3,0]",",",".","%d-%m-%Y","%H:%M:%S","True","1"
1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format translatable week_start
11 base.lang_zh_CN Chinese (Simplified) / 简体中文 zh_CN zh_CN Left-to-Right [3,0] . , %Y年%m月%d日 %H时%M分%S秒 True 7
12 base.lang_zh_HK Chinese (HK) zh_HK zh_HK Left-to-Right [3,0] . , %Y年%m月%d日 %A %I時%M分%S秒 True 7
13 base.lang_zh_TW Chinese (Traditional) / 正體字 zh_TW zh_TW Left-to-Right [3,0] . , %Y年%m月%d日 %H時%M分%S秒 True 7
14 base.lang_hr Croatian / hrvatski jezik hr_HR hr Left-to-Right [] [3,0] , . %d.%m.%Y %H:%M:%S True 1
15 base.lang_cs_CZ Czech / Čeština cs_CZ cs_CZ Left-to-Right [3,0] ,   %d.%m.%Y %H:%M:%S True 1
16 base.lang_da_DK Danish / Dansk da_DK da_DK Left-to-Right [3,0] , . %d-%m-%Y %H:%M:%S True 1
17 base.lang_nl_BE Dutch (BE) / Nederlands (BE) nl_BE nl_BE Left-to-Right [3,0] , . %d-%m-%Y %H:%M:%S True 1
@@ -700,6 +700,7 @@
<field name="code">hr</field>
<field file="base/static/img/country_flags/hr.png" name="image" type="base64" />
<field name="currency_id" ref="HRK" />
<field eval="'%(street)s \n%(street2)s \n%(zip)s %(city)s \n%(country_name)s'" name="address_format" />
<field eval="385" name="phone_code" />
<field name="vat_label">VAT</field>
</record>
@@ -1104,6 +1105,7 @@
<field name="code">mu</field>
<field file="base/static/img/country_flags/mu.png" name="image" type="base64" />
<field name="currency_id" ref="MUR" />
<field eval="'%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s'" name="address_format" />
<field eval="230" name="phone_code" />
</record>
<record id="mv" model="res.country">
+11 -5
View File
@@ -639,11 +639,17 @@ class IrModelFields(models.Model):
for view in views:
view._check_xml()
except Exception:
raise UserError("\n".join([
_("Cannot rename/delete fields that are still present in views:"),
_("Fields: %s") % ", ".join(str(f) for f in fields),
_("View: %s") % view.name,
]))
if not self._context.get(MODULE_UNINSTALL_FLAG):
raise UserError("\n".join([
_("Cannot rename/delete fields that are still present in views:"),
_("Fields: %s") % ", ".join(str(f) for f in fields),
_("View: %s") % view.name,
]))
else:
# uninstall mode
_logger.warn("The following fields were force-deleted to prevent a registry crash "
+ ", ".join(str(f) for f in fields)
+ " the following view might be broken %s" % view.name)
finally:
# the registry has been modified, restore it
self.pool.setup_models(self._cr)
+7 -2
View File
@@ -457,6 +457,11 @@ class Partner(models.Model):
"""Returns the list of address fields that are synced from the parent."""
return list(ADDRESS_FIELDS)
@api.model
def _formatting_address_fields(self):
"""Returns the list of address fields usable to format addresses."""
return self._address_fields()
@api.multi
def update_address(self, vals):
addr_vals = {key: vals[key] for key in self._address_fields() if key in vals}
@@ -932,7 +937,7 @@ class Partner(models.Model):
'country_name': self._get_country_name(),
'company_name': self.commercial_company_name or '',
}
for field in self._address_fields():
for field in self._formatting_address_fields():
args[field] = getattr(self, field) or ''
if without_company:
args['company_name'] = ''
@@ -942,7 +947,7 @@ class Partner(models.Model):
def _display_address_depends(self):
# field dependencies of method _display_address()
return self._address_fields() + [
return self._formatting_address_fields() + [
'country_id.address_format', 'country_id.code', 'country_id.name',
'company_name', 'state_id.code', 'state_id.name',
]
-10
View File
@@ -3168,7 +3168,6 @@ Fields:
Data = self.env['ir.model.data'].sudo().with_context({})
Defaults = self.env['ir.default'].sudo()
Attachment = self.env['ir.attachment']
Translation = self.env['ir.translation'].sudo()
for sub_ids in cr.split_for_in_conditions(self.ids):
query = "DELETE FROM %s WHERE id IN %%s" % self._table
@@ -3199,15 +3198,6 @@ Fields:
if attachments:
attachments.sudo().unlink()
if any(field.translate for field in self._fields.values()):
# For the same reason, remove the relevant records in ir_translation
translations = Translation.search([
('type', 'in', ['model', 'model_terms']),
('name', '=like', self._name+',%'),
('res_id', 'in', sub_ids)])
if translations:
translations.unlink()
# invalidate the *whole* cache, since the orm does not handle all
# changes made in the database, like cascading delete!
self.invalidate_cache()