addons portados en el repo de ecocentral

This commit is contained in:
GitHub Copilot 2026-08-07 16:34:38 +02:00
parent f2194c5367
commit 65818b0155
72 changed files with 2474 additions and 3 deletions

View file

@ -0,0 +1 @@
from . import models

View file

@ -0,0 +1,22 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# Copyright 2026 Ecocentral, Criptomart
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Stock Picking Report Undelivered Product",
"summary": "Display undelivered product lines on the delivery slip report",
"version": "18.0.1.0.0",
"development_status": "Beta",
"author": "Tecnativa, Ecocentral, Criptomart, Odoo Community Association (OCA)",
"website": "https://github.com/Ecocentral/ecocentral",
"category": "Warehouse",
"license": "AGPL-3",
"depends": [
"stock",
],
"data": [
"views/product_views.xml",
"views/res_partner_views.xml",
"views/res_config_settings_views.xml",
"views/report_deliveryslip.xml",
],
}

View file

@ -0,0 +1,5 @@
from . import product_template
from . import res_config_settings
from . import res_partner
from . import stock_move
from . import stock_move_line

View file

@ -0,0 +1,10 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields
from odoo import models
class ProductTemplate(models.Model):
_inherit = "product.template"
display_undelivered_in_picking = fields.Boolean(default=True)

View file

@ -0,0 +1,33 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields
from odoo import models
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
undelivered_product_slip_report_method = fields.Selection(
related="company_id.undelivered_product_slip_report_method",
readonly=False,
)
class ResCompany(models.Model):
_inherit = "res.company"
undelivered_product_slip_report_method = fields.Selection(
[
("all", "Display all undelivered product lines"),
(
"partially_undelivered",
"Display only partially undelivered product lines",
),
(
"completely_undelivered",
"Display only completely undelivered product lines",
),
],
string="Method to display undelivered product lines in report picking",
default="all",
)

View file

@ -0,0 +1,10 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields
from odoo import models
class ResPartner(models.Model):
_inherit = "res.partner"
display_undelivered_in_picking = fields.Boolean(default=True)

View file

@ -0,0 +1,18 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields
from odoo import models
class StockMove(models.Model):
_inherit = "stock.move"
splitted_stock_move_orig_id = fields.Many2one(
comodel_name="stock.move", string="Splitted from", readonly=True
)
def _prepare_move_split_vals(self, qty):
"""Store origin stock move which created the splitted move."""
vals = super()._prepare_move_split_vals(qty)
vals["splitted_stock_move_orig_id"] = self.id
return vals

View file

@ -0,0 +1,23 @@
# Copyright 2022 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import models
class StockMoveLine(models.Model):
_inherit = "stock.move.line"
def _get_aggregated_product_quantities(self, **kwargs):
"""Odoo displays undelivered products in the main table too.
Remove them from the aggregated dict so they only show up in the
bottom "remaining products" table.
"""
aggregated_move_lines = super()._get_aggregated_product_quantities(**kwargs)
keys_to_remove = {
key
for key, values in aggregated_move_lines.items()
if not values["quantity"]
}
for key in keys_to_remove:
aggregated_move_lines.pop(key, None)
return aggregated_move_lines

View file

@ -0,0 +1,8 @@
Muestra en el informe de albarán (delivery slip) las líneas de producto que
no se han servido: aquellas cuyo ``stock.move`` acaba cancelado (sin
backorder) porque el producto pedido no llegó a entregarse, total o
parcialmente.
Configurable por partner y por producto (casilla "Mostrar no servidos en el
albarán", activada por defecto), y por compañía (mostrar todas las líneas no
servidas, solo las parcialmente servidas, o solo las totalmente no servidas).

View file

@ -0,0 +1,5 @@
En Inventario > Configuración > Ajustes, sección "Report picking undelivered
products", elegir el método (todas / solo parciales / solo completas).
En la ficha del partner o del producto, casilla "Display Undelivered In
Picking" para excluirlo del bloque de no servidos.

View file

@ -0,0 +1 @@
from . import test_stock_picking_report_undelivered_product

View file

