[IMP] mrp, product, {purchase_, sale_}stock{_picking_batch}: add reception report
New reception report added for non-outgoing transfers. This is intended to support easier stock allocations by allowing dynamic MTO assignment/unassignment, e.g. if we have an incoming transfer with products we want to assign to an existing outgoing transfer, then we can use the report to create a MTO link to the specific outgoing moves. To support this assignment flow, the report also allows printing of labels to place on the product so stock workers know which transfer/MO the product has been assigned to. Expected use case is when a product is purchased to fulfil a sale. Demo data has been added so reception report can be immediately seen/used for this flow. Implementation Notes: - Report has been made flexible to work with batch transfers. - Only done moves can be assigned to moves that already have quants reserved (prevents undesired behavior + this makes sense logically) - Confirmed (+ Done and everything inbetween) moves can be assigned to any confirmed moves that are not already assigned. - Report does not affect quant reservation/unreservation at all. It works only with linking moves (i.e. move_orig_id/move_dest_id) so move/transfer linkage traceability is stored within db (i.e. this isn't possible with quants) Limitations: To keep code simple for now, this assignment flow will break in certain cases: 1. Done amount of an assigned move is less than the Demand amount (linked move will not autoreserve correctly when assigned move is validated, same issue already occurs in multi-step transfer). 2. If a linked move is unreserved after its assigned move is validated, then another move can reserve its quants and its assignment link will not be accurate. 3. Already reserved SNs + assign move, may lead to mismatching SNs between report and what's actually reserved. 4. Changing a linked move's Demand amount after a move is assigned to it. 5. Potential moves to assign to are only checked for being in same warehouse, not in a matching source location to destination location. User is expected to make this match on their own. Task: 2500844 ENT PR: odoo/enterprise#18268 Upgrade PR: odoo/upgrade#2731 closes odoo/odoo#70669 Signed-off-by: Arnold Moyaux <amoyaux@users.noreply.github.com>
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
'report/mrp_report_bom_structure.xml',
|
||||
'report/mrp_production_templates.xml',
|
||||
'report/report_stock_forecasted.xml',
|
||||
'report/report_stock_reception.xml',
|
||||
'report/report_stock_rule.xml',
|
||||
'report/mrp_zebra_production_templates.xml',
|
||||
],
|
||||
@@ -68,6 +69,7 @@
|
||||
'mrp/static/src/js/mrp_document_kanban_view.js',
|
||||
'mrp/static/src/js/mrp_should_consume.js',
|
||||
'mrp/static/src/js/mrp_field_one2many_with_copy.js',
|
||||
'mrp/static/src/js/report_stock_reception.js',
|
||||
],
|
||||
'web.assets_common': [
|
||||
'mrp/static/src/scss/mrp_bom_report.scss',
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
|
||||
from . import mrp_report_bom_structure
|
||||
from . import report_stock_forecasted
|
||||
from . import report_stock_reception
|
||||
from . import report_stock_rule
|
||||
|
||||
@@ -40,5 +40,15 @@
|
||||
<field name="binding_model_id" ref="model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="label_production_order" model="ir.actions.report">
|
||||
<field name="name">Order Label</field>
|
||||
<field name="model">mrp.production</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">mrp.report_reception_report_label_mrp</field>
|
||||
<field name="report_file">mrp.report_reception_report_label_mrp</field>
|
||||
<field name="paperformat_id" ref="product.paperformat_label_sheet_dymo"/>
|
||||
<field name="binding_model_id" ref="model_mrp_production"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import format_date
|
||||
|
||||
|
||||
class ReceptionReport(models.AbstractModel):
|
||||
_inherit = 'report.stock.report_reception'
|
||||
|
||||
def _get_formatted_scheduled_date(self, source):
|
||||
if source._name == 'mrp.production':
|
||||
return format_date(self.env, source.date_planned_start)
|
||||
return super()._get_formatted_scheduled_date(source)
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Reception Report Labels -->
|
||||
<template id="report_reception_report_label_mrp">
|
||||
<t t-if="quantity" t-set="qtys" t-value="[int(q) for q in quantity.split(',')]"/>
|
||||
<t t-else="" t-set="qtys" t-value="[1 for q in range(len(docs))]"/>
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="range(len(docs))" t-as="index">
|
||||
<t t-set="mo" t-value="docs[index]"/>
|
||||
<t t-set="qty" t-value="qtys[index]"/>
|
||||
<t t-foreach="range(qty)" t-as="j">
|
||||
<div class="o_label_page o_label_dymo">
|
||||
<div t-esc="mo.name"/>
|
||||
<div class="font-weight-bold" t-esc="mo.product_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import ReceptionReport from '@stock/js/report_stock_reception';
|
||||
|
||||
ReceptionReport.include({
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_onClickPrintLabel: function (ev) {
|
||||
return this._super(ev).then(() => {
|
||||
return this._printLabel(ev, 'mrp.report_reception_report_label_mrp', 'mrp.production');
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
@@ -782,6 +782,15 @@
|
||||
<field name="currency_id" ref="base.USD"/>
|
||||
</record>
|
||||
|
||||
<record id="product_supplierinfo_26" model="product.supplierinfo">
|
||||
<field name="product_tmpl_id" ref="product_product_5_product_template"/>
|
||||
<field name="name" ref="base.res_partner_10"/>
|
||||
<field name="delay">1</field>
|
||||
<field name="min_qty">0</field>
|
||||
<field name="price">145</field>
|
||||
<field name="currency_id" ref="base.USD"/>
|
||||
</record>
|
||||
|
||||
<record forcecreate="True" id="property_product_pricelist_demo" model="ir.property">
|
||||
<field name="name">property_product_pricelist</field>
|
||||
<field name="fields_id" search="[('model','=','res.partner'),('name','=','property_product_pricelist')]"/>
|
||||
|
||||
@@ -63,5 +63,20 @@
|
||||
<field name="report_name">product.report_pricelist</field>
|
||||
<field name="report_file">product.report_pricelist</field>
|
||||
</record>
|
||||
|
||||
<record id="paperformat_label_sheet_dymo" model="report.paperformat">
|
||||
<field name="name">Dymo Label Sheet</field>
|
||||
<field name="default" eval="True" />
|
||||
<field name="format">custom</field>
|
||||
<field name="page_height">57</field>
|
||||
<field name="page_width">32</field>
|
||||
<field name="orientation">Landscape</field>
|
||||
<field name="margin_top">0</field>
|
||||
<field name="margin_bottom">0</field>
|
||||
<field name="margin_left">0</field>
|
||||
<field name="margin_right">0</field>
|
||||
<field name="disable_shrinking" eval="True"/>
|
||||
<field name="dpi">96</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_5" model="product.product">
|
||||
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
|
||||
</record>
|
||||
|
||||
<record id="product.product_product_9" model="product.product">
|
||||
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
|
||||
</record>
|
||||
|
||||
@@ -114,10 +114,35 @@
|
||||
<field name="price_unit">140.00</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_22" model="sale.order">
|
||||
<field name="partner_id" ref="base.res_partner_3"/>
|
||||
<field name="partner_invoice_id" ref="base.res_partner_address_25"/>
|
||||
<field name="partner_shipping_id" ref="base.res_partner_address_25"/>
|
||||
<field name="user_id" ref="base.user_demo"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="team_id" ref="sales_team.team_sales_department"/>
|
||||
<field name="campaign_id" ref="utm.utm_campaign_email_campaign_products"/>
|
||||
<field name="medium_id" ref="utm.utm_medium_email"/>
|
||||
<field name="source_id" ref="sale.utm_source_sale_order_0"/>
|
||||
<field name="date_order" eval="(datetime.now() + relativedelta(days=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
|
||||
<field name="warehouse_id" ref="stock.warehouse0"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_line_47" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_22"/>
|
||||
<field name="name" model="sale.order.line" eval="obj().env.ref('product.product_product_5').get_product_multiline_description_sale()"/>
|
||||
<field name="product_id" ref="product.product_product_5"/>
|
||||
<field name="product_uom_qty">10</field>
|
||||
<field name="product_uom" ref="uom.product_uom_unit"/>
|
||||
<field name="price_unit">199.00</field>
|
||||
</record>
|
||||
|
||||
|
||||
<function model="sale.order" name="action_confirm" eval="[[
|
||||
ref('sale_order_19'),
|
||||
ref('sale_order_20'),
|
||||
ref('sale_order_21'),
|
||||
ref('sale_order_22'),
|
||||
]]"/>
|
||||
|
||||
<!-- Change date of those sale orders' delivery -->
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
'report/report_stock_forecasted.xml',
|
||||
'report/report_stock_quantity.xml',
|
||||
'report/report_stock_reception.xml',
|
||||
'report/stock_report_views.xml',
|
||||
'report/report_package_barcode.xml',
|
||||
'report/report_lot_barcode.xml',
|
||||
@@ -94,6 +95,7 @@
|
||||
'web/static/src/legacy/scss/views.scss',
|
||||
'web/static/src/legacy/scss/graph_view.scss',
|
||||
'stock/static/src/scss/report_stock_forecasted.scss',
|
||||
'stock/static/src/scss/report_stock_reception.scss',
|
||||
'stock/static/src/scss/report_stock_rule.scss',
|
||||
],
|
||||
'web.assets_common': [
|
||||
@@ -106,6 +108,7 @@
|
||||
'stock/static/src/js/inventory_singleton_list_controller.js',
|
||||
'stock/static/src/js/inventory_singleton_list_view.js',
|
||||
'stock/static/src/js/report_stock_forecasted.js',
|
||||
'stock/static/src/js/report_stock_reception.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_controller.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_model.js',
|
||||
'stock/static/src/js/stock_orderpoint_list_view.js',
|
||||
@@ -132,6 +135,7 @@
|
||||
'stock/static/src/xml/popover_widget.xml',
|
||||
'stock/static/src/xml/forecast_widget.xml',
|
||||
'stock/static/src/xml/report_stock_forecasted.xml',
|
||||
'stock/static/src/xml/report_stock_reception.xml',
|
||||
'stock/static/src/xml/stock_orderpoint.xml',
|
||||
'stock/static/src/xml/stock_traceability_report_backend.xml',
|
||||
],
|
||||
|
||||
@@ -43,6 +43,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
'Storage Categories', implied_group='stock.group_stock_storage_categories')
|
||||
annual_inventory_month = fields.Selection(related='company_id.annual_inventory_month', readonly=False)
|
||||
annual_inventory_day = fields.Integer(related='company_id.annual_inventory_day', readonly=False)
|
||||
group_stock_reception_report = fields.Boolean("Reception Report", implied_group='stock.group_reception_report')
|
||||
group_stock_auto_reception_report = fields.Boolean("Show Reception Report at Validation", implied_group='stock.group_auto_reception_report')
|
||||
|
||||
@api.onchange('group_stock_multi_locations')
|
||||
def _onchange_group_stock_multi_locations(self):
|
||||
|
||||
@@ -114,7 +114,7 @@ class StockMove(models.Model):
|
||||
procure_method = fields.Selection([
|
||||
('make_to_stock', 'Default: Take From Stock'),
|
||||
('make_to_order', 'Advanced: Apply Procurement Rules')], string='Supply Method',
|
||||
default='make_to_stock', required=True,
|
||||
default='make_to_stock', required=True, copy=False,
|
||||
help="By default, the system will take from the stock in the source location and passively wait for availability. "
|
||||
"The other possibility allows you to directly create a procurement on the source location (and thus ignore "
|
||||
"its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, "
|
||||
|
||||
@@ -954,6 +954,24 @@ class Picking(models.Model):
|
||||
pickings_to_backorder = self
|
||||
pickings_not_to_backorder.with_context(cancel_backorder=True)._action_done()
|
||||
pickings_to_backorder.with_context(cancel_backorder=False)._action_done()
|
||||
|
||||
if self.user_has_groups('stock.group_reception_report') \
|
||||
and self.user_has_groups('stock.group_auto_reception_report') \
|
||||
and self.filtered(lambda p: p.picking_type_id.code != 'outgoing'):
|
||||
lines = self.move_lines.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel' and m.quantity_done and not m.move_dest_ids)
|
||||
if lines:
|
||||
# don't show reception report if all already assigned/nothing to assign
|
||||
wh_location_ids = self.env['stock.location'].search([('id', 'child_of', self.picking_type_id.warehouse_id.view_location_id.id), ('location_id.usage', '!=', 'supplier')]).ids
|
||||
if self.env['stock.move'].search([
|
||||
('state', 'in', ['confirmed', 'partially_available', 'waiting', 'assigned']),
|
||||
('product_qty', '>', 0),
|
||||
('location_id', 'in', wh_location_ids),
|
||||
('move_orig_ids', '=', False),
|
||||
('picking_id', 'not in', self.ids),
|
||||
('product_id', 'in', lines.product_id.ids)], limit=1):
|
||||
action = self.action_view_reception_report()
|
||||
action['context'] = {'default_picking_ids': self.ids}
|
||||
return action
|
||||
return True
|
||||
|
||||
def action_set_quantities_to_reservation(self):
|
||||
@@ -1400,6 +1418,9 @@ class Picking(models.Model):
|
||||
action['domain'] = [('picking_id', 'in', self.ids)]
|
||||
return action
|
||||
|
||||
def action_view_reception_report(self):
|
||||
return self.env["ir.actions.actions"]._for_xml_id("stock.stock_reception_action")
|
||||
|
||||
def _attach_sign(self):
|
||||
""" Render the delivery report in pdf and attach it to the picking in `self`. """
|
||||
self.ensure_one()
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
|
||||
from . import report_stock_forecasted
|
||||
from . import report_stock_quantity
|
||||
from . import report_stock_reception
|
||||
from . import report_stock_rule
|
||||
from . import stock_traceability
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.tools import float_compare, float_is_zero, format_date
|
||||
|
||||
|
||||
class ReceptionReport(models.AbstractModel):
|
||||
_name = 'report.stock.report_reception'
|
||||
_description = "Stock Reception Report"
|
||||
|
||||
@api.model
|
||||
def _get_report_values(self, docids, data=None):
|
||||
''' This report is flexibly designed to work with both individual and batch pickings.
|
||||
'''
|
||||
docids = self.env.context.get('default_picking_ids', docids)
|
||||
pickings = self.env['stock.picking'].search([('id', 'in', docids), ('picking_type_code', '!=', 'outgoing'), ('state', '!=', 'cancel')])
|
||||
picking_states = pickings.mapped('state')
|
||||
# unsupported cases
|
||||
if not pickings:
|
||||
msg = _("No transfers selected or a delivery order selected")
|
||||
elif 'done' in picking_states and len(set(picking_states)) > 1:
|
||||
pickings = False
|
||||
msg = _("This report cannot be used for done and not done transfers at the same time")
|
||||
if not pickings:
|
||||
return {'pickings': False, 'reason': msg}
|
||||
|
||||
# incoming move qtys
|
||||
product_to_qty_draft = defaultdict(float)
|
||||
product_to_qty_to_assign = defaultdict(list)
|
||||
product_to_total_assigned = defaultdict(lambda: [0.0, []])
|
||||
|
||||
# to support batch pickings we need to track the total already assigned
|
||||
move_lines = pickings.move_lines.filtered(lambda m: m.product_id.type == 'product' and m.state != 'cancel')
|
||||
assigned_moves = move_lines.mapped('move_dest_ids')
|
||||
product_to_assigned_qty = defaultdict(float)
|
||||
for assigned in assigned_moves:
|
||||
product_to_assigned_qty[assigned.product_id] += assigned.product_qty
|
||||
|
||||
for move in move_lines:
|
||||
qty_already_assigned = 0
|
||||
if move.move_dest_ids:
|
||||
qty_already_assigned = min(product_to_assigned_qty[move.product_id], move.product_qty)
|
||||
product_to_assigned_qty[move.product_id] -= qty_already_assigned
|
||||
if qty_already_assigned:
|
||||
product_to_total_assigned[move.product_id][0] += qty_already_assigned
|
||||
product_to_total_assigned[move.product_id][1].append(move.id)
|
||||
if move.product_qty != qty_already_assigned:
|
||||
if move.state == 'draft':
|
||||
product_to_qty_draft[move.product_id] += move.product_qty - qty_already_assigned
|
||||
else:
|
||||
quantity_to_assign = move.product_qty
|
||||
if move.picking_id.immediate_transfer:
|
||||
# if immediate transfer is not Done and quantity_done hasn't been edited, then move.product_qty will incorrectly = 1 (due to default)
|
||||
quantity_to_assign = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id, rounding_method='HALF-UP')
|
||||
product_to_qty_to_assign[move.product_id].append((quantity_to_assign - qty_already_assigned, move))
|
||||
|
||||
# only match for non-mto moves in same warehouse
|
||||
warehouse = pickings[0].picking_type_id.warehouse_id
|
||||
wh_location_ids = self.env['stock.location'].search([('id', 'child_of', warehouse.view_location_id.id), ('location_id.usage', '!=', 'supplier')]).ids
|
||||
|
||||
allowed_states = ['confirmed', 'partially_available', 'waiting']
|
||||
if 'done' in picking_states:
|
||||
# only done moves are allowed to be assigned to already reserved moves
|
||||
allowed_states += ['assigned']
|
||||
|
||||
outs = self.env['stock.move'].search(
|
||||
[
|
||||
('state', 'in', allowed_states),
|
||||
('product_qty', '>', 0),
|
||||
('location_id', 'in', wh_location_ids),
|
||||
('move_orig_ids', '=', False),
|
||||
('picking_id', 'not in', pickings.ids),
|
||||
('product_id', 'in',
|
||||
[p.id for p in list(product_to_qty_to_assign.keys()) + list(product_to_qty_draft.keys())]),
|
||||
],
|
||||
order='reservation_date, priority desc, date, id')
|
||||
|
||||
products_to_outs = defaultdict(list)
|
||||
for out in outs:
|
||||
products_to_outs[out.product_id].append(out)
|
||||
|
||||
sources_to_lines = defaultdict(list) # group by source so we can print them together
|
||||
# show potential moves that can be assigned
|
||||
for product_id, outs in products_to_outs.items():
|
||||
for out in outs:
|
||||
# only show pickings or MOs, no SOs
|
||||
source = out.picking_id if out.picking_id else out._get_source_document()
|
||||
if not source:
|
||||
continue
|
||||
|
||||
qty_to_reserve = out.product_qty
|
||||
product_uom = out.product_id.uom_id
|
||||
if 'done' not in picking_states and out.state == 'partially_available':
|
||||
qty_to_reserve -= out.product_uom._compute_quantity(out.reserved_availability, product_uom)
|
||||
moves_in_ids = []
|
||||
qty_done = 0
|
||||
for move_in_qty, move_in in product_to_qty_to_assign[out.product_id]:
|
||||
moves_in_ids.append(move_in.id)
|
||||
if float_compare(qty_done + move_in_qty, qty_to_reserve, precision_rounding=product_uom.rounding) <= 0:
|
||||
qty_to_add = move_in_qty
|
||||
move_in_qty = 0
|
||||
else:
|
||||
qty_to_add = qty_to_reserve - qty_done
|
||||
move_in_qty -= qty_to_add
|
||||
qty_done += qty_to_add
|
||||
if move_in_qty:
|
||||
product_to_qty_to_assign[out.product_id][0] = (move_in_qty, move_in)
|
||||
else:
|
||||
product_to_qty_to_assign[out.product_id] = product_to_qty_to_assign[out.product_id][1:]
|
||||
if float_compare(qty_to_reserve, qty_done, precision_rounding=product_uom.rounding) == 0:
|
||||
break
|
||||
|
||||
if not float_is_zero(qty_done, precision_rounding=product_uom.rounding):
|
||||
sources_to_lines[source].append(self._prepare_report_line(qty_done, product_id, out, source, move_ins=self.env['stock.move'].browse(moves_in_ids)))
|
||||
|
||||
# draft qtys can be shown but not assigned
|
||||
qty_expected = product_to_qty_draft.get(product_id, 0)
|
||||
if float_compare(qty_to_reserve, qty_done, precision_rounding=product_uom.rounding) > 0 and\
|
||||
not float_is_zero(qty_expected, precision_rounding=product_uom.rounding):
|
||||
to_expect = min(qty_expected, qty_to_reserve - qty_done)
|
||||
sources_to_lines[source].append(self._prepare_report_line(to_expect, product_id, out, source, is_qty_assignable=False))
|
||||
product_to_qty_draft[product_id] -= to_expect
|
||||
|
||||
# show already assigned moves
|
||||
for product_id, qty_and_ins in product_to_total_assigned.items():
|
||||
total_assigned = qty_and_ins[0]
|
||||
moves_in = self.env['stock.move'].browse(qty_and_ins[1])
|
||||
out_moves = moves_in.move_dest_ids
|
||||
|
||||
for out_move in out_moves:
|
||||
if float_is_zero(total_assigned, precision_rounding=out_move.product_id.uom_id.rounding):
|
||||
# it is possible there are different in moves linked to the same out moves due to batch
|
||||
# => we guess as to which outs correspond to this report...
|
||||
continue
|
||||
source = out_move.picking_id if out_move.picking_id else out_move._get_source_document()
|
||||
if not source:
|
||||
continue
|
||||
qty_assigned = min(total_assigned, out_move.product_qty)
|
||||
sources_to_lines[source].append(
|
||||
self._prepare_report_line(qty_assigned, product_id, out_move, source, is_assigned=True, move_ins=moves_in))
|
||||
|
||||
# dates aren't auto-formatted when printed in report :(
|
||||
sources_to_formatted_scheduled_date = defaultdict(list)
|
||||
for source, dummy in sources_to_lines.items():
|
||||
sources_to_formatted_scheduled_date[source] = self._get_formatted_scheduled_date(source)
|
||||
|
||||
return {
|
||||
'data': data,
|
||||
'doc_ids': docids,
|
||||
'doc_model': 'stock.picking',
|
||||
'sources_to_lines': sources_to_lines,
|
||||
'precision': self.env['decimal.precision'].precision_get('Product Unit of Measure'),
|
||||
'pickings': pickings,
|
||||
'sources_to_formatted_scheduled_date': sources_to_formatted_scheduled_date,
|
||||
}
|
||||
|
||||
def _prepare_report_line(self, quantity, product, move_out, source=False, is_assigned=False, is_qty_assignable=True, move_ins=False):
|
||||
return {
|
||||
'source': source,
|
||||
'product': {
|
||||
'id': product.id,
|
||||
'display_name': product.display_name
|
||||
},
|
||||
'uom': product.uom_id.display_name,
|
||||
'quantity': quantity,
|
||||
'is_qty_assignable': is_qty_assignable,
|
||||
'move_out': move_out,
|
||||
'is_assigned': is_assigned,
|
||||
'move_ins': move_ins and move_ins.ids or False,
|
||||
}
|
||||
|
||||
def _get_formatted_scheduled_date(self, source):
|
||||
""" Unfortunately different source record types have different field names for their "Scheduled Date"
|
||||
Therefore an extendable method is needed.
|
||||
"""
|
||||
if source._name == 'stock.picking':
|
||||
return format_date(self.env, source.scheduled_date)
|
||||
return False
|
||||
|
||||
def action_assign(self, move_ids, qtys, in_ids):
|
||||
""" Assign picking move(s) [i.e. link] to other moves (i.e. make them MTO)
|
||||
:param move_id ids: the ids of the moves to make MTO
|
||||
:param qtys list: the quantities that are being assigned to the move_ids (in same order as move_ids)
|
||||
:param in_ids ids: the ids of the moves that are to be assigned to move_ids
|
||||
"""
|
||||
outs = self.env['stock.move'].browse(move_ids)
|
||||
# Split outs with only part of demand assigned to prevent reservation problems later on.
|
||||
# We do this first so we can create their split moves in batch
|
||||
out_to_new_out = OrderedDict()
|
||||
new_move_vals = []
|
||||
for out, qty_to_link in zip(outs, qtys):
|
||||
if float_compare(out.product_qty, qty_to_link, precision_rounding=out.product_id.uom_id.rounding) == 1:
|
||||
new_move_vals += out._split(out.product_qty - qty_to_link)
|
||||
out_to_new_out[out.id] = self.env['stock.move']
|
||||
new_outs = self.env['stock.move'].create(new_move_vals)
|
||||
# don't do action confirm to avoid creating additional unintentional reservations
|
||||
new_outs.write({'state': 'confirmed'})
|
||||
for i, k in enumerate(out_to_new_out.keys()):
|
||||
out_to_new_out[k] = new_outs[i]
|
||||
|
||||
for out, qty_to_link, ins in zip(outs, qtys, in_ids):
|
||||
potential_ins = self.env['stock.move'].browse(ins)
|
||||
if out.id in out_to_new_out:
|
||||
new_out = out_to_new_out[out.id]
|
||||
if potential_ins[0].state != 'done' and out.reserved_availability:
|
||||
# let's assume if 1 of the potential_ins isn't done, then none of them are => we are only assigning the not-reserved
|
||||
# qty and the new move should have all existing reserved quants (i.e. move lines) assigned to it
|
||||
out.move_line_ids.move_id = new_out
|
||||
elif potential_ins[0].state == 'done' and out.reserved_availability > qty_to_link:
|
||||
# let's assume if 1 of the potential_ins is done, then all of them are => we can link them to already reserved moves, but we
|
||||
# need to make sure the reserved qtys still match the demand amount the move (we're assigning).
|
||||
out.move_line_ids.move_id = new_out
|
||||
assigned_amount = 0
|
||||
for move_line_id in new_out.move_line_ids:
|
||||
if assigned_amount + move_line_id.product_qty > qty_to_link:
|
||||
new_move_line = move_line_id.copy({'product_uom_qty': 0, 'qty_done': 0})
|
||||
new_move_line.product_uom_qty = move_line_id.product_uom_qty
|
||||
move_line_id.product_uom_qty = out.product_id.uom_id._compute_quantity(qty_to_link - assigned_amount, out.product_uom, rounding_method='HALF-UP')
|
||||
new_move_line.product_uom_qty -= out.product_id.uom_id._compute_quantity(move_line_id.product_qty, out.product_uom, rounding_method='HALF-UP')
|
||||
move_line_id.move_id = out
|
||||
assigned_amount += move_line_id.product_qty
|
||||
if float_compare(assigned_amount, qty_to_link, precision_rounding=out.product_id.uom_id.rounding) == 0:
|
||||
break
|
||||
|
||||
for in_move in reversed(potential_ins):
|
||||
quantity_remaining = in_move.product_qty - sum(in_move.move_dest_ids.mapped('product_qty'))
|
||||
if in_move.product_id != out.product_id or float_compare(0, quantity_remaining, precision_rounding=in_move.product_id.uom_id.rounding) >= 0:
|
||||
# in move is already completely linked (e.g. during another assign click) => don't count it again
|
||||
potential_ins = potential_ins[1:]
|
||||
continue
|
||||
|
||||
linked_qty = min(in_move.product_qty, qty_to_link)
|
||||
in_move.move_dest_ids |= out
|
||||
out.procure_method = 'make_to_order'
|
||||
quantity_remaining -= linked_qty
|
||||
qty_to_link -= linked_qty
|
||||
if float_is_zero(qty_to_link, precision_rounding=out.product_id.uom_id.rounding):
|
||||
break # we have satistfied the qty_to_link
|
||||
|
||||
(outs | new_outs)._recompute_state()
|
||||
|
||||
# always try to auto-assign to prevent another move from reserving the quant if incoming move is done
|
||||
self.env['stock.move'].browse(move_ids)._action_assign()
|
||||
|
||||
def action_unassign(self, move_id, qty, in_ids):
|
||||
""" Unassign moves [i.e. unlink] from a move (i.e. make non-MTO)
|
||||
:param move_id id: the id of the move to make non-MTO
|
||||
:param qty float: the total quantity that is being unassigned from move_id
|
||||
:param in_ids ids: the ids of the moves that are to be unassigned from move_id
|
||||
"""
|
||||
out = self.env['stock.move'].browse(move_id)
|
||||
ins = self.env['stock.move'].browse(in_ids)
|
||||
|
||||
amount_unassigned = 0
|
||||
for in_move in ins:
|
||||
if out.id not in in_move.move_dest_ids.ids:
|
||||
continue
|
||||
in_move.move_dest_ids -= out
|
||||
amount_unassigned += min(qty, in_move.product_qty)
|
||||
if float_compare(qty, amount_unassigned, precision_rounding=out.product_id.uom_id.rounding) <= 0:
|
||||
break
|
||||
if out.move_orig_ids:
|
||||
# annoying use case: batch reserved + individual picking unreserved, need to split the out move
|
||||
new_move_vals = out._split(amount_unassigned)
|
||||
if new_move_vals:
|
||||
new_move_vals[0]['procure_method'] = 'make_to_order'
|
||||
new_out = self.env['stock.move'].create(new_move_vals)
|
||||
# don't do action confirm to avoid creating additional unintentional reservations
|
||||
new_out.write({'state': 'confirmed'})
|
||||
out.move_line_ids.move_id = new_out
|
||||
(out | new_out)._compute_reserved_availability()
|
||||
if new_out.reserved_availability > new_out.product_qty:
|
||||
# extra reserved amount goes to no longer linked out
|
||||
reserved_amount_to_remain = new_out.reserved_availability - new_out.product_qty
|
||||
for move_line_id in new_out.move_line_ids:
|
||||
if reserved_amount_to_remain <= 0:
|
||||
break
|
||||
if move_line_id.product_qty > reserved_amount_to_remain:
|
||||
new_move_line = move_line_id.copy({'product_uom_qty': 0, 'qty_done': 0})
|
||||
new_move_line.product_uom_qty = out.product_id.uom_id._compute_quantity(move_line_id.product_qty - reserved_amount_to_remain, move_line_id.product_uom_id, rounding_method='HALF-UP')
|
||||
move_line_id.product_uom_qty -= new_move_line.product_uom_qty
|
||||
new_move_line.move_id = out
|
||||
break
|
||||
else:
|
||||
move_line_id.move_id = out
|
||||
reserved_amount_to_remain -= move_line_id.product_qty
|
||||
(out | new_out)._compute_reserved_availability()
|
||||
out.move_orig_ids = False
|
||||
new_out._recompute_state()
|
||||
out.procure_method = 'make_to_stock'
|
||||
out._recompute_state()
|
||||
@@ -0,0 +1,165 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Reception Report Labels -->
|
||||
<template id="report_reception_report_label">
|
||||
<t t-if="quantity" t-set="qtys" t-value="[int(q) for q in quantity.split(',')]"/>
|
||||
<t t-else="" t-set="qtys" t-value="[1 for q in range(len(docs))]"/>
|
||||
<t t-call="web.basic_layout">
|
||||
<div class="page">
|
||||
<t t-foreach="range(len(docs))" t-as="index">
|
||||
<t t-set="picking" t-value="docs[index]"/>
|
||||
<t t-set="qty" t-value="qtys[index]"/>
|
||||
<t t-foreach="range(qty)" t-as="j">
|
||||
<div class="o_label_page o_label_dymo">
|
||||
<span t-if="picking._name == 'stock.picking' and picking.origin" t-esc="picking.origin"/>
|
||||
<span t-else="" t-esc="picking.name"/>
|
||||
<div class="address"
|
||||
t-if="picking._name == 'stock.picking' and picking.partner_id"
|
||||
t-field="picking.partner_id"
|
||||
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True}'/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<!-- Reception Report -->
|
||||
<record id="stock_reception_report_action" model="ir.actions.report">
|
||||
<field name="name">Reception Report</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="report_type">qweb-html</field>
|
||||
<field name="report_name">stock.report_reception</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_reception_action" model="ir.actions.client">
|
||||
<field name="name">Reception Report</field>
|
||||
<field name="tag">reception_report</field>
|
||||
</record>
|
||||
|
||||
<template id="report_reception_body">
|
||||
<div class="o_report_reception justify-content-between">
|
||||
<div class="o_report_reception_header my-4">
|
||||
<h3>
|
||||
<t t-if="pickings">
|
||||
<t t-foreach="pickings" t-as="receipt">
|
||||
<div>
|
||||
<a href="#" res-model="stock.picking" view-type="form" t-att-res-id="receipt.id">
|
||||
<t t-esc="receipt.display_name"/>
|
||||
</a>
|
||||
<span t-field="receipt.state" t-attf-class="badge badge-pill #{'bg-success-light' if receipt.state == 'done' else 'bg-info-light'}"/>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="reason"/>
|
||||
</t>
|
||||
</h3>
|
||||
</div>
|
||||
<div><table t-if="sources_to_lines" class="o_report_reception_table table table-sm">
|
||||
<t t-foreach="sources_to_lines" t-as="source">
|
||||
<thead t-if="report_type == 'html' or any(line['is_assigned'] for line in sources_to_lines[source])">
|
||||
<tr class="bg-light">
|
||||
<th>
|
||||
<i t-if="source.priority == '1'" class="o_priority o_priority_star fa fa-star"/>
|
||||
<a name="source_link" href="#" t-att-res-model="source._name" view-type="form" t-att-res-id="source.id" t-esc="source.display_name"/>
|
||||
<span t-if="source._name == 'stock.picking' and source.origin">
|
||||
(<t t-esc="source.origin"/>)
|
||||
</span>
|
||||
<span t-if="source._name == 'stock.picking' and source.partner_id">:
|
||||
<a name="source_link" href="#"
|
||||
t-att-res-model="source.partner_id._name"
|
||||
t-att-res-id="source.partner_id.id"
|
||||
view-type="form"
|
||||
t-esc="source.partner_id.name"/>
|
||||
</span>
|
||||
</th>
|
||||
<th>Expected Delivery: <t t-esc="sources_to_formatted_scheduled_date[source]"/></th>
|
||||
<th t-if="report_type == 'html' and any(s['move_ins'] for s in sources_to_lines[source])">
|
||||
<button t-if="any(not s['is_assigned'] and s['is_qty_assignable'] for s in sources_to_lines[source])"
|
||||
class="btn btn-sm btn-primary o_report_reception_assign o_assign_all"
|
||||
name="assign_source_link">
|
||||
Assign All
|
||||
</button>
|
||||
</th>
|
||||
<th t-if="report_type == 'html'">
|
||||
<button t-if="any(s['move_ins'] for s in sources_to_lines[source])"
|
||||
class="btn btn-sm btn-primary o_print_label o_print_label_all"
|
||||
t-att-disabled="not any(s['is_assigned'] for s in sources_to_lines[source])"
|
||||
name="print_labels">
|
||||
Print Labels
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="possible_moves" t-value="sources_to_lines[source]"/>
|
||||
<t t-foreach="possible_moves" t-as="line">
|
||||
<!-- don't print non-assigned lines when printing pdf -->
|
||||
<tr t-if="report_type == 'html' or line['is_assigned']">
|
||||
<td>
|
||||
<t t-esc="line['product']['display_name']"/>
|
||||
</td>
|
||||
<td>
|
||||
<t t-esc="line['quantity']"/>
|
||||
<t groups="uom.group_uom" t-esc="line['uom']"/>
|
||||
<button t-if="report_type == 'html'"
|
||||
class="o_report_reception_forecasted btn btn-link fa fa-area-chart"
|
||||
t-att-move-id="line['move_out'].id"
|
||||
name="forecasted_report_link">
|
||||
</button>
|
||||
</td>
|
||||
<td t-if="report_type == 'html' and line['is_qty_assignable']">
|
||||
<button t-if="not line['is_assigned']"
|
||||
class="btn btn-sm btn-primary o_report_reception_assign"
|
||||
t-attf-model="stock.move"
|
||||
t-att-move-id="line['move_out'].id"
|
||||
t-att-move-ins-ids="line['move_ins']"
|
||||
t-att-qty="line['quantity']"
|
||||
name="assign_link">
|
||||
Assign
|
||||
</button>
|
||||
<button t-if="line['is_assigned']"
|
||||
class="btn btn-sm btn-primary o_report_reception_unassign"
|
||||
t-attf-model="stock.move"
|
||||
t-att-move-id="line['move_out'].id"
|
||||
t-att-move-ins-ids="line['move_ins']"
|
||||
t-att-qty="line['quantity']"
|
||||
name="unassign_link">
|
||||
Unassign
|
||||
</button>
|
||||
</td>
|
||||
<td t-if="report_type == 'html'">
|
||||
<button t-if="line['is_qty_assignable'] and line['source']"
|
||||
t-att-source-id="line['source'].id"
|
||||
t-att-source-model="line['source']._name"
|
||||
t-att-qty="line['quantity']"
|
||||
t-attf-class="btn btn-sm btn-primary o_print_label"
|
||||
t-att-disabled="not line['is_assigned']"
|
||||
name="print_label">
|
||||
Print Label
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</table>
|
||||
<p t-else="">
|
||||
No allocation need found for incoming products.
|
||||
</p></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="report_reception">
|
||||
<t t-call="web.html_container">
|
||||
<t t-if="report_type == 'pdf'" t-call="web.internal_layout">
|
||||
<t t-call="stock.report_reception_body"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-call="stock.report_reception_body"/>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -176,5 +176,15 @@
|
||||
<field name="binding_model_id" ref="model_stock_picking_type"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
<record id="label_picking" model="ir.actions.report">
|
||||
<field name="name">Picking Label</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="report_name">stock.report_reception_report_label</field>
|
||||
<field name="report_file">stock.report_reception_report_label</field>
|
||||
<field name="paperformat_id" ref="product.paperformat_label_sheet_dymo"/>
|
||||
<field name="binding_model_id" ref="model_stock_picking"/>
|
||||
<field name="binding_type">report</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -68,6 +68,16 @@
|
||||
<field name="name">Manage Storage Categories</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_reception_report" model="res.groups">
|
||||
<field name="name">Use Reception Report</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_auto_reception_report" model="res.groups">
|
||||
<field name="name">Display Reception Report at Validation</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="base.default_user" model="res.users">
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import clientAction from 'report.client_action';
|
||||
import core from 'web.core';
|
||||
|
||||
const qweb = core.qweb;
|
||||
|
||||
const ReceptionReport = clientAction.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, action, options) {
|
||||
this._super(...arguments);
|
||||
this.context = Object.assign(action.context || {}, {
|
||||
active_ids: action.context.default_picking_ids,
|
||||
});
|
||||
this.report_name = `stock.report_reception`;
|
||||
this.report_url = `/report/html/${this.report_name}/?context=${JSON.stringify(this.context)}`;
|
||||
this._title = action.name;
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
return Promise.all([
|
||||
this._super(...arguments),
|
||||
]).then(() => {
|
||||
this._renderButtons();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
on_attach_callback: function () {
|
||||
this._super();
|
||||
this.iframe.addEventListener("load",
|
||||
() => this._bindAdditionalActionHandlers(),
|
||||
{ once: true }
|
||||
);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Renders extra report buttons in control panel
|
||||
*/
|
||||
_renderButtons: function () {
|
||||
this.$buttons.append(qweb.render('reception_report_buttons', {}));
|
||||
this.$buttons.on('click', '.o_report_reception_assign', this._onClickAssign.bind(this));
|
||||
this.$buttons.on('click', '.o_print_label', this._onClickPrintLabel.bind(this));
|
||||
this.controlPanelProps.cp_content = {
|
||||
$buttons: this.$buttons,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind additional <button> action handlers
|
||||
*/
|
||||
_bindAdditionalActionHandlers: function () {
|
||||
const rr = $(this.iframe).contents().find('.o_report_reception');
|
||||
rr.on('click', '.o_report_reception_assign', this._onClickAssign.bind(this));
|
||||
rr.on('click', '.o_report_reception_unassign', this._onClickUnassign.bind(this));
|
||||
rr.on('click', '.o_report_reception_forecasted', this._onClickForecastReport.bind(this));
|
||||
rr.on('click', '.o_print_label', this._onClickPrintLabel.bind(this));
|
||||
},
|
||||
|
||||
|
||||
_switchButton: function (button) {
|
||||
button.innerText = button.innerText.includes('Unassign') ? "Assign" : "Unassign";
|
||||
button.name = button.name === 'assign_link' ? 'unassign_link' : 'assign_link';
|
||||
button.classList.toggle("o_report_reception_assign");
|
||||
button.classList.toggle("o_report_reception_unassign");
|
||||
},
|
||||
|
||||
/**
|
||||
* Assign the specified move(s)
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onClickAssign: function (ev) {
|
||||
const el = ev.currentTarget;
|
||||
const quantities = []; // incoming qty amounts to assign
|
||||
const moveIds = [];
|
||||
const inIds = [];
|
||||
let nodeToAssign = [];
|
||||
if (el.name === 'assign_link') { // One line "Assign"
|
||||
nodeToAssign = [el];
|
||||
el.closest('tbody').previousElementSibling.querySelectorAll('.o_print_label_all').forEach(button => button.removeAttribute('disabled'));
|
||||
} else {
|
||||
el.style.display = 'none';
|
||||
if (el.name === "assign_all_link") { // Global "Assign All"
|
||||
const iframe = this.iframe.contentDocument;
|
||||
iframe.querySelectorAll('.o_assign_all').forEach(button => button.style.display = 'none');
|
||||
iframe.querySelectorAll('.o_print_label_all').forEach(button => button.removeAttribute('disabled'));
|
||||
nodeToAssign = iframe.querySelectorAll('.o_report_reception_assign:not(.o_assign_all)');
|
||||
} else { // Local assign all
|
||||
nodeToAssign = el.closest('thead').nextElementSibling.querySelectorAll('.o_report_reception_assign:not(.o_assign_all)');
|
||||
el.closest('thead').nextElementSibling.querySelectorAll('.o_print_label_all').forEach(button => button.removeAttribute('disabled'));
|
||||
}
|
||||
}
|
||||
nodeToAssign.forEach(node => {
|
||||
node.closest('td').nextElementSibling.querySelectorAll('.o_print_label').forEach(button => button.removeAttribute('disabled'));
|
||||
moveIds.push(parseInt(node.getAttribute('move-id')));
|
||||
quantities.push(parseFloat(node.getAttribute('qty')));
|
||||
inIds.push(JSON.parse(node.getAttribute('move-ins-ids')));
|
||||
this._switchButton(node);
|
||||
});
|
||||
|
||||
return this._rpc({
|
||||
model: 'report.stock.report_reception',
|
||||
args: [false, moveIds, quantities, inIds],
|
||||
method: 'action_assign'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Unassign the specified move
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onClickUnassign: function (ev) {
|
||||
const el = ev.currentTarget;
|
||||
this._switchButton(el);
|
||||
const quantity = parseFloat(el.getAttribute('qty'));
|
||||
const modelId = parseInt(el.getAttribute('move-id'));
|
||||
const inIds = JSON.parse("[" + el.getAttribute('move-ins-ids') + "]");
|
||||
el.closest('td').nextElementSibling.querySelectorAll('.o_print_label').forEach(button => button.setAttribute('disabled', true));
|
||||
return this._rpc({
|
||||
model: 'report.stock.report_reception',
|
||||
args: [false, modelId, quantity, inIds[0]],
|
||||
method: 'action_unassign'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the forecast report for the product of the selected move.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onClickForecastReport: function (ev) {
|
||||
const modelId = parseInt(ev.currentTarget.getAttribute('move-id'));
|
||||
return this._rpc({
|
||||
model: 'stock.move',
|
||||
args: [[modelId]],
|
||||
method: 'action_product_forecast_report'
|
||||
}).then((action) => {
|
||||
return this.do_action(action);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Print the corresponding source label
|
||||
*/
|
||||
_onClickPrintLabel: function (ev) {
|
||||
// unfortunately, due to different reports needed for different models, we will handle
|
||||
// pickings here and others models will have to be extended/printed separately until better
|
||||
// technique to merge into continuous pdf to written
|
||||
return this._printLabel(ev, 'stock.report_reception_report_label', 'stock.picking');
|
||||
},
|
||||
|
||||
_printLabel: function (ev, report_file, sourceModel) {
|
||||
const el = ev.currentTarget;
|
||||
const modelIds = [];
|
||||
const productQtys = [];
|
||||
let nodeToPrint = [];
|
||||
|
||||
if (el.name === 'print_label') { // One line print
|
||||
nodeToPrint = [el];
|
||||
} else {
|
||||
if (el.name === "print_all_labels") { // Global "Print Labels"
|
||||
nodeToPrint = this.iframe.contentDocument.querySelectorAll('.o_print_label:not(.o_print_label_all):not(:disabled)');
|
||||
} else { // Local "Print Labels"
|
||||
nodeToPrint = el.closest('thead').nextElementSibling.querySelectorAll('.o_print_label:not(.o_print_label_all):not(:disabled)');
|
||||
}
|
||||
}
|
||||
|
||||
nodeToPrint.forEach(node => {
|
||||
if (node.getAttribute('source-model') === sourceModel) {
|
||||
modelIds.push(parseInt(node.getAttribute('source-id')));
|
||||
productQtys.push(Math.ceil(node.getAttribute('qty')) || '1');
|
||||
}
|
||||
});
|
||||
|
||||
if (!modelIds.length) { // Nothing to print for this model.
|
||||
return Promise.resolve();
|
||||
}
|
||||
const report_name = `${report_file}?docids=${modelIds}&report_type=qweb-pdf&quantity=${productQtys}`;
|
||||
const action = {
|
||||
type: 'ir.actions.report',
|
||||
report_type: 'qweb-pdf',
|
||||
report_name,
|
||||
report_file,
|
||||
};
|
||||
return this.do_action(action);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
core.action_registry.add('reception_report', ReceptionReport);
|
||||
|
||||
export default ReceptionReport;
|
||||
@@ -0,0 +1,46 @@
|
||||
.o_report_reception {
|
||||
|
||||
.o_priority {
|
||||
&.o_priority_star {
|
||||
font-size: 1.35em;
|
||||
&.fa-star {
|
||||
color: gold;
|
||||
}
|
||||
}
|
||||
}
|
||||
& .btn {
|
||||
&.btn-primary {
|
||||
background-color: $o-brand-primary;
|
||||
border-color: $o-brand-primary;
|
||||
&:hover:not([disabled]) {
|
||||
background-color: darken($o-brand-primary, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
& .badge {
|
||||
line-height: .75;
|
||||
}
|
||||
|
||||
@each $-name, $-bg-color in $theme-colors {
|
||||
$-safe-text-color: color-yiq(mix($-bg-color, $o-view-background-color));
|
||||
@include bg-variant(".bg-#{$-name}-light", rgba(theme-color($-name), 0.5), $-safe-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.o_label_page {
|
||||
margin-left: -3mm;
|
||||
margin-right: -3mm;
|
||||
overflow: hidden;
|
||||
page-break-before: always;
|
||||
padding: 1mm 0mm 0mm;
|
||||
|
||||
&.o_label_dymo {
|
||||
font-size:80%;
|
||||
width: 57mm;
|
||||
height: 32mm;
|
||||
}
|
||||
|
||||
span[itemprop="name"] {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="reception_report_buttons">
|
||||
<button
|
||||
name="assign_all_link"
|
||||
class="btn btn-secondary o_report_reception_assign o_assign_all">
|
||||
Assign All
|
||||
</button>
|
||||
<button
|
||||
name="print_all_labels"
|
||||
class="btn btn-secondary o_print_label o_print_label_all">
|
||||
Print Labels
|
||||
</button>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1173,3 +1173,345 @@ class TestReports(TestReportsCommon):
|
||||
self.assertEqual(lines[1]['document_out'].id, delivery_at_confirm.id)
|
||||
self.assertEqual(lines[2]['document_out'].id, delivery_by_date_priority.id)
|
||||
self.assertEqual(lines[3]['document_out'].id, delivery_manual.id)
|
||||
|
||||
def test_report_reception_1_one_receipt(self):
|
||||
""" Create 2 deliveries and 1 receipt where some of the products being received
|
||||
can be reserved for the deliveries. Check that the reception report correctly
|
||||
shows these corresponding potential allocations + correctly reserves incoming moves
|
||||
when reserve button is pushed.
|
||||
"""
|
||||
product2 = self.env['product.product'].create({
|
||||
'name': 'Extra Product',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
product3 = self.env['product.product'].create({
|
||||
'name': 'Unpopular Product',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
# Creates some deliveries for reception report to match against
|
||||
delivery_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
delivery_form.partner_id = self.partner
|
||||
delivery_form.picking_type_id = self.picking_type_out
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 5
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = product2
|
||||
move_line.product_uom_qty = 10
|
||||
delivery1 = delivery_form.save()
|
||||
delivery1.action_confirm()
|
||||
|
||||
delivery_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
delivery_form.partner_id = self.partner
|
||||
delivery_form.picking_type_id = self.picking_type_out
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 2
|
||||
delivery2 = delivery_form.save()
|
||||
delivery2.action_confirm()
|
||||
|
||||
# Create a receipt
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
# incoming qty greater than total (2 moves) outgoing amount => 2 report lines, each = outgoing qty
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 15
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
# outgoing qty greater than incoming amount => report line = incoming qty
|
||||
move_line.product_id = product2
|
||||
move_line.product_uom_qty = 5
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
# not outgoing => shouldn't appear in report
|
||||
move_line.product_id = product3
|
||||
move_line.product_uom_qty = 5
|
||||
receipt = receipt_form.save()
|
||||
|
||||
# check that reception report has correct number of deliveries/outgoing moves
|
||||
# but the quantities aren't available for assignment yet (i.e. can link as chained moves)
|
||||
report = self.env['report.stock.report_reception']
|
||||
report_values = report._get_report_values(docids=[receipt.id])
|
||||
sources_to_lines = report_values['sources_to_lines']
|
||||
self.assertEqual(len(sources_to_lines), 2, "The report has wrong number of outgoing pickings.")
|
||||
all_lines = []
|
||||
for dummy, lines in sources_to_lines.items():
|
||||
for line in lines:
|
||||
self.assertFalse(line['is_qty_assignable'], "The receipt IS DRAFT => its move quantities ARE NOT available to assign.")
|
||||
all_lines.append(line)
|
||||
self.assertEqual(len(all_lines), 3, "The report has wrong number of outgoing moves.")
|
||||
# we expect this order based on move creation
|
||||
self.assertEqual(all_lines[0]['quantity'], 5, "The first move has wrong incoming qty.")
|
||||
self.assertEqual(all_lines[0]['product']['id'], self.product.id, "The first move has wrong incoming product to assign.")
|
||||
self.assertEqual(all_lines[1]['quantity'], 5, "The second move has wrong incoming qty.")
|
||||
self.assertEqual(all_lines[1]['product']['id'], product2.id, "The second move has wrong incoming product to assign.")
|
||||
self.assertEqual(all_lines[2]['quantity'], 2, "The last move has wrong incoming qty.")
|
||||
self.assertEqual(all_lines[2]['product']['id'], self.product.id, "The third move has wrong incoming product to assign.")
|
||||
|
||||
# check that report correctly realizes outgoing moves can be linked when receipt is done
|
||||
receipt.action_confirm()
|
||||
for move in receipt.move_lines:
|
||||
move.quantity_done = move.product_uom_qty
|
||||
receipt.button_validate()
|
||||
report_values = report._get_report_values(docids=[receipt.id])
|
||||
|
||||
sources_to_lines = report_values['sources_to_lines']
|
||||
all_lines = []
|
||||
move_ids = []
|
||||
qtys = []
|
||||
in_ids = []
|
||||
for dummy, lines in sources_to_lines.items():
|
||||
for line in lines:
|
||||
self.assertTrue(line['is_qty_assignable'], "The receipt IS DONE => all of its move quantities ARE assignable")
|
||||
all_lines.append(line)
|
||||
move_ids.append(line['move_out'].id)
|
||||
qtys.append(line['quantity'])
|
||||
in_ids += line['move_ins']
|
||||
# line quantities should be the same when receipt is done compared to when it was draft
|
||||
self.assertEqual(len(all_lines), 3, "The report has wrong number of outgoing moves.")
|
||||
self.assertEqual(all_lines[0]['quantity'], 5, "The first move has wrong incoming qty to reserve.")
|
||||
self.assertEqual(all_lines[0]['product']['id'], self.product.id, "The first move has wrong product to reserve.")
|
||||
self.assertEqual(all_lines[1]['quantity'], 5, "The second move has wrong incoming qty to reserve.")
|
||||
self.assertEqual(all_lines[1]['product']['id'], product2.id, "The second move has wrong product to reserve.")
|
||||
self.assertEqual(all_lines[2]['quantity'], 2, "The last move has wrong incoming qty to reserve.")
|
||||
self.assertEqual(all_lines[2]['product']['id'], self.product.id, "The third move has wrong product to reserve.")
|
||||
|
||||
# check that report assign button works correctly
|
||||
report.action_assign(move_ids, qtys, in_ids)
|
||||
self.assertEqual(len(receipt.move_lines[0].move_dest_ids.ids), 2, "Demand qty of first and last moves should now be linked to incoming.")
|
||||
self.assertEqual(len(receipt.move_lines[1].move_dest_ids.ids), 1, "Demand qty of second move should now be linked to incoming.")
|
||||
self.assertEqual(len(receipt.move_lines[2].move_dest_ids.ids), 0, "product3 should have no moves linked to it.")
|
||||
self.assertEqual(len(delivery1.move_lines.filtered(lambda m: m.product_id == product2)), 2, "product2 outgoing move should be split between linked and non-linked quantities.")
|
||||
|
||||
def test_report_reception_2_two_receipts(self):
|
||||
""" Create 1 delivery and 2 receipts where the products being received
|
||||
can be reserved for the delivery. Check that the reception report correctly
|
||||
shows corresponding potential allocations when receipts have differing states.
|
||||
"""
|
||||
# Creates delivery for reception report to match against
|
||||
delivery_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
delivery_form.partner_id = self.partner
|
||||
delivery_form.picking_type_id = self.picking_type_out
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 100
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
# Create 2 receipts and check its reception report values
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 5
|
||||
receipt1 = receipt_form.save()
|
||||
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 3
|
||||
receipt2 = receipt_form.save()
|
||||
|
||||
# check that report correctly merges not draft incoming quantities
|
||||
report = self.env['report.stock.report_reception']
|
||||
report_values = report._get_report_values(docids=[receipt1.id, receipt2.id])
|
||||
self.assertEqual(len(report_values['pickings']), 2, "There should be 2 receipts to assign from in this report")
|
||||
sources_to_lines = report_values['sources_to_lines']
|
||||
self.assertEqual(len(sources_to_lines), 1, "The report has wrong number of outgoing pickings.")
|
||||
all_lines = list(sources_to_lines.values())[0]
|
||||
self.assertEqual(len(all_lines), 1, "The report has wrong number of outgoing move lines.")
|
||||
self.assertFalse(all_lines[0]['is_qty_assignable'], "The receipt IS NOT done => its move quantities ARE NOT available to reserve (i.e. done).")
|
||||
self.assertEqual(all_lines[0]['quantity'], 8, "The move has wrong incoming qty.")
|
||||
|
||||
# check that report splits assignable and non-assignable quantities when 1 receipt is draft and other is confirmed
|
||||
receipt1.action_confirm()
|
||||
for move in receipt1.move_lines:
|
||||
move.quantity_done = move.product_uom_qty
|
||||
report_values = report._get_report_values(docids=[receipt1.id, receipt2.id])
|
||||
|
||||
sources_to_lines = report_values['sources_to_lines']
|
||||
all_lines = list(sources_to_lines.values())[0]
|
||||
# line quantities depends on done vs not done incoming quantities => should be 2 lines now
|
||||
self.assertEqual(len(all_lines), 2, "The report has wrong number of lines (1 assignable + 1 not).")
|
||||
self.assertEqual(all_lines[0]['quantity'], 5, "The first move has wrong incoming qty to assign.")
|
||||
self.assertTrue(all_lines[0]['is_qty_assignable'], "1 receipt is done => should have 1 reservable move.")
|
||||
self.assertEqual(all_lines[1]['quantity'], 3, "The second move has wrong (expected) incoming qty.")
|
||||
self.assertFalse(all_lines[1]['is_qty_assignable'], "1 receipt is draft => should have 1 non-assignable move.")
|
||||
|
||||
# check that report doesn't allow done and non-done moves at same time
|
||||
receipt1.button_validate()
|
||||
reason = report._get_report_values(docids=[receipt1.id, receipt2.id])['reason']
|
||||
self.assertEqual(reason, "This report cannot be used for done and not done transfers at the same time", "empty report reason not shown")
|
||||
|
||||
def test_report_reception_3_multiwarehouse(self):
|
||||
""" Check that reception report respects same warehouse for
|
||||
receipts and deliveries.
|
||||
"""
|
||||
# Warehouse config.
|
||||
wh_2 = self.env['stock.warehouse'].create({
|
||||
'name': 'Other Warehouse',
|
||||
'code': 'OTHER',
|
||||
})
|
||||
picking_type_out_2 = self.env['stock.picking.type'].search([
|
||||
('code', '=', 'outgoing'),
|
||||
('warehouse_id', '=', wh_2.id),
|
||||
])
|
||||
|
||||
# Creates delivery in warehouse2
|
||||
delivery_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
delivery_form.partner_id = self.partner
|
||||
delivery_form.picking_type_id = picking_type_out_2
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 100
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
# Create a receipt in warehouse1
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 15
|
||||
receipt = receipt_form.save()
|
||||
|
||||
report = self.env['report.stock.report_reception']
|
||||
report_values = report._get_report_values(docids=[receipt.id])
|
||||
self.assertEqual(len(report_values['sources_to_lines']), 0, "The receipt and delivery are in different warehouses => no moves to link to should be found.")
|
||||
|
||||
def test_report_reception_4_pick_pack(self):
|
||||
""" Check that reception report ignores outgoing moves that are not beginning of chain
|
||||
"""
|
||||
|
||||
warehouse = self.env['stock.warehouse'].search([('lot_stock_id', '=', self.stock_location.id)], limit=1)
|
||||
warehouse.write({'delivery_steps': 'pick_pack_ship'})
|
||||
|
||||
ship_move = self.env['stock.move'].create({
|
||||
'name': 'The ship move',
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'location_id': warehouse.wh_output_stock_loc_id.id,
|
||||
'location_dest_id': self.env.ref('stock.stock_location_customers').id,
|
||||
'warehouse_id': warehouse.id,
|
||||
'picking_type_id': warehouse.out_type_id.id,
|
||||
'procure_method': 'make_to_order',
|
||||
'state': 'draft',
|
||||
})
|
||||
|
||||
# create chained pick/pack moves to test with
|
||||
ship_move._assign_picking()
|
||||
ship_move._action_confirm()
|
||||
pack_move = ship_move.move_orig_ids[0]
|
||||
pick_move = pack_move.move_orig_ids[0]
|
||||
|
||||
self.assertEqual(pack_move.state, 'waiting', "Pack move wasn't created...")
|
||||
self.assertEqual(pick_move.state, 'confirmed', "Pick move wasn't created...")
|
||||
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = 15
|
||||
receipt = receipt_form.save()
|
||||
|
||||
report = self.env['report.stock.report_reception']
|
||||
report_values = report._get_report_values(docids=[receipt.id])
|
||||
self.assertEqual(len(report_values['sources_to_lines']), 1, "There should only be 1 line (pick move)")
|
||||
|
||||
def test_report_reception_5_move_splitting(self):
|
||||
""" Check the complicated use cases of correct move splitting when assigning/unassigning when:
|
||||
1. Qty to assign is less than delivery qty demand
|
||||
2. Delivery already has some reserved quants
|
||||
"""
|
||||
incoming_qty = 4
|
||||
outgoing_qty = 10
|
||||
qty_in_stock = outgoing_qty - incoming_qty
|
||||
self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.stock_location.id,
|
||||
'inventory_quantity': qty_in_stock
|
||||
}).action_apply_inventory()
|
||||
|
||||
# create delivery + receipt
|
||||
delivery_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
delivery_form.picking_type_id = self.picking_type_out
|
||||
with delivery_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = outgoing_qty
|
||||
delivery = delivery_form.save()
|
||||
delivery.action_confirm()
|
||||
|
||||
receipt_form = Form(self.env['stock.picking'].with_context(
|
||||
force_detailed_view=True
|
||||
), view='stock.view_picking_form')
|
||||
receipt_form.partner_id = self.partner
|
||||
receipt_form.picking_type_id = self.picking_type_in
|
||||
with receipt_form.move_ids_without_package.new() as move_line:
|
||||
move_line.product_id = self.product
|
||||
move_line.product_uom_qty = incoming_qty
|
||||
receipt = receipt_form.save()
|
||||
receipt.action_confirm()
|
||||
|
||||
self.assertEqual(len(delivery.move_ids_without_package), 1)
|
||||
report = self.env['report.stock.report_reception']
|
||||
|
||||
# -------------------
|
||||
# check report assign
|
||||
# -------------------
|
||||
report.action_assign(delivery.move_ids_without_package.ids, [incoming_qty], receipt.move_ids_without_package.ids)
|
||||
mto_move = delivery.move_ids_without_package.filtered(lambda m: m.procure_method == 'make_to_order')
|
||||
non_mto_move = delivery.move_ids_without_package - mto_move
|
||||
|
||||
# check that delivery move splits correctly when receipt move is assigned to it
|
||||
self.assertEqual(len(delivery.move_ids_without_package), 2, "Delivery moves should have split into assigned + not assigned")
|
||||
self.assertEqual(len(delivery.move_ids_without_package.mapped('move_orig_ids')), 1, "Only 1 delivery + 1 receipt move should be assigned")
|
||||
self.assertEqual(len(receipt.move_ids_without_package.mapped('move_dest_ids')), 1, "Receipt move should remain unsplit")
|
||||
|
||||
# check that assigned (MTO) move is correctly created
|
||||
self.assertEqual(len(mto_move), 1, "Only 1 delivery move should be MTO")
|
||||
self.assertEqual(mto_move.product_uom_qty, incoming_qty, "Incorrect quantity split for MTO move")
|
||||
self.assertEqual(mto_move.reserved_availability, 0, "Receipt is not done => assigned move can't have a reserved qty")
|
||||
self.assertEqual(mto_move.state, 'waiting', "MTO move state not correctly set")
|
||||
|
||||
# check that non-assigned move has correct values
|
||||
self.assertEqual(non_mto_move.product_uom_qty, outgoing_qty - incoming_qty, "Incorrect quantity split for non-MTO move")
|
||||
self.assertEqual(non_mto_move.reserved_availability, qty_in_stock, "Reserved qty not correctly linked to non-MTO move")
|
||||
self.assertEqual(non_mto_move.state, 'assigned', "Fully reserved move has not correctly set state")
|
||||
|
||||
# ---------------------
|
||||
# check report unassign
|
||||
# ---------------------
|
||||
report.action_unassign([mto_move.id], incoming_qty, receipt.move_ids_without_package.ids)
|
||||
self.assertEqual(mto_move.product_uom_qty, incoming_qty, "Move quantities should be unchanged")
|
||||
self.assertEqual(mto_move.procure_method, 'make_to_stock', "Procure method not correctly reset")
|
||||
self.assertEqual(mto_move.state, 'confirmed', "Move state not correctly reset (to non-MTO state)")
|
||||
|
||||
@@ -79,6 +79,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box" id="reception_report">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="group_stock_reception_report"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="group_stock_reception_report"/>
|
||||
<div class="text-muted">
|
||||
View and allocate received quantities.
|
||||
</div>
|
||||
<div class="row mt-2" attrs="{'invisible': [('group_stock_reception_report','=',False)]}">
|
||||
<field name="group_stock_auto_reception_report" class="col-lg-1 ml16 mr0"/>
|
||||
<div class="col pl-0">
|
||||
<label for="group_stock_auto_reception_report"/>
|
||||
<div class="text-muted">
|
||||
Automatically open reception report when a receipt is validated.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Barcode</h2>
|
||||
<div class="row mt16 o_settings_container" name="barcode_setting_container">
|
||||
|
||||
@@ -261,6 +261,11 @@
|
||||
class="oe_stat_button" icon="fa-cubes"
|
||||
attrs="{'invisible': [('has_packages', '=', False)]}"/>
|
||||
<button name="%(action_stock_report)d" icon="fa-arrow-up" class="oe_stat_button" string="Traceability" type="action" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('has_tracking', '=', False)]}" groups="stock.group_production_lot"/>
|
||||
<button name="action_view_reception_report" string="Allocation" type="object"
|
||||
context="{'default_picking_ids': [id]}"
|
||||
class="oe_stat_button" icon="fa-list"
|
||||
attrs="{'invisible': ['|', ('picking_type_code', '=', 'outgoing'), ('picking_type_code', '=', False)]}"
|
||||
groups="stock.group_reception_report"/>
|
||||
<!-- Use the following button to avoid onchange on one2many -->
|
||||
<button name="action_picking_move_tree"
|
||||
class="oe_stat_button"
|
||||
|
||||
@@ -50,6 +50,8 @@ class StockPickingBatch(models.Model):
|
||||
picking_type_id = fields.Many2one(
|
||||
'stock.picking.type', 'Operation Type', check_company=True, copy=False,
|
||||
readonly=True, states={'draft': [('readonly', False)]})
|
||||
picking_type_code = fields.Selection(
|
||||
related='picking_type_id.code')
|
||||
scheduled_date = fields.Datetime(
|
||||
'Scheduled Date', copy=False, store=True, readonly=False, compute="_compute_scheduled_date",
|
||||
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
||||
@@ -226,6 +228,11 @@ class StockPickingBatch(models.Model):
|
||||
else:
|
||||
raise UserError(_("Please add 'Done' quantities to the batch picking to create a new pack."))
|
||||
|
||||
def action_view_reception_report(self):
|
||||
action = self.picking_ids[0].action_view_reception_report()
|
||||
action['context'] = {'default_picking_ids': self.picking_ids.ids}
|
||||
return action
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Miscellaneous
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<form string="Stock Batch Transfer">
|
||||
<field name="show_check_availability" invisible="1"/>
|
||||
<field name="show_validate" invisible="1"/>
|
||||
<field name="picking_type_code" invisible="1"/>
|
||||
<header>
|
||||
<button name="action_confirm" states="draft" string="Confirm" type="object" class="oe_highlight"/>
|
||||
<button name="action_done" string="Validate" type="object" class="oe_highlight"
|
||||
@@ -105,6 +106,12 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,in_progress,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_view_reception_report" string="Allocation" type="object"
|
||||
class="oe_stat_button" icon="fa-list"
|
||||
attrs="{'invisible': ['|', ('picking_ids', '=', []), ('picking_type_code', '=', 'outgoing')]}"
|
||||
groups="stock.group_reception_report"/>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" class="oe_inline"/></h1>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user