mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 23102022
This commit is contained in:
@@ -1069,7 +1069,7 @@ class AccountBankStatementLine(models.Model):
|
||||
reconciliation_overview = []
|
||||
|
||||
total_balance = liquidity_lines.balance
|
||||
total_amount_currency = liquidity_lines.amount_currency
|
||||
total_amount_currency = -self._prepare_move_line_default_vals()[1]['amount_currency']
|
||||
|
||||
# Step 1: Split 'lines_vals_list' into two batches:
|
||||
# - The existing account.move.lines that need to be reconciled with the statement line.
|
||||
|
||||
@@ -4855,6 +4855,7 @@ class AccountMoveLine(models.Model):
|
||||
exchange_diff_move_vals['date'] = max(exchange_diff_move_vals['date'], company._get_user_fiscal_lock_date())
|
||||
|
||||
exchange_move = self.env['account.move'].create(exchange_diff_move_vals)
|
||||
exchange_move.line_ids.write({'tax_exigible': True}) # Enforce exigibility in case some cash basis adjustments were made in this exchange difference
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ class AccountPartialReconcile(models.Model):
|
||||
partial_amount_currency = 0.0
|
||||
rate_amount = 0.0
|
||||
rate_amount_currency = 0.0
|
||||
|
||||
if partial.debit_move_id.move_id == move:
|
||||
partial_amount += partial.amount
|
||||
partial_amount_currency += partial.debit_amount_currency
|
||||
@@ -176,6 +177,7 @@ class AccountPartialReconcile(models.Model):
|
||||
rate_amount_currency -= partial.credit_move_id.amount_currency
|
||||
source_line = partial.debit_move_id
|
||||
counterpart_line = partial.credit_move_id
|
||||
|
||||
if partial.credit_move_id.move_id == move:
|
||||
partial_amount += partial.amount
|
||||
partial_amount_currency += partial.credit_amount_currency
|
||||
@@ -184,6 +186,16 @@ class AccountPartialReconcile(models.Model):
|
||||
source_line = partial.credit_move_id
|
||||
counterpart_line = partial.debit_move_id
|
||||
|
||||
if partial.debit_move_id.move_id.is_invoice(include_receipts=True) and partial.credit_move_id.move_id.is_invoice(include_receipts=True):
|
||||
# Will match when reconciling a refund with an invoice.
|
||||
# In this case, we want to use the rate of each businness document to compute its cash basis entry,
|
||||
# not the rate of what it's reconciled with.
|
||||
rate_amount = source_line.balance
|
||||
rate_amount_currency = source_line.amount_currency
|
||||
payment_date = move.date
|
||||
else:
|
||||
payment_date = counterpart_line.date
|
||||
|
||||
if move_values['currency'] == move.company_id.currency_id:
|
||||
# Percentage made on company's currency.
|
||||
percentage = partial_amount / move_values['total_balance']
|
||||
@@ -198,7 +210,7 @@ class AccountPartialReconcile(models.Model):
|
||||
counterpart_line.company_currency_id,
|
||||
source_line.currency_id,
|
||||
counterpart_line.company_id,
|
||||
counterpart_line.date,
|
||||
payment_date,
|
||||
)
|
||||
elif rate_amount:
|
||||
payment_rate = rate_amount_currency / rate_amount
|
||||
|
||||
@@ -1422,6 +1422,51 @@ class TestAccountBankStatementLine(TestAccountBankStatementCommon):
|
||||
{'name': 'whatever', 'account_id': random_acc_1.id, 'balance': -100.0},
|
||||
])
|
||||
|
||||
def test_reconciliation_statement_line_foreign_currency(self):
|
||||
statement = self.env['account.bank.statement'].create({
|
||||
'name': 'test_statement',
|
||||
'date': '2019-01-01',
|
||||
'journal_id': self.bank_journal_1.id,
|
||||
'line_ids': [
|
||||
(0, 0, {
|
||||
'date': '2019-01-01',
|
||||
'payment_ref': 'line_1',
|
||||
'partner_id': self.partner_a.id,
|
||||
'foreign_currency_id': self.currency_2.id,
|
||||
'amount': -80.0,
|
||||
'amount_currency': -120.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
statement.button_post()
|
||||
statement_line = statement.line_ids
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'in_invoice',
|
||||
'invoice_date': '2019-01-01',
|
||||
'date': '2019-01-01',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': self.currency_2.id,
|
||||
'invoice_line_ids': [
|
||||
(0, None, {
|
||||
'name': 'counterpart line, same amount',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'quantity': 1,
|
||||
'price_unit': 120.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
invoice.action_post()
|
||||
invoice_line = invoice.line_ids.filtered(lambda line: line.account_internal_type == 'payable')
|
||||
|
||||
statement_line.reconcile([{'id': invoice_line.id}])
|
||||
|
||||
self.assertRecordValues(statement_line.line_ids, [
|
||||
# pylint: disable=bad-whitespace
|
||||
{'amount_currency': -80.0, 'currency_id': self.currency_1.id, 'balance': -80.0, 'reconciled': False},
|
||||
{'amount_currency': 120.0, 'currency_id': self.currency_2.id, 'balance': 80.0, 'reconciled': True},
|
||||
])
|
||||
|
||||
def test_reconciliation_statement_line_with_generated_payments(self):
|
||||
self.statement.button_post()
|
||||
|
||||
|
||||
@@ -2153,6 +2153,165 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
{'debit': 0.0, 'credit': 50.0, 'currency_id': currency_id, 'account_id': self.cash_basis_base_account.id},
|
||||
])
|
||||
|
||||
def test_reconcile_cash_basis_refund_multicurrency(self):
|
||||
self.env.company.tax_exigibility = True
|
||||
rates_data = self.setup_multi_currency_data(default_values={
|
||||
'name': 'Playmock',
|
||||
'symbol': '🦌',
|
||||
'rounding': 0.01,
|
||||
'currency_unit_label': 'Playmock',
|
||||
'currency_subunit_label': 'Cent',
|
||||
}, rate2016=0.5, rate2017=0.33333333333333333)
|
||||
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'invoice_date': '2016-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'dudu',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
|
||||
})],
|
||||
})
|
||||
|
||||
refund = self.env['account.move'].create({
|
||||
'move_type': 'out_refund',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'invoice_date': '2017-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'name': 'dudu',
|
||||
'account_id': self.company_data['default_account_revenue'].id,
|
||||
'price_unit': 100.0,
|
||||
'tax_ids': [(6, 0, self.cash_basis_tax_a_third_amount.ids)],
|
||||
})],
|
||||
})
|
||||
|
||||
invoice.action_post()
|
||||
refund.action_post()
|
||||
|
||||
(refund + invoice).line_ids.filtered(lambda x: x.account_id.user_type_id.type == 'receivable').reconcile()
|
||||
|
||||
# Check the cash basis moves
|
||||
self.assertRecordValues(
|
||||
self.env['account.move'].search([('tax_cash_basis_move_id', '=', invoice.id)]).line_ids,
|
||||
[
|
||||
{
|
||||
'debit': 200,
|
||||
'credit': 0,
|
||||
'amount_currency': 100,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 0,
|
||||
'credit': 200,
|
||||
'amount_currency': -100,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': self.cash_basis_tax_a_third_amount.ids,
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': self.tax_tags[0].ids,
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 66.66,
|
||||
'credit': 0,
|
||||
'amount_currency': 33.33,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 0,
|
||||
'credit': 66.66,
|
||||
'amount_currency': -33.33,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_tag_ids': self.tax_tags[1].ids,
|
||||
'tax_exigible': True,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
self.assertRecordValues(
|
||||
self.env['account.move'].search([('tax_cash_basis_move_id', '=', refund.id)]).line_ids,
|
||||
[
|
||||
{
|
||||
'debit': 0,
|
||||
'credit': 300,
|
||||
'amount_currency': -100,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 300,
|
||||
'credit': 0,
|
||||
'amount_currency': 100,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': self.cash_basis_tax_a_third_amount.ids,
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': self.tax_tags[2].ids,
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 0,
|
||||
'credit': 99.99,
|
||||
'amount_currency': -33.33,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
'tax_exigible': True,
|
||||
},
|
||||
{
|
||||
'debit': 99.99,
|
||||
'credit': 0,
|
||||
'amount_currency': 33.33,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': self.cash_basis_tax_a_third_amount.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax').id,
|
||||
'tax_tag_ids': self.tax_tags[3].ids,
|
||||
'tax_exigible': True,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
# Check the exchange difference move, to be sure no cash basis rounding data was added into it
|
||||
self.assertRecordValues(
|
||||
invoice.line_ids.full_reconcile_id.exchange_move_id.line_ids,
|
||||
[
|
||||
{
|
||||
'debit': 133.33,
|
||||
'credit': 0,
|
||||
'amount_currency': 0,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
},
|
||||
{
|
||||
'debit': 0,
|
||||
'credit': 133.33,
|
||||
'amount_currency': 0,
|
||||
'currency_id': rates_data['currency'].id,
|
||||
'tax_ids': [],
|
||||
'tax_repartition_line_id': None,
|
||||
'tax_tag_ids': [],
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
def test_reconcile_cash_basis_revert(self):
|
||||
''' Ensure the cash basis journal entry can be reverted. '''
|
||||
self.env.company.tax_exigibility = True
|
||||
|
||||
@@ -91,8 +91,11 @@ class SaleOrder(models.Model):
|
||||
|
||||
def _create_delivery_line(self, carrier, price_unit):
|
||||
SaleOrderLine = self.env['sale.order.line']
|
||||
context = {}
|
||||
if self.partner_id:
|
||||
# set delivery detail in the customer language
|
||||
# used in local scope translation process
|
||||
context['lang'] = self.partner_id.lang
|
||||
carrier = carrier.with_context(lang=self.partner_id.lang)
|
||||
|
||||
# Apply fiscal position
|
||||
@@ -102,12 +105,12 @@ class SaleOrder(models.Model):
|
||||
taxes_ids = self.fiscal_position_id.map_tax(taxes, carrier.product_id, self.partner_id).ids
|
||||
|
||||
# Create the sales order line
|
||||
carrier_with_partner_lang = carrier.with_context(lang=self.partner_id.lang)
|
||||
if carrier_with_partner_lang.product_id.description_sale:
|
||||
so_description = '%s: %s' % (carrier_with_partner_lang.name,
|
||||
carrier_with_partner_lang.product_id.description_sale)
|
||||
|
||||
if carrier.product_id.description_sale:
|
||||
so_description = '%s: %s' % (carrier.name,
|
||||
carrier.product_id.description_sale)
|
||||
else:
|
||||
so_description = carrier_with_partner_lang.name
|
||||
so_description = carrier.name
|
||||
values = {
|
||||
'order_id': self.id,
|
||||
'name': so_description,
|
||||
@@ -127,6 +130,7 @@ class SaleOrder(models.Model):
|
||||
if self.order_line:
|
||||
values['sequence'] = self.order_line[-1].sequence + 1
|
||||
sol = SaleOrderLine.sudo().create(values)
|
||||
del context
|
||||
return sol
|
||||
|
||||
def _format_currency_amount(self, amount):
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "Fetchmail Outlook",
|
||||
"version": "1.0.1.0.0",
|
||||
"version": "1.0",
|
||||
"category": "Hidden",
|
||||
"description": "OAuth authentication for incoming Outlook mail server",
|
||||
"depends": [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import fetchmail_server
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import _, api, models
|
||||
from flectra.exceptions import UserError
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_fetchmail_outlook
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import controllers
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
"name": "Microsoft Outlook",
|
||||
"version": "1.0.1.0.0",
|
||||
"version": "1.0",
|
||||
"category": "Hidden",
|
||||
"description": "Outlook support for outgoing mail servers",
|
||||
"depends": [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import microsoft_outlook_mixin
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# Part of Flectra. See LICENSE file for full copyright and licensing details.
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import fields, models
|
||||
|
||||
|
||||
@@ -776,7 +776,7 @@ class MrpProduction(models.Model):
|
||||
# Remove from `move_finished_ids` the by-product moves and then move `move_byproduct_ids`
|
||||
# into `move_finished_ids` to avoid duplicate and inconsistency.
|
||||
if values.get('move_finished_ids', False):
|
||||
values['move_finished_ids'] = list(filter(lambda move: move[2]['byproduct_id'] is False, values['move_finished_ids']))
|
||||
values['move_finished_ids'] = list(filter(lambda move: move[2].get('byproduct_id', False) is False, values['move_finished_ids']))
|
||||
if values.get('move_byproduct_ids', False):
|
||||
values['move_finished_ids'] = values.get('move_finished_ids', []) + values['move_byproduct_ids']
|
||||
del values['move_byproduct_ids']
|
||||
|
||||
@@ -282,6 +282,8 @@ class PosOrder(models.Model):
|
||||
@api.onchange('payment_ids', 'lines')
|
||||
def _onchange_amount_all(self):
|
||||
for order in self:
|
||||
if not order.pricelist_id.currency_id:
|
||||
raise UserError(_("You can't: create a pos order from the backend interface, or unset the pricelist, or create a pos.order in a python test with Form tool, or edit the form view in studio if no PoS order exist"))
|
||||
currency = order.pricelist_id.currency_id
|
||||
order.amount_paid = sum(payment.amount for payment in order.payment_ids)
|
||||
order.amount_return = sum(payment.amount < 0 and payment.amount or 0 for payment in order.payment_ids)
|
||||
|
||||
@@ -66,7 +66,6 @@ class PurchaseReport(models.Model):
|
||||
|
||||
def _select(self):
|
||||
select_str = """
|
||||
WITH currency_rate as (%s)
|
||||
SELECT
|
||||
po.id as order_id,
|
||||
min(l.id) as id,
|
||||
@@ -101,7 +100,7 @@ class PurchaseReport(models.Model):
|
||||
then sum(l.product_qty / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
|
||||
else sum(l.qty_received / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor)
|
||||
end as qty_to_be_billed
|
||||
""" % self.env['res.currency']._select_companies_rates()
|
||||
"""
|
||||
return select_str
|
||||
|
||||
def _from(self):
|
||||
@@ -115,10 +114,6 @@ class PurchaseReport(models.Model):
|
||||
left join uom_uom line_uom on (line_uom.id=l.product_uom)
|
||||
left join uom_uom product_uom on (product_uom.id=t.uom_id)
|
||||
left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id)
|
||||
left join currency_rate cr on (cr.currency_id = po.currency_id and
|
||||
cr.company_id = po.company_id and
|
||||
cr.date_start <= coalesce(po.date_order, now()) and
|
||||
(cr.date_end is null or cr.date_end > coalesce(po.date_order, now())))
|
||||
left join {currency_table} ON currency_table.company_id = po.company_id
|
||||
""".format(
|
||||
currency_table=self.env['res.currency']._get_query_currency_table({'multi_company': True, 'date': {'date_to': fields.Date.today()}}),
|
||||
|
||||
@@ -403,6 +403,8 @@ class SnailmailLetter(models.Model):
|
||||
])
|
||||
for letter in letters_send:
|
||||
letter._snailmail_print()
|
||||
if letter.error_code == 'CREDIT_ERROR':
|
||||
break # avoid spam
|
||||
# Commit after every letter sent to avoid to send it again in case of a rollback
|
||||
if autocommit:
|
||||
self.env.cr.commit()
|
||||
|
||||
@@ -388,8 +388,12 @@ var AbstractField = Widget.extend({
|
||||
* @returns {string}
|
||||
*/
|
||||
_formatValue: function (value, formatType) {
|
||||
formatType = formatType || this.formatType;
|
||||
if (!formatType) {
|
||||
throw new Error(`Missing format type for '${this.name}' value from the '${this.model}' model`);
|
||||
}
|
||||
var options = _.extend({}, this.nodeOptions, { data: this.recordData }, this.formatOptions);
|
||||
return field_utils.format[formatType || this.formatType](value, this.field, options);
|
||||
return field_utils.format[formatType](value, this.field, options);
|
||||
},
|
||||
/**
|
||||
* Returns the className corresponding to a given decoration. A
|
||||
|
||||
@@ -351,8 +351,14 @@ var KanbanModel = BasicModel.extend({
|
||||
var data = results[1];
|
||||
_.each(list.data, function (groupID) {
|
||||
var group = self.localData[groupID];
|
||||
var value = group.value;
|
||||
if (value === true) {
|
||||
value = "True";
|
||||
} else if (value === false) {
|
||||
value = "False";
|
||||
}
|
||||
group.progressBarValues = _.extend({
|
||||
counts: data[group.value] || {},
|
||||
counts: data[value] || {},
|
||||
}, list.progressBar);
|
||||
});
|
||||
return list;
|
||||
|
||||
@@ -1593,6 +1593,15 @@ var MockServer = Class.extend({
|
||||
_.each(records, function (record) {
|
||||
var groupByValue = record[groupBy]; // always technical value here
|
||||
|
||||
// special case for bool values: rpc call response with capitalized strings
|
||||
if (!(groupByValue in data)) {
|
||||
if (groupByValue === true) {
|
||||
groupByValue = "True";
|
||||
} else if (groupByValue === false) {
|
||||
groupByValue = "False";
|
||||
}
|
||||
}
|
||||
|
||||
if (!(groupByValue in data)) {
|
||||
data[groupByValue] = {};
|
||||
_.each(progress_bar.colors, function (val, key) {
|
||||
|
||||
@@ -1389,7 +1389,23 @@ const ColorpickerUserValueWidget = SelectUserValueWidget.extend({
|
||||
} else if (weUtils.isColorCombinationName(this._value)) {
|
||||
this.colorPreviewEl.classList.add('o_cc', `o_cc${this._value}`);
|
||||
} else {
|
||||
this.colorPreviewEl.classList.add(`bg-${this._value}`);
|
||||
// Checking if the className actually exists seems overkill but
|
||||
// it is actually needed to prevent a crash. As an example, if a
|
||||
// colorpicker widget is linked to a SnippetOption instance's
|
||||
// `selectStyle` method designed to handle the "border-color"
|
||||
// property of an element, the value received can be split if
|
||||
// the item uses different colors for its top/right/bottom/left
|
||||
// borders. For instance, you could receive "red blue" if the
|
||||
// item as red top and bottom borders and blue left and right
|
||||
// borders, in which case you would reach this `else` and try to
|
||||
// add the class "bg-red blue" which would crash because of the
|
||||
// space inside). In that case, we simply do not show any color.
|
||||
// We could choose to handle this split-value case specifically
|
||||
// but it was decided that this is enough for the moment.
|
||||
const className = `bg-${this._value}`;
|
||||
if (classes.includes(className)) {
|
||||
this.colorPreviewEl.classList.add(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2085,6 +2101,13 @@ const SnippetOptionWidget = Widget.extend({
|
||||
* @param {boolean} previewMode - @see this.selectClass
|
||||
* @param {string} widgetValue
|
||||
* @param {Object} params
|
||||
* @param {string} [params.forceStyle] if undefined, the method will not
|
||||
* set the inline style (and thus even remove it) if the item would
|
||||
* already have the given style without it (thanks to a CSS rule for
|
||||
* example). If defined (as a string), it acts as the "priority" param
|
||||
* of @see CSSStyleDeclaration.setProperty: it should be 'important' to
|
||||
* set the style as important or '' otherwise. Note that if forceStyle
|
||||
* is undefined, the style is always set as important when applied.
|
||||
* @returns {Promise|undefined}
|
||||
*/
|
||||
selectStyle: function (previewMode, widgetValue, params) {
|
||||
@@ -2179,8 +2202,11 @@ const SnippetOptionWidget = Widget.extend({
|
||||
hasUserValue = applyCSS.call(this, cssProps[0], values.join(' '), styles) || hasUserValue;
|
||||
|
||||
function applyCSS(cssProp, cssValue, styles) {
|
||||
if (!weUtils.areCssValuesEqual(styles[cssProp], cssValue, cssProp, this.$target[0])) {
|
||||
this.$target[0].style.setProperty(cssProp, cssValue, 'important');
|
||||
const forceStyle = (typeof params.forceStyle !== 'undefined');
|
||||
if (forceStyle
|
||||
|| !weUtils.areCssValuesEqual(styles[cssProp], cssValue, cssProp, this.$target[0])) {
|
||||
const priority = forceStyle ? params.forceStyle : 'important';
|
||||
this.$target[0].style.setProperty(cssProp, cssValue, priority);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -17,8 +17,12 @@ class Tour(models.Model):
|
||||
def consume(self, tour_names):
|
||||
""" Sets given tours as consumed, meaning that
|
||||
these tours won't be active anymore for that user """
|
||||
if not self.env.user.has_group('base.group_user'):
|
||||
# Only internal users can use this method.
|
||||
# TODO master: update ir.model.access records instead of using sudo()
|
||||
return
|
||||
for name in tour_names:
|
||||
self.create({'name': name, 'user_id': self.env.uid})
|
||||
self.sudo().create({'name': name, 'user_id': self.env.uid})
|
||||
|
||||
@api.model
|
||||
def get_consumed_tours(self):
|
||||
|
||||
@@ -27,6 +27,7 @@ const BaseAnimatedHeader = animations.Animation.extend({
|
||||
this.fixedHeader = false;
|
||||
this.scrolledPoint = 0;
|
||||
this.hasScrolled = false;
|
||||
this.scrollHeightTooShort = false;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -155,6 +156,33 @@ const BaseAnimatedHeader = animations.Animation.extend({
|
||||
}
|
||||
this.$main.css('padding-top', this.fixedHeader ? this.headerHeight : '');
|
||||
},
|
||||
/**
|
||||
* Checks if the size of the header will decrease by adding the
|
||||
* 'o_header_is_scrolled' class. If so, we do not add this class if the
|
||||
* remaining scroll height is not enough to stay above 'this.scrolledPoint'
|
||||
* after the transition, otherwise it causes the scroll position to move up
|
||||
* again below 'this.scrolledPoint' and trigger an infinite loop.
|
||||
*
|
||||
* @todo header effects should be improved in the future to not ever change
|
||||
* the page scroll-height during their animation. The code would probably be
|
||||
* simpler but also prevent having weird scroll "jumps" during animations
|
||||
* (= depending on the logo height after/before scroll, a scroll step (one
|
||||
* mousewheel event for example) can be bigger than other ones).
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_scrollHeightTooShort() {
|
||||
const scrollEl = $().getScrollingElement()[0];
|
||||
const remainingScroll = (scrollEl.scrollHeight - scrollEl.clientHeight) - this.scrolledPoint;
|
||||
const clonedHeader = this.el.cloneNode(true);
|
||||
scrollEl.append(clonedHeader);
|
||||
clonedHeader.classList.add('o_header_is_scrolled', 'o_header_affixed', 'o_header_no_transition');
|
||||
const endHeaderHeight = clonedHeader.offsetHeight;
|
||||
clonedHeader.remove();
|
||||
const heightDiff = this.headerHeight - endHeaderHeight;
|
||||
return heightDiff > 0 ? remainingScroll <= heightDiff : false;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
@@ -180,9 +208,12 @@ const BaseAnimatedHeader = animations.Animation.extend({
|
||||
// Indicates the page is scrolled, the logo size is changed.
|
||||
const headerIsScrolled = (scroll > this.scrolledPoint);
|
||||
if (this.headerIsScrolled !== headerIsScrolled) {
|
||||
this.el.classList.toggle('o_header_is_scrolled', headerIsScrolled);
|
||||
this.$el.trigger('flectra-transitionstart');
|
||||
this.headerIsScrolled = headerIsScrolled;
|
||||
this.scrollHeightTooShort = headerIsScrolled && this._scrollHeightTooShort();
|
||||
if (!this.scrollHeightTooShort) {
|
||||
this.el.classList.toggle('o_header_is_scrolled', headerIsScrolled);
|
||||
this.$el.trigger('flectra-transitionstart');
|
||||
this.headerIsScrolled = headerIsScrolled;
|
||||
}
|
||||
}
|
||||
|
||||
// Close opened menus
|
||||
@@ -222,6 +253,13 @@ publicWidget.registry.StandardAffixedHeader = BaseAnimatedHeader.extend({
|
||||
this.headerHeight = this.$el.outerHeight();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this.$el.css('transform', '');
|
||||
this._super(...arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
@@ -236,14 +274,14 @@ publicWidget.registry.StandardAffixedHeader = BaseAnimatedHeader.extend({
|
||||
/**
|
||||
* Called when the window is scrolled
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
* @param {integer} scroll
|
||||
*/
|
||||
_updateHeaderOnScroll: function (scroll) {
|
||||
this._super(...arguments);
|
||||
|
||||
const mainPosScrolled = (scroll > this.headerHeight + this.topGap);
|
||||
const reachPosScrolled = (scroll > this.scrolledPoint + this.topGap);
|
||||
const reachPosScrolled = (scroll > this.scrolledPoint + this.topGap) && !this.scrollHeightTooShort;
|
||||
const fixedUpdate = (this.fixedHeader !== mainPosScrolled);
|
||||
const showUpdate = (this.fixedHeaderShow !== reachPosScrolled);
|
||||
|
||||
|
||||
@@ -2687,6 +2687,58 @@ options.registry.ScrollButton = options.Class.extend({
|
||||
},
|
||||
});
|
||||
|
||||
// TODO there is no data-js associated to this but a data-option-name, somehow
|
||||
// it acts as data-js... it will be reviewed in master.
|
||||
options.registry.minHeight = options.Class.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_renderCustomXML(uiFragment) {
|
||||
// TODO adapt in master. This sets up a different UI for the image
|
||||
// gallery snippet: for this one, we allow to force a specific height
|
||||
// in auto mode. It was done in stable as without it, the default height
|
||||
// is difficult to understand for the user as it depends on screen
|
||||
// height of the one who edited the website and not on the added images.
|
||||
// It was also a regression as in <= 11.0, this was a possibility.
|
||||
if (this.$target[0].dataset.snippet !== 's_image_gallery') {
|
||||
return;
|
||||
}
|
||||
const minHeightEl = uiFragment.querySelector('we-button-group');
|
||||
if (!minHeightEl) {
|
||||
return;
|
||||
}
|
||||
minHeightEl.setAttribute('string', _t("Min-Height"));
|
||||
const heightEl = document.createElement('we-input');
|
||||
heightEl.setAttribute('string', _t("└ Height"));
|
||||
heightEl.dataset.name = 'image_gallery_height_opt';
|
||||
heightEl.dataset.unit = 'px';
|
||||
heightEl.dataset.selectStyle = '';
|
||||
heightEl.dataset.cssProperty = 'height';
|
||||
// For this setting, we need to always force the style (= if the block
|
||||
// is naturally 800px tall and the user enters 800px for this setting,
|
||||
// we set 800px as inline style anyway). Indeed, this snippet's style
|
||||
// is based on the height that is forced but once the related public
|
||||
// widgets are started, the inner carousel items receive a min-height
|
||||
// which makes it so the snippet "natural" height is equal to the
|
||||
// initially forced height... so if the style is not forced, it would
|
||||
// ultimately be removed by mistake thinking it is not necessary.
|
||||
// Note: this is forced as not important as we still need the height to
|
||||
// be reset to 'auto' in mobile (generic css rules).
|
||||
heightEl.dataset.forceStyle = '';
|
||||
uiFragment.appendChild(heightEl);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_computeWidgetVisibility(widgetName, params) {
|
||||
if (widgetName === 'image_gallery_height_opt') {
|
||||
return !this.$target[0].classList.contains('o_half_screen_height')
|
||||
&& !this.$target[0].classList.contains('o_full_screen_height');
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
UrlPickerUserValueWidget: UrlPickerUserValueWidget,
|
||||
FontFamilyPickerUserValueWidget: FontFamilyPickerUserValueWidget,
|
||||
|
||||
@@ -1001,6 +1001,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Min height of section -->
|
||||
<!-- TODO adapt in master, this is patched in JS -->
|
||||
<div data-option-name="minHeight" data-selector="section">
|
||||
<we-button-group string="Height">
|
||||
<we-button data-select-class="" title="Fit content">Auto</we-button>
|
||||
|
||||
Reference in New Issue
Block a user