[IMP] stock: better returns

Improve the returns process by adding the following improvements:
- Removal of the 'return' operations type. Returns of deliveries now
become receipts.
- Update of the `stock_picking_return` wizard by removing the onchange
on picking_id.
- Show returns in the sale portal, with a new 'return label' PDF report
- Enable returns for multi-step receipts on purchases

Community PR: https://github.com/odoo/odoo/pull/118568
Enterprise PR: https://github.com/odoo/enterprise/pull/39761

closes odoo/odoo#118568

Task: 3081370
Related: odoo/upgrade#4660
Related: odoo/enterprise#39761
Signed-off-by: Tiffany Chang <tic@odoo.com>
This commit is contained in:
Pieter Claeys (clpi)
2023-06-23 18:00:05 +02:00
parent f13d537a42
commit ccb41edf2b
22 changed files with 318 additions and 97 deletions
@@ -105,7 +105,7 @@ class StockMove(models.Model):
'show_lots_text': False,
})
elif self.env.user.has_group('base.group_portal'):
if self.picking_type_id.show_reserved:
if self.show_reserved:
action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_operations').id, 'form')]
else:
action['views'] = [(self.env.ref('mrp_subcontracting.mrp_subcontracting_view_stock_move_nosuggest_operations').id, 'form')]
@@ -16,11 +16,12 @@ class ReturnPicking(models.TransientModel):
record.picking_id.company_id
).property_stock_subcontractor
@api.onchange('picking_id')
def _onchange_picking_id(self):
res = super(ReturnPicking, self)._onchange_picking_id()
if any(return_line.quantity > 0 and return_line.move_id.is_subcontract for return_line in self.product_return_moves):
self.location_id = self.picking_id.partner_id.with_company(self.picking_id.company_id).property_stock_subcontractor
@api.depends('picking_id')
def _compute_moves_locations(self):
res = super()._compute_moves_locations()
for wizard in self:
if any(return_line.quantity > 0 and return_line.move_id.is_subcontract for return_line in wizard.product_return_moves):
wizard.location_id = wizard.picking_id.partner_id.with_company(wizard.picking_id.company_id).property_stock_subcontractor
return res
def _prepare_move_default_values(self, return_line, new_picking):
+9 -1
View File
@@ -75,9 +75,17 @@ class ReturnPicking(models.TransientModel):
def _prepare_move_default_values(self, return_line, new_picking):
vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
vals['purchase_line_id'] = return_line.move_id.purchase_line_id.id
if self.location_id.usage == "supplier":
vals['purchase_line_id'], vals['partner_id'] = return_line.move_id._get_purchase_line_and_partner_from_chain()
return vals
def _create_returns(self):
new_picking_id, picking_type_id = super()._create_returns()
picking = self.env['stock.picking'].browse(new_picking_id)
if len(picking.move_ids.partner_id) == 1:
picking.partner_id = picking.move_ids.partner_id
return new_picking_id, picking_type_id
class Orderpoint(models.Model):
_inherit = "stock.warehouse.orderpoint"
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import deque
from odoo import api, Command, fields, models, _
from odoo.tools.float_utils import float_round, float_is_zero, float_compare
@@ -219,3 +220,16 @@ class StockMove(models.Model):
def _get_all_related_sm(self, product):
return super()._get_all_related_sm(product) | self.filtered(lambda m: m.purchase_line_id.product_id == product)
def _get_purchase_line_and_partner_from_chain(self):
moves_to_check = deque(self)
seen_moves = set()
while moves_to_check:
current_move = moves_to_check.popleft()
if current_move.purchase_line_id:
return current_move.purchase_line_id.id, current_move.picking_id.partner_id.id
seen_moves.add(current_move)
moves_to_check.extend(
[move for move in current_move.move_orig_ids if move not in moves_to_check and move not in seen_moves]
)
return None, None
@@ -322,4 +322,6 @@ class StockRule(models.Model):
def _push_prepare_move_copy_values(self, move_to_copy, new_date):
res = super(StockRule, self)._push_prepare_move_copy_values(move_to_copy, new_date)
res['purchase_line_id'] = None
if self.location_dest_id.usage == "supplier":
res['purchase_line_id'], res['partner_id'] = move_to_copy._get_purchase_line_and_partner_from_chain()
return res
@@ -640,3 +640,67 @@ class TestCreatePicking(common.TestProductCommon):
# check that the price has been updated in the purchase order line and in the stock.move
self.assertEqual(purchase_order.order_line.price_unit, 0)
self.assertEqual(purchase_order.picking_ids.move_ids.price_unit, 0)
def test_return_to_vendor_multi_step(self):
self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
self.env.user.groups_id += self.env.ref('stock.group_adv_location')
warehouse = self.env['stock.warehouse'].search([], limit=1)
with Form(warehouse) as w:
w.reception_steps = 'three_steps'
vendor_returns_loc = self.env['stock.location'].create({
'name': 'Vendor returns processing',
'usage': 'internal',
'location_id': warehouse.view_location_id.id,
})
self.env['stock.rule'].create({
'name': 'Vendor returns',
'route_id': warehouse.reception_route_id.id,
'location_dest_id': self.env.ref('stock.stock_location_suppliers').id,
'location_src_id': vendor_returns_loc.id,
'action': 'push',
'auto': 'manual',
'picking_type_id': self.env.ref('stock.picking_type_out').id,
})
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as line:
line.product_id = self.product_id_1
line.product_qty = 10
po = po_form.save()
po.button_approve()
first_picking = po.picking_ids
first_picking.move_ids.quantity_done = 10
first_picking.button_validate()
second_picking = first_picking.move_ids.move_dest_ids.picking_id
second_picking.move_ids.quantity_done = 10
second_picking.button_validate()
self.assertEqual(po.order_line.qty_received, 10)
stock_return_picking_form = Form(
self.env['stock.return.picking'].with_context(
active_ids=second_picking.ids,
active_id=second_picking.ids[0],
active_model='stock.picking'
)
)
stock_return_picking_form.location_id = vendor_returns_loc
stock_return_picking_form.product_return_moves._records[0]['quantity'] = 2
stock_return_picking = stock_return_picking_form.save()
stock_return_picking_action = stock_return_picking.create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.action_assign()
return_pick.move_ids.quantity_done = 2
return_pick._action_done()
push_pick = return_pick.move_ids.move_dest_ids.picking_id
push_pick.action_assign()
push_pick.move_ids.quantity_done = 2
push_pick._action_done()
self.assertEqual(po.order_line.qty_received, 8)
self.assertEqual(push_pick.partner_id, po.partner_id)
+1 -1
View File
@@ -337,7 +337,7 @@ class TestRepair(AccountTestInvoicingCommon):
# Required for `location_dest_id` to be visible in the view
self.env.user.groups_id += self.env.ref('stock.group_stock_multi_locations')
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = self.stock_warehouse.return_type_id
picking_form.picking_type_id = self.stock_warehouse.in_type_id
picking_form.partner_id = self.res_partner_1
picking_form.location_dest_id = self.stock_location_14
return_picking = picking_form.save()
+20 -2
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.exceptions import NotFound
from odoo import exceptions, SUPERUSER_ID
from odoo.addons.sale.controllers.portal import CustomerPortal
@@ -26,8 +27,8 @@ class SaleStockPortal(CustomerPortal):
to be sure customer has access """
try:
picking_sudo = self._stock_picking_check_access(picking_id, access_token=access_token)
except exceptions.AccessError:
return request.redirect('/my')
except (exceptions.AccessError, exceptions.MissingError):
return NotFound()
# print report with sudo, since it require access to product, taxes, payment term etc.. and portal does not have those access rights.
pdf = request.env['ir.actions.report'].sudo()._render_qweb_pdf('stock.action_report_delivery', [picking_sudo.id])[0]
@@ -36,3 +37,20 @@ class SaleStockPortal(CustomerPortal):
('Content-Length', len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
@route(['/my/picking/return/pdf/<int:picking_id>'], type='http', auth="public", website=True)
def portal_my_picking_return_report(self, picking_id, access_token=None, **kw):
""" Print return label for customer, using either access rights or access token
to be sure customer has access """
try:
picking_sudo = self._stock_picking_check_access(picking_id, access_token=access_token)
except (exceptions.AccessError, exceptions.MissingError):
return NotFound()
pdf = \
request.env['ir.actions.report'].sudo()._render_qweb_pdf('stock.return_label_report', [picking_sudo.id])[0]
pdfhttpheaders = [
('Content-Type', 'application/pdf'),
('Content-Length', len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
@@ -38,10 +38,15 @@
t-options="{'date_only': True}"/>
</div>
</div>
<span t-if="picking.state == 'done'"
class="small badge text-bg-success orders_label_text_align">
<i class="fa fa-fw fa-truck"/> <b>Shipped</b>
</span>
<div t-if="picking.state == 'done'">
<span class="small badge text-bg-success orders_label_text_align">
<i class="fa fa-fw fa-truck"/> <b>Shipped</b>
</span>
<a class="badge text-bg-secondary orders_label_text_align" target="_blank"
t-att-href="'/my/picking/return/pdf/%s?%s' % (picking.id, keep_query())">
RETURN
</a>
</div>
<span t-elif="picking.state == 'cancel'"
class="small badge text-bg-danger orders_label_text_align">
<i class="fa fa-fw fa-times"/> <b>Cancelled</b>
@@ -54,6 +59,48 @@
</t>
</div>
</t>
<t t-set="returns" t-value="sale_order.picking_ids.filtered(lambda picking: picking.picking_type_id.code == 'incoming')"/>
<t t-if="returns">
<div class="mt-4">
<strong>Returns</strong>
</div>
<div>
<t t-foreach="returns" t-as="picking">
<t t-set="delivery_report_url"
t-value="'/my/picking/pdf/%s?%s' % (picking.id, keep_query())"/>
<div name="return"
class="d-flex flex-wrap align-items-center justify-content-between">
<div name="return_details">
<a t-att-href="delivery_report_url">
<span t-esc="picking.name"/>
</a>
<div class="small d-lg-inline-block ms-3">
Date:
<span class="text-muted"
t-field="picking.date_done"
t-options="{'date_only': True}"/>
<span t-if="picking.state in ['draft', 'waiting', 'confirmed', 'assigned']"
class="text-muted"
t-field="picking.scheduled_date"
t-options="{'date_only': True}"/>
</div>
</div>
<span t-if="picking.state == 'done'"
class="small badge text-bg-success orders_label_text_align">
<i class="fa fa-fw fa-truck"/> <b>Received</b>
</span>
<span t-elif="picking.state == 'cancel'"
class="small badge text-bg-danger orders_label_text_align">
<i class="fa fa-fw fa-times"/> <b>Cancelled</b>
</span>
<span t-elif="picking.state in ['draft', 'waiting', 'confirmed', 'assigned']"
class="small badge text-bg-info orders_label_text_align">
<i class="fa fa-fw fa-clock-o"/> <b>Awaiting arrival</b>
</span>
</div>
</t>
</div>
</t>
</xpath>
</template>
+1
View File
@@ -41,6 +41,7 @@
'report/picking_templates.xml',
'report/product_templates.xml',
'report/product_packaging.xml',
'report/report_return_slip.xml',
'data/mail_template_data.xml',
'views/stock_menu_views.xml',
+1 -4
View File
@@ -42,6 +42,7 @@
<field name="name">Vendors</field>
<field name="location_id" ref="stock_location_locations_partner"/>
<field name="usage">supplier</field>
<field name="return_location">True</field>
<field name="company_id"></field>
</record>
<record id="stock_location_customers" model="stock.location">
@@ -115,10 +116,6 @@
'xml_id': 'stock.picking_type_out',
'record': obj().env.ref('stock.warehouse0').out_type_id,
'noupdate': True,
},{
'xml_id': 'stock.picking_type_return',
'record': obj().env.ref('stock.warehouse0').return_type_id,
'noupdate': True,
}]"/>
</function>
+14 -8
View File
@@ -189,6 +189,7 @@ class StockMove(models.Model):
help="Computes when a move should be reserved")
product_packaging_id = fields.Many2one('product.packaging', 'Packaging', domain="[('product_id', '=', product_id)]", check_company=True)
from_immediate_transfer = fields.Boolean(related="picking_id.immediate_transfer")
show_reserved = fields.Boolean(compute='_compute_show_reserved')
@api.depends('product_id')
def _compute_product_uom(self):
@@ -310,7 +311,7 @@ class StockMove(models.Model):
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
In some context, such as MRP, it is necessary to compute quantity_done on filtered sock.move.line."""
self.ensure_one()
if self.picking_type_id.show_reserved is False:
if not self.show_reserved:
return self.move_line_nosuggest_ids
return self.move_line_ids
@@ -327,6 +328,11 @@ class StockMove(models.Model):
else:
move.delay_alert_date = False
@api.depends('picking_type_id', 'origin_returned_move_id')
def _compute_show_reserved(self):
for move in self:
move.show_reserved = move.picking_type_id.show_reserved or move.origin_returned_move_id
def _quantity_done_sml(self):
self.ensure_one()
quantity = 0
@@ -551,14 +557,14 @@ Please change the quantity done or the rounding precision of your unit of measur
)
lots_by_move_id_list.append({move.id: lot_ids for move, lot_ids in lots_by_move_id})
for move in self:
move.lot_ids = lots_by_move_id_list[0 if move.picking_type_id.show_reserved else 1].get(move._origin.id, [])
move.lot_ids = lots_by_move_id_list[0 if move.show_reserved else 1].get(move._origin.id, [])
def _set_lot_ids(self):
for move in self:
if move.product_id.tracking != 'serial':
continue
move_lines_commands = []
if move.picking_type_id.show_reserved is False:
if not move.show_reserved:
mls = move.move_line_nosuggest_ids
else:
mls = move.move_line_ids
@@ -789,7 +795,7 @@ Please change the quantity done or the rounding precision of your unit of measur
# reserved move lines. We do this by displaying `move_line_nosuggest_ids`. We use
# different views to display one field or another so that the webclient doesn't have to
# fetch both.
if self.picking_type_id.show_reserved:
if self.show_reserved:
view = self.env.ref('stock.view_stock_move_operations')
else:
view = self.env.ref('stock.view_stock_move_nosuggest_operations')
@@ -834,7 +840,7 @@ Please change the quantity done or the rounding precision of your unit of measur
since there's no way to undo the action.
"""
self.ensure_one()
if self.picking_type_id.show_reserved:
if self.show_reserved:
move_lines = self.move_line_ids
else:
move_lines = self.move_line_nosuggest_ids
@@ -910,7 +916,7 @@ Please change the quantity done or the rounding precision of your unit of measur
lot_names = self.env['stock.lot'].generate_lot_names(next_serial, next_serial_count or self.next_serial_count)
field_data = [{'lot_name': lot_name[0], 'qty_done': lot_name[1]} for lot_name in lot_names]
move_lines_commands = self._generate_serial_move_line_commands(field_data)
if self.picking_type_id.show_reserved:
if self.show_reserved:
self.move_line_ids = move_lines_commands
else:
self.move_line_nosuggest_ids = move_lines_commands
@@ -954,7 +960,7 @@ Please change the quantity done or the rounding precision of your unit of measur
break
move_lines_vals.append(move_line_vals)
move_lines_commands = self._generate_serial_move_line_commands(move_lines_vals, location_dest_id=location_id)
if self.picking_type_id.show_reserved:
if self.show_reserved:
self.update({'move_line_ids': move_lines_commands})
else:
self.update({'move_line_nosuggest_ids': move_lines_commands})
@@ -1279,7 +1285,7 @@ Please change the quantity done or the rounding precision of your unit of measur
'product_uom_id': self.product_id.uom_id.id,
}
# Select the right move lines depending of the picking type's configuration.
move_lines = self['move_line_ids' if self.picking_type_id.show_reserved else 'move_line_nosuggest_ids']
move_lines = self['move_line_ids' if self.show_reserved else 'move_line_nosuggest_ids']
move_lines = move_lines.filtered(lambda ml: not ml.lot_id and not ml.lot_name)
if origin_move_line:
+32 -1
View File
@@ -43,6 +43,9 @@ class PickingType(models.Model):
'stock.location', 'Default Destination Location',
check_company=True,
help="This is the default destination location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ")
default_location_return_id = fields.Many2one('stock.location', 'Default returns location', check_company=True,
help="This is the default location for returns created from a picking with this operation type.",
domain="[('return_location', '=', 'True')]")
code = fields.Selection([('incoming', 'Receipt'), ('outgoing', 'Delivery'), ('internal', 'Internal Transfer')], 'Type of Operation', required=True)
return_picking_type_id = fields.Many2one(
'stock.picking.type', 'Operation Type for Returns',
@@ -67,7 +70,8 @@ class PickingType(models.Model):
show_reserved = fields.Boolean(
'Pre-fill Detailed Operations', default=True,
help="If this checkbox is ticked, Odoo will automatically pre-fill the detailed "
"operations with the corresponding products, locations and lot/serial numbers.")
"operations with the corresponding products, locations and lot/serial numbers. "
"For moves that are returns, the detailed operations will always be prefilled, regardless of this option.")
reservation_method = fields.Selection(
[('at_confirm', 'At Confirmation'), ('manual', 'Manually'), ('by_date', 'Before scheduled date')],
'Reservation Method', required=True, default='at_confirm',
@@ -301,6 +305,11 @@ class Picking(models.Model):
check_company=True,
help="If this shipment was split, then this field links to the shipment which contains the already processed part.")
backorder_ids = fields.One2many('stock.picking', 'backorder_id', 'Back Orders')
return_id = fields.Many2one('stock.picking', 'Return of', copy=False, index='btree_not_null', readonly=True, check_company=True,
help="If this picking was created as a return of another picking, this field links to the original picking.")
return_ids = fields.One2many('stock.picking', 'return_id', 'Returns')
return_count = fields.Integer('# Returns', compute='_compute_return_count', compute_sudo=False)
move_type = fields.Selection([
('direct', 'As soon as possible'), ('one', 'When all products are ready')], 'Shipping Policy',
default='direct', required=True,
@@ -685,6 +694,11 @@ class Picking(models.Model):
picking.location_id = location_id
picking.location_dest_id = location_dest_id
@api.depends('return_ids')
def _compute_return_count(self):
for picking in self:
picking.return_count = len(picking.return_ids)
def _get_show_allocation(self, picking_type_id):
""" Helper method for computing "show_allocation" value.
Separated out from _compute function so it can be reused in other models (e.g. batch).
@@ -1612,5 +1626,22 @@ class Picking(models.Model):
)
return True
def action_see_returns(self):
self.ensure_one()
if len(self.return_ids) == 1:
return {
"type": "ir.actions.act_window",
"res_model": "stock.picking",
"views": [[False, "form"]],
"res_id": self.return_ids.id
}
return {
'name': _('Returns'),
"type": "ir.actions.act_window",
"res_model": "stock.picking",
"views": [[False, "tree"], [False, "form"]],
"domain": [('id', 'in', self.return_ids.ids)],
}
def _get_report_lang(self):
return self.move_ids and self.move_ids[0].partner_id.lang or self.partner_id.lang or self.env.lang
+2 -22
View File
@@ -76,7 +76,6 @@ class Warehouse(models.Model):
out_type_id = fields.Many2one('stock.picking.type', 'Out Type', check_company=True)
in_type_id = fields.Many2one('stock.picking.type', 'In Type', check_company=True)
int_type_id = fields.Many2one('stock.picking.type', 'Internal Type', check_company=True)
return_type_id = fields.Many2one('stock.picking.type', 'Return Type', check_company=True)
crossdock_route_id = fields.Many2one('stock.route', 'Crossdock Route', ondelete='restrict')
reception_route_id = fields.Many2one('stock.route', 'Receipt Route', ondelete='restrict')
delivery_route_id = fields.Many2one('stock.route', 'Delivery Route', ondelete='restrict')
@@ -349,7 +348,7 @@ class Warehouse(models.Model):
warehouse_data[picking_type] = PickingType.create(values).id
if 'out_type_id' in warehouse_data:
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data.get('return_type_id', False)})
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data.get('in_type_id', False)})
if 'in_type_id' in warehouse_data:
PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data.get('out_type_id', False)})
return warehouse_data
@@ -941,11 +940,7 @@ class Warehouse(models.Model):
},
'int_type_id': {
'barcode': self.code.replace(" ", "").upper() + "-INTERNAL",
},
'return_type_id': {
'default_location_dest_id': output_loc.id,
'barcode': self.code.replace(" ", "").upper() + "-RETURNS",
},
}
}
def _get_picking_type_create_values(self, max_sequence):
@@ -1007,16 +1002,6 @@ class Warehouse(models.Model):
'sequence': max_sequence + 2,
'sequence_code': 'INT',
'company_id': self.company_id.id,
}, 'return_type_id': {
'name': _('Returns'),
'code': 'incoming',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': False,
'sequence': max_sequence + 6,
'show_reserved': True,
'sequence_code': 'IN',
'company_id': self.company_id.id,
},
}, max_sequence + 6
@@ -1052,11 +1037,6 @@ class Warehouse(models.Model):
'prefix': code + '/INT/', 'padding': 5,
'company_id': self.company_id.id,
},
'return_type_id': {
'name': name + ' ' + _('Sequence return'),
'prefix': code + '/RET/', 'padding': 5,
'company_id': self.company_id.id,
},
}
def _format_rulename(self, from_loc, dest_loc, suffix):
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="stock.report_return_slip">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<div class="oe_structure"/>
<div class="row mt8">
<div class="col-8">
<h2>
<span>RETURN OF <t t-out="o.name"/></span>
</h2>
<p>
Please put this document inside your return parcel.<br/>
Your parcel must be sent to this address:
</p>
<div t-field="o.location_id.warehouse_id.partner_id"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}'/>
</div>
<div class="col-4 text-center mt-4">
<div>
<div t-field="o.name" t-options="{'widget': 'barcode', 'img_style': 'width:100%;'}"/>
<span t-field="o.name"/>
</div>
<div class="mt-4">
<div t-out="'O-BTN.return'" t-options="{'widget': 'barcode', 'img_style': 'width:100%;'}" />
<span>O-BTN.return</span>
</div>
</div>
</div>
</div>
</t>
</t>
</t>
</template>
<record id="return_label_report" model="ir.actions.report">
<field name="name">Return slip</field>
<field name="model">stock.picking</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">stock.report_return_slip</field>
<field name="report_file">return_slip</field>
<field name="binding_model_id" ref="model_stock_picking"/>
<field name="binding_type">report</field>
</record>
</odoo>
+1 -1
View File
@@ -858,7 +858,7 @@ class TestPickShip(TestStockCommon):
self.assertEqual(return_pick.move_line_ids[0].reserved_uom_qty, 4)
self.assertEqual(return_pick.move_line_ids[1].lot_id, lot3)
self.assertEqual(return_pick.move_line_ids[1].reserved_uom_qty, 6)
self.assertEqual(return_pick.picking_type_id, picking_client.location_id.warehouse_id.return_type_id)
self.assertEqual(return_pick.picking_type_id, picking_client.location_id.warehouse_id.in_type_id)
class TestSinglePicking(TestStockCommon):
def test_backorder_1(self):
@@ -32,11 +32,11 @@ class TestReturnPicking(TestStockCommon):
move_1.quantity_done = 2
move_2.quantity_done = 1
picking_out.button_validate()
return_wizard = StockReturnObj.with_context(active_id=picking_out.id, active_ids=picking_out.ids).create({
return_picking = StockReturnObj.with_context(active_id=picking_out.id, active_ids=picking_out.ids).create({
'location_id': self.stock_location,
'picking_id': picking_out.id,
})
return_wizard._onchange_picking_id()
return_picking._compute_moves_locations()
ReturnPickingLineObj = self.env['stock.return.picking.line']
# Check return line of uom_unit move
@@ -98,6 +98,7 @@
<group>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field attrs='{"invisible": [("code", "not in", ["incoming", "outgoing", "internal"])]}' name="return_picking_type_id" string="Returns Type"/>
<field name="default_location_return_id" attrs='{"invisible": [("code", "not in", ["incoming", "outgoing", "internal"])]}' groups="stock.group_stock_multi_locations"/>
<field name="create_backorder"/>
<field name="show_operations"/>
<field name="show_reserved" attrs="{'invisible': [('code', '!=', 'incoming')]}"/>
@@ -149,6 +149,11 @@
<div class="oe_button_box" name="button_box">
<field name="has_scrap_move" invisible="True"/>
<field name="has_tracking" invisible="True"/>
<button name="action_see_returns" type="object"
class="oe_stat_button" icon="fa-rotate-left"
attrs="{'invisible': [('return_count', '=', 0)]}">
<field string="Returns" name="return_count" widget="statinfo"/>
</button>
<button name="action_see_move_scrap" string="Scraps" type="object"
class="oe_stat_button" icon="oi-arrows-v"
attrs="{'invisible': [('has_scrap_move', '=', False)]}"/>
+38 -36
View File
@@ -34,46 +34,47 @@ class ReturnPicking(models.TransientModel):
return res
picking_id = fields.Many2one('stock.picking')
product_return_moves = fields.One2many('stock.return.picking.line', 'wizard_id', 'Moves')
move_dest_exists = fields.Boolean('Chained Move Exists', readonly=True)
original_location_id = fields.Many2one('stock.location')
parent_location_id = fields.Many2one('stock.location')
product_return_moves = fields.One2many('stock.return.picking.line', 'wizard_id', 'Moves', compute='_compute_moves_locations', readonly=False, store=True)
move_dest_exists = fields.Boolean('Chained Move Exists', compute='_compute_moves_locations', store=True)
original_location_id = fields.Many2one('stock.location', compute='_compute_moves_locations', store=True)
parent_location_id = fields.Many2one('stock.location', compute='_compute_moves_locations', store=True)
company_id = fields.Many2one(related='picking_id.company_id')
location_id = fields.Many2one(
'stock.location', 'Return Location',
'stock.location', 'Return Location', compute='_compute_moves_locations', readonly=False, store=True,
domain="['|', ('id', '=', original_location_id), '|', '&', ('return_location', '=', True), ('company_id', '=', False), '&', ('return_location', '=', True), ('company_id', '=', company_id)]")
@api.onchange('picking_id')
def _onchange_picking_id(self):
move_dest_exists = False
product_return_moves = [(5,)]
if self.picking_id and self.picking_id.state != 'done':
raise UserError(_("You may only return Done pickings."))
# In case we want to set specific default values (e.g. 'to_refund'), we must fetch the
# default values for creation.
line_fields = [f for f in self.env['stock.return.picking.line']._fields.keys()]
product_return_moves_data_tmpl = self.env['stock.return.picking.line'].default_get(line_fields)
for move in self.picking_id.move_ids:
if move.state == 'cancel':
continue
if move.scrapped:
continue
if move.move_dest_ids:
move_dest_exists = True
product_return_moves_data = dict(product_return_moves_data_tmpl)
product_return_moves_data.update(self._prepare_stock_return_picking_line_vals_from_move(move))
product_return_moves.append((0, 0, product_return_moves_data))
if self.picking_id and not product_return_moves:
raise UserError(_("No products to return (only lines in Done state and not fully returned yet can be returned)."))
if self.picking_id:
self.product_return_moves = product_return_moves
self.move_dest_exists = move_dest_exists
self.parent_location_id = self.picking_id.picking_type_id.warehouse_id and self.picking_id.picking_type_id.warehouse_id.view_location_id.id or self.picking_id.location_id.location_id.id
self.original_location_id = self.picking_id.location_id.id
location_id = self.picking_id.location_id.id
if self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.return_location:
location_id = self.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.id
self.location_id = location_id
@api.depends('picking_id')
def _compute_moves_locations(self):
for wizard in self:
move_dest_exists = False
product_return_moves = [(5,)]
if wizard.picking_id and wizard.picking_id.state != 'done':
raise UserError(_("You may only return Done pickings."))
# In case we want to set specific default values (e.g. 'to_refund'), we must fetch the
# default values for creation.
line_fields = [f for f in self.env['stock.return.picking.line']._fields.keys()]
product_return_moves_data_tmpl = self.env['stock.return.picking.line'].default_get(line_fields)
for move in wizard.picking_id.move_ids:
if move.state == 'cancel':
continue
if move.scrapped:
continue
if move.move_dest_ids:
move_dest_exists = True
product_return_moves_data = dict(product_return_moves_data_tmpl)
product_return_moves_data.update(wizard._prepare_stock_return_picking_line_vals_from_move(move))
product_return_moves.append((0, 0, product_return_moves_data))
if wizard.picking_id and not product_return_moves:
raise UserError(_("No products to return (only lines in Done state and not fully returned yet can be returned)."))
if wizard.picking_id:
wizard.product_return_moves = product_return_moves
wizard.move_dest_exists = move_dest_exists
wizard.parent_location_id = wizard.picking_id.picking_type_id.warehouse_id and wizard.picking_id.picking_type_id.warehouse_id.view_location_id.id or wizard.picking_id.location_id.location_id.id
wizard.original_location_id = wizard.picking_id.location_id.id
location_id = wizard.picking_id.location_id.id
if wizard.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.return_location:
location_id = wizard.picking_id.picking_type_id.return_picking_type_id.default_location_dest_id.id
wizard.location_id = location_id
@api.model
def _prepare_stock_return_picking_line_vals_from_move(self, stock_move):
@@ -115,6 +116,7 @@ class ReturnPicking(models.TransientModel):
'move_ids': [],
'picking_type_id': self.picking_id.picking_type_id.return_picking_type_id.id or self.picking_id.picking_type_id.id,
'state': 'draft',
'return_id': self.picking_id.id,
'origin': _("Return of %s") % self.picking_id.name,
}
# TestPickShip.test_mto_moves_return, TestPickShip.test_mto_moves_return_extra,
+1 -1
View File
@@ -14,7 +14,7 @@ _logger = logging.getLogger(__name__)
class StockMove(models.Model):
_inherit = "stock.move"
to_refund = fields.Boolean(string="Update quantities on SO/PO", copy=False,
to_refund = fields.Boolean(string="Update quantities on SO/PO", copy=True,
help='Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order')
account_move_ids = fields.One2many('account.move', 'stock_move_id')
stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_move_id')
@@ -14,14 +14,11 @@ class StockReturnPicking(models.TransientModel):
vals.update({'to_refund': True})
return res
def _create_returns(self):
new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns()
new_picking = self.env['stock.picking'].browse([new_picking_id])
for move in new_picking.move_ids:
return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id)[:1]
if return_picking_line and return_picking_line.to_refund:
move.to_refund = True
return new_picking_id, pick_type_id
def _prepare_move_default_values(self, return_line, new_picking):
vals = super(StockReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
if return_line.to_refund:
vals['to_refund'] = True
return vals
class StockReturnPickingLine(models.TransientModel):