[MERGE] forward port branch 12.0 up to e11bacfe51
This commit is contained in:
@@ -435,6 +435,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
|
||||
@@ -442,19 +443,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
|
||||
@@ -463,9 +468,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 \
|
||||
@@ -476,22 +480,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 ''
|
||||
@@ -611,8 +664,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
|
||||
@@ -633,32 +684,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:
|
||||
@@ -708,6 +739,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'})
|
||||
|
||||
@@ -54,12 +54,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.one
|
||||
@@ -731,22 +733,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."""
|
||||
|
||||
@@ -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
|
||||
@@ -321,6 +328,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')
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -806,6 +806,12 @@ QUnit.test('save two searches to dashboard', function (assert) {
|
||||
testUtils.dom.click(actionManager.$('.o_apply_filter'));
|
||||
// Add it to dashboard
|
||||
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();
|
||||
});
|
||||
testUtils.dom.click(actionManager.$('.o_add_to_board'));
|
||||
testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
|
||||
// Remove it
|
||||
@@ -818,6 +824,12 @@ QUnit.test('save two searches to dashboard', function (assert) {
|
||||
testUtils.dom.click(actionManager.$('.o_apply_filter'));
|
||||
// Add it to dashboard
|
||||
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();
|
||||
});
|
||||
testUtils.dom.click(actionManager.$('.o_add_to_board'));
|
||||
testUtils.dom.click(actionManager.$('.o_add_to_board_confirm_button'));
|
||||
|
||||
|
||||
@@ -409,8 +409,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -64,7 +64,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()
|
||||
@@ -74,7 +74,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -138,10 +138,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]
|
||||
@@ -149,9 +155,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
|
||||
|
||||
@@ -226,7 +238,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"
|
||||
|
||||
@@ -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''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 '',
|
||||
|
||||
@@ -365,7 +365,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;
|
||||
|
||||
@@ -197,7 +197,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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -36,6 +36,18 @@ odoo.define('payment.payment_form', function (require) {
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
},
|
||||
|
||||
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> </span>');
|
||||
},
|
||||
|
||||
enableButton: function (button) {
|
||||
$(button).attr('disabled', false);
|
||||
$(button).children('.fa').addClass('fa-lock');
|
||||
$(button).find('span.o_loader').remove();
|
||||
},
|
||||
|
||||
payEvent: function (ev) {
|
||||
ev.preventDefault();
|
||||
var form = this.el;
|
||||
@@ -90,10 +102,7 @@ odoo.define('payment.payment_form', function (require) {
|
||||
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> </span>');
|
||||
|
||||
this.disableButton(button);
|
||||
var verify_validity = this.$el.find('input[name="verify_validity"]');
|
||||
|
||||
if (verify_validity.length>0) {
|
||||
@@ -128,14 +137,10 @@ odoo.define('payment.payment_form', function (require) {
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}).fail(function (error, event) {
|
||||
// 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'),
|
||||
@@ -200,6 +205,7 @@ odoo.define('payment.payment_form', function (require) {
|
||||
}
|
||||
}
|
||||
else { // if the user is using an old payment then we just submit the form
|
||||
this.disableButton(button);
|
||||
form.submit();
|
||||
return $.Deferred();
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -887,6 +887,9 @@ exports.PosModel = Backbone.Model.extend({
|
||||
}}).done(function () {
|
||||
invoiced.resolve();
|
||||
done.resolve();
|
||||
}).fail(function (error) {
|
||||
invoiced.reject({code:401, message:'Backend Invoice', data:{order: order}});
|
||||
done.reject();
|
||||
});
|
||||
} 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)
|
||||
|
||||
@@ -47,7 +47,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'),
|
||||
|
||||
@@ -499,6 +499,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)
|
||||
|
||||
@@ -507,6 +510,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
|
||||
@@ -528,14 +532,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]]:
|
||||
|
||||
@@ -88,7 +88,7 @@ var 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":
|
||||
@@ -101,7 +101,7 @@ var 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;
|
||||
}
|
||||
|
||||
@@ -446,7 +446,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)
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
odoo.define("sale_stock.tour", function (require) {
|
||||
var deps = ['web_tour.tour'];
|
||||
|
||||
if (_.contains(odoo._modules, 'sale_management')) {
|
||||
deps.push('sale.product_configurator_tour');
|
||||
deps.push('sale.product_configurator_pricelist_tour');
|
||||
}
|
||||
|
||||
odoo.define("sale_stock.tour", deps, function (require) {
|
||||
"use strict";
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
require('sale.tour');
|
||||
|
||||
var quotation_product_selected_step_index = _.findIndex(tour.tours.sale_product_configurator_tour.steps, function (step) {
|
||||
return (step.id === "quotation_product_selected");
|
||||
});
|
||||
if (_.has(odoo.__DEBUG__.services, 'sale.product_configurator_tour')) {
|
||||
var quotation_product_selected_step_index = _.findIndex(tour.tours.sale_product_configurator_tour.steps, function (step) {
|
||||
return (step.id === "quotation_product_selected");
|
||||
});
|
||||
|
||||
tour.tours.sale_product_configurator_tour.steps[quotation_product_selected_step_index].run = function (actions) {
|
||||
actions.auto();
|
||||
};
|
||||
tour.tours.sale_product_configurator_tour.steps[quotation_product_selected_step_index].run = function (actions) {
|
||||
actions.auto();
|
||||
};
|
||||
|
||||
tour.tours.sale_product_configurator_tour.steps.splice(quotation_product_selected_step_index+1, 0, {
|
||||
trigger: ".o_dialog_warning + .modal-footer .btn-primary",
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
|
||||
}
|
||||
}, {
|
||||
trigger: ".o_dialog_warning + .modal-footer .btn-primary",
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
|
||||
}
|
||||
}, {
|
||||
trigger: "body:not(:has(.o_dialog_warning))",
|
||||
auto: true,
|
||||
in_modal: false,
|
||||
run: function (actions) {
|
||||
if ($('.modal-footer .btn-primary').length){
|
||||
actions.auto('.modal-footer .btn-primary');
|
||||
tour.tours.sale_product_configurator_tour.steps.splice(quotation_product_selected_step_index+1, 0, {
|
||||
trigger: ".o_dialog_warning + .modal-footer .btn-primary",
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
|
||||
}
|
||||
},
|
||||
});
|
||||
}, {
|
||||
trigger: ".o_dialog_warning + .modal-footer .btn-primary",
|
||||
auto: true,
|
||||
run: function (actions) {
|
||||
actions.auto('.o_dialog_warning + .modal-footer .btn-primary');
|
||||
}
|
||||
}, {
|
||||
trigger: "body:not(:has(.o_dialog_warning))",
|
||||
auto: true,
|
||||
in_modal: false,
|
||||
run: function (actions) {
|
||||
if ($('.modal-footer .btn-primary').length){
|
||||
actions.auto('.modal-footer .btn-primary');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check if sale_management is installed since sale_stock is adding an extra
|
||||
// step to add to SO (not enough inventory)
|
||||
if ('sale.product_configurator_pricelist_tour' in odoo.__DEBUG__.services) {
|
||||
if (_.has(odoo.__DEBUG__.services, 'sale.product_configurator_pricelist_tour')) {
|
||||
var steps = tour.tours.sale_product_configurator_pricelist_tour.steps;
|
||||
for (var k=0; k<steps.length; k++) {
|
||||
if (steps[k].content === "add to SO") {
|
||||
|
||||
@@ -200,7 +200,7 @@ class SaleTimesheetController(http.Controller):
|
||||
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
|
||||
|
||||
@@ -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);
|
||||
@@ -332,6 +334,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.
|
||||
*
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -362,15 +361,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,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;
|
||||
}
|
||||
|
||||
@@ -1326,13 +1326,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>
|
||||
@@ -1516,7 +1517,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"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -44,6 +44,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"},
|
||||
@@ -2394,6 +2395,49 @@ QUnit.module('Views', {
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('drag and drop on month mode with date_start and date_delay', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var calendar = createView({
|
||||
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");
|
||||
var $input = $('.modal-body input:first');
|
||||
$input.val("An event").trigger('input');
|
||||
$('.modal button.btn:contains(Create)').trigger('click');
|
||||
|
||||
// Move event to another day (on 27 november)
|
||||
testUtils.dragAndDrop(
|
||||
calendar.$('.fc-event').first(),
|
||||
calendar.$('.fc-day-top').first()
|
||||
);
|
||||
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('form_view_id attribute works (for creating events)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -1272,7 +1272,7 @@ QUnit.module('Search View', {
|
||||
});
|
||||
|
||||
QUnit.test('Customizing filter does not close the filter dropdown', function (assert) {
|
||||
assert.expect(3);
|
||||
assert.expect(5);
|
||||
var self = this;
|
||||
|
||||
_.each(this.data.partner.records.slice(), function (rec) {
|
||||
@@ -1296,6 +1296,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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
testUtils.fields.many2one.clickOpenDropdown('bar');
|
||||
@@ -1318,9 +1325,22 @@ QUnit.module('Search View', {
|
||||
$input.click();
|
||||
$input.click();
|
||||
});
|
||||
|
||||
assert.isVisible($filterDropdown);
|
||||
|
||||
// Favorites Menu
|
||||
var $modal = $('.modal');
|
||||
testUtils.dom.click($modal.find('.o_favorites_menu_button'));
|
||||
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();
|
||||
});
|
||||
testUtils.fields.editInput($modal.find('div.o_favorite_name input'), 'Fire on the bayou');
|
||||
testUtils.dom.click($modal.find('.o_add_favorite ~ div label:contains(Use by default)'));
|
||||
testUtils.dom.click($modal.find('.o_save_favorite button'));
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<template id="report_assets_common">
|
||||
<t t-call="web._assets_helpers">
|
||||
<!-- FIXME web_editor ? -->
|
||||
<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>
|
||||
|
||||
|
||||
@@ -83,5 +83,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);
|
||||
|
||||
@@ -33,8 +33,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 = [];
|
||||
@@ -229,13 +229,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;
|
||||
|
||||
@@ -356,7 +356,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),
|
||||
|
||||
@@ -43,7 +43,7 @@ sAnimations.registry.websiteSaleDelivery = sAnimations.Class.extend({
|
||||
var $amountUntaxed = $('#order_total_untaxed span.oe_currency_value');
|
||||
var $amountTax = $('#order_total_taxes span.oe_currency_value');
|
||||
var $amountTotal = $('#order_total span.oe_currency_value');
|
||||
var $carrierBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge.d-none');
|
||||
var $carrierBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .badge:not(.o_delivery_compute)');
|
||||
var $computeBadge = $('#delivery_carrier input[name="delivery_type"][value=' + result.carrier_id + '] ~ .o_delivery_compute');
|
||||
var $discount = $('#order_discounted');
|
||||
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
<label class="label-optional" t-field="delivery.name" t-att-for="'delivery_%i' % delivery.id"/>
|
||||
<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} d-none" 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}'/>
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'] %}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
-------------------
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -443,6 +443,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}
|
||||
@@ -914,7 +919,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'] = ''
|
||||
@@ -924,7 +929,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',
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user