[MERGE] forward port branch 11.0 up to 152519cd65

This commit is contained in:
Christophe Simonis
2018-06-27 11:37:52 +02:00
46 changed files with 607 additions and 57 deletions
-16
View File
@@ -35,22 +35,6 @@ class ProductTemplate(models.Model):
domain=[('deprecated', '=', False)],
help="The expense is accounted for when a vendor bill is validated, except in anglo-saxon accounting with perpetual inventory valuation in which case the expense (Cost of Goods Sold account) is recognized at the customer invoice validation. If the field is empty, it uses the one defined in the product category.")
@api.multi
def write(self, vals):
#TODO: really? i don't see the reason we'd need that constraint..
check = self.ids and 'uom_po_id' in vals
if check:
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
uoms = dict(self._cr.fetchall())
res = super(ProductTemplate, self).write(vals)
if check:
self._cr.execute("SELECT id, uom_po_id FROM product_template WHERE id IN %s", [tuple(self.ids)])
if dict(self._cr.fetchall()) != uoms:
products = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids)])
if self.env['account.move.line'].search_count([('product_id', 'in', products.ids)]):
raise UserError(_('You can not change the unit of measure of a product that has been already used in an account journal item. If you need to change the unit of measure, you may deactivate this product.'))
return res
@api.multi
def _get_product_accounts(self):
return {
+1 -1
View File
@@ -155,5 +155,5 @@ class ResUsers(models.Model):
sup = super(ResUsers, self)
if not default or not default.get('email'):
# avoid sending email to the user we are duplicating
sup = super(ResUsers, self.with_context(reset_password=False))
sup = super(ResUsers, self.with_context(no_reset_password=True))
return sup.copy(default=default)
+8
View File
@@ -5,3 +5,11 @@ from . import controllers
from . import models
from . import report
from . import wizard
from odoo import api, SUPERUSER_ID
def uninstall_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
teams = env['crm.team'].search([('dashboard_graph_model', '=', 'crm.opportunity.report')])
teams.update({'dashboard_graph_model': None})
+1
View File
@@ -53,4 +53,5 @@
'installable': True,
'application': True,
'auto_install': False,
'uninstall_hook': 'uninstall_hook',
}
+2 -1
View File
@@ -442,7 +442,8 @@ class Lead(models.Model):
'res_id': self.id,
'views': [(form_view.id, 'form'),],
'type': 'ir.actions.act_window',
'target': 'inline'
'target': 'inline',
'context': {'default_type': 'opportunity'}
}
# ----------------------------------------
@@ -56,8 +56,7 @@
<group string="Assign these opportunities to">
<field name="team_id"/>
<field name="user_ids" widget="many2many_tags" domain="[('share', '=', False)]"/>
<!-- Uncomment me in trunk -->
<!-- <field name="force_assignation" /> -->
<field name="force_assignation"/>
</group>
<label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}">
+4
View File
@@ -124,3 +124,7 @@ class SaleOrderLine(models.Model):
if not line.product_id or not line.product_uom or not line.product_uom_qty:
return 0.0
line.product_qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id)
def _is_delivery(self):
self.ensure_one()
return self.is_delivery
+7
View File
@@ -41,5 +41,12 @@
]
</field>
</record>
<record model="ir.rule" id="event_registration_portal">
<field name="name">Event/Registration: Portal</field>
<field name="model_id" ref="model_event_registration"/>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
<field name="domain_force">['|', ('email', '=', user.partner_id.email), ('partner_id', '=', user.partner_id.id)]
</field>
</record>
</data>
</odoo>
+2 -1
View File
@@ -5,7 +5,8 @@ access_event_event_portal,event.event.portal,model_event_event,,1,0,0,0
access_event_event_user,event.event.user,model_event_event,event.group_event_user,1,0,0,0
access_event_event_manager,event.event.manager,model_event_event,event.group_event_manager,1,1,1,1
access_event_registration,event.registration,model_event_registration,event.group_event_user,1,1,1,1
access_event_registration_portal,event.registration,model_event_registration,,0,0,0,0
access_event_registration_employee,event.registration,model_event_registration,base.group_user,1,0,0,0
access_event_registration_portal,event.registration,model_event_registration,base.group_portal,1,0,0,0
access_event_mail,event.mail,model_event_mail,event.group_event_user,1,0,0,0
access_event_mail_manager,event.mail manager,model_event_mail,event.group_event_manager,1,1,1,1
access_event_mail_registration,event.mail.registration,model_event_mail_registration,event.group_event_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
5 access_event_event_user event.event.user model_event_event event.group_event_user 1 0 0 0
6 access_event_event_manager event.event.manager model_event_event event.group_event_manager 1 1 1 1
7 access_event_registration event.registration model_event_registration event.group_event_user 1 1 1 1
8 access_event_registration_portal access_event_registration_employee event.registration model_event_registration base.group_user 0 1 0 0 0
9 access_event_registration_portal event.registration model_event_registration base.group_portal 1 0 0 0
10 access_event_mail event.mail model_event_mail event.group_event_user 1 0 0 0
11 access_event_mail_manager event.mail manager model_event_mail event.group_event_manager 1 1 1 1
12 access_event_mail_registration event.mail.registration model_event_mail_registration event.group_event_user 1 0 0 0
+1
View File
@@ -26,6 +26,7 @@ this event.
'data/event_sale_data.xml',
'report/event_event_templates.xml',
'security/ir.model.access.csv',
'security/event_security.xml',
'wizard/event_edit_registration.xml',
],
'demo': ['data/event_demo.xml'],
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="sales_team.group_sale_salesman" model="res.groups">
<field name="implied_ids" eval="[(4, ref('event.group_event_user'))]"/>
</record>
</data>
</odoo>
+1 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.exceptions import UserError, ValidationError
from odoo.tools.safe_eval import safe_eval
from odoo.addons import decimal_precision as dp
+3 -1
View File
@@ -5,6 +5,7 @@ import logging
import re
from email.utils import formataddr
from openerp.http import request
from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
from odoo.exceptions import UserError, AccessError
@@ -288,11 +289,12 @@ class Message(models.Model):
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
attachments_data = attachments.sudo().read(['id', 'datas_fname', 'name', 'mimetype'])
safari = request and request.httprequest.user_agent.browser == 'safari'
attachments_tree = dict((attachment['id'], {
'id': attachment['id'],
'filename': attachment['datas_fname'],
'name': attachment['name'],
'mimetype': attachment['mimetype'],
'mimetype': 'application/octet-stream' if safari and 'video' in attachment['mimetype'] else attachment['mimetype'],
}) for attachment in attachments_data)
# 3. Tracking values
@@ -10,7 +10,9 @@
name="action_mass_mailing_attendees"
icon="fa-envelope-o"
attrs="{'invisible': [('seats_expected', '=', 0)]}">
Mail Attendees
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Mail Attendees</span>
</div>
</button>
</button>
</field>
@@ -36,7 +36,7 @@
<span t-att-res-id="bom_line['product_id'].id" res-model="product.product" view-type="form" t-esc="bom_line['product_id'].name"/>
</td>
<td class="text-right">
<span t-esc="bom_line['product_uom_qty']"/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/>
<span t-esc="bom_line['product_uom_qty']" t-esc-options='{"widget": "float", "decimal_precision": "Product Unit of Measure"}'/> <span t-esc="bom_line['product_uom'].name" groups="product.group_uom"/>
</td>
<td class="text-right">
<span t-esc="bom_line['price_unit']" t-options='{"widget": "monetary", "display_currency": currency}'/>
+3 -2
View File
@@ -332,10 +332,11 @@ class PosConfig(models.Model):
def name_get(self):
result = []
for config in self:
if (not config.session_ids) or (config.session_ids[0].state == 'closed'):
last_session = self.env['pos.session'].search([('config_id', '=', config.id)], limit=1)
if (not last_session) or (last_session.state == 'closed'):
result.append((config.id, config.name + ' (' + _('not used') + ')'))
continue
result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')'))
result.append((config.id, config.name + ' (' + last_session.user_id.name + ')'))
return result
@api.model
@@ -215,7 +215,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
var self = this;
function status(){
self.connection.rpc('/hw_proxy/status_json',{},{timeout:2500})
self.connection.rpc('/hw_proxy/status_json',{},{shadow: true, timeout:2500})
.then(function(driver_status){
self.set_connection_status('connected',driver_status);
},function(){
@@ -239,7 +239,7 @@ var ProxyDevice = core.Class.extend(mixins.PropertiesMixin,{
callbacks[i](params);
}
if(this.get('status').status !== 'disconnected'){
return this.connection.rpc('/hw_proxy/' + name, params || {});
return this.connection.rpc('/hw_proxy/' + name, params || {}, {shadow: true});
}else{
return (new $.Deferred()).reject();
}
@@ -588,7 +588,7 @@ var BarcodeReader = core.Class.extend({
this.remote_active = 1;
function waitforbarcode(){
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{timeout:7500})
return self.proxy.connection.rpc('/hw_proxy/scanner',{},{shadow: true, timeout:7500})
.then(function(barcode){
if(!self.remote_scanning){
self.remote_active = 0;
@@ -141,7 +141,7 @@
</div>
</div>
<div t-if="widget.editable" class="o_kanban_card_manage_settings row">
<div t-if="widget.editable" class="o_kanban_card_manage_settings row" groups="point_of_sale.group_pos_manager">
<div class="col-xs-12 text-right">
<a type="edit">Settings</a>
</div>
@@ -27,11 +27,14 @@ var Printer = core.Class.extend(mixins.PropertiesMixin,{
function send_printing_job(){
if(self.receipt_queue.length > 0){
var r = self.receipt_queue.shift();
self.connection.rpc('/hw_proxy/print_xml_receipt',{receipt: r},{timeout: 5000})
var options = {shadow: true, timeout: 5000};
self.connection.rpc('/hw_proxy/print_xml_receipt', {receipt: r}, options)
.then(function(){
send_printing_job();
},function(){
},function(error, event){
self.receipt_queue.unshift(r);
console.log('There was an error while trying to print the order:');
console.log(error);
});
}
}
@@ -55,7 +58,7 @@ models.load_models({
for(var i = 0; i < printers.length; i++){
if(active_printers[printers[i].id]){
var url = printers[i].proxy_ip;
var url = printers[i].proxy_ip || '';
if(url.indexOf('//') < 0){
url = 'http://'+url;
}
+3 -3
View File
@@ -964,12 +964,12 @@ class Task(models.Model):
@api.multi
def message_get_suggested_recipients(self):
recipients = super(Task, self).message_get_suggested_recipients()
for task in self.filtered('partner_id'):
reason = _('Customer Email') if task.partner_id.email else _('Customer')
for task in self:
if task.partner_id:
reason = _('Customer Email') if task.partner_id.email else _('Customer')
task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason)
elif task.email_from:
task._message_add_suggested_recipient(recipients, partner=task.email_from, reason=reason)
task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
return recipients
@api.multi
+4 -1
View File
@@ -402,6 +402,8 @@ class PurchaseOrder(models.Model):
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
pick.action_cancel()
order.order_line.write({'move_dest_ids':[(5,0,0)]})
self.write({'state': 'cancel'})
@api.multi
@@ -947,7 +949,8 @@ class ProcurementRule(models.Model):
cache[domain] = po
if not po:
vals = self._prepare_purchase_order(product_id, product_qty, product_uom, origin, values, partner)
po = self.env['purchase.order'].sudo().create(vals)
company_id = values.get('company_id') and values['company_id'].id or self.env.user.company_id.id
po = self.env['purchase.order'].with_context(force_company=company_id).sudo().create(vals)
cache[domain] = po
elif not po.origin or origin not in po.origin.split(', '):
if po.origin:
+5
View File
@@ -1239,3 +1239,8 @@ class SaleOrderLine(models.Model):
discount = (new_list_price - price) / new_list_price * 100
if discount > 0:
self.discount = discount
def _is_delivery(self):
self.ensure_one()
return False
@@ -347,7 +347,7 @@
</div>
<h2>Invoicing</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="col-xs-12 col-md-6 o_setting_box hidden" title="This default value is applied to any new product created. This can be changed in the product detail form.">
<div class="o_setting_right_pane">
<label for="default_invoice_policy"/>
<div class="text-muted">
@@ -48,6 +48,8 @@ tour.register('sale_tour', {
in_modal: false,
run: function (actions) {
actions.auto();
// There might be a modal because of the view:
// sale.order.form.editable.list, enabled by some groups
if ($(".modal-footer .btn-primary").length) {
actions.auto(".modal-footer .btn-primary");
}
+1 -1
View File
@@ -33,7 +33,7 @@ class SaleOrder(models.Model):
for order in self:
dates_list = []
order_datetime = fields.Datetime.from_string(order.date_order)
for line in order.order_line.filtered(lambda x: x.state != 'cancel'):
for line in order.order_line.filtered(lambda x: x.state != 'cancel' and not x._is_delivery()):
dt = order_datetime + timedelta(days=line.customer_lead or 0.0)
dates_list.append(dt)
if dates_list:
+20
View File
@@ -585,3 +585,23 @@ class ProductCategory(models.Model):
category = category.parent_id
routes |= category.route_ids
self.total_route_ids = routes
class ProductUoM(models.Model):
_inherit = 'product.uom'
def write(self, values):
# Users can not update the factor if open stock moves are based on it
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
stock_move_lines = self.env['stock.move.line'].search_count([
('product_uom_id.category_id', '=', self.category_id.id),
('state', '!=', 'cancel'),
])
if stock_move_lines:
raise UserError(_(
"You cannot change the ratio of this unit of mesure as some"
" products with this UoM have already been moved or are "
"currently reserved."
))
return super(ProductUoM, self).write(values)
+15
View File
@@ -83,6 +83,21 @@ class Location(models.Model):
if 'usage' in values and values['usage'] == 'view':
if self.mapped('quant_ids'):
raise UserError(_("This location's usage cannot be changed to view as it contains products."))
if 'usage' in values or 'scrap_location' in values:
modified_locations = self.filtered(
lambda l: l.usage != values.get('usage') or
l.scrap_location != values.get('scrap_location')
)
reserved_quantities = self.env['stock.move.line'].search_count([
('location_id', 'in', modified_locations.ids),
('product_qty', '>', 0),
])
if reserved_quantities:
raise UserError(_(
"You cannot change the location type or its use as a scrap"
" location as there are products reserved in this location."
" Please unreserve the products first."
))
return super(Location, self).write(values)
def name_get(self):
+1 -1
View File
@@ -787,7 +787,7 @@ class StockMove(models.Model):
group_id = False
return {
'company_id': self.company_id,
'date_planned': self.date,
'date_planned': self.date_expected,
'move_dest_ids': self,
'group_id': group_id,
'route_ids': self.route_ids,
+6
View File
@@ -439,6 +439,12 @@ class QuantPackage(models.Model):
if move_lines_to_remove:
move_lines_to_remove.write({'result_package_id': False})
else:
move_line_to_modify = self.env['stock.move.line'].search([
('package_id', '=', package.id),
('state', 'in', ('assigned', 'partially_available')),
('product_qty', '!=', 0),
])
move_line_to_modify.write({'package_id': False})
package.mapped('quant_ids').write({'package_id': False})
def action_view_picking(self):
+1 -1
View File
@@ -397,7 +397,7 @@ class Warehouse(models.Model):
pull_rules_list = supplier_wh._get_supply_pull_rules_values(
[self.Routing(output_location, transit_location, supplier_wh.out_type_id)],
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id})
values={'route_id': inter_wh_route.id})
pull_rules_list += self._get_supply_pull_rules_values(
[self.Routing(transit_location, input_location, self.in_type_id)],
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id})
+1
View File
@@ -9,6 +9,7 @@ from . import test_quant
from . import test_inventory
from . import test_move
from . import test_move2
from . import test_robustness
from . import test_packing
from . import test_packing_neg
from . import test_proc_rule
+147
View File
@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
class TestRobustness(TransactionCase):
def setUp(self):
super(TestRobustness, self).setUp()
self.stock_location = self.env.ref('stock.stock_location_stock')
self.customer_location = self.env.ref('stock.stock_location_customers')
self.uom_unit = self.env.ref('product.product_uom_unit')
self.uom_dozen = self.env.ref('product.product_uom_dozen')
self.product1 = self.env['product.product'].create({
'name': 'Product A',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
def test_uom_factor(self):
""" Changing the factor of a unit of measure shouldn't be allowed while
quantities are reserved, else the existing move lines won't be consistent
with the `reserved_quantity` on quants.
"""
# make some stock
self.env['stock.quant']._update_available_quantity(
self.product1,
self.stock_location,
12,
)
# reserve a dozen
move1 = self.env['stock.move'].create({
'name': 'test_uom_rounding',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_dozen.id,
'product_uom_qty': 1,
})
move1._action_confirm()
move1._action_assign()
self.assertEqual(move1.state, 'assigned')
quant = self.env['stock.quant']._gather(
self.product1,
self.stock_location,
)
# assert the reservation
self.assertEqual(quant.reserved_quantity, 12)
self.assertEqual(move1.product_qty, 12)
# change the factor
with self.assertRaises(UserError):
with self.cr.savepoint():
move1.product_uom.factor = 0.05
# assert the reservation
self.assertEqual(quant.reserved_quantity, 12)
self.assertEqual(move1.state, 'assigned')
self.assertEqual(move1.product_qty, 12)
# unreserve
move1._do_unreserve()
def test_location_usage(self):
""" Changing the usage of a location shouldn't be allowed while
quantities are reserved, else the existing move lines won't be
consistent with the `reserved_quantity` on the quants.
"""
# change stock usage
self.stock_location.scrap_location = True
# make some stock
self.env['stock.quant']._update_available_quantity(
self.product1,
self.stock_location,
1,
)
# reserve a unit
move1 = self.env['stock.move'].create({
'name': 'test_location_archive',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 1,
})
move1._action_confirm()
move1._action_assign()
self.assertEqual(move1.state, 'assigned')
quant = self.env['stock.quant']._gather(
self.product1,
self.stock_location,
)
# assert the reservation
self.assertEqual(quant.reserved_quantity, 0) # reservation is bypassed in scrap location
self.assertEqual(move1.product_qty, 1)
# change the stock usage
with self.assertRaises(UserError):
with self.cr.savepoint():
self.stock_location.scrap_location = False
# unreserve
move1._do_unreserve()
def test_package_unpack(self):
""" Unpack a package that contains quants with a reservation
should also remove the package on the reserved move lines.
"""
package = self.env['stock.quant.package'].create({
'name': 'Shell Helix HX7 10W30',
})
self.env['stock.quant']._update_available_quantity(
self.product1,
self.stock_location,
10,
package_id=package
)
# reserve a dozen
move1 = self.env['stock.move'].create({
'name': 'test_uom_rounding',
'location_id': self.stock_location.id,
'location_dest_id': self.customer_location.id,
'product_id': self.product1.id,
'product_uom': self.uom_unit.id,
'product_uom_qty': 10,
})
move1._action_confirm()
move1._action_assign()
move1.result_package_id = False
package.unpack()
# unreserve
move1._do_unreserve()
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location)), 1)
self.assertEqual(len(self.env['stock.quant']._gather(self.product1, self.stock_location, package_id=package)), 0)
self.assertEqual(self.env['stock.quant']._gather(self.product1, self.stock_location).reserved_quantity, 0)
+209
View File
@@ -228,6 +228,215 @@ class TestWarehouse(TestStockCommon):
quant = self.env['stock.quant'].search([('product_id', '=', productA.id), ('location_id', '=', location_loss.id)])
self.assertEqual(len(quant), 1)
def test_resupply_route(self):
""" Simulate a resupply chain between warehouses.
Stock -> transit -> Dist. -> transit -> Shop -> Customer
Create the move from Shop to Customer and ensure that all the pull
rules are triggered in order to complete the move chain to Stock.
"""
warehouse_stock = self.env['stock.warehouse'].create({
'name': 'Stock.',
'code': 'STK',
})
warehouse_distribution = self.env['stock.warehouse'].create({
'name': 'Dist.',
'code': 'DIST',
'default_resupply_wh_id': warehouse_stock.id,
'resupply_wh_ids': [(6, 0, [warehouse_stock.id])]
})
warehouse_shop = self.env['stock.warehouse'].create({
'name': 'Shop',
'code': 'SHOP',
'default_resupply_wh_id': warehouse_distribution.id,
'resupply_wh_ids': [(6, 0, [warehouse_distribution.id])]
})
route_stock_to_dist = warehouse_distribution.resupply_route_ids
route_dist_to_shop = warehouse_shop.resupply_route_ids
# Change the procure_method on the pull rules between dist and shop
# warehouses. Since mto and resupply routes are both on product it will
# select one randomly between them and if it select the resupply it is
# 'make to stock' and it will not create the picking between stock and
# dist warehouses.
route_dist_to_shop.pull_ids.write({'procure_method': 'make_to_order'})
product = self.env['product.product'].create({
'name': 'Fakir',
'type': 'product',
'route_ids': [(4, route_id) for route_id in [route_stock_to_dist.id, route_dist_to_shop.id, self.env.ref('stock.route_warehouse0_mto').id]],
})
picking_out = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out.id,
'location_id': warehouse_shop.lot_stock_id.id,
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
'warehouse_id': warehouse_shop.id,
'procure_method': 'make_to_order',
})
picking_out.action_confirm()
moves = self.env['stock.move'].search([('product_id', '=', product.id)])
# Shop/Stock -> Customer
# Transit -> Shop/Stock
# Dist/Stock -> Transit
# Transit -> Dist/Stock
# Stock/Stock -> Transit
self.assertEqual(len(moves), 5, 'Invalid moves number.')
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_stock.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_dest_id', '=', warehouse_distribution.lot_stock_id.id)]))
self.assertTrue(self.env['stock.move'].search([('location_id', '=', warehouse_distribution.lot_stock_id.id)]))
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)]))
def test_mutiple_resupply_warehouse(self):
""" Simulate the following situation:
- 2 shops with stock are resupply by 2 distinct warehouses
- Shop Namur is resupply by the warehouse stock Namur
- Shop Wavre is resupply by the warehouse stock Wavre
- Simulate 2 moves for the same product but in different shop.
This test ensure that the move are supplied by the correct distribution
warehouse.
"""
customer_location = self.env.ref('stock.stock_location_customers')
warehouse_distribution_wavre = self.env['stock.warehouse'].create({
'name': 'Stock Wavre.',
'code': 'WV',
})
warehouse_shop_wavre = self.env['stock.warehouse'].create({
'name': 'Shop Wavre',
'code': 'SHWV',
'default_resupply_wh_id': warehouse_distribution_wavre.id,
'resupply_wh_ids': [(6, 0, [warehouse_distribution_wavre.id])]
})
warehouse_distribution_namur = self.env['stock.warehouse'].create({
'name': 'Stock Namur.',
'code': 'NM',
})
warehouse_shop_namur = self.env['stock.warehouse'].create({
'name': 'Shop Namur',
'code': 'SHNM',
'default_resupply_wh_id': warehouse_distribution_namur.id,
'resupply_wh_ids': [(6, 0, [warehouse_distribution_namur.id])]
})
route_shop_namur = warehouse_shop_namur.resupply_route_ids
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
# The product contains the 2 resupply routes.
product = self.env['product.product'].create({
'name': 'Fakir',
'type': 'product',
'route_ids': [(4, route_id) for route_id in [route_shop_namur.id, route_shop_wavre.id, self.env.ref('stock.route_warehouse0_mto').id]],
})
# Add 1 quant in each distribution warehouse.
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_wavre.lot_stock_id, 1.0)
self.env['stock.quant']._update_available_quantity(product, warehouse_distribution_namur.lot_stock_id, 1.0)
# Create the move for the shop Namur. Should create a resupply from
# distribution warehouse Namur.
picking_out_namur = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop_namur.lot_stock_id.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out_namur.id,
'location_id': warehouse_shop_namur.lot_stock_id.id,
'location_dest_id': customer_location.id,
'warehouse_id': warehouse_shop_namur.id,
'procure_method': 'make_to_order',
})
picking_out_namur.action_confirm()
# Validate the picking
# Dist. warehouse Namur -> transit Location -> Shop Namur
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_namur.lot_stock_id.id)])
self.assertTrue(picking_stock_transit)
picking_stock_transit.action_assign()
picking_stock_transit.move_lines[0].quantity_done = 1.0
picking_stock_transit.action_done()
picking_transit_shop_namur = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_namur.lot_stock_id.id)])
self.assertTrue(picking_transit_shop_namur)
picking_transit_shop_namur.action_assign()
picking_transit_shop_namur.move_lines[0].quantity_done = 1.0
picking_transit_shop_namur.action_done()
picking_out_namur.action_assign()
picking_out_namur.move_lines[0].quantity_done = 1.0
picking_out_namur.action_done()
# Check that the correct quantity has been provided to customer
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 1)
# Ensure there still no quants in distribution warehouse
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_namur.lot_stock_id)), 0)
# Create the move for the shop Wavre. Should create a resupply from
# distribution warehouse Wavre.
picking_out_wavre = self.env['stock.picking'].create({
'partner_id': self.env.ref('base.res_partner_2').id,
'picking_type_id': self.env.ref('stock.picking_type_out').id,
'location_id': warehouse_shop_wavre.lot_stock_id.id,
'location_dest_id': customer_location.id,
})
self.env['stock.move'].create({
'name': product.name,
'product_id': product.id,
'product_uom_qty': 1,
'product_uom': product.uom_id.id,
'picking_id': picking_out_wavre.id,
'location_id': warehouse_shop_wavre.lot_stock_id.id,
'location_dest_id': customer_location.id,
'warehouse_id': warehouse_shop_wavre.id,
'procure_method': 'make_to_order',
})
picking_out_wavre.action_confirm()
# Validate the picking
# Dist. warehouse Wavre -> transit Location -> Shop Wavre
picking_stock_transit = self.env['stock.picking'].search([('location_id', '=', warehouse_distribution_wavre.lot_stock_id.id)])
self.assertTrue(picking_stock_transit)
picking_stock_transit.action_assign()
picking_stock_transit.move_lines[0].quantity_done = 1.0
picking_stock_transit.action_done()
picking_transit_shop_wavre = self.env['stock.picking'].search([('location_dest_id', '=', warehouse_shop_wavre.lot_stock_id.id)])
self.assertTrue(picking_transit_shop_wavre)
picking_transit_shop_wavre.action_assign()
picking_transit_shop_wavre.move_lines[0].quantity_done = 1.0
picking_transit_shop_wavre.action_done()
picking_out_wavre.action_assign()
picking_out_wavre.move_lines[0].quantity_done = 1.0
picking_out_wavre.action_done()
# Check that the correct quantity has been provided to customer
self.assertEqual(self.env['stock.quant']._gather(product, customer_location).quantity, 2)
# Ensure there still no quants in distribution warehouse
self.assertEqual(len(self.env['stock.quant']._gather(product, warehouse_distribution_wavre.lot_stock_id)), 0)
class TestResupply(TestStockCommon):
def setUp(self):
+2
View File
@@ -146,11 +146,13 @@ class ProductProduct(models.Model):
'account_id': debit_account_id,
'debit': abs(diff * qty_available),
'credit': 0,
'product_id': product.id,
}), (0, 0, {
'name': _('Standard Price changed - %s') % (product.display_name),
'account_id': credit_account_id,
'debit': 0,
'credit': abs(diff * qty_available),
'product_id': product.id,
})],
}
move = AccountMove.create(move_vals)
@@ -41,7 +41,8 @@ var CrashManager = core.Class.extend({
core.bus.trigger('connection_lost');
this.connection_lost = true;
var timeinterval = setInterval(function() {
ajax.jsonRpc('/web/webclient/version_info').then(function() {
var options = {shadow: true};
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, options).then(function () {
clearInterval(timeinterval);
core.bus.trigger('connection_restored');
self.connection_lost = false;
@@ -2579,6 +2579,11 @@ var BasicModel = AbstractModel.extend({
relationField: field.relation_field,
viewType: view ? view.type : fieldInfo.viewType,
});
// set existing changes to the list
if (record._changes && record._changes[fieldName]) {
list._changes = self.localData[record._changes[fieldName]]._changes;
record._changes[fieldName] = list.id;
}
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
var def = self._readUngroupedList(list).then(function () {
@@ -143,10 +143,12 @@ QUnit.module('relational_fields', {
user: {
fields: {
name: {string: "Name", type: "char"},
partner_ids: {string: "one2many partners field", type: "one2many", relation: 'partner', relation_field: 'user_id'},
},
records: [{
id: 17,
name: "Aline",
partner_ids: [1, 2],
}, {
id: 19,
name: "Christine",
@@ -9474,6 +9476,94 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('onchange affecting inline unopened list view', function (assert) {
// when we got onchange result for fields of record that were not
// already available because they were in a inline view not already
// opened, in a given configuration the change were applied ignoring
// existing data, thus a line of a one2many field inside a one2many
// field could be duplicated unexplectedly
assert.expect(5);
var numUserOnchange = 0;
this.data.user.onchanges = {
partner_ids: function (obj) {
if (numUserOnchange === 0) {
// simulate proper server onchange after save of modal with new record
obj.partner_ids = [
[5],
[1, 1, {
display_name: 'first record',
turtles: [
[5],
[1, 2, {'display_name': 'donatello'}],
],
}],
[1, 2, {
display_name: 'second record',
turtles: [
[5],
obj.partner_ids[1][2].turtles[0],
],
}],
];
}
numUserOnchange++;
},
};
var form = createView({
View: FormView,
model: 'user',
data: this.data,
arch: '<form><sheet><group>' +
'<field name="partner_ids">' +
'<form>'+
'<field name="turtles">' +
'<tree editable="bottom">' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</form>' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'</field>' +
'</group></sheet></form>',
res_id: 17,
});
// add a turtle on second partner
form.$buttons.find('.o_form_button_edit').click();
form.$('.o_data_row:eq(1)').click();
$('.modal .o_field_x2many_list_row_add a').click();
$('.modal input[name="display_name"]').val('michelangelo').change();
$('.modal .btn-primary').click();
// open first partner so changes from previous action are applied
form.$('.o_data_row:eq(0)').click();
$('.modal .btn-primary').click();
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(numUserOnchange, 2,
'there should 2 and only 2 onchange from closing the partner modal');
form.$('.o_data_row:eq(0)').click();
assert.strictEqual($('.modal .o_data_row').length, 1,
'only 1 turtle for first partner');
assert.strictEqual($('.modal .o_data_row').text(), 'donatello',
'first partner turtle is donatello');
$('.modal .o_form_button_cancel').click();
form.$('.o_data_row:eq(1)').click();
assert.strictEqual($('.modal .o_data_row').length, 1,
'only 1 turtle for second partner');
assert.strictEqual($('.modal .o_data_row').text(), 'michelangelo',
'second partner turtle is michelangelo');
$('.modal .o_form_button_cancel').click();
form.destroy();
});
QUnit.module('FieldMany2Many');
QUnit.test('many2many kanban: edition', function (assert) {
+1 -1
View File
@@ -253,7 +253,7 @@ class Web_Editor(http.Controller):
## @param bundles - True if the bundles views must be fetched (default to False)
## @param bundles_restriction - Names of the bundle in which to look for less files (if empty, search in all of them)
## @returns a dictionary with views info in the views key and style info in the less key
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user")
@http.route("/web_editor/get_assets_editor_resources", type="json", auth="user", website=True)
def get_assets_editor_resources(self, key, get_views=True, get_less=True, bundles=False, bundles_restriction=[]):
# Related views must be fetched if the user wants the views and/or the style
views = request.env["ir.ui.view"].get_related_views(key, bundles=bundles)
@@ -108,9 +108,14 @@ function getMatchedCSSRules(a) {
// The css generates all the attributes separately and not in simplified form.
// In order to have a better compatibility (outlook for example) we simplify the css tags.
// e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none
_.each([['margin'], ['padding'], ['border', 'style']], function (attr) {
var p = attr[0];
var e = attr[1] ? '-' + attr[1] : '';
_.each([
{property: 'margin'},
{property: 'padding'},
{property: 'border', propertyEnd: '-style', defaultValue: 'none'},
], function (propertyInfo) {
var p = propertyInfo.property;
var e = propertyInfo.propertyEnd || '';
var defVal = propertyInfo.defaultValue || 0;
if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) {
if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) {
@@ -119,7 +124,7 @@ function getMatchedCSSRules(a) {
}
else {
// keep => property: [top value] [right value] [bottom value] [left value];
style[p+e] = (style[p+'-top'+e] || 0) + ' ' + (style[p+'-right'+e] || 0) + ' ' + (style[p+'-bottom'+e] || 0) + ' ' + (style[p+'-left'+e] || 0);
style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal);
if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) {
// keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
delete style[p+e];
+1 -1
View File
@@ -175,7 +175,7 @@ class WebsiteEventController(http.Controller):
'event': event,
'main_object': event,
'range': range,
'registrable': event._is_event_registrable()
'registrable': event.sudo()._is_event_registrable()
}
return request.render("website_event.event_description_full", values)
@@ -480,7 +480,7 @@
<t t-if="event.state != 'draft'" itemprop="availability" content="http://schema.org/Discontinued">
Event registration is closed.
</t>
<t t-if="uid">
<t t-if="request.env.user.has_group('event.group_event_manager')">
<a t-attf-href="/web#id=#{event.id}&amp;view_type=form&amp;model=event.event">
<i class="fa fa-plus-circle"></i>
<em>Configure and Launch Event Registration</em>
@@ -8,6 +8,7 @@ sAnimation.registry.follow_alias = sAnimation.Class.extend({
start: function () {
var self = this;
this.is_user = false;
var unsubscribePage = window.location.search.slice(1).split('&').indexOf("unsubscribe") >= 0;
this._rpc({
route: '/groups/is_member',
params: {
@@ -19,6 +20,9 @@ sAnimation.registry.follow_alias = sAnimation.Class.extend({
self.is_user = data.is_user;
self.email = data.email;
self.$target.find('.js_mg_link').attr('href', '/groups/' + self.$target.data('id'));
if (unsubscribePage && self.is_user) {
self.$target.find(".js_mg_follow_form").remove();
}
self.toggle_subscription(data.is_member ? 'on' : 'off', data.email);
self.$target.removeClass("hidden");
});
@@ -43,7 +43,7 @@ class ResConfigSettings(models.TransientModel):
order_mail_template = fields.Many2one('mail.template', string='Order Confirmation Email',
default=_default_order_mail_template, domain="[('model', '=', 'sale.order')]",
help="Email sent to customer at the end of the checkout process")
help="Email sent to customer at the end of the checkout process", readonly=True)
automatic_invoice = fields.Boolean("Automatic Invoice",
help="The invoice is generated automatically and available in the customer portal "
@@ -9,7 +9,7 @@ require('website_sale.website_sale');
$('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
.off('click')
.removeClass('a-submit')
.click(function (event) {
.click(_.debounce(function (event) {
var $form = $(this).closest('form');
var quantity = parseFloat($form.find('input[name="add_qty"]').val() || 1);
var product_id = parseInt($form.find('input[type="hidden"][name="product_id"], input[type="radio"][name="product_id"]:checked').first().val(),10);
@@ -96,6 +96,6 @@ $('.oe_website_sale #add_to_cart, .oe_website_sale #products_grid .a-submit')
});
});
return false;
});
}, 200, true));
});
+1 -1
View File
@@ -189,7 +189,7 @@ class Property(models.Model):
if not values:
return
if not default_value:
if default_value is None:
domain = self._get_domain(name, model)
if domain is None:
raise Exception()
+16 -3
View File
@@ -174,7 +174,7 @@ class IrQWeb(models.AbstractModel, QWeb):
value=ast.Call(
func=ast.Attribute(
value=ast.Name(id='self', ctx=ast.Load()),
attr='_get_asset',
attr='_get_asset_nodes',
ctx=ast.Load()
),
args=[
@@ -286,15 +286,28 @@ class IrQWeb(models.AbstractModel, QWeb):
# method called by computing code
def get_asset_bundle(self, xmlid, files, remains=None, env=None):
return AssetsBundle(xmlid, files, remains=remains, env=env)
# compatibility to remove after v11 - DEPRECATED
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
)
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
files, remains = self._get_asset_content(xmlid, options)
asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url))
@tools.conditional(
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
)
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
files, remains = self._get_asset_content(xmlid, options)
asset = AssetsBundle(xmlid, files, env=self.env)
asset = self.get_asset_bundle(xmlid, files, env=self.env)
remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')]
return remains + asset.to_node(css=css, js=js, debug=debug, async=async)