mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 27042022
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<flectra>
|
||||
<flectra noupdate="1">
|
||||
<record id="ir_cron_edi_network" model="ir.cron">
|
||||
<field name="name">EDI : Perform web services operations</field>
|
||||
<field name="model_id" ref="model_account_edi_document"/>
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
import logging
|
||||
import poplib
|
||||
from ssl import SSLError
|
||||
from socket import gaierror, timeout
|
||||
import socket
|
||||
|
||||
from imaplib import IMAP4, IMAP4_SSL
|
||||
from poplib import POP3, POP3_SSL
|
||||
from socket import gaierror, timeout
|
||||
from ssl import SSLError
|
||||
|
||||
from flectra import api, fields, models, tools, _
|
||||
from flectra.exceptions import UserError
|
||||
@@ -19,6 +21,11 @@ MAIL_TIMEOUT = 60
|
||||
# Workaround for Python 2.7.8 bug https://bugs.python.org/issue23906
|
||||
poplib._MAXLINE = 65536
|
||||
|
||||
# Add timeout to IMAP connections
|
||||
# HACK https://bugs.python.org/issue38615
|
||||
# TODO: clean in Python 3.9
|
||||
IMAP4._create_socket = lambda self, timeout=MAIL_TIMEOUT: socket.create_connection((self.host or None, self.port), timeout)
|
||||
|
||||
|
||||
class FetchmailServer(models.Model):
|
||||
"""Incoming POP/IMAP mail server account"""
|
||||
@@ -108,15 +115,13 @@ flectra_mailgate: "|/path/to/flectra-mailgate.py --host=localhost -u %(uid)d -p
|
||||
self._imap_login(connection)
|
||||
elif self.server_type == 'pop':
|
||||
if self.is_ssl:
|
||||
connection = POP3_SSL(self.server, int(self.port))
|
||||
connection = POP3_SSL(self.server, int(self.port), timeout=MAIL_TIMEOUT)
|
||||
else:
|
||||
connection = POP3(self.server, int(self.port))
|
||||
connection = POP3(self.server, int(self.port), timeout=MAIL_TIMEOUT)
|
||||
#TODO: use this to remove only unread messages
|
||||
#connection.user("recent:"+server.user)
|
||||
connection.user(self.user)
|
||||
connection.pass_(self.password)
|
||||
# Add timeout on socket
|
||||
connection.sock.settimeout(MAIL_TIMEOUT)
|
||||
return connection
|
||||
|
||||
def _imap_login(self, connection):
|
||||
|
||||
@@ -467,6 +467,12 @@ var MassMailingFieldHtml = FieldHtml.extend({
|
||||
$dropdown.find('.dropdown-item:eq(' + themesParams.indexOf(selectedTheme) + ')').addClass('selected');
|
||||
});
|
||||
|
||||
// Prevent expansion of drop-down while clicking on empty area during theme selection
|
||||
$dropdown.on("click", ".dropdown-menu", function (ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
|
||||
/**
|
||||
* If the user opens the theme selection screen, indicates which one is active and
|
||||
* saves the information...
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: $o-we-sidebar-bg;
|
||||
}
|
||||
|
||||
&.selected .o_thumb {
|
||||
border: 2px solid $o-brand-odoo;
|
||||
background-color: $o-we-sidebar-bg;
|
||||
|
||||
@@ -16,8 +16,10 @@ class MailMessage(models.Model):
|
||||
|
||||
def _portal_message_format(self, fields_list):
|
||||
vals_list = self._message_format(fields_list)
|
||||
message_subtype_note_id = self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note')
|
||||
IrAttachmentSudo = self.env['ir.attachment'].sudo()
|
||||
for vals in vals_list:
|
||||
vals['is_message_subtype_note'] = message_subtype_note_id and vals.get('subtype_id', [False])[0] == message_subtype_note_id
|
||||
for attachment in vals.get('attachment_ids', []):
|
||||
if not attachment.get('access_token'):
|
||||
attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0]
|
||||
|
||||
@@ -114,7 +114,10 @@
|
||||
|
||||
<!-- Chatter: internal toggle widget -->
|
||||
<t t-name="portal.chatter_internal_toggle">
|
||||
<div t-attf-class="float-right o_portal_chatter_js_is_internal #{message.is_internal and 'o_portal_message_internal_on' or 'o_portal_message_internal_off'}"
|
||||
<div t-if="message.is_message_subtype_note" class="float-right">
|
||||
<button class="btn btn-secondary" title="Internal notes are only displayed to internal users." disabled="true">Internal Note</button>
|
||||
</div>
|
||||
<div t-else="" t-attf-class="float-right o_portal_chatter_js_is_internal #{message.is_internal and 'o_portal_message_internal_on' or 'o_portal_message_internal_off'}"
|
||||
t-att-data-message-id="message.id"
|
||||
t-att-data-is-internal="message.is_internal">
|
||||
<button class="btn btn-danger"
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
|
||||
<div class="clearfix" name="so_total_summary">
|
||||
<div id="total" class="row" name="total">
|
||||
<div t-attf-class="#{'col-4' if report_type != 'html' else 'col-sm-7 col-md-5'} ml-auto">
|
||||
<div t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6'} ml-auto">
|
||||
<table class="table table-sm">
|
||||
<tr class="border-black o_subtotal" style="">
|
||||
<td name="td_amount_untaxed_label"><strong>Subtotal</strong></td>
|
||||
|
||||
@@ -516,7 +516,7 @@
|
||||
</table>
|
||||
|
||||
<div id="total" class="row" name="total" style="page-break-inside: avoid;">
|
||||
<div t-attf-class="#{'col-4' if report_type != 'html' else 'col-sm-7 col-md-5'} ml-auto">
|
||||
<div t-attf-class="#{'col-6' if report_type != 'html' else 'col-sm-7 col-md-6'} ml-auto">
|
||||
<t t-call="sale.sale_order_portal_content_totals_table"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -343,3 +343,39 @@ class TestProgramWithCodeOperations(TestSaleCouponCommon):
|
||||
}).process_coupon()
|
||||
self.assertEqual(len(order.order_line), 2, "You should get a discount line")
|
||||
|
||||
def test_apply_program_no_reward_link(self):
|
||||
# Tests that applying a promo code that does not generate reward lines
|
||||
# does not link on the order
|
||||
self.env['coupon.program'].create({
|
||||
'name': 'Code for 10% on orders',
|
||||
'promo_code_usage': 'code_needed',
|
||||
'promo_code': 'test_10pc',
|
||||
'discount_type': 'percentage',
|
||||
'discount_percentage': 10.0,
|
||||
'program_type': 'promotion_program',
|
||||
})
|
||||
self.empty_order.write({'order_line': [
|
||||
(0, False, {
|
||||
'product_id': self.product_C.id,
|
||||
'name': '1 Product C',
|
||||
'product_uom': self.uom_unit.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'price_unit': 0,
|
||||
})
|
||||
]})
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=self.empty_order.id).create({
|
||||
'coupon_code': 'test_10pc',
|
||||
}).process_coupon()
|
||||
self.assertFalse(self.empty_order.code_promo_program_id, 'The program should not be linked to the order')
|
||||
|
||||
# Same for a coupon's code
|
||||
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
|
||||
'generation_type': 'nbr_coupon',
|
||||
'nbr_coupons': 1,
|
||||
}).generate_coupon()
|
||||
coupon = self.code_promotion_program_with_discount.coupon_ids
|
||||
self.env['sale.coupon.apply.code'].with_context(active_id=self.empty_order.id).create({
|
||||
'coupon_code': coupon.code,
|
||||
}).process_coupon()
|
||||
self.assertFalse(self.empty_order.applied_coupon_ids, 'No coupon should be linked to the order')
|
||||
self.assertEqual(coupon.state, 'new', 'Coupon should be in a new state')
|
||||
|
||||
@@ -39,16 +39,22 @@ class SaleCouponApplyCode(models.TransientModel):
|
||||
}
|
||||
}
|
||||
else: # The program is applied on this order
|
||||
# Only link the promo program if reward lines were created
|
||||
order_line_count = len(order.order_line)
|
||||
order._create_reward_line(program)
|
||||
order.code_promo_program_id = program
|
||||
if order_line_count < len(order.order_line):
|
||||
order.code_promo_program_id = program
|
||||
else:
|
||||
coupon = self.env['coupon.coupon'].search([('code', '=', coupon_code)], limit=1)
|
||||
if coupon:
|
||||
error_status = coupon._check_coupon_code(order)
|
||||
if not error_status:
|
||||
# Consume coupon only if reward lines were created
|
||||
order_line_count = len(order.order_line)
|
||||
order._create_reward_line(coupon.program_id)
|
||||
order.applied_coupon_ids += coupon
|
||||
coupon.write({'state': 'used'})
|
||||
if order_line_count < len(order.order_line):
|
||||
order.applied_coupon_ids += coupon
|
||||
coupon.write({'state': 'used'})
|
||||
else:
|
||||
error_status = {'not_found': _('This coupon is invalid (%s).') % (coupon_code)}
|
||||
return error_status
|
||||
|
||||
@@ -408,8 +408,9 @@ class StockWarehouseOrderpoint(models.Model):
|
||||
|
||||
orderpoints = self.env['stock.warehouse.orderpoint'].with_user(SUPERUSER_ID).create(orderpoint_values_list)
|
||||
for orderpoint in orderpoints:
|
||||
orderpoint.route_id = orderpoint.product_id.route_ids[:1]
|
||||
orderpoints.filtered(lambda o: not o.route_id)._set_default_route_id()
|
||||
orderpoint_wh = orderpoint.location_id.get_warehouse()
|
||||
orderpoint.route_id = next((r for r in orderpoint.product_id.route_ids if not r.supplied_wh_id or r.supplied_wh_id == orderpoint_wh), None) \
|
||||
or orderpoint._set_default_route_id()
|
||||
return action
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -321,6 +321,60 @@ class TestProcRule(TransactionCase):
|
||||
self.assertAlmostEqual(picking_pick.move_lines.product_uom_qty, 5.0)
|
||||
self.assertAlmostEqual(picking_ship.move_lines.product_uom_qty, 3.0)
|
||||
|
||||
def test_orderpoint_replenishment_view(self):
|
||||
""" Create two warehouses + two moves
|
||||
verify that the replenishment view is consistent"""
|
||||
warehouse_1 = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
|
||||
warehouse_2, warehouse_3 = self.env['stock.warehouse'].create([{
|
||||
'name': 'Warehouse Two',
|
||||
'code': 'WH2',
|
||||
'resupply_wh_ids': [warehouse_1.id],
|
||||
}, {
|
||||
'name': 'Warehouse Three',
|
||||
'code': 'WH3',
|
||||
'resupply_wh_ids': [warehouse_1.id],
|
||||
}])
|
||||
route_2 = self.env['stock.location.route'].search([
|
||||
('supplied_wh_id', '=', warehouse_2.id),
|
||||
('supplier_wh_id', '=', warehouse_1.id),
|
||||
])
|
||||
route_3 = self.env['stock.location.route'].search([
|
||||
('supplied_wh_id', '=', warehouse_3.id),
|
||||
('supplier_wh_id', '=', warehouse_1.id),
|
||||
])
|
||||
product = self.env['product.product'].create({
|
||||
'name': 'Super Product',
|
||||
'type': 'product',
|
||||
'route_ids': [route_2.id, route_3.id]
|
||||
})
|
||||
moves = self.env['stock.move'].create([{
|
||||
'name': 'Move WH2',
|
||||
'location_id': warehouse_2.lot_stock_id.id,
|
||||
'location_dest_id': self.partner.property_stock_customer.id,
|
||||
'product_id': product.id,
|
||||
'product_uom': product.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
}, {
|
||||
'name': 'Move WH3',
|
||||
'location_id': warehouse_3.lot_stock_id.id,
|
||||
'location_dest_id': self.partner.property_stock_customer.id,
|
||||
'product_id': product.id,
|
||||
'product_uom': product.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
}])
|
||||
moves._action_confirm()
|
||||
# activate action of opening the replenishment view
|
||||
self.env['report.stock.quantity'].flush()
|
||||
self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
|
||||
replenishments = self.env['stock.warehouse.orderpoint'].search([
|
||||
('product_id', '=', product.id),
|
||||
])
|
||||
# Verify that the location and the route make sense
|
||||
self.assertRecordValues(replenishments, [
|
||||
{'location_id': warehouse_2.lot_stock_id.id, 'route_id': route_2.id},
|
||||
{'location_id': warehouse_3.lot_stock_id.id, 'route_id': route_3.id},
|
||||
])
|
||||
|
||||
|
||||
class TestProcRuleLoad(TransactionCase):
|
||||
def setUp(cls):
|
||||
|
||||
@@ -768,7 +768,7 @@ class Website(models.Model):
|
||||
endpoint.routing['auth'] in ('none', 'public') and
|
||||
endpoint.routing.get('website', False) and
|
||||
all(hasattr(converter, 'generate') for converter in converters)):
|
||||
return False
|
||||
return False
|
||||
|
||||
# dont't list routes without argument having no default value or converter
|
||||
sign = inspect.signature(endpoint.method.original_func)
|
||||
@@ -966,6 +966,17 @@ class Website(models.Model):
|
||||
self.ensure_one()
|
||||
return self._get_http_domain() or super(BaseModel, self).get_base_url()
|
||||
|
||||
@tools.ormcache('path', 'lang')
|
||||
def _get_canonical_url_localized_cached(self, path, args, lang):
|
||||
router = http.root.get_db_router(request.db).bind_to_environ(request.httprequest.environ)
|
||||
for key, val in list(args.items()):
|
||||
if isinstance(val, models.BaseModel):
|
||||
if val.env.context.get('lang') != lang:
|
||||
args[key] = val.with_context(lang=lang)
|
||||
endpoint = router.match(path_info=path, return_rule=True)[0].endpoint
|
||||
return router.build(endpoint, args)
|
||||
|
||||
|
||||
def _get_canonical_url_localized(self, lang, canonical_params):
|
||||
"""Returns the canonical URL for the current request with translatable
|
||||
elements appropriately translated in `lang`.
|
||||
@@ -976,13 +987,11 @@ class Website(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
if request.endpoint:
|
||||
router = http.root.get_db_router(request.db).bind('')
|
||||
arguments = dict(request.endpoint_arguments)
|
||||
for key, val in list(arguments.items()):
|
||||
if isinstance(val, models.BaseModel):
|
||||
if val.env.context.get('lang') != lang.code:
|
||||
arguments[key] = val.with_context(lang=lang.code)
|
||||
path = router.build(request.endpoint, arguments)
|
||||
path = self._get_canonical_url_localized_cached(
|
||||
request.httprequest.path,
|
||||
dict(request.endpoint_arguments),
|
||||
lang.code
|
||||
)
|
||||
else:
|
||||
# The build method returns a quoted URL so convert in this case for consistency.
|
||||
path = urls.url_quote_plus(request.httprequest.path, safe='/')
|
||||
|
||||
@@ -712,11 +712,13 @@ registry.mediaVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, {
|
||||
// Unsupported domain, don't inject iframe
|
||||
return;
|
||||
}
|
||||
return this.$target.append($('<iframe/>', {
|
||||
const iframeEl = $('<iframe/>', {
|
||||
src: src,
|
||||
frameborder: '0',
|
||||
allowfullscreen: 'allowfullscreen',
|
||||
}))[0];
|
||||
})[0];
|
||||
this.$target.append(iframeEl);
|
||||
return iframeEl;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -103,13 +103,13 @@
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-if="request and request.is_frontend_multilang and website">
|
||||
<t t-if="request and request.is_frontend_multilang and website and website.is_public_user()">
|
||||
<t t-set="alternate_languages" t-value="website._get_alternate_languages(canonical_params=canonical_params)"/>
|
||||
<t t-foreach="alternate_languages" t-as="lg">
|
||||
<link rel="alternate" t-att-hreflang="lg['hreflang']" t-att-href="lg['href']"/>
|
||||
</t>
|
||||
</t>
|
||||
<link t-if="request and website" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
|
||||
<link t-if="request and website and website.is_public_user()" rel="canonical" t-att-href="website._get_canonical_url(canonical_params=canonical_params)"/>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin=""/>
|
||||
</xpath>
|
||||
|
||||
@@ -720,6 +720,10 @@ class WebsiteSale(http.Controller):
|
||||
values = kw
|
||||
else:
|
||||
partner_id = self._checkout_form_save(mode, post, kw)
|
||||
# We need to validate _checkout_form_save return, because when partner_id not in shippings
|
||||
# it returns Forbidden() instead the partner_id
|
||||
if isinstance(partner_id, Forbidden):
|
||||
return partner_id
|
||||
if mode[1] == 'billing':
|
||||
order.partner_id = partner_id
|
||||
order.with_context(not_self_saleperson=True).onchange_partner_id()
|
||||
|
||||
Reference in New Issue
Block a user