[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):
|
||||
|
||||
Reference in New Issue
Block a user