[PATCH] Upstream patch - 24032022

This commit is contained in:
Parthiv Patel
2022-03-24 08:35:44 +00:00
parent 5b4f14dd5e
commit 7d2cfc7cab
40 changed files with 590 additions and 86 deletions

View File

@@ -715,7 +715,7 @@ class AccountBankStatementLine(models.Model):
**counterpart_vals,
'name': counterpart_vals.get('name', move_line.name if move_line else ''),
'move_id': self.move_id.id,
'partner_id': self.partner_id.id or (move_line.partner_id.id if move_line else False),
'partner_id': self.partner_id.id or counterpart_vals.get('partner_id', move_line.partner_id.id if move_line else False),
'currency_id': currency_id,
'account_id': counterpart_vals.get('account_id', move_line.account_id.id if move_line else False),
'debit': balance if balance > 0.0 else 0.0,

View File

@@ -7,6 +7,7 @@ from ldap.filter import filter_format
from flectra import _, api, fields, models, tools
from flectra.exceptions import AccessDenied
from flectra.tools.misc import str2bool
from flectra.tools.pycompat import to_text
_logger = logging.getLogger(__name__)
@@ -74,6 +75,9 @@ class CompanyLDAP(models.Model):
uri = 'ldap://%s:%d' % (conf['ldap_server'], conf['ldap_server_port'])
connection = ldap.initialize(uri)
ldap_chase_ref_disabled = self.env['ir.config_parameter'].sudo().get_param('auth_ldap.disable_chase_ref')
if str2bool(ldap_chase_ref_disabled):
connection.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
if conf['ldap_tls']:
connection.start_tls_s()
return connection

View File

@@ -265,7 +265,7 @@ class DeliveryCarrier(models.Model):
'price': 0.0,
'error_message': _('Error: this delivery method is not available for this address.'),
'warning_message': False}
price = self.fixed_price
price = order.pricelist_id.get_product_price(self.product_id, 1.0, order.partner_id)
company = self.company_id or order.company_id or self.env.company
if company.currency_id and company.currency_id != order.currency_id:
price = company.currency_id._convert(price, order.currency_id, company, fields.Date.today())

View File

@@ -155,3 +155,44 @@ class TestDeliveryCost(common.TransactionCase):
self.default_delivery_policy = self.SaleConfigSetting.create({})
self.default_delivery_policy.execute()
def test_01_delivery_cost_from_pricelist(self):
""" This test aims to validate the use of a pricelist to compute the delivery cost in the case the associated
product of the shipping method is defined in the pricelist """
# Create pricelist with a custom price for the standard shipping method
my_pricelist = self.env['product.pricelist'].create({
'name': 'shipping_cost_change',
'item_ids': [(0, 0, {
'compute_price': 'fixed',
'fixed_price': 5,
'applied_on': '0_product_variant',
'product_id': self.normal_delivery.product_id.id,
})],
})
# Create sales order with Normal Delivery Charges
sale_pricelist_based_delivery_charges = self.SaleOrder.create({
'partner_id': self.partner_18.id,
'pricelist_id': my_pricelist.id,
'order_line': [(0, 0, {
'name': 'PC Assamble + 2GB RAM',
'product_id': self.product_4.id,
'product_uom_qty': 1,
'product_uom': self.product_uom_unit.id,
'price_unit': 750.00,
})],
})
# Add of delivery cost in Sales order
delivery_wizard = Form(self.env['choose.delivery.carrier'].with_context({
'default_order_id': sale_pricelist_based_delivery_charges.id,
'default_carrier_id': self.normal_delivery.id
}))
self.assertEqual(delivery_wizard.delivery_price, 5.0, "Delivery cost does not correspond to 5.0 in wizard")
delivery_wizard.save().button_confirm()
line = self.SaleOrderLine.search([('order_id', '=', sale_pricelist_based_delivery_charges.id),
('product_id', '=', self.normal_delivery.product_id.id)])
self.assertEqual(len(line), 1, "Delivery cost hasn't been added to SO")
self.assertEqual(line.price_subtotal, 5.0, "Delivery cost does not correspond to 5.0")

View File

