[PATCH] Upstream patch - 04032023

This commit is contained in:
Parthiv Patel
2023-03-04 08:34:19 +00:00
parent 1396401b7a
commit 75f61b0edc
23 changed files with 394 additions and 46 deletions
@@ -8,6 +8,7 @@ from flectra.tools import html_escape
from flectra.exceptions import RedirectWarning
from lxml import etree
from struct import error as StructError
import base64
import io
import logging
@@ -393,7 +394,7 @@ class AccountEdiFormat(models.Model):
try:
for xml_name, content in pdf_reader.getAttachments():
to_process.extend(self._decode_xml(xml_name, content))
except NotImplementedError as e:
except (NotImplementedError, StructError) as e:
_logger.warning("Unable to access the attachments of %s. Tried to decrypt it, but %s." % (filename, e))
# Process the pdf itself.
+1 -1
View File
@@ -35,7 +35,7 @@ def validate_iban(iban):
raise ValidationError(_("The IBAN is invalid, it should begin with the country code"))
iban_template = _map_iban_template[country_code]
if len(iban) != len(iban_template.replace(' ', '')):
if len(iban) != len(iban_template.replace(' ', '')) or not re.fullmatch("[a-zA-Z0-9]+", iban):
raise ValidationError(_("The IBAN does not seem to be correct. You should have entered something like this %s\n"
"Where B = National bank code, S = Branch code, C = Account No, k = Check digit") % iban_template)
+1 -1
View File
@@ -50,7 +50,7 @@
<field name="model_id" ref="model_hr_contract"/>
<field name="type">ir.actions.server</field>
<field name="state">code</field>
<field name="code">model.update_state()</field>
<field name="code">model.with_context(from_cron=True).update_state()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
+35 -10
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import threading
from datetime import date
from dateutil.relativedelta import relativedelta
@@ -9,6 +11,10 @@ from flectra.exceptions import ValidationError
from flectra.osv import expression
import logging
_logger = logging.getLogger(__name__)
class Contract(models.Model):
_name = 'hr.contract'
_description = 'Contract'
@@ -130,6 +136,7 @@ class Contract(models.Model):
@api.model
def update_state(self):
from_cron = 'from_cron' in self.env.context
contracts = self.search([
('state', '=', 'open'), ('kanban_state', '!=', 'blocked'),
'|',
@@ -147,20 +154,23 @@ class Contract(models.Model):
_("The contract of %s is about to expire.", contract.employee_id.name),
user_id=contract.hr_responsible_id.id or self.env.uid)
contracts.write({'kanban_state': 'blocked'})
if contracts:
contracts._safe_write_for_cron({'kanban_state': 'blocked'}, from_cron)
self.search([
contracts_to_close = self.search([
('state', '=', 'open'),
'|',
('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))),
]).write({
'state': 'close'
})
])
self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({
'state': 'open'
})
if contracts_to_close:
contracts_to_close._safe_write_for_cron({'state': 'close'}, from_cron)
contracts_to_open = self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),])
if contracts_to_open:
contracts_to_open._safe_write_for_cron({'state': 'open'}, from_cron)
contract_ids = self.search([('date_end', '=', False), ('state', '=', 'close'), ('employee_id', '!=', False)])
# Ensure all closed contract followed by a new contract have a end date.
@@ -172,17 +182,32 @@ class Contract(models.Model):
('date_start', '>', contract.date_start)
], order="date_start asc", limit=1)
if next_contract:
contract.date_end = next_contract.date_start - relativedelta(days=1)
contract._safe_write_for_cron({'date_end': next_contract.date_start - relativedelta(days=1)}, from_cron)
continue
next_contract = self.search([
('employee_id', '=', contract.employee_id.id),
('date_start', '>', contract.date_start)
], order="date_start asc", limit=1)
if next_contract:
contract.date_end = next_contract.date_start - relativedelta(days=1)
contract._safe_write_for_cron({'date_end': next_contract.date_start - relativedelta(days=1)}, from_cron)
return True
def _safe_write_for_cron(self, vals, from_cron=False):
if from_cron:
auto_commit = not getattr(threading.current_thread(), 'testing', False)
for contract in self:
try:
with self.env.cr.savepoint():
contract.write(vals)
except ValidationError as e:
_logger.warning(e)
else:
if auto_commit:
self.env.cr.commit()
else:
self.write(vals)
def _assign_open_contract(self):
for contract in self:
contract.employee_id.sudo().write({'contract_id': contract.id})
+4 -8
View File
@@ -513,10 +513,9 @@
<field name="model">purchase.order</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree string="Purchase Order" multi_edit="1" decoration-bf="message_unread==True"
<tree string="Purchase Order" multi_edit="1"
decoration-muted="state=='cancel'" decoration-info="state in ('wait','confirmed')" sample="1">
<field name="priority" optional="show" widget="priority" nolabel="1"/>
<field name="message_unread" invisible="1"/>
<field name="partner_ref" optional="hide"/>
<field name="name" string="Reference" readonly="1"/>
<field name="date_order" invisible="not context.get('quotation_only', False)" optional="show"/>
@@ -544,13 +543,12 @@
<field name="model">purchase.order</field>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<tree string="Purchase Order" multi_edit="1" decoration-bf="message_unread==True"
class="o_purchase_order" js_class="purchase_list_dashboard" sample="1">
<tree string="Purchase Order" multi_edit="1" class="o_purchase_order"
js_class="purchase_list_dashboard" sample="1">
<header>
<button name="action_create_invoice" type="object" string="Create Bills"/>
</header>
<field name="priority" optional="show" widget="priority" nolabel="1"/>
<field name="message_unread" invisible="1"/>
<field name="partner_ref" optional="hide"/>
<field name="name" string="Reference" readonly="1" decoration-bf="1"/>
<field name="date_approve" invisible="context.get('quotation_only', False)" optional="show"/>
@@ -577,8 +575,7 @@
<field name="name">purchase.order.view.tree</field>
<field name="model">purchase.order</field>
<field name="arch" type="xml">
<tree decoration-bf="message_unread==True"
decoration-muted="state=='cancel'"
<tree decoration-muted="state=='cancel'"
decoration-info="state in ('wait','confirmed')"
string="Purchase Order"
class="o_purchase_order"
@@ -587,7 +584,6 @@
<button name="action_create_invoice" type="object" string="Create Bills"/>
</header>
<field name="priority" optional="show" widget="priority" nolabel="1"/>
<field name="message_unread" invisible="1"/>
<field name="partner_ref" optional="hide"/>
<field name="name" string="Reference" readonly="1" decoration-bf="1"/>
<field name="date_approve" widget="date" invisible="context.get('quotation_only', False)" optional="show"/>
+1 -6
View File
@@ -16,12 +16,7 @@ class AccountMove(models.Model):
for line in line_ids:
try:
line.sale_line_ids.tax_id = line.tax_ids
if all(line.tax_ids.mapped('price_include')):
line.sale_line_ids.price_unit = line.price_unit
else:
#To keep positive amount on the sale order and to have the right price for the invoice
#We need the - before our untaxed_amount_to_invoice
line.sale_line_ids.price_unit = -line.sale_line_ids.untaxed_amount_to_invoice
line.sale_line_ids.price_unit = line.price_unit
except UserError:
# a UserError here means the SO was locked, which prevents changing the taxes
# just ignore the error - this is a nice to have feature and should not be blocking
+46
View File
@@ -133,6 +133,52 @@ class TestSaleToInvoice(TestSaleCommon):
self.assertEqual(len(invoice.invoice_line_ids.filtered(lambda l: l.display_type == 'line_section' and l.name == "Down Payments")), 1, 'A single section for downpayments should be present')
self.assertEqual(invoice.amount_total, self.sale_order.amount_total - sum(downpayment_line.mapped('price_unit')), 'Downpayment should be applied')
def test_downpayment_line_remains_on_SO(self):
""" Test downpayment's SO line is created and remains unchanged even if everything is invoiced
"""
# Create the SO with one line
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
})
sale_order_line = self.env['sale.order.line'].with_context(tracking_disable=True).create({
'name': self.company_data['product_order_no'].name,
'product_id': self.company_data['product_order_no'].id,
'product_uom_qty': 5,
'product_uom': self.company_data['product_order_no'].uom_id.id,
'price_unit': self.company_data['product_order_no'].list_price,
'order_id': sale_order.id,
'tax_id': False,
})
# Confirm the SO
sale_order.action_confirm()
# Update delivered quantity of SO line
sale_order_line.write({'qty_delivered': 5.0})
context = {
'active_model': 'sale.order',
'active_ids': [sale_order.id],
'active_id': sale_order.id,
'default_journal_id': self.company_data['default_journal_sale'].id,
}
# Let's do an invoice for a down payment of 50
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
'advance_payment_method': 'fixed',
'fixed_amount': 50,
'deposit_account_id': self.company_data['default_account_revenue'].id
})
downpayment.create_invoices()
# Let's do the invoice
payment = self.env['sale.advance.payment.inv'].with_context(context).create({
'deposit_account_id': self.company_data['default_account_revenue'].id
})
payment.create_invoices()
# Confirm all invoices
for invoice in sale_order.invoice_ids:
invoice.action_post()
downpayment_line = sale_order.order_line.filtered(lambda l: l.is_downpayment)
self.assertEqual(downpayment_line[0].price_unit, 50, 'The down payment unit price should not change on SO')
def test_downpayment_percentage_tax_icl(self):
""" Test invoice with a percentage downpayment and an included tax
Check the total amount of invoice is correct and equal to a respective sale order's total amount
+5 -1
View File
@@ -15,8 +15,12 @@ class AccountMoveLine(models.Model):
if bom:
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
posted_invoice_lines = so_line.invoice_lines.filtered(lambda l: l.move_id.state == 'posted' and bool(l.move_id.reversed_entry_id) == is_line_reversing)
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
posted_invoice_lines = account_moves.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
reversal_cogs = posted_invoice_lines.move_id.reversal_move_id.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
moves = so_line.move_ids
average_price_unit = 0
components_qty = so_line._get_bom_component_qty(bom)
@@ -2358,3 +2358,76 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
self.assertEqual(price, 10)
def test_fifo_reverse_and_create_new_invoice(self):
"""
FIFO automated
Kit with one component
Receive the component: 1@10, 1@50
Deliver 1 kit
Post the invoice, add a credit note with option 'new draft inv'
Post the second invoice
COGS should be based on the delivered kit
"""
kit = self._create_product('Simple Kit', self.uom_unit)
categ_form = Form(self.env['product.category'])
categ_form.name = 'Super Fifo'
categ_form.property_cost_method = 'fifo'
categ_form.property_valuation = 'real_time'
categ = categ_form.save()
(kit + self.component_a).categ_id = categ
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})]
})
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': self.component_a.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.component_a.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 50]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
so = self.env['sale.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id,
'order_line': [
(0, 0, {
'name': kit.name,
'product_id': kit.id,
'product_uom_qty': 1.0,
'product_uom': kit.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
picking = so.picking_ids
picking.move_lines.quantity_done = 1.0
picking.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
'refund_method': 'modify',
})
invoice02 = self.env['account.move'].browse(wizard.reverse_moves()['res_id'])
invoice02.action_post()
amls = invoice02.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == categ.property_stock_account_output_categ_id)
self.assertEqual(stock_out_aml.debit, 0)
self.assertEqual(stock_out_aml.credit, 10)
cogs_aml = amls.filtered(lambda aml: aml.account_id == categ.property_account_expense_categ_id)
self.assertEqual(cogs_aml.debit, 10)
self.assertEqual(cogs_aml.credit, 0)
+5
View File
@@ -120,10 +120,15 @@ class AccountMoveLine(models.Model):
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
posted_cogs = account_moves.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
qty_invoiced = sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in posted_cogs])
value_invoiced = sum(posted_cogs.mapped('balance'))
reversal_cogs = posted_cogs.move_id.reversal_move_id.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
qty_invoiced -= sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in reversal_cogs])
value_invoiced -= sum(reversal_cogs.mapped('balance'))
product = self.product_id.with_company(self.company_id).with_context(is_returned=is_line_reversing, value_invoiced=value_invoiced)
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, so_line.move_ids)
if average_price_unit:
@@ -1561,3 +1561,62 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
(invoice01 | invoice03).action_post()
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
self.assertEqual(sum(cogs.mapped('credit')), total_value)
def test_fifo_reverse_and_create_new_invoice(self):
"""
FIFO automated
Receive 1@10, 1@50
Deliver 1
Post the invoice, add a credit note with option 'new draft inv'
Post the second invoice
COGS should be based on the delivered product
"""
self.product.categ_id.property_cost_method = 'fifo'
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': self.product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.product.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 50]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_uom_qty': 1.0,
'product_uom': self.product.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
picking = so.picking_ids
picking.move_lines.quantity_done = 1.0
picking.button_validate()
invoice01 = so._create_invoices()
invoice01.action_post()
wizard = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=invoice01.ids).create({
'refund_method': 'modify',
})
invoice02 = self.env['account.move'].browse(wizard.reverse_moves()['res_id'])
invoice02.action_post()
amls = invoice02.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
self.assertEqual(stock_out_aml.debit, 0)
self.assertEqual(stock_out_aml.credit, 10)
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
self.assertEqual(cogs_aml.debit, 10)
self.assertEqual(cogs_aml.credit, 0)
+4 -1
View File
@@ -78,7 +78,10 @@ class AccountMoveLine(models.Model):
return [
('so_line', 'in', sale_line_delivery.ids),
('project_id', '!=', False),
'|', ('timesheet_invoice_id', '=', False), ('timesheet_invoice_id.state', '=', 'cancel')
'|', '|',
('timesheet_invoice_id', '=', False),
('timesheet_invoice_id.state', '=', 'cancel'),
('timesheet_invoice_id.payment_state', '=', 'reversed')
]
def unlink(self):
+4 -2
View File
@@ -192,8 +192,10 @@ class ProductProduct(models.Model):
fifo_vals = self._run_fifo(abs(quantity), company)
vals['remaining_qty'] = fifo_vals.get('remaining_qty')
# In case of AVCO, fix rounding issue of standard price when needed.
if self.product_tmpl_id.cost_method == 'average':
rounding_error = currency.round(self.standard_price * self.quantity_svl - self.value_svl)
if self.product_tmpl_id.cost_method == 'average' and not float_is_zero(self.quantity_svl, precision_rounding=self.uom_id.rounding):
rounding_error = currency.round(
(self.standard_price * self.quantity_svl - self.value_svl) * abs(quantity / self.quantity_svl)
)
if rounding_error:
# If it is bigger than the (smallest number of the currency * quantity) / 2,
# then it isn't a rounding error but a stock valuation error, we shouldn't fix it under the hood ...
@@ -2087,7 +2087,7 @@ class TestStockValuation(SavepointCase):
move5.move_line_ids.qty_done = 30.0
move5._action_done()
self.assertEqual(move5.stock_valuation_layer_ids.value, -477.5)
self.assertEqual(move5.stock_valuation_layer_ids.value, -477.56)
# Receives 10 units but assign them to an owner, the valuation should not be impacted.
move6 = self.env['stock.move'].create({
@@ -2121,7 +2121,7 @@ class TestStockValuation(SavepointCase):
move7.move_line_ids.qty_done = 50.0
move7._action_done()
self.assertEqual(move7.stock_valuation_layer_ids.value, -796.0)
self.assertEqual(move7.stock_valuation_layer_ids.value, -795.94)
self.assertAlmostEqual(self.product1.quantity_svl, 0.0)
self.assertAlmostEqual(self.product1.value_svl, 0.0)
@@ -543,6 +543,17 @@ class TestStockValuationAVCO(TestStockValuationCommon):
self.assertEqual(self.product1.quantity_svl, 0)
self.assertEqual(self.product1.standard_price, 1.01)
def test_rounding_svl_3(self):
self._make_in_move(self.product1, 1000, unit_cost=0.17)
self._make_in_move(self.product1, 800, unit_cost=0.23)
self.assertEqual(self.product1.standard_price, 0.20)
self._make_out_move(self.product1, 1000, create_picking=True)
self._make_out_move(self.product1, 800, create_picking=True)
self.assertEqual(self.product1.value_svl, 0)
def test_return_delivery_2(self):
self.product1.write({"standard_price": 1})
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
@@ -202,3 +202,86 @@ class TestStockLandedCostsRounding(TestStockLandedCostsCommon):
lc.compute_landed_cost()
self.assertEqual(sum(lc.valuation_adjustment_lines.mapped('additional_landed_cost')), 1000.0)
def test_stock_landed_costs_rounding_03(self):
"""
Storable AVCO product
Receive:
5 @ 5
5 @ 8
5 @ 7
20 @ 7.33
Add landed cost of $5 to each receipt (except the first one)
Deliver:
23
2
10
At the end, the SVL value should be zero
"""
self.product_a.type = 'product'
self.product_a.categ_id.property_cost_method = 'average'
stock_location = self.warehouse.lot_stock_id
supplier_location_id = self.ref('stock.stock_location_suppliers')
customer_location_id = self.ref('stock.stock_location_customers')
receipts = self.env['stock.picking'].create([{
'picking_type_id': self.warehouse.in_type_id.id,
'location_id': supplier_location_id,
'location_dest_id': stock_location.id,
'move_lines': [(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'price_unit': price,
'product_uom': self.product_a.uom_id.id,
'product_uom_qty': qty,
'location_id': supplier_location_id,
'location_dest_id': stock_location.id,
})]
} for qty, price in [
(5, 5.0),
(5, 8.0),
(5, 7.0),
(20, 7.33),
]])
receipts.action_confirm()
for m in receipts.move_lines:
m.quantity_done = m.product_uom_qty
receipts.button_validate()
landed_costs = self.env['stock.landed.cost'].create([{
'picking_ids': [(6, 0, picking.ids)],
'account_journal_id': self.expenses_journal.id,
'cost_lines': [(0, 0, {
'name': 'equal split',
'split_method': 'equal',
'price_unit': 5.0,
'product_id': self.landed_cost.id
})],
} for picking in receipts[1:]])
landed_costs.compute_landed_cost()
landed_costs.button_validate()
self.assertEqual(self.product_a.standard_price, 7.47)
deliveries = self.env['stock.picking'].create([{
'picking_type_id': self.warehouse.out_type_id.id,
'location_id': stock_location.id,
'location_dest_id': customer_location_id,
'move_lines': [(0, 0, {
'name': self.product_a.name,
'product_id': self.product_a.id,
'product_uom': self.product_a.uom_id.id,
'product_uom_qty': qty,
'location_id': stock_location.id,
'location_dest_id': customer_location_id,
})]
} for qty in [23, 2, 10]])
deliveries.action_confirm()
for m in deliveries.move_lines:
m.quantity_done = m.product_uom_qty
deliveries.button_validate()
self.assertEqual(self.product_a.value_svl, 0)
+18 -8
View File
@@ -537,10 +537,15 @@ var dom = {
return size;
},
/**
* @param {HTMLElement} el - the element to stroll to (limitation: if the
* element is using a fixed position, this function cannot work except
* if is the header (with the "top" id) or the footer (with the
* "bottom" id) for which exceptions have been made)
* @param {HTMLElement|string} el - the element to scroll to. If "el" is a
* string, it must be a valid selector of an element in the DOM or
* '#top' or '#bottom'. If it is an HTML element, it must be present
* in the DOM.
* Limitation: if the element is using a fixed position, this
* function cannot work except if is the header (el is then either a
* string set to '#top' or an HTML element with the "top" id) or the
* footer (el is then a string set to '#bottom' or an HTML element
* with the "bottom" id) for which exceptions have been made.
* @param {number} [options] - same as animate of jQuery
* @param {number} [options.extraOffset=0]
* extra offset to add on top of the automatic one (the automatic one
@@ -552,15 +557,19 @@ var dom = {
*/
scrollTo(el, options = {}) {
const $el = $(el);
const $scrollable = $el.parent().closestScrollable();
if (typeof(el) === 'string' && $el[0]) {
el = $el[0];
}
const isTopOrBottomHidden = (el === '#top' || el === '#bottom');
const $topLevelScrollable = $().getScrollingElement();
const $scrollable = isTopOrBottomHidden ? $topLevelScrollable : $el.parent().closestScrollable();
const isTopScroll = $scrollable.is($topLevelScrollable);
function _computeScrollTop() {
if (el.id === 'top') {
if (el === '#top' || el.id === 'top') {
return 0;
}
if (el.id === 'bottom') {
if (el === '#bottom' || el.id === 'bottom') {
return $scrollable[0].scrollHeight - $scrollable[0].clientHeight;
}
@@ -595,7 +604,8 @@ var dom = {
options.progress.apply(this, ...arguments);
}
const newScrollTop = _computeScrollTop();
if (Math.abs(newScrollTop - originalScrollTop) <= 1.0 && !(el.classList.contains('o_transitioning'))) {
if (Math.abs(newScrollTop - originalScrollTop) <= 1.0
&& (isTopOrBottomHidden || !(el.classList.contains('o_transitioning')))) {
return;
}
$scrollable.stop();
@@ -986,6 +986,19 @@ registry.anchorSlide = publicWidget.Widget.extend({
return;
}
var hash = this.$target[0].hash;
if (hash === '#top' || hash === '#bottom') {
// If the anchor targets #top or #bottom, directly call the
// "scrollTo" function. The reason is that the header or the footer
// could have been removed from the DOM. By receiving a string as
// parameter, the "scrollTo" function handles the scroll to the top
// or to the bottom of the document even if the header or the
// footer is removed from the DOM.
dom.scrollTo(hash, {
duration: 500,
extraOffset: this._computeExtraOffset(),
});
return;
}
if (!utils.isValidAnchor(hash)) {
return;
}
+12 -1
View File
@@ -22,9 +22,20 @@ function loadAnchors(url) {
resolve();
}
}).then(function (response) {
return _.map($(response).find('[id][data-anchor=true]'), function (el) {
const anchors = _.map($(response).find('[id][data-anchor=true]'), function (el) {
return '#' + el.id;
});
// Always suggest the top and the bottom of the page as internal link
// anchor even if the header and the footer are not in the DOM. Indeed,
// the "scrollTo" function handles the scroll towards those elements
// even when they are not in the DOM.
if (!anchors.includes('#top')) {
anchors.unshift('#top');
}
if (!anchors.includes('#bottom')) {
anchors.push('#bottom');
}
return anchors;
}).catch(error => {
console.debug(error);
return [];
+1 -1
View File
@@ -422,7 +422,7 @@ font[class*='bg-'] {
// Probably outdated
// Disable fixed height
@media (max-width: 400px) {
@include media-breakpoint-down(sm) {
section,
.parallax,
.row,
@@ -23,7 +23,7 @@ const TableOfContent = publicWidget.Widget.extend({
*/
destroy() {
this.$target.css('top', '');
this.$target.find('.s_table_of_content_navbar').css('top', '');
this.$target.find('.s_table_of_content_navbar').css({top: '', maxHeight: ''});
this._super(...arguments);
},
@@ -42,8 +42,10 @@ const TableOfContent = publicWidget.Widget.extend({
this.$target.css('top', isHorizontalNavbar ? position : '');
this.$target.find('.s_table_of_content_navbar').css('top', isHorizontalNavbar ? '' : position + 20);
const $mainNavBar = $('#oe_main_menu_navbar');
position += $mainNavBar.length ? $mainNavBar.outerHeight() : 0;
const mainNavBarHidden = document.body.classList.contains('o_fullscreen') || this.editableMode;
position += !mainNavBarHidden && $mainNavBar.length ? $mainNavBar.outerHeight() : 0;
position += isHorizontalNavbar ? this.$target.outerHeight() : 0;
this.$target.find('.s_table_of_content_navbar').css('maxHeight', isHorizontalNavbar ? '' : `calc(100vh - ${position + 40}px)`);
if (this.previousPosition !== position) {
// The scrollSpy must be destroyed before calling it again.
// Otherwise the call has no effect.
@@ -4,6 +4,9 @@
&.s_table_of_content_horizontal_navbar, &.s_table_of_content_vertical_navbar .s_table_of_content_navbar {
@include o-position-sticky($top: 0px);
}
&.s_table_of_content_vertical_navbar .s_table_of_content_navbar {
overflow: auto;
}
}
&:not(.s_table_of_content_navbar_sticky) {
&, .s_table_of_content_navbar {
@@ -9,7 +9,6 @@
overflow-y: auto;
table {
table-layout: fixed;
width: 100%;
input {
@@ -22,12 +21,19 @@
font-family: $o-we-sidebar-content-field-input-font-family;
}
tr {
// Since the sortable list's <tr> loses its connection with
// the table when dragged, the <td> with the input no longer
// takes up the full width, causing a visual issue. To solve
// this problem, we added the 'flex' display property.
display: flex;
border: 1px solid rgba(white, 0.1);
border-left: none;
border-right: none;
}
td {
flex-grow: 1;
&:first-child, &:last-child {
flex-grow: 0;
width: 28px;
}
}