@ -0,0 +1,188 @@
# Copyright 2020 Tecnativa - Sergio Teruel
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.tests import Form
from odoo.tests import TransactionCase
class TestStockPickingReportUndeliveredProduct(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ResPartner = cls.env["res.partner"]
cls.ProductProduct = cls.env["product.product"]
cls.StockPicking = cls.env["stock.picking"]
cls.StockQuant = cls.env["stock.quant"]
cls.BackOrderWiz = cls.env["stock.backorder.confirmation"]
cls.warehouse = cls.env.ref("stock.warehouse0")
cls.picking_type_out = cls.env.ref("stock.picking_type_out")
cls.partner_display = cls.ResPartner.create(
{"name": "Partner for test display", "display_undelivered_in_picking": True}
)
cls.partner_no_display = cls.ResPartner.create(
{
"name": "Partner for test on display",
"display_undelivered_in_picking": False,
}
)
cls.product_display = cls.ProductProduct.create(
{
"name": "Test product undelivered display",
"display_undelivered_in_picking": True,
"type": "consu",
"is_storable": True,
}
)
cls.product_no_display = cls.ProductProduct.create(
{
"name": "Test product undelivered no display",
"display_undelivered_in_picking": False,
"type": "consu",
"is_storable": True,
}
)
cls.product_no_display_wo_stock = cls.ProductProduct.create(
{
"name": "Test product undelivered no display without stock",
"display_undelivered_in_picking": False,
"type": "consu",
"is_storable": True,
}
)
cls.StockQuant.create(
{
"product_id": cls.product_no_display.id,
"location_id": cls.warehouse.lot_stock_id.id,
"quantity": 2000,
}
)
def _create_picking(self, partner):
picking_form = Form(self.StockPicking)
picking_form.picking_type_id = self.picking_type_out
picking_form.partner_id = partner
with picking_form.move_ids_without_package.new() as line:
line.product_id = self.product_display
line.product_uom_qty = 50.00
with picking_form.move_ids_without_package.new() as line:
line.product_id = self.product_no_display
line.product_uom_qty = 20.00
with picking_form.move_ids_without_package.new() as line:
line.product_id = self.product_no_display_wo_stock
line.product_uom_qty = 20.00
return picking_form.save()
def _transfer_picking_no_backorder(self, picking):
# Transfer picking with no create backorder option
picking.move_line_ids.picked = True
backorder_wizard = self.BackOrderWiz.create({"pick_ids": [(4, picking.id)]})
backorder_wizard.with_context(
button_validate_picking_ids=picking.id
).process_cancel_backorder()
def _render(self, picking):
return self.env["ir.actions.report"]._render_qweb_html(
"stock.report_deliveryslip", picking.ids
)
def test_displayed_customer(self):
picking = self._create_picking(self.partner_display)
picking.action_confirm()
picking.action_assign()
picking.move_line_ids.filtered(
lambda ml: ml.product_id == self.product_display
).quantity = 10.00
self._transfer_picking_no_backorder(picking)
res = self._render(picking)
self.assertIn("undelivered_product", str(res[0]))
def test_no_displayed_customer(self):
picking = self._create_picking(self.partner_no_display)
picking.action_confirm()
picking.action_assign()
picking.move_line_ids.filtered(
lambda ml: ml.product_id == self.product_display
).quantity = 10.00
self._transfer_picking_no_backorder(picking)
res = self._render(picking)
self.assertNotIn("undelivered_product", str(res[0]))
def test_no_displayed_product(self):
picking = self._create_picking(self.partner_display)
picking.move_ids.filtered(
lambda move: move.product_id == self.product_display
).unlink()
picking.action_confirm()
picking.action_assign()
picking.move_line_ids.quantity = 10.00
self._transfer_picking_no_backorder(picking)
res = self._render(picking)
self.assertNotIn("undelivered_product", str(res[0]))
def test_picking_report_method(self):
product = self.ProductProduct.create(
{
"name": "test01",
"display_undelivered_in_picking": True,
"type": "consu",
"is_storable": True,
}
)
product2 = self.ProductProduct.create(
{
"name": "test02",
"display_undelivered_in_picking": True,
"type": "consu",
"is_storable": True,
}
)
self.StockQuant.create(
{
"product_id": product.id,
"location_id": self.warehouse.lot_stock_id.id,
"quantity": 2000,
}
)
picking_form = Form(self.StockPicking)
picking_form.picking_type_id = self.picking_type_out
picking_form.partner_id = self.partner_display
with picking_form.move_ids_without_package.new() as line:
line.product_id = product
line.product_uom_qty = 50.00
with picking_form.move_ids_without_package.new() as line:
line.product_id = product2
line.product_uom_qty = 20.00
picking = picking_form.save()
picking.action_confirm()
picking.action_assign()
picking.move_line_ids.filtered(lambda ml: ml.product_id == product).quantity = (
10.00
)
self._transfer_picking_no_backorder(picking)
# Empty setting method field
picking.company_id.undelivered_product_slip_report_method = False
res = self._render(picking)
self.assertIn("test02", str(res[0]))
# Print all undelivered lines, partial and completely lines
picking.company_id.undelivered_product_slip_report_method = "all"
res = self._render(picking)
self.assertIn("test02", str(res[0]))
# Print only partial undelivered lines
picking.company_id.undelivered_product_slip_report_method = (
"partially_undelivered"
)
res = self._render(picking)
self.assertNotIn("test02", str(res[0]))
# Print only completely undelivered lines
picking.company_id.undelivered_product_slip_report_method = (
"completely_undelivered"
)
res = self._render(picking)
self.assertNotIn("partially_undelivered_line", str(res[0]))

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_template_property_form" model="ir.ui.view">
<field
name="name"
>product.template.form.inherit.undelivered.product</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="stock.view_template_property_form" />
<field name="arch" type="xml">
<group name="group_lots_and_weight" position="inside">
<field name="display_undelivered_in_picking" />
</group>
</field>
</record>
</odoo>

View file

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<template id="undeliverd_product">
<t
t-if="o.partner_id.display_undelivered_in_picking and o.state == 'done'"
>
<!-- Get stock moves from backorder according to setting values -->
<t
t-if="not o.company_id.undelivered_product_slip_report_method or o.company_id.undelivered_product_slip_report_method == 'all'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.quantity == 0.0 and l.state == 'cancel' and l.product_id.display_undelivered_in_picking)"
/>
</t>
<t
t-if="o.company_id.undelivered_product_slip_report_method == 'partially_undelivered'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.state == 'cancel' and l.product_id.display_undelivered_in_picking and l.splitted_stock_move_orig_id)"
/>
</t>
<t
t-if="o.company_id.undelivered_product_slip_report_method == 'completely_undelivered'"
>
<t
t-set="undelivered_moves"
t-value="o.mapped('move_ids').filtered(lambda l: l.state == 'cancel' and l.product_id.display_undelivered_in_picking and not l.splitted_stock_move_orig_id)"
/>
</t>
<table
t-if="undelivered_moves"
class="table table-sm table-bordered mt0"
id="undelivered_product"
>
<thead>
<tr><th colspan="7"><strong
>Remaining products (not delivered yet)</strong></th></tr>
<tr>
<th><strong>Reference</strong></th>
<th><strong>Product</strong></th>
<th class="text-end"><strong>Quantity</strong></th>
</tr>
</thead>
<tbody>
<t
t-foreach="undelivered_moves.sorted(key=lambda l: (l.product_id.default_code or '', l.product_id.name))"
t-as="undelivered_move"
>
<tr
t-att-class="'partially_undelivered_line' if undelivered_move.splitted_stock_move_orig_id else 'completely_undelivered_line'"
>
<td>
<span
t-field="undelivered_move.product_id.default_code"
/>
</td>
<td>
<span
t-field="undelivered_move.product_id.name"
/>
</td>
<td class="text-end">
<span
t-field="undelivered_move.product_uom_qty"
/> <span
groups="uom.group_uom"
t-field="undelivered_move.product_uom"
/>
</td>
</tr>
</t>
</tbody>
</table>
</t>
</template>
<template
id="report_delivery_document"
inherit_id="stock.report_delivery_document"
priority="200"
>
<xpath expr="//table[@name='stock_move_line_table']" position="after">
<t
t-call="stock_picking_report_undelivered_product.undeliverd_product"
/>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field
name="name"
>res.config.settings.view.form.inherit.undelivered.product</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form" />
<field name="arch" type="xml">
<block name="operations_setting_container" position="inside">
<setting
id="undelivered_product_slip_report_method"
string="Undelivered products"
help="Method to display undelivered product lines in the delivery slip report"
>
<field
name="undelivered_product_slip_report_method"
class="o_light_label"
widget="selection"
/>
</setting>
</block>
</field>
</record>
</odoo>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_partner_stock_form" model="ir.ui.view">
<field name="name">res.partner.form.inherit.undelivered.product</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="stock.view_partner_stock_form" />
<field name="arch" type="xml">
<group name="inventory" position="inside">
<field name="display_undelivered_in_picking" />
</group>
</field>
</record>
</odoo>