@@ -854,11 +854,10 @@ class HolidaysRequest(models.Model):
# Business methods
####################################################
def _create_resource_leave(self):
""" This method will create entry in resource calendar time off object at the time of holidays validated
:returns: created `resource.calendar.leaves`
def _prepare_resource_leave_vals_list(self):
"""Hook method for others to inject data
"""
vals_list = [{
return [{
'name': leave.name,
'date_from': leave.date_from,
'holiday_id': leave.id,
@@ -866,7 +865,13 @@ class HolidaysRequest(models.Model):
'resource_id': leave.employee_id.resource_id.id,
'calendar_id': leave.employee_id.resource_calendar_id.id,
'time_type': leave.holiday_status_id.time_type,
} for leave in self]
} for leave in self]
def _create_resource_leave(self):
""" This method will create entry in resource calendar time off object at the time of holidays validated
:returns: created `resource.calendar.leaves`
"""
vals_list = self._prepare_resource_leave_vals_list()
return self.env['resource.calendar.leaves'].sudo().create(vals_list)
def _remove_resource_leave(self):

View File

@@ -21,7 +21,11 @@ class AccountMove(models.Model):
self.journal_id.l10n_latam_use_documents:
return super()._get_l10n_latam_documents_domain()
if self.journal_id.type == 'sale':
domain = [('country_id.code', '=', "CL"), ('internal_type', '!=', 'invoice_in')]
if self.move_type == 'out_refund':
internal_types_domain = ('internal_type', '=', 'credit_note')
else:
internal_types_domain = ('internal_type', 'not in', ['invoice_in', 'credit_note'])
domain = [('country_id.code', '=', 'CL'), internal_types_domain]
if self.company_id.partner_id.l10n_cl_sii_taxpayer_type == '1':
domain += [('code', '!=', '71')] # Companies with VAT Affected doesn't have "Boleta de honorarios Electrónica"
return domain

View File

@@ -28,6 +28,8 @@ class SaleOrder(models.Model):
data.append((_('Customer Reference'), record.client_order_ref))
if record.user_id:
data.append((_("Salesperson"), record.user_id.name))
if 'incoterm' in record._fields and record.incoterm:
data.append((_("Incoterm"), record.incoterm.code))
def _compute_l10n_de_document_title(self):
for record in self:

View File

@@ -242,7 +242,7 @@ class MockModels {
email_cc: { type: 'char' },
partner_ids: {
string: "Related partners",
type: 'many2one',
type: 'one2many',
relation: 'res.partner'
},
},

View File

@@ -108,6 +108,18 @@ class MrpBom(models.Model):
bom_product=bom_line.parent_product_tmpl_id.display_name
))
@api.onchange('bom_line_ids', 'product_qty')
def onchange_bom_structure(self):
if self.type == 'phantom' and self._origin and self.env['stock.move'].search([('bom_line_id', 'in', self._origin.bom_line_ids.ids)], limit=1):
return {
'warning': {
'title': _('Warning'),
'message': _(
'The product has already been used at least once, editing its structure may lead to undesirable behaviours. '
'You should rather archive the product and create a new one with a new bill of materials.'),
}
}
@api.onchange('product_uom_id')
def onchange_product_uom_id(self):
res = {}

View File

@@ -12,7 +12,7 @@ from dateutil.relativedelta import relativedelta
from itertools import groupby
from flectra import api, fields, models, _
from flectra.exceptions import AccessError, UserError
from flectra.exceptions import AccessError, UserError, ValidationError
from flectra.tools import float_compare, float_round, float_is_zero, format_datetime
from flectra.tools.misc import format_date
@@ -709,6 +709,13 @@ class MrpProduction(models.Model):
else:
self.workorder_ids = False
@api.constrains('product_id', 'move_raw_ids')
def _check_production_lines(self):
for production in self:
for move in production.move_raw_ids:
if production.product_id == move.product_id:
raise ValidationError(_("The component %s should not be the same as the product to produce.") % production.product_id.display_name)
def write(self, vals):
if 'workorder_ids' in self:
production_to_replan = self.filtered(lambda p: p.is_planned)

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import lib
from . import tools
from . import models
from . import wizard

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import phonenumbers_patch

View File

@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
try:
# import for usage in phonenumbers_patch/region_*.py files
from phonenumbers.phonemetadata import NumberFormat, PhoneNumberDesc, PhoneMetadata # pylint: disable=unused-import
except ImportError:
pass

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2009 The Libphonenumber Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# https://github.com/google/libphonenumber
from flectra.tools.parse_version import parse_version
try:
import phonenumbers
# MONKEY PATCHING phonemetadata of Ivory Coast if phonenumbers is too old
if parse_version('7.6.1') <= parse_version(phonenumbers.__version__) < parse_version('8.12.32'):
def _local_load_region(code):
__import__("region_%s" % code, globals(), locals(),
fromlist=["PHONE_METADATA_%s" % code], level=1)
# loading updated region_CI.py from current directory
# https://github.com/daviddrysdale/python-phonenumbers/blob/v8.12.32/python/phonenumbers/data/region_CI.py
phonenumbers.phonemetadata.PhoneMetadata.register_region_loader('CI', _local_load_region)
except ImportError:
pass

View File

@@ -0,0 +1,9 @@
"""Auto-generated file, do not edit by hand. CI metadata"""
from ..phonemetadata import NumberFormat, PhoneNumberDesc, PhoneMetadata
PHONE_METADATA_CI = PhoneMetadata(id='CI', country_code=225, international_prefix='00',
general_desc=PhoneNumberDesc(national_number_pattern='[02]\\d{9}', possible_length=(10,)),
fixed_line=PhoneNumberDesc(national_number_pattern='2(?:[15]\\d{3}|7(?:2(?:0[23]|1[2357]|[23][45]|4[3-5])|3(?:06|1[69]|[2-6]7)))\\d{5}', example_number='2123456789', possible_length=(10,)),
mobile=PhoneNumberDesc(national_number_pattern='0704[0-7]\\d{5}|0(?:[15]\\d\\d|7(?:0[0-37-9]|[4-9][7-9]))\\d{6}', example_number='0123456789', possible_length=(10,)),
number_format=[NumberFormat(pattern='(\\d{2})(\\d{2})(\\d)(\\d{5})', format='\\1 \\2 \\3 \\4', leading_digits_pattern=['2']),
NumberFormat(pattern='(\\d{2})(\\d{2})(\\d{2})(\\d{4})', format='\\1 \\2 \\3 \\4', leading_digits_pattern=['0'])])

View File

@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_phonenumbers_patch

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
try:
import phonenumbers
except ImportError:
phonenumbers = None
from flectra.tests.common import BaseCase
from flectra.tools.parse_version import parse_version
from flectra.addons.phone_validation.lib import phonenumbers_patch
class TestPhonenumbersPatch(BaseCase):
def test_region_CI_monkey_patch(self):
"""Test if the patch is apply on the good version of the lib
And test some phonenumbers"""
if not phonenumbers:
self.skipTest('Cannot test without phonenumbers module installed.')
# MONKEY PATCHING phonemetadata of Ivory Coast if phonenumbers is too old
if parse_version('7.6.1') <= parse_version(phonenumbers.__version__) < parse_version('8.12.32'):
# check that _local_load_region is set to `flectra.addons.phone_validation.lib.phonenumbers_patch._local_load_region`
# check that you can load a new ivory coast phone number without error
parsed_phonenumber_1 = phonenumbers.parse("20 25/35-51 ", region="CI", keep_raw_input=True)
self.assertEqual(parsed_phonenumber_1.national_number, 20253551, "The national part of the phonenumber should be 22522586")
self.assertEqual(parsed_phonenumber_1.country_code, 225, "The country code of Ivory Coast is 225")
parsed_phonenumber_2 = phonenumbers.parse("+225 22 52 25 86 ", region="CI", keep_raw_input=True)
self.assertEqual(parsed_phonenumber_2.national_number, 22522586, "The national part of the phonenumber should be 22522586")
self.assertEqual(parsed_phonenumber_2.country_code, 225, "The country code of Ivory Coast is 225")
else:
self.assertFalse(hasattr(phonenumbers_patch, '_local_load_region'),
"The code should not be monkey patched with phonenumbers > 8.12.32.")

View File

@@ -21,7 +21,7 @@
<h2>Request for Quotation <span t-field="o.name"/></h2>
<table class="table table-sm">
<thead>
<thead style="display: table-row-group">
<tr>
<th name="th_description"><strong>Description</strong></th>
<th name="th_expected_date" class="text-center"><strong>Expected Date</strong></th>

View File

@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models
from flectra import _, models
from flectra.tools.float_utils import float_is_zero
from flectra.exceptions import UserError
class StockMove(models.Model):
@@ -24,4 +26,6 @@ class StockMove(models.Model):
}
valuation_total_qty = self._compute_kit_quantities(related_aml.product_id, order_qty, kit_bom, filters)
valuation_total_qty = kit_bom.product_uom_id._compute_quantity(valuation_total_qty, related_aml.product_id.uom_id)
if float_is_zero(valuation_total_qty, precision_rounding=related_aml.product_uom_id.rounding or related_aml.product_id.uom_id.rounding):
raise UserError(_('Flectra is not able to generate the anglo saxon entries. The total valuation of %s is zero.') % related_aml.product_id.display_name)
return valuation_price_unit_total, valuation_total_qty

View File

@@ -338,7 +338,7 @@ class SaleOrder(models.Model):
for order in self:
total = 0.0
for line in order.order_line:
total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ??
total += (line.price_subtotal * 100)/(100-line.discount) if line.discount != 100 else (line.price_unit * line.product_uom_qty)
order.amount_undiscounted = total
@api.depends('state')
@@ -477,24 +477,10 @@ class SaleOrder(models.Model):
def update_prices(self):
self.ensure_one()
lines_to_update = []
for line in self._get_update_prices_lines():
product = line.product_id.with_context(
partner=self.partner_id,
quantity=line.product_uom_qty,
date=self.date_order,
pricelist=self.pricelist_id.id,
uom=line.product_uom.id
)
price_unit = self.env['account.tax']._fix_tax_included_price_company(
line._get_display_price(product), line.product_id.taxes_id, line.tax_id, line.company_id)
if self.pricelist_id.discount_policy == 'without_discount' and price_unit:
price_discount_unrounded = self.pricelist_id.get_product_price(product, line.product_uom_qty, self.partner_id, self.date_order, line.product_uom.id)
discount = max(0, (price_unit - price_discount_unrounded) * 100 / price_unit)
else:
discount = 0
lines_to_update.append((1, line.id, {'price_unit': price_unit, 'discount': discount}))
self.update({'order_line': lines_to_update})
line.product_uom_change()
line.discount = 0 # Force 0 as discount for the cases when _onchange_discount directly returns
line._onchange_discount()
self.show_update_pricelist = False
self.message_post(body=_("Product prices have been recomputed according to pricelist <b>%s<b> ", self.pricelist_id.display_name))

View File

@@ -583,6 +583,59 @@ class TestSaleOrder(TestSaleCommon):
self.assertEqual(line.price_subtotal, 17527.41)
self.assertEqual(line.untaxed_amount_to_invoice, line.price_subtotal)
def test_discount_and_amount_undiscounted(self):
"""When adding a discount on a SO line, this test ensures that amount undiscounted is
consistent with the used tax"""
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [(0, 0, {
'product_id': self.product_a.id,
'product_uom_qty': 1,
'price_unit': 100.0,
'discount': 1.00,
})]
})
sale_order.action_confirm()
line = sale_order.order_line
# test discount and qty 1
self.assertEqual(sale_order.amount_undiscounted, 100.0)
self.assertEqual(line.price_subtotal, 99.0)
# more quantity 1 -> 3
sale_form = Form(sale_order)
with sale_form.order_line.edit(0) as line_form:
line_form.product_uom_qty = 3.0
line_form.price_unit = 100.0
sale_order = sale_form.save()
self.assertEqual(sale_order.amount_undiscounted, 300.0)
self.assertEqual(line.price_subtotal, 297.0)
# undiscounted
with sale_form.order_line.edit(0) as line_form:
line_form.discount = 0.0
sale_order = sale_form.save()
self.assertEqual(line.price_subtotal, 300.0)
self.assertEqual(sale_order.amount_undiscounted, 300.0)
# Same with an included-in-price tax
sale_order = sale_order.copy()
line = sale_order.order_line
line.tax_id = [(0, 0, {
'name': 'Super Tax',
'amount_type': 'percent',
'amount': 10.0,
'price_include': True,
})]
line.discount = 50.0
sale_order.action_confirm()
# 300 with 10% incl tax -> 272.72 total tax excluded without discount
# 136.36 price tax excluded with discount applied
self.assertEqual(sale_order.amount_undiscounted, 272.72)
self.assertEqual(line.price_subtotal, 136.36)
def test_free_product_and_price_include_fixed_tax(self):
""" Check that fixed tax include are correctly computed while the price_unit is 0
"""

View File

@@ -71,6 +71,18 @@ class TestSaleOrder(TestSaleCommon):
'percent_price': 20,
})
# Create a pricelist without discount policy: percentage on all products
cls.pricelist_discount_excl_global = cls.env['product.pricelist'].create({
'name': 'Pricelist C',
'discount_policy': 'without_discount',
'company_id': cls.env.company.id,
'item_ids': [(0, 0, {
'applied_on': '3_global',
'compute_price': 'percentage',
'percent_price': 54,
})],
})
# create a generic Sale Order with all classical products and empty pricelist
cls.sale_order = SaleOrder.create({
'partner_id': cls.partner_a.id,
@@ -164,18 +176,7 @@ class TestSaleOrder(TestSaleCommon):
def test_sale_change_of_pricelists_excluded_value_discount(self):
""" Test SO with the pricelist 'discount displayed' and check displayed percentage value after multiple changes of pricelist """
# Create a pricelist without discount policy: percentage on all products
pricelist_discount_excl_global = self.env['product.pricelist'].create({
'name': 'Pricelist C',
'discount_policy': 'without_discount',
'company_id': self.env.company.id,
'item_ids': [(0, 0, {
'applied_on': '3_global',
'compute_price': 'percentage',
'percent_price': 54,
})],
})
self.env.user.write({'groups_id': [(4, self.env.ref('product.group_discount_per_so_line').id)]})
# Create a product with a very low price
amazing_product = self.env['product.product'].create({
@@ -200,7 +201,7 @@ class TestSaleOrder(TestSaleCommon):
})
# Change the pricelist
sale_order.write({'pricelist_id': pricelist_discount_excl_global.id})
sale_order.write({'pricelist_id': self.pricelist_discount_excl_global.id})
# Update Prices
sale_order.update_prices()
@@ -222,3 +223,79 @@ class TestSaleOrder(TestSaleCommon):
sale_order.order_line.tax_id,
"Wrong tax applied for specified product & pricelist"
)
def test_sale_change_of_pricelists_excluded_value_discount_on_tax_included_price_mapped_to_tax_excluded_price(self):
self.env.user.write({'groups_id': [(4, self.env.ref('product.group_discount_per_so_line').id)]})
# setting up the taxes:
tax_a = self.tax_sale_a.copy()
tax_b = self.tax_sale_a.copy()
tax_a.price_include = True
tax_b.amount = 6
# setting up fiscal position:
fiscal_pos = self.fiscal_pos_a.copy()
fiscal_pos.auto_apply = True
country = self.env["res.country"].search([('name', '=', 'Belgium')], limit=1)
fiscal_pos.country_id = country
fiscal_pos.tax_ids = [
(0, None,
{
'tax_src_id': tax_a.id,
'tax_dest_id': tax_b.id
})
]
# setting up partner:
self.partner_a.country_id = country
# creating product:
my_product = self.env['product.product'].create({
'name': 'my Product',
'lst_price': 115,
'taxes_id': [tax_a.id]
})
# creating SO
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'partner_invoice_id': self.partner_a.id,
'partner_shipping_id': self.partner_a.id,
'pricelist_id': self.company_data['default_pricelist'].id,
'order_line': [(0, 0, {
'name': my_product.name,
'product_id': my_product.id,
'product_uom_qty': 1,
'product_uom': my_product.uom_id.id,
})],
})
# Apply fiscal position
sale_order.fiscal_position_id = fiscal_pos.id
# Change the pricelist
sale_order.write({'pricelist_id': self.pricelist_discount_excl_global.id})
# Update Prices
sale_order.update_prices()
# Check that the discount displayed is the correct one
self.assertEqual(
sale_order.order_line.discount, 54,
"Wrong discount computed for specified product & pricelist"
)
# Additional to check for overall consistency
self.assertEqual(
sale_order.order_line.price_unit, 100,
"Wrong unit price computed for specified product & pricelist"
)
self.assertEqual(
sale_order.order_line.price_subtotal, 46,
"Wrong subtotal price computed for specified product & pricelist"
)
self.assertEqual(
sale_order.order_line.tax_id.id, tax_b.id,
"Wrong tax applied for specified product & pricelist"
)

View File

@@ -86,8 +86,7 @@ class SaleOrder(models.Model):
def update_prices(self):
self.ensure_one()
res = super().update_prices()
for line in self.sale_order_option_ids:
line.price_unit = self.pricelist_id.get_product_price(line.product_id, line.quantity, self.partner_id, uom_id=line.uom_id.id)
self.sale_order_option_ids._update_price_and_discount()
return res
@api.onchange('sale_order_template_id')
@@ -150,9 +149,12 @@ class SaleOrder(models.Model):
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
if self.env.su:
self = self.with_user(SUPERUSER_ID)
for order in self:
if order.sale_order_template_id and order.sale_order_template_id.mail_template_id:
order.sale_order_template_id.mail_template_id.with_user(SUPERUSER_ID).send_mail(order.id)
order.sale_order_template_id.mail_template_id.send_mail(order.id)
return res
def get_access_action(self, access_uid=None):
@@ -208,6 +210,26 @@ class SaleOrderOption(models.Model):
quantity = fields.Float('Quantity', required=True, digits='Product Unit of Measure', default=1)
sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of optional products.")
def _update_price_and_discount(self):
for option in self:
if not option.product_id:
continue
# To compute the discount a so line is created in cache
values = option._get_values_to_add_to_order()
new_sol = option.env['sale.order.line'].new(values)
new_sol._onchange_discount()
option.discount = new_sol.discount
if option.order_id.pricelist_id and option.order_id.partner_id:
product = option.product_id.with_context(
partner=option.order_id.partner_id,
quantity=option.quantity,
date=option.order_id.date_order,
pricelist=option.order_id.pricelist_id.id,
uom=option.uom_id.id,
fiscal_position=option.env.context.get('fiscal_position')
)
option.price_unit = new_sol._get_display_price(product)
@api.depends('line_id', 'order_id.order_line', 'product_id')
def _compute_is_present(self):
# NOTE: this field cannot be stored as the line_id is usually removed
@@ -226,22 +248,9 @@ class SaleOrderOption(models.Model):
return
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.quantity,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.uom_id.id,
fiscal_position=self.env.context.get('fiscal_position')
)
self.name = product.get_product_multiline_description_sale()
self.uom_id = self.uom_id or product.uom_id
# To compute the discount a so line is created in cache
values = self._get_values_to_add_to_order()
new_sol = self.env['sale.order.line'].new(values)
new_sol._onchange_discount()
self.discount = new_sol.discount
if self.order_id.pricelist_id and self.order_id.partner_id:
self.price_unit = new_sol._get_display_price(product)
self._update_price_and_discount()
def button_add_to_order(self):
self.add_option_to_order()

View File

@@ -283,3 +283,61 @@ class TestSaleOrder(TestSaleCommon):
"If a pricelist is set without discount included, the discount "
"shall be computed according to the price unit and the subtotal."
"price")
def test_04_update_pricelist_option_line(self):
"""
This test checks that option line's values are correctly
updated after a pricelist update
"""
# Necessary for _onchange_discount() check
self.env.user.write({
'groups_id': [(4, self.env.ref('product.group_discount_per_so_line').id)],
})
self.sale_order.write({
'sale_order_template_id': self.quotation_template_no_discount.id
})
self.sale_order.onchange_sale_order_template_id()
self.assertEqual(
self.sale_order.sale_order_option_ids[0].price_unit,
self.pub_option_price,
"If no pricelist is set, the unit price shall be the option's product price.")
self.assertEqual(
self.sale_order.sale_order_option_ids[0].discount, 0,
"If no pricelist is set, the discount should be 0.")
self.sale_order.write({
'pricelist_id': self.discount_included_price_list.id,
})
self.sale_order.update_prices()
self.assertEqual(
self.sale_order.sale_order_option_ids[0].price_unit,
self.pl_option_price,
"If a pricelist is set with discount included,"
" the unit price shall be the option's product discounted price.")
self.assertEqual(
self.sale_order.sale_order_option_ids[0].discount, 0,
"If a pricelist is set with discount included,"
" the discount should be 0.")
self.sale_order.write({
'pricelist_id': self.discount_excluded_price_list.id,
})
self.sale_order.update_prices()
self.assertEqual(
self.sale_order.sale_order_option_ids[0].price_unit,
self.pub_option_price,
"If a pricelist is set without discount included,"
" the unit price shall be the option's product sale price.")
self.assertEqual(
self.sale_order.sale_order_option_ids[0].discount,
self.pl_option_discount,
"If a pricelist is set without discount included,"
" the discount should be correctly computed.")

View File

@@ -122,12 +122,12 @@ class SaleOrderLine(models.Model):
index=True, copy=False, help="Task generated by the sales order item")
is_service = fields.Boolean("Is a Service", compute='_compute_is_service', store=True, compute_sudo=True, help="Sales Order item should generate a task and/or a project, depending on the product settings.")
@api.depends('product_id')
@api.depends('product_id.type')
def _compute_is_service(self):
for so_line in self:
so_line.is_service = so_line.product_id.type == 'service'
@api.depends('product_id')
@api.depends('product_id.type')
def _compute_product_updatable(self):
for line in self:
if line.product_id.type == 'service' and line.state == 'sale':

View File

@@ -139,3 +139,24 @@ class TestSaleProject(SavepointCase):
# service_tracking 'project_only'
self.assertFalse(so_line_order_only_project.task_id, "Task should not be created")
self.assertTrue(so_line_order_only_project.project_id, "Sales order line should be linked to newly created project")
def test_sol_product_type_update(self):
partner = self.env['res.partner'].create({'name': "Mur en brique"})
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
'partner_id': partner.id,
'partner_invoice_id': partner.id,
'partner_shipping_id': partner.id,
})
self.product_order_service3.type = 'consu'
sale_order_line = self.env['sale.order.line'].create({
'order_id': sale_order.id,
'name': self.product_order_service3.name,
'product_id': self.product_order_service3.id,
'product_uom_qty': 5,
'product_uom': self.product_order_service3.uom_id.id,
'price_unit': self.product_order_service3.list_price
})
self.assertFalse(sale_order_line.is_service, "As the product is consumable, the SOL should not be a service")
self.product_order_service3.type = 'service'
self.assertTrue(sale_order_line.is_service, "As the product is a service, the SOL should be a service")

View File

@@ -648,19 +648,22 @@ class Picking(models.Model):
# As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
# As it is a create the format will be a list of (0, 0, dict)
moves = vals.get('move_lines', []) + vals.get('move_ids_without_package', [])
if moves and vals.get('location_id') and vals.get('location_dest_id'):
if moves and ((vals.get('location_id') and vals.get('location_dest_id')) or vals.get('partner_id')):
for move in moves:
if len(move) == 3 and move[0] == 0:
move[2]['location_id'] = vals['location_id']
move[2]['location_dest_id'] = vals['location_dest_id']
# When creating a new picking, a move can have no `company_id` (create before
# picking type was defined) or a different `company_id` (the picking type was
# changed for an another company picking type after the move was created).
# So, we define the `company_id` in one of these cases.
picking_type = self.env['stock.picking.type'].browse(vals['picking_type_id'])
if 'picking_type_id' not in move[2] or move[2]['picking_type_id'] != picking_type.id:
move[2]['picking_type_id'] = picking_type.id
move[2]['company_id'] = picking_type.company_id.id
if vals.get('location_id') and vals.get('location_dest_id'):
move[2]['location_id'] = vals['location_id']
move[2]['location_dest_id'] = vals['location_dest_id']
# When creating a new picking, a move can have no `company_id` (create before
# picking type was defined) or a different `company_id` (the picking type was
# changed for an another company picking type after the move was created).
# So, we define the `company_id` in one of these cases.
picking_type = self.env['stock.picking.type'].browse(vals['picking_type_id'])
if 'picking_type_id' not in move[2] or move[2]['picking_type_id'] != picking_type.id:
move[2]['picking_type_id'] = picking_type.id
move[2]['company_id'] = picking_type.company_id.id
if vals.get('partner_id'):
move[2]['partner_id'] = vals.get('partner_id')
# make sure to write `schedule_date` *after* the `stock.move` creation in
# order to get a determinist execution of `_set_scheduled_date`
scheduled_date = vals.pop('scheduled_date', False)
@@ -700,6 +703,8 @@ class Picking(models.Model):
after_vals['location_id'] = vals['location_id']
if vals.get('location_dest_id'):
after_vals['location_dest_id'] = vals['location_dest_id']
if 'partner_id' in vals:
after_vals['partner_id'] = vals['partner_id']
if after_vals:
self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
if vals.get('move_lines'):

View File

@@ -626,7 +626,8 @@ The correction could unreserve some operations with problematics products.""", p
:param domain: List for the domain, empty by default.
:param extend: If True, enables form, graph and pivot views. False by default.
"""
self._quant_tasks()
if not self.env['ir.config_parameter'].sudo().get_param('stock.skip_quant_tasks'):
self._quant_tasks()
ctx = dict(self.env.context or {})
ctx.pop('group_by', None)
action = {

View File

@@ -293,6 +293,15 @@ class StockRule(models.Model):
if not self.location_id.should_bypass_reservation():
move_dest_ids = values.get('move_dest_ids', False) and [(4, x.id) for x in values['move_dest_ids']] or []
# when create chained moves for inter-warehouse transfers, set the warehouses as partners
if not partner and move_dest_ids:
move_dest = values['move_dest_ids']
if location_id == company_id.internal_transit_location_id:
partners = move_dest.location_dest_id.get_warehouse().partner_id
if len(partners) == 1:
partner = partners
move_dest.partner_id = partner
move_values = {
'name': name[:2000],
'company_id': self.company_id.id or self.location_src_id.company_id.id or self.location_id.company_id.id or company_id.id,

View File

@@ -5,10 +5,16 @@
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
<t t-foreach="docs" t-as="picking">
<t t-set="picking_qty_done" t-value="any(picking.move_lines.move_line_ids.mapped('qty_done'))"/>
<t t-foreach="picking.move_lines" t-as="move">
<t t-foreach="move.move_line_ids" t-as="move_line">
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
<t t-set="qty" t-value="int(move_line.qty_done)"/>
<t t-if="picking_qty_done">
<t t-set="qty" t-value="int(move_line.qty_done)"/>
</t>
<t t-else="">
<t t-set="qty" t-value="int(move_line.product_uom_qty)"/>
</t>
</t>
<t t-else="">
<t t-set="qty" t-value="1"/>
@@ -42,10 +48,16 @@
<div class="page">
<t t-set="uom_categ_unit" t-value="env.ref('uom.product_uom_categ_unit')"/>
<t t-foreach="docs" t-as="picking">
<t t-set="picking_qty_done" t-value="any(picking.move_lines.move_line_ids.mapped('qty_done'))"/>
<t t-foreach="picking.move_lines" t-as="move">
<t t-foreach="move.move_line_ids" t-as="move_line">
<t t-if="move_line.product_uom_id.category_id == uom_categ_unit">
<t t-set="qty" t-value="int(move_line.qty_done)"/>
<t t-if="picking_qty_done">
<t t-set="qty" t-value="int(move_line.qty_done)"/>
</t>
<t t-else="">
<t t-set="qty" t-value="int(move_line.product_uom_qty)"/>
</t>
</t>
<t t-else="">
<t t-set="qty" t-value="1"/>

View File

@@ -2139,3 +2139,25 @@ class TestStockFlow(TestStockCommon):
validate_picking(in02)
self.assertEqual(out02.state, 'confirmed')
self.assertEqual(out03.state, 'assigned')
def test_stock_move_with_partner_id(self):
""" Ensure that the partner_id of the picking entry is
transmitted to the SM upon object creation.
"""
partner_1 = self.env['res.partner'].create({'name': 'Hubert Bonisseur de la Bath'})
partner_2 = self.env['res.partner'].create({'name': 'Donald Clairvoyant du Bled'})
product = self.env['product.product'].create({'name': 'Un petit coup de polish', 'type': 'product'})
wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
f = Form(self.env['stock.picking'])
f.partner_id = partner_1
f.picking_type_id = wh.out_type_id
with f.move_ids_without_package.new() as move:
move.product_id = product
move.product_uom_qty = 5
picking = f.save()
self.assertEqual(picking.move_lines.partner_id, partner_1)
picking.write({'partner_id': partner_2.id})
self.assertEqual(picking.move_lines.partner_id, partner_2)

View File

@@ -256,10 +256,12 @@ class TestWarehouse(TestStockCommon):
'code': 'STK',
})
distribution_partner = self.env['res.partner'].create({'name': 'Distribution Center'})
warehouse_distribution = self.env['stock.warehouse'].create({
'name': 'Dist.',
'code': 'DIST',
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])],
'partner_id': distribution_partner.id,
})
warehouse_shop = self.env['stock.warehouse'].create({
@@ -316,6 +318,9 @@ class TestWarehouse(TestStockCommon):
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_shop.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_shop.lot_stock_id.id)]))
self.assertTrue(self.env['stock.picking'].search([('location_id', '=', self.env.company.internal_transit_location_id.id), ('partner_id', '=', distribution_partner.id)]))
self.assertTrue(self.env['stock.picking'].search([('location_dest_id', '=', self.env.company.internal_transit_location_id.id), ('partner_id', '=', distribution_partner.id)]))
def test_mutiple_resupply_warehouse(self):
""" Simulate the following situation:
- 2 shops with stock are resupply by 2 distinct warehouses

