mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 01102022
This commit is contained in:
@@ -68,7 +68,7 @@ class AccountPaymentTerm(models.Model):
|
||||
if dist:
|
||||
last_date = result and result[-1][0] or fields.Date.context_today(self)
|
||||
result.append((last_date, dist))
|
||||
return result
|
||||
return sorted(result, key=lambda k: k[0])
|
||||
|
||||
def unlink(self):
|
||||
for terms in self:
|
||||
|
||||
@@ -135,6 +135,7 @@ class ReSequenceWizard(models.TransientModel):
|
||||
if self.move_ids.journal_id and self.move_ids.journal_id.restrict_mode_hash_table:
|
||||
if self.ordering == 'date':
|
||||
raise UserError(_('You can not reorder sequence by date when the journal is locked with a hash.'))
|
||||
self.move_ids._check_fiscalyear_lock_date()
|
||||
self.env['account.move'].browse(int(k) for k in new_values.keys()).name = False
|
||||
for move_id in self.move_ids:
|
||||
if str(move_id.id) in new_values:
|
||||
|
||||
@@ -96,10 +96,8 @@ class AccountMove(models.Model):
|
||||
for num, line in enumerate(lines):
|
||||
sign = -1 if line.move_id.is_inbound() else 1
|
||||
price_subtotal = (line.balance * sign) if convert_to_euros else line.price_subtotal
|
||||
# The price_subtotal should be inverted when:
|
||||
# The line has downpayment lines, but is not a downpayment (i.e. the final invoice, from which downpayment lines are subtracted) or,
|
||||
# the line is a reverse charge refund.
|
||||
if (line._get_downpayment_lines() and not is_downpayment) or reverse_charge_refund:
|
||||
# The price_subtotal should be inverted when the line is a reverse charge refund.
|
||||
if reverse_charge_refund:
|
||||
price_subtotal = -price_subtotal
|
||||
|
||||
# Unit price
|
||||
|
||||
@@ -477,7 +477,10 @@ class MrpProduction(models.Model):
|
||||
production.state = 'draft'
|
||||
elif all(move.state == 'cancel' for move in production.move_raw_ids):
|
||||
production.state = 'cancel'
|
||||
elif all(move.state in ('cancel', 'done') for move in production.move_raw_ids):
|
||||
elif (
|
||||
all(move.state in ('cancel', 'done') for move in production.move_raw_ids)
|
||||
and all(move.state in ('cancel', 'done') for move in production.move_finished_ids)
|
||||
):
|
||||
production.state = 'done'
|
||||
elif production.workorder_ids and all(wo_state in ('done', 'cancel') for wo_state in production.workorder_ids.mapped('state')):
|
||||
production.state = 'to_close'
|
||||
|
||||
@@ -424,13 +424,13 @@ class MrpWorkorder(models.Model):
|
||||
raise UserError(_('The planned end date of the work order cannot be prior to the planned start date, please correct this to save the work order.'))
|
||||
if 'duration_expected' not in values and not self.env.context.get('bypass_duration_calculation'):
|
||||
if values.get('date_planned_start') and values.get('date_planned_finished'):
|
||||
computed_finished_time = self._calculate_date_planned_finished(start_date)
|
||||
computed_finished_time = workorder._calculate_date_planned_finished(start_date)
|
||||
values['date_planned_finished'] = computed_finished_time
|
||||
elif values.get('date_planned_start'):
|
||||
computed_duration = self._calculate_duration_expected(date_planned_start=start_date)
|
||||
computed_duration = workorder._calculate_duration_expected(date_planned_start=start_date)
|
||||
values['duration_expected'] = computed_duration
|
||||
elif values.get('date_planned_finished'):
|
||||
computed_duration = self._calculate_duration_expected(date_planned_finished=end_date)
|
||||
computed_duration = workorder._calculate_duration_expected(date_planned_finished=end_date)
|
||||
values['duration_expected'] = computed_duration
|
||||
# Update MO dates if the start date of the first WO or the
|
||||
# finished date of the last WO is update.
|
||||
|
||||
@@ -84,6 +84,35 @@ class TestMrpByProduct(common.TransactionCase):
|
||||
# I see that stock moves of External Hard Disk including Headset USB are done now.
|
||||
self.assertFalse(any(move.state != 'done' for move in moves), 'Moves are not done!')
|
||||
|
||||
def test_01_mrp_byproduct(self):
|
||||
self.env["stock.quant"].create({
|
||||
"product_id": self.product_c_id,
|
||||
"location_id": self.warehouse.lot_stock_id.id,
|
||||
"quantity": 4,
|
||||
})
|
||||
bom_product_a = self.MrpBom.create({
|
||||
'product_tmpl_id': self.product_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'normal',
|
||||
'product_uom_id': self.uom_unit_id,
|
||||
'bom_line_ids': [(0, 0, {'product_id': self.product_c_id, 'product_uom_id': self.uom_unit_id, 'product_qty': 2})]
|
||||
})
|
||||
mnf_product_a_form = Form(self.env['mrp.production'])
|
||||
mnf_product_a_form.product_id = self.product_a
|
||||
mnf_product_a_form.bom_id = bom_product_a
|
||||
mnf_product_a_form.product_qty = 2.0
|
||||
mnf_product_a = mnf_product_a_form.save()
|
||||
mnf_product_a.action_confirm()
|
||||
self.assertEqual(mnf_product_a.state, "confirmed")
|
||||
mnf_product_a.move_raw_ids._action_assign()
|
||||
mnf_product_a.move_raw_ids.quantity_done = mnf_product_a.move_raw_ids.product_uom_qty
|
||||
mnf_product_a.move_raw_ids._action_done()
|
||||
self.assertEqual(mnf_product_a.state, "progress")
|
||||
mnf_product_a.qty_producing = 2
|
||||
mnf_product_a.button_mark_done()
|
||||
self.assertTrue(mnf_product_a.move_finished_ids)
|
||||
self.assertEqual(mnf_product_a.state, "done")
|
||||
|
||||
def test_change_product(self):
|
||||
""" Create a production order for a specific product with a BoM. Then change the BoM and the finished product for
|
||||
other ones and check the finished product of the first mo did not became a byproduct of the second one."""
|
||||
|
||||
@@ -807,3 +807,116 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
wizard.process()
|
||||
|
||||
self.assertEqual(picking_receipt.state, 'done')
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSubcontractingPurchaseFlows(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# todo 15.0: move in `mrp_subcontracting_purchase`
|
||||
if 'purchase.order' not in self.env:
|
||||
self.skipTest('`purchase` is not installed')
|
||||
|
||||
self.subcontractor = self.env['res.partner'].create({'name': 'SuperSubcontractor'})
|
||||
|
||||
self.finished, self.compo = self.env['product.product'].create([{
|
||||
'name': 'SuperProduct',
|
||||
'type': 'product',
|
||||
}, {
|
||||
'name': 'Component',
|
||||
'type': 'consu',
|
||||
}])
|
||||
|
||||
self.bom = self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.finished.product_tmpl_id.id,
|
||||
'type': 'subcontract',
|
||||
'subcontractor_ids': [(6, 0, self.subcontractor.ids)],
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.compo.id,
|
||||
'product_qty': 1,
|
||||
})],
|
||||
})
|
||||
|
||||
def test_purchase_and_return01(self):
|
||||
"""
|
||||
The user buys 10 x a subcontracted product P. He receives the 10
|
||||
products and then does a return with 3 x P. The test ensures that the
|
||||
final received quantity is correctly computed
|
||||
"""
|
||||
po = self.env['purchase.order'].create({
|
||||
'partner_id': self.subcontractor.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': self.finished.name,
|
||||
'product_id': self.finished.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.finished.uom_id.id,
|
||||
'price_unit': 1,
|
||||
})],
|
||||
})
|
||||
po.button_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertTrue(mo)
|
||||
|
||||
receipt = po.picking_ids
|
||||
receipt.move_lines.quantity_done = 10
|
||||
receipt.button_validate()
|
||||
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
|
||||
with return_form.product_return_moves.edit(0) as line:
|
||||
line.quantity = 3
|
||||
line.to_refund = True
|
||||
return_wizard = return_form.save()
|
||||
return_id, _ = return_wizard._create_returns()
|
||||
|
||||
return_picking = self.env['stock.picking'].browse(return_id)
|
||||
return_picking.move_lines.quantity_done = 3
|
||||
return_picking.button_validate()
|
||||
|
||||
self.assertEqual(self.finished.qty_available, 7.0)
|
||||
self.assertEqual(po.order_line.qty_received, 7.0)
|
||||
|
||||
def test_purchase_and_return02(self):
|
||||
"""
|
||||
The user buys 10 x a subcontracted product P. He receives the 10
|
||||
products and then does a return with 3 x P (with the flag to_refund
|
||||
disabled and the subcontracting location as return location). The test
|
||||
ensures that the final received quantity is correctly computed
|
||||
"""
|
||||
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
|
||||
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
|
||||
|
||||
po = self.env['purchase.order'].create({
|
||||
'partner_id': self.subcontractor.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': self.finished.name,
|
||||
'product_id': self.finished.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.finished.uom_id.id,
|
||||
'price_unit': 1,
|
||||
})],
|
||||
})
|
||||
po.button_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertTrue(mo)
|
||||
|
||||
receipt = po.picking_ids
|
||||
receipt.move_lines.quantity_done = 10
|
||||
receipt.button_validate()
|
||||
|
||||
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt.id, active_model='stock.picking'))
|
||||
return_form.location_id = self.env.company.subcontracting_location_id
|
||||
with return_form.product_return_moves.edit(0) as line:
|
||||
line.quantity = 3
|
||||
line.to_refund = False
|
||||
return_wizard = return_form.save()
|
||||
return_id, _ = return_wizard._create_returns()
|
||||
|
||||
return_picking = self.env['stock.picking'].browse(return_id)
|
||||
return_picking.move_lines.quantity_done = 3
|
||||
return_picking.button_validate()
|
||||
|
||||
self.assertEqual(self.finished.qty_available, 7.0)
|
||||
self.assertEqual(po.order_line.qty_received, 10.0)
|
||||
|
||||
@@ -14,7 +14,10 @@ class StockPicking(models.Model):
|
||||
|
||||
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
|
||||
res = super()._prepare_subcontract_mo_vals(subcontract_move, bom)
|
||||
if not res.get('picking_type_id') and subcontract_move.location_dest_id.usage == 'customer':
|
||||
if not res.get('picking_type_id') and (
|
||||
subcontract_move.location_dest_id.usage == 'customer'
|
||||
or subcontract_move.partner_id.property_stock_subcontractor.parent_path in subcontract_move.location_dest_id.parent_path
|
||||
):
|
||||
# If the if-condition is respected, it means that `subcontract_move` is not
|
||||
# related to a specific warehouse. This can happen if, for instance, the user
|
||||
# confirms a PO with a subcontracted product that should be delivered to a
|
||||
|
||||
@@ -192,3 +192,73 @@ class TestSubcontractingDropshippingFlows(TestMrpSubcontractingCommon):
|
||||
self.assertEqual(delivery.state, 'done')
|
||||
self.assertEqual(mo.state, 'done')
|
||||
self.assertEqual(po.order_line.qty_received, 1)
|
||||
|
||||
def test_po_to_subcontractor(self):
|
||||
"""
|
||||
Create and confirm a PO with a subcontracted move. The bought product is
|
||||
also a component of another subcontracted product. The picking type of
|
||||
the PO is 'Dropship' and the delivery address is the other subcontractor
|
||||
"""
|
||||
subcontractor, super_subcontractor = self.env['res.partner'].create([
|
||||
{'name': 'Subcontractor'},
|
||||
{'name': 'SuperSubcontractor'},
|
||||
])
|
||||
super_subcontractor.property_stock_customer = super_subcontractor.property_stock_subcontractor
|
||||
|
||||
super_product, product, component = self.env['product.product'].create([{
|
||||
'name': 'Super Product',
|
||||
'type': 'product',
|
||||
'seller_ids': [(0, 0, {'name': super_subcontractor.id})],
|
||||
}, {
|
||||
'name': 'Product',
|
||||
'type': 'product',
|
||||
'seller_ids': [(0, 0, {'name': subcontractor.id})],
|
||||
}, {
|
||||
'name': 'Component',
|
||||
'type': 'consu',
|
||||
}])
|
||||
|
||||
_, bom_product = self.env['mrp.bom'].create([{
|
||||
'product_tmpl_id': super_product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'subcontract',
|
||||
'subcontractor_ids': [(6, 0, super_subcontractor.ids)],
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': product.id, 'product_qty': 1}),
|
||||
],
|
||||
}, {
|
||||
'product_tmpl_id': product.product_tmpl_id.id,
|
||||
'product_qty': 1,
|
||||
'type': 'subcontract',
|
||||
'subcontractor_ids': [(6, 0, subcontractor.ids)],
|
||||
'bom_line_ids': [
|
||||
(0, 0, {'product_id': component.id, 'product_qty': 1}),
|
||||
],
|
||||
}])
|
||||
|
||||
dropship_picking_type = self.env['stock.picking.type'].search([
|
||||
('company_id', '=', self.env.company.id),
|
||||
('default_location_src_id.usage', '=', 'supplier'),
|
||||
('default_location_dest_id.usage', '=', 'customer'),
|
||||
], limit=1, order='sequence')
|
||||
|
||||
po = self.env['purchase.order'].create({
|
||||
"partner_id": subcontractor.id,
|
||||
"picking_type_id": dropship_picking_type.id,
|
||||
"dest_address_id": super_subcontractor.id,
|
||||
"order_line": [(0, 0, {
|
||||
'product_id': product.id,
|
||||
'name': product.name,
|
||||
'product_qty': 1.0,
|
||||
})],
|
||||
})
|
||||
po.button_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', bom_product.id)])
|
||||
self.assertEqual(mo.picking_type_id, self.warehouse.subcontracting_type_id)
|
||||
|
||||
delivery = po.picking_ids
|
||||
delivery.move_line_ids.qty_done = 1.0
|
||||
delivery.button_validate()
|
||||
|
||||
self.assertEqual(po.order_line.qty_received, 1.0)
|
||||
|
||||
@@ -109,6 +109,16 @@ class PurchaseOrder(models.Model):
|
||||
class PurchaseOrderLine(models.Model):
|
||||
_inherit = 'purchase.order.line'
|
||||
|
||||
def _compute_account_analytic_id(self):
|
||||
for rec in self:
|
||||
if not rec.order_id.requisition_id:
|
||||
super(PurchaseOrderLine, self)._compute_account_analytic_id()
|
||||
|
||||
def _compute_analytic_tag_ids(self):
|
||||
for rec in self:
|
||||
if not rec.order_id.requisition_id:
|
||||
super(PurchaseOrderLine, self)._compute_analytic_tag_ids()
|
||||
|
||||
@api.onchange('product_qty', 'product_uom')
|
||||
def _onchange_quantity(self):
|
||||
res = super(PurchaseOrderLine, self)._onchange_quantity()
|
||||
|
||||
@@ -174,8 +174,8 @@ class PurchaseRequisitionLine(models.Model):
|
||||
qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities')
|
||||
requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade')
|
||||
company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env.company)
|
||||
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
|
||||
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
|
||||
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', store=True, compute='_compute_account_analytic_id', readonly=False)
|
||||
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', store=True, compute='_compute_analytic_tag_ids', readonly=False)
|
||||
schedule_date = fields.Date(string='Scheduled Date')
|
||||
supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id')
|
||||
|
||||
@@ -239,6 +239,30 @@ class PurchaseRequisitionLine(models.Model):
|
||||
else:
|
||||
line.qty_ordered = 0
|
||||
|
||||
@api.depends('product_id', 'schedule_date')
|
||||
def _compute_account_analytic_id(self):
|
||||
for line in self:
|
||||
default_analytic_account = line.env['account.analytic.default'].sudo().account_get(
|
||||
product_id=line.product_id.id,
|
||||
partner_id=line.requisition_id.vendor_id.id,
|
||||
user_id=line.env.uid,
|
||||
date=line.schedule_date,
|
||||
company_id=line.company_id.id,
|
||||
)
|
||||
line.account_analytic_id = default_analytic_account.analytic_id
|
||||
|
||||
@api.depends('product_id', 'schedule_date')
|
||||
def _compute_analytic_tag_ids(self):
|
||||
for line in self:
|
||||
default_analytic_account = line.env['account.analytic.default'].sudo().account_get(
|
||||
product_id=line.product_id.id,
|
||||
partner_id=line.requisition_id.vendor_id.id,
|
||||
user_id=line.env.uid,
|
||||
date=line.schedule_date,
|
||||
company_id=line.company_id.id,
|
||||
)
|
||||
line.analytic_tag_ids = default_analytic_account.analytic_tag_ids
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
if self.product_id:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.purchase_requisition.tests.common import TestPurchaseRequisitionCommon
|
||||
from flectra.tests import Form
|
||||
|
||||
|
||||
class TestPurchaseRequisition(TestPurchaseRequisitionCommon):
|
||||
@@ -113,3 +114,22 @@ class TestPurchaseRequisition(TestPurchaseRequisitionCommon):
|
||||
('name', '=', vendor.id)
|
||||
]) - supplier_info
|
||||
self.assertEqual(new_si.purchase_requisition_id, requisition_blanket, 'the blanket order is not linked to the supplier info')
|
||||
|
||||
def test_07_purchase_requisition(self):
|
||||
"""
|
||||
Check that the analytic account and the account tag defined in the purchase requisition line
|
||||
is used in the purchase order line when creating a PO.
|
||||
"""
|
||||
analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account'})
|
||||
analytic_tag = self.env['account.analytic.tag'].create({'name': 'test_analytic_tag'})
|
||||
self.assertEqual(len(self.requisition1.line_ids), 1)
|
||||
self.requisition1.line_ids[0].write({
|
||||
'account_analytic_id': analytic_account,
|
||||
'analytic_tag_ids': analytic_tag,
|
||||
})
|
||||
# Create purchase order from purchase requisition
|
||||
po_form = Form(self.env['purchase.order'].with_context(default_requisition_id=self.requisition1.id))
|
||||
po_form.partner_id = self.res_partner_1
|
||||
po = po_form.save()
|
||||
self.assertEqual(po.order_line.account_analytic_id.id, analytic_account.id, 'The analytic account defined in the purchase requisition line must be the same as the one from the purchase order line.')
|
||||
self.assertEqual(po.order_line.analytic_tag_ids.id, analytic_tag.id, 'The analytic account tag defined in the purchase requisition line must be the same as the one from the purchase order line.')
|
||||
|
||||
@@ -306,6 +306,7 @@ class PurchaseOrderLine(models.Model):
|
||||
elif (
|
||||
move.location_dest_id.usage == "internal"
|
||||
and move.location_id.usage != "supplier"
|
||||
and move.warehouse_id
|
||||
and move.location_dest_id
|
||||
not in self.env["stock.location"].search(
|
||||
[("id", "child_of", move.warehouse_id.view_location_id.id)]
|
||||
|
||||
@@ -188,14 +188,12 @@ class SaleOrder(models.Model):
|
||||
|
||||
discount_amount -= discount_line_amount_price * lines_total / lines_price
|
||||
|
||||
tax_name = ""
|
||||
if len(tax_ids) == 1:
|
||||
tax_name = " - " + _("On product with following tax: ") + ', '.join(tax_ids.mapped('name'))
|
||||
elif len(tax_ids) > 1:
|
||||
tax_name = " - " + _("On product with following taxes: ") + ', '.join(tax_ids.mapped('name'))
|
||||
|
||||
reward_lines[tax_ids] = {
|
||||
'name': _("Discount: ") + program.name + tax_name,
|
||||
'name': _(
|
||||
"Discount: %(program)s - On product with following taxes: %(taxes)s",
|
||||
program=program.name,
|
||||
taxes=", ".join(tax_ids.mapped('name')),
|
||||
),
|
||||
'product_id': program.discount_line_product_id.id,
|
||||
'price_unit': -discount_line_amount_price,
|
||||
'product_uom_qty': 1.0,
|
||||
|
||||
@@ -9,12 +9,13 @@
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='informations']" position="inside">
|
||||
<t t-if="sale_order.picking_ids">
|
||||
<t t-set="delivery_orders" t-value="sale_order.picking_ids.filtered(lambda picking: picking.picking_type_id.code == 'outgoing')"/>
|
||||
<t t-if="delivery_orders">
|
||||
<div>
|
||||
<strong>Delivery Orders</strong>
|
||||
</div>
|
||||
<div>
|
||||
<t t-foreach="sale_order.picking_ids.filtered(lambda picking: picking.picking_type_id.code != 'internal')" t-as="i">
|
||||
<t t-foreach="delivery_orders" t-as="i">
|
||||
<t t-set="delivery_report_url" t-value="'/my/picking/pdf/%s?%s' % (i.id, keep_query())"/>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between o_sale_stock_picking">
|
||||
<div>
|
||||
|
||||
@@ -537,7 +537,10 @@ var dom = {
|
||||
return size;
|
||||
},
|
||||
/**
|
||||
* @param {HTMLElement} el - the element to stroll to
|
||||
* @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 {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
|
||||
@@ -554,13 +557,22 @@ var dom = {
|
||||
const isTopScroll = $scrollable.is($topLevelScrollable);
|
||||
|
||||
function _computeScrollTop() {
|
||||
if (el.id === 'top') {
|
||||
return 0;
|
||||
}
|
||||
if (el.id === 'bottom') {
|
||||
return $scrollable[0].scrollHeight - $scrollable[0].clientHeight;
|
||||
}
|
||||
|
||||
let offsetTop = $el.offset().top;
|
||||
if (el.classList.contains('d-none')) {
|
||||
el.classList.remove('d-none');
|
||||
offsetTop = $el.offset().top;
|
||||
el.classList.add('d-none');
|
||||
}
|
||||
const elPosition = $scrollable[0].scrollTop + (offsetTop - $scrollable.offset().top);
|
||||
const isDocScrollingEl = $scrollable.is(el.ownerDocument.scrollingElement);
|
||||
const elPosition = offsetTop
|
||||
- ($scrollable.offset().top - (isDocScrollingEl ? 0 : $scrollable[0].scrollTop));
|
||||
let offset = options.forcedOffset;
|
||||
if (offset === undefined) {
|
||||
offset = (isTopScroll ? dom.scrollFixedOffset() : 0) + (options.extraOffset || 0);
|
||||
|
||||
18
addons/web/static/src/js/libs/jquery.js
vendored
18
addons/web/static/src/js/libs/jquery.js
vendored
@@ -123,11 +123,23 @@ $.fn.extend({
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @todo Should really be converted to no jQuery and probably even removed
|
||||
* from jQuery utilities in master
|
||||
* @return {jQuery}
|
||||
*/
|
||||
closestScrollable() {
|
||||
const document = this.length ? this[0].ownerDocument : window.document;
|
||||
|
||||
let $el = this;
|
||||
while ($el[0] !== document.scrollingElement) {
|
||||
if (!$el.length || $el[0] instanceof Document) {
|
||||
// Ensure that $().closestScrollable() -> $() and handle the
|
||||
// case of elements not attached to the DOM.
|
||||
// Also, .parent() used to loop through ancestors can
|
||||
// theoretically reach the document if nothing up to the HTML
|
||||
// included is not scrollable.
|
||||
return $();
|
||||
}
|
||||
if ($el.isScrollable()) {
|
||||
return $el;
|
||||
}
|
||||
@@ -194,9 +206,13 @@ $.fn.extend({
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isScrollable() {
|
||||
if (!this.length) {
|
||||
return false;
|
||||
}
|
||||
const overflow = this.css('overflow-y');
|
||||
const el = this[0];
|
||||
return overflow === 'auto' || overflow === 'scroll'
|
||||
|| (overflow === 'visible' && this === document.scrollingElement);
|
||||
|| (overflow === 'visible' && el === el.ownerDocument.scrollingElement);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4624,9 +4624,13 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
value = choice ? choice[1] : false;
|
||||
}
|
||||
// When group_by_no_leaf key is present FIELD_ID_count doesn't exist
|
||||
// we have to get the count from `__count` instead
|
||||
// see _read_group_raw in models.py
|
||||
const countKey = rawGroupBy + '_count';
|
||||
var newGroup = self._makeDataPoint({
|
||||
modelName: list.model,
|
||||
count: group[rawGroupBy + '_count'],
|
||||
count: countKey in group ? group[countKey] : group.__count,
|
||||
domain: group.__domain,
|
||||
context: list.context,
|
||||
fields: list.fields,
|
||||
|
||||
@@ -1547,7 +1547,8 @@ var MockServer = Class.extend({
|
||||
|
||||
// compute count key to match dumb server logic...
|
||||
var countKey;
|
||||
if (kwargs.lazy) {
|
||||
const groupByNoLeaf = kwargs.context ? 'group_by_no_leaf' in kwargs.context : false;
|
||||
if (kwargs.lazy && (groupBy.length >= 2 || !groupByNoLeaf)) {
|
||||
countKey = groupBy[0].split(':')[0] + "_count";
|
||||
} else {
|
||||
countKey = "__count";
|
||||
|
||||
@@ -4453,6 +4453,30 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list with group_by_no_leaf and group by', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree expand="1"><field name="foo"/></tree>',
|
||||
groupBy: ['currency_id'],
|
||||
context: { group_by_no_leaf: true },
|
||||
});
|
||||
const groups = list.el.querySelectorAll(".o_group_name");
|
||||
const groupsRecords = [...list.el.querySelectorAll(".o_data_row .o_data_cell")];
|
||||
|
||||
assert.strictEqual(groups.length, 2, "There should be 2 groups");
|
||||
assert.strictEqual(groups[0].textContent, "EUR (1)", "First group should have 1 record");
|
||||
assert.strictEqual(groups[1].textContent, "USD (3)", "Second group should have 3 records");
|
||||
assert.deepEqual(
|
||||
groupsRecords.map(groupEl => groupEl.textContent),
|
||||
["yop", "blip", "gnap", "blip"],
|
||||
"Groups should contains correct records");
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("non empty list with sample data", async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
|
||||
@@ -1175,6 +1175,9 @@ var SnippetsMenu = Widget.extend({
|
||||
// Hide the active overlay when scrolling.
|
||||
// Show it again and recompute all the overlays after the scroll.
|
||||
this.$scrollingElement = $().getScrollingElement();
|
||||
this.$scrollingTarget = this.$scrollingElement.is(this.ownerDocument.scrollingElement)
|
||||
? $(this.ownerDocument.defaultView)
|
||||
: this.$scrollingElement;
|
||||
this._onScrollingElementScroll = _.throttle(() => {
|
||||
for (const editor of this.snippetEditors) {
|
||||
editor.toggleOverlayVisibility(false);
|
||||
@@ -1192,7 +1195,7 @@ var SnippetsMenu = Widget.extend({
|
||||
// Setting capture to true allows to take advantage of event bubbling
|
||||
// for events that otherwise don’t support it. (e.g. useful when
|
||||
// scrolling a modal)
|
||||
this.$scrollingElement[0].addEventListener('scroll', this._onScrollingElementScroll, {capture: true});
|
||||
this.$scrollingTarget[0].addEventListener('scroll', this._onScrollingElementScroll, {capture: true});
|
||||
|
||||
// Auto-selects text elements with a specific class and remove this
|
||||
// on text changes
|
||||
@@ -1257,7 +1260,10 @@ var SnippetsMenu = Widget.extend({
|
||||
this.$snippetEditorArea.remove();
|
||||
this.$window.off('.snippets_menu');
|
||||
this.$document.off('.snippets_menu');
|
||||
this.$scrollingElement[0].removeEventListener('scroll', this._onScrollingElementScroll, {capture: true});
|
||||
|
||||
if (this.$scrollingTarget) {
|
||||
this.$scrollingTarget[0].removeEventListener('scroll', this._onScrollingElementScroll, {capture: true});
|
||||
}
|
||||
}
|
||||
core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
|
||||
delete this.cacheSnippetTemplate[this.options.snippets];
|
||||
|
||||
@@ -284,7 +284,7 @@ class Website(Home):
|
||||
for name, url, mod in current_website.get_suggested_controllers():
|
||||
if needle.lower() in name.lower() or needle.lower() in url.lower():
|
||||
module_sudo = mod and request.env.ref('base.module_%s' % mod, False).sudo()
|
||||
icon = mod and "<img src='%s' width='24px' class='mr-2 rounded' /> " % (module_sudo and module_sudo.icon or mod) or ''
|
||||
icon = mod and "<img src='%s' width='24px' height='24px' class='mr-2 rounded' /> " % (module_sudo and module_sudo.icon or mod) or ''
|
||||
suggested_controllers.append({
|
||||
'value': url,
|
||||
'label': '%s%s (%s)' % (icon, url, name),
|
||||
|
||||
@@ -41,12 +41,12 @@ const UrlPickerUserValueWidget = InputUserValueWidget.extend({
|
||||
this.inputEl.classList.add('text-left');
|
||||
const options = {
|
||||
position: {
|
||||
collision: 'flip fit',
|
||||
collision: 'flip flipfit',
|
||||
},
|
||||
classes: {
|
||||
"ui-autocomplete": 'o_website_ui_autocomplete'
|
||||
},
|
||||
}
|
||||
};
|
||||
wUtils.autocompleteWithPages(this, $(this.inputEl), options);
|
||||
},
|
||||
|
||||
|
||||
@@ -579,7 +579,7 @@ class WebsiteSale(http.Controller):
|
||||
# prevent name change if invoices exist
|
||||
if data.get('partner_id'):
|
||||
partner = request.env['res.partner'].browse(int(data['partner_id']))
|
||||
if partner.exists() and not partner.sudo().can_edit_vat() and 'name' in data and (data['name'] or False) != (partner.name or False):
|
||||
if partner.exists() and partner.name and not partner.sudo().can_edit_vat() and 'name' in data and (data['name'] or False) != (partner.name or False):
|
||||
error['name'] = 'error'
|
||||
error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user