View File

@@ -129,6 +129,23 @@ var ListController = BasicController.extend({
return self.model.get(db_id, {raw: true});
});
},
/**
* Returns the list of currently selected records (with the check boxes on
* the left) or the whole domain records if it is selected
*
* @returns {Promise<{id, display_name}[]>}
*/
getSelectedRecordsWithDomain: async function () {
if (this.isDomainSelected) {
const state = this.model.get(this.handle, {raw: true});
return await this._domainToRecords(state.getDomain(), session.active_ids_limit);
} else {
return Promise.resolve(this.selectedRecords.map(localId => {
const data = this.model.localData[localId].data;
return { id: data.id, display_name: data.display_name };
}));
}
},
/**
* Display and bind all buttons in the control panel
*
@@ -384,6 +401,25 @@ var ListController = BasicController.extend({
self.updateButtons('readonly');
});
},
/**
* Returns the records matching the given domain.
*
* @private
* @param {Array[]} domain
* @param {integer} [limit]
* @returns {Promise<{id, display_name}[]>}
*/
_domainToRecords: function (domain, limit) {
return this._rpc({
model: this.modelName,
method: 'search_read',
args: [domain],
kwargs: {
fields: ['display_name'],
limit: limit,
},
});
},
/**
* Returns the ids of records matching the given domain.
*

View File

@@ -462,8 +462,7 @@ var SelectCreateDialog = ViewDialog.extend({
disabled: true,
close: true,
click: async () => {
const resIds = await this.viewController.getSelectedIdsWithDomain();
const values = resIds.map(e => ({id: e}));
const values = await this.viewController.getSelectedRecordsWithDomain();
this.on_selected(values);
},
});

View File

@@ -583,7 +583,7 @@ QUnit.module('Views', {
});
QUnit.test('SelectCreateDialog calls on_selected with every record matching the domain', async function (assert) {
assert.expect(1);
assert.expect(3);
const parent = await createParent({
data: this.data,
@@ -604,7 +604,9 @@ QUnit.module('Views', {
new dialogs.SelectCreateDialog(parent, {
res_model: 'partner',
on_selected: function(records) {
assert.equal(records.length, 3)
assert.equal(records.length, 3);
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver,Jack O'Neill");
assert.strictEqual(records.map((r) => r.id).toString(), "1,2,3");
}
}).open();
await testUtils.nextTick();
@@ -616,6 +618,42 @@ QUnit.module('Views', {
parent.destroy();
});
QUnit.test('SelectCreateDialog calls on_selected with every record matching without selecting a domain', async function (assert) {
assert.expect(3);
const parent = await createParent({
data: this.data,
archs: {
'partner,false,list':
'<tree limit="2" string="Partner">' +
'<field name="display_name"/>' +
'<field name="foo"/>' +
'</tree>',
'partner,false,search':
'<search>' +
'<field name="foo"/>' +
'</search>',
},
session: {},
});
new dialogs.SelectCreateDialog(parent, {
res_model: 'partner',
on_selected: function(records) {
assert.equal(records.length, 2);
assert.strictEqual(records.map((r) => r.display_name).toString(), "blipblip,macgyver");
assert.strictEqual(records.map((r) => r.id).toString(), "1,2");
}
}).open();
await testUtils.nextTick();
await testUtils.dom.click($('thead .o_list_record_selector input'));
await testUtils.dom.click($('.o_list_selection_box '));
await testUtils.dom.click($('.modal .o_select_button'));
parent.destroy();
});
QUnit.test('propagate can_create onto the search popup o2m', async function (assert) {
assert.expect(4);

View File

@@ -173,7 +173,7 @@
</t>
<t t-else="">
<t t-set='groups_tooltip'>More than one group has been set on the view.</t>
<a class="show_group_id btn btn-link mx-auto" href="/web#model=ir.ui.view&amp;id=681" t-att-title='groups_tooltip'>Discard &amp; Edit in backend</a>
<a class="show_group_id btn btn-link mx-auto" t-attf-href="/web#id=#{widget.page.view_id[0]}&amp;view_type=form&amp;model=ir.ui.view" t-att-title='groups_tooltip'>Discard &amp; Edit in backend</a>
</t>
</div>
</div>

View File

@@ -728,6 +728,10 @@ class WebsiteSale(http.Controller):
if not kw.get('use_same'):
kw['callback'] = kw.get('callback') or \
(not order.only_services and (mode[0] == 'edit' and '/shop/checkout' or '/shop/address'))
# We need to update the pricelist(by the one selected by the customer), because onchange_partner reset it
# We only need to update the pricelist when it is not redirected to /confirm_order
if kw.get('callback', '') != '/shop/confirm_order':
request.website.sale_get_order(update_pricelist=True)
elif mode[1] == 'shipping':
order.partner_shipping_id = partner_id

View File

@@ -45,7 +45,7 @@ class CrmTeam(models.Model):
'type': 'ir.actions.act_window',
'view_mode': 'tree,form',
'domain': [('is_abandoned_cart', '=', True)],
'search_view_id': self.env.ref('sale.sale_order_view_search_inherit_sale').id,
'search_view_id': [self.env.ref('sale.sale_order_view_search_inherit_sale').id],
'context': {
'search_default_team_id': self.id,
'default_team_id': self.id,

View File

@@ -59,7 +59,8 @@
<field name="name">Online Sales Analysis</field>
<field name="res_model">sale.report</field>
<field name="view_mode">pivot,graph</field>
<field name="domain">[('state','in',('sale', 'done')), ('website_id', '!=', False)]</field>
<field name="domain">[('website_id', '!=', False)]</field>
<field name="context">{'search_default_confirmed': 1}</field>
<field name="search_view_id" ref="sale_report_view_search_website"/>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, models
from flectra import api, models, _
class Users(models.Model):
@@ -27,6 +27,6 @@ class Users(models.Model):
res = super(Users, self).get_gamification_redirection_data()
res.append({
'url': '/slides',
'label': 'See our eLearning'
'label': _('See our eLearning')
})
return res