Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 28dddcb

Browse files
committedNov 28, 2024·
[ADD] delivery_roulier_picking_batch
1 parent 85abe2c commit 28dddcb

16 files changed

+1384
-0
lines changed
 
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
==============================
2+
Delivery Roulier Picking Batch
3+
==============================
4+
5+
..
6+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
7+
!! This file is generated by oca-gen-addon-readme !!
8+
!! changes will be overwritten. !!
9+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
10+
!! source digest: sha256:3874042f9be1a0b3f52c99bbcaf8cde90bc384969de0112059d1fffd216be41c
11+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12+
13+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
14+
:target: https://odoo-community.org/page/development-status
15+
:alt: Beta
16+
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
17+
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
18+
:alt: License: AGPL-3
19+
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdelivery--carrier-lightgray.png?logo=github
20+
:target: https://github.com/OCA/delivery-carrier/tree/14.0/delivery_roulier_picking_batch
21+
:alt: OCA/delivery-carrier
22+
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
23+
:target: https://translation.odoo-community.org/projects/delivery-carrier-14-0/delivery-carrier-14-0-delivery_roulier_picking_batch
24+
:alt: Translate me on Weblate
25+
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
26+
:target: https://runboat.odoo-community.org/builds?repo=OCA/delivery-carrier&target_branch=14.0
27+
:alt: Try me on Runboat
28+
29+
|badge1| |badge2| |badge3| |badge4| |badge5|
30+
31+
This module allows to generate a unique delivery label/tracking for a
32+
whole batch of pickings.
33+
34+
The batch pickings operations will be grouped in a single delivery
35+
package if they are not already in a delivery package. In case of a
36+
batch with multiple packages, a label per package will be created.
37+
38+
This module is only compatible with ``delivery_roulier`` carriers.
39+
40+
**Table of contents**
41+
42+
.. contents::
43+
:local:
44+
45+
Usage
46+
=====
47+
48+
Create a picking batch from
49+
``Inventory > Operations > Batch Transfers``, add some pickings to it
50+
and then enter a valid roulier compatible carrier in the batch
51+
``Additional Info`` notebook page.
52+
53+
Then validate the batch and a delivery label will be generated for each
54+
package in the batch. If there's some operations that are not in a
55+
package, a new package will be created for them.
56+
57+
A tracking number will be generated for each package and the tracking
58+
will be available by clicking the ``Tracking`` smart button.
59+
60+
Bug Tracker
61+
===========
62+
63+
Bugs are tracked on `GitHub Issues <https://github.com/OCA/delivery-carrier/issues>`_.
64+
In case of trouble, please check there if your issue has already been reported.
65+
If you spotted it first, help us to smash it by providing a detailed and welcomed
66+
`feedback <https://github.com/OCA/delivery-carrier/issues/new?body=module:%20delivery_roulier_picking_batch%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
67+
68+
Do not contact contributors directly about support or help with technical issues.
69+
70+
Credits
71+
=======
72+
73+
Authors
74+
-------
75+
76+
* Akretion
77+
78+
Contributors
79+
------------
80+
81+
- Florian Mounier florian.mounier@akretion.com
82+
83+
Maintainers
84+
-----------
85+
86+
This module is maintained by the OCA.
87+
88+
.. image:: https://odoo-community.org/logo.png
89+
:alt: Odoo Community Association
90+
:target: https://odoo-community.org
91+
92+
OCA, or the Odoo Community Association, is a nonprofit organization whose
93+
mission is to support the collaborative development of Odoo features and
94+
promote its widespread use.
95+
96+
This module is part of the `OCA/delivery-carrier <https://github.com/OCA/delivery-carrier/tree/14.0/delivery_roulier_picking_batch>`_ project on GitHub.
97+
98+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
{
6+
"name": "Delivery Roulier Picking Batch",
7+
"version": "14.0.1.0.0",
8+
"author": "Akretion, Odoo Community Association (OCA)",
9+
"summary": "Use roulier in batch picking",
10+
"category": "Warehouse",
11+
"depends": [
12+
"delivery_roulier",
13+
"stock_picking_batch",
14+
],
15+
"website": "https://github.com/OCA/delivery-carrier",
16+
"data": [
17+
"views/stock_picking_batch_views.xml",
18+
],
19+
"demo": [],
20+
"installable": True,
21+
"license": "AGPL-3",
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import stock_picking
2+
from . import stock_quant_package
3+
from . import stock_picking_batch
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from functools import reduce
5+
6+
from odoo import _, models
7+
from odoo.exceptions import UserError
8+
9+
10+
class StockPicking(models.Model):
11+
_inherit = "stock.picking"
12+
13+
def _is_batch_roulier(self):
14+
# Check if the picking is part of a batch with a roulier carrier
15+
self.ensure_one()
16+
return (
17+
self.batch_id
18+
and self.batch_id.carrier_id
19+
and self.batch_id.carrier_id._is_roulier()
20+
)
21+
22+
def send_to_shipper(self):
23+
self.ensure_one()
24+
if self._is_batch_roulier():
25+
# We are in a batch with a roulier carrier
26+
# We need to send unsent packages
27+
packages = self.package_ids - self.batch_id.sent_package_ids
28+
if not packages:
29+
# Nothing to send
30+
return
31+
32+
# First sanity checks
33+
# Check that all package pickings have the same sender/receiver:
34+
for package in packages:
35+
package_pickings = (
36+
self.env["stock.move.line"]
37+
.search(
38+
[
39+
"|",
40+
("result_package_id", "=", package.id),
41+
("package_id", "=", package.id),
42+
]
43+
)
44+
.mapped("picking_id")
45+
)
46+
47+
# Set carrier on pickings
48+
package_pickings.write({"carrier_id": self.carrier_id.id})
49+
50+
# Check sender/receiver uniformity
51+
for kind in ("sender", "receiver"):
52+
addresses = reduce(
53+
lambda x, y: x | y,
54+
(
55+
getattr(package_picking, f"_get_{kind}")()
56+
or self.env["res.partner"]
57+
for package_picking in package_pickings
58+
),
59+
)
60+
if not addresses:
61+
raise UserError(
62+
_(
63+
"Can't determine %(kind)s address for pickings: %(pickings)s"
64+
)
65+
% {
66+
"kind": kind,
67+
"pickings": ", ".join(package_pickings.mapped("name")),
68+
}
69+
)
70+
if len(addresses) > 1:
71+
raise UserError(
72+
_(
73+
"Multiple %(kind)s addresses found for pickings: %(pickings)s"
74+
)
75+
% {
76+
"kind": kind,
77+
"pickings": ", ".join(package_pickings.mapped("name")),
78+
}
79+
)
80+
81+
# Send packages
82+
res = self.batch_id.carrier_id.send_shipping(self)[0]
83+
# Mark packages as sent (for use in _roulier_generate_labels)
84+
self.batch_id.sent_package_ids |= packages
85+
# Update tracking number
86+
if res["tracking_number"]:
87+
self.batch_id.carrier_tracking_ref = ";".join(
88+
[
89+
tracking
90+
for tracking in (
91+
self.batch_id.carrier_tracking_ref,
92+
res["tracking_number"],
93+
)
94+
if tracking
95+
]
96+
)
97+
return
98+
99+
return super().send_to_shipper()
100+
101+
def _roulier_generate_labels(self):
102+
if self._is_batch_roulier():
103+
label_info = []
104+
for picking in self:
105+
# Generate labels only for unsent packages
106+
packages = picking.package_ids - picking.batch_id.sent_package_ids
107+
label_info.append(packages._generate_labels(picking))
108+
return label_info
109+
110+
return super()._roulier_generate_labels()
111+
112+
def get_shipping_label_values(self, label):
113+
self.ensure_one()
114+
if self._is_batch_roulier():
115+
# Attach the label to the batch instead of the picking
116+
return {
117+
"name": label["name"],
118+
"res_id": self.batch_id.id,
119+
"res_model": "stock.picking.batch",
120+
"datas": label["file"],
121+
"file_type": label["file_type"],
122+
}
123+
return super().get_shipping_label_values(label)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
from odoo import _, api, fields, models
5+
from odoo.exceptions import UserError
6+
from odoo.tools.float_utils import float_compare
7+
8+
9+
class StockPickingBatch(models.Model):
10+
_inherit = "stock.picking.batch"
11+
12+
def _get_default_weight_uom(self):
13+
return self.env[
14+
"product.template"
15+
]._get_weight_uom_name_from_ir_config_parameter()
16+
17+
carrier_price = fields.Float(string="Shipping Cost")
18+
delivery_type = fields.Selection(related="carrier_id.delivery_type", readonly=True)
19+
carrier_id = fields.Many2one(
20+
"delivery.carrier", string="Carrier", check_company=True
21+
)
22+
weight = fields.Float(
23+
compute="_compute_weight",
24+
digits="Stock Weight",
25+
store=True,
26+
help="Total weight of the products in the picking.",
27+
compute_sudo=True,
28+
)
29+
carrier_tracking_ref = fields.Char(string="Tracking Reference", copy=False)
30+
carrier_tracking_url = fields.Char(
31+
string="Tracking URL", compute="_compute_carrier_tracking_url"
32+
)
33+
weight_uom_name = fields.Char(
34+
string="Weight unit of measure label",
35+
compute="_compute_weight_uom_name",
36+
readonly=True,
37+
default=_get_default_weight_uom,
38+
)
39+
sent_package_ids = fields.One2many(
40+
"stock.quant.package",
41+
"batch_id",
42+
string="Packages",
43+
)
44+
45+
@api.constrains("carrier_id")
46+
def _check_carrier_id_is_roulier(self):
47+
for batch in self:
48+
if batch.carrier_id and not batch.carrier_id._is_roulier():
49+
raise UserError(_("Only Roulier carrier is supported"))
50+
51+
def _compute_weight_uom_name(self):
52+
for package in self:
53+
package.weight_uom_name = self.env[
54+
"product.template"
55+
]._get_weight_uom_name_from_ir_config_parameter()
56+
57+
@api.depends("carrier_id", "carrier_tracking_ref")
58+
def _compute_carrier_tracking_url(self):
59+
for batch in self:
60+
batch.carrier_tracking_url = (
61+
(
62+
# Similar flawed logic as in delivery_roulier
63+
batch.picking_ids.package_ids[0]._get_tracking_link()
64+
if batch.carrier_tracking_ref
65+
and len(batch.picking_ids.package_ids) > 0
66+
else False
67+
)
68+
if batch.carrier_id and batch.carrier_id._is_roulier()
69+
else False
70+
)
71+
72+
@api.depends("move_ids")
73+
def _compute_weight(self):
74+
for batch in self:
75+
batch.weight = sum(
76+
move.weight for move in batch.move_ids if move.state != "cancel"
77+
)
78+
79+
def cancel_shipment(self):
80+
for batch in self:
81+
batch.carrier_id.cancel_shipment(self)
82+
msg = "Shipment %s cancelled" % batch.carrier_tracking_ref
83+
batch.message_post(body=msg)
84+
batch.carrier_tracking_ref = False
85+
batch.sent_package_ids = [(5, 0, 0)]
86+
87+
def action_done(self):
88+
if not self.carrier_id or not (
89+
self.carrier_id.integration_level == "rate_and_ship"
90+
and self.picking_type_id.code != "incoming"
91+
):
92+
return super().action_done()
93+
94+
self.ensure_one()
95+
pickings = self.picking_ids.filtered(
96+
lambda picking: picking.state not in ("cancel", "done")
97+
)
98+
if pickings.carrier_id - self.carrier_id:
99+
raise UserError(
100+
_("Pickings %(pickings)s already have a different carrier")
101+
% {
102+
"pickings": ", ".join(
103+
pickings.filtered(
104+
lambda p: p.carrier_id and p.carrier_id != self.carrier_id
105+
).mapped("name")
106+
)
107+
}
108+
)
109+
pickings.write({"carrier_id": self.carrier_id.id})
110+
# Delivery Roulier works with packages, so we need to generate a package
111+
# if it doesn't exist, this is simalar to action_put_in_pack but without
112+
# the checks
113+
picking_move_lines = self.move_line_ids
114+
move_line_ids = picking_move_lines.filtered(
115+
lambda ml: float_compare(
116+
ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding
117+
)
118+
> 0
119+
and not ml.result_package_id
120+
)
121+
if not move_line_ids:
122+
move_line_ids = picking_move_lines.filtered(
123+
lambda ml: float_compare(
124+
ml.product_uom_qty,
125+
0.0,
126+
precision_rounding=ml.product_uom_id.rounding,
127+
)
128+
> 0
129+
and float_compare(
130+
ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding
131+
)
132+
== 0
133+
)
134+
if move_line_ids:
135+
move_line_ids.picking_id[0]._put_in_pack(move_line_ids, False)
136+
137+
return super().action_done()
138+
139+
def open_website_url(self):
140+
"""Open tracking page.
141+
142+
More than 1 tracking number: display a list of packages
143+
Else open directly the tracking page
144+
"""
145+
self.ensure_one()
146+
if not self.carrier_id or not self.carrier_id._is_roulier():
147+
return super().open_website_url()
148+
149+
packages = self.sent_package_ids
150+
if len(packages) == 0:
151+
raise UserError(_("No packages found for this picking"))
152+
elif len(packages) == 1:
153+
return packages.open_website_url() # shortpath
154+
155+
# display a list of pickings
156+
xmlid = "stock.action_package_view"
157+
action = self.env["ir.actions.act_window"]._for_xml_id(xmlid)
158+
action["domain"] = [("id", "in", packages.ids)]
159+
action["context"] = {"batch_id": self.id}
160+
return action
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2024 Akretion (http://www.akretion.com).
2+
# @author Florian Mounier <florian.mounier@akretion.com>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
4+
5+
from odoo import fields, models
6+
7+
8+
class StockQuantPackage(models.Model):
9+
_inherit = "stock.quant.package"
10+
11+
batch_id = fields.Many2one("stock.picking.batch", string="Package sent from batch")
12+
13+
def _roulier_prepare_attachments(self, picking, response):
14+
attachments = super()._roulier_prepare_attachments(picking, response)
15+
if picking._is_batch_roulier():
16+
for attachment in attachments:
17+
# We need to change the attachment res_model and res_id for it
18+
# to be linked to the batch instead of the picking
19+
if attachment["res_model"] == "stock.picking":
20+
attachment["res_model"] = "stock.picking.batch"
21+
attachment["res_id"] = picking.batch_id.id
22+
return attachments
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Florian Mounier <florian.mounier@akretion.com>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
This module allows to generate a unique delivery label/tracking for a whole batch of
2+
pickings.
3+
4+
The batch pickings operations will be grouped in a single delivery package if they are
5+
not already in a delivery package. In case of a batch with multiple packages, a label
6+
per package will be created.
7+
8+
This module is only compatible with `delivery_roulier` carriers.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Create a picking batch from `Inventory > Operations > Batch Transfers`, add some
2+
pickings to it and then enter a valid roulier compatible carrier in the batch
3+
`Additional Info` notebook page.
4+
5+
Then validate the batch and a delivery label will be generated for each package in the
6+
batch. If there's some operations that are not in a package, a new package will be
7+
created for them.
8+
9+
A tracking number will be generated for each package and the tracking will be available
10+
by clicking the `Tracking` smart button.

‎delivery_roulier_picking_batch/static/description/index.html

+441
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_delivery_roulier_picking_batch

‎delivery_roulier_picking_batch/tests/test_delivery_roulier_picking_batch.py

+422
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<!--
3+
Copyright 2024 Akretion (http://www.akretion.com).
4+
@author Florian Mounier <florian.mounier@akretion.com>
5+
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
6+
-->
7+
<odoo>
8+
9+
<record id="stock_picking_batch_form" model="ir.ui.view">
10+
<field name="model">stock.picking.batch</field>
11+
<field name="inherit_id" ref="stock_picking_batch.stock_picking_batch_form" />
12+
<field name="arch" type="xml">
13+
<xpath expr="//notebook/page[2]" position="after">
14+
<page string="Additional Info" name="extra">
15+
<group name='carrier_data' string="Shipping Information">
16+
<field
17+
name="carrier_id"
18+
attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"
19+
options="{'no_create': True, 'no_open': True}"
20+
/>
21+
<field name="delivery_type" attrs="{'invisible':True}" />
22+
<label for="carrier_tracking_ref" />
23+
<div name="tracking">
24+
<field
25+
name="carrier_tracking_ref"
26+
class="oe_inline"
27+
attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"
28+
/>
29+
<button
30+
type='object'
31+
class="fa fa-arrow-right oe_link"
32+
name="cancel_shipment"
33+
string="Cancel"
34+
attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"
35+
/>
36+
</div>
37+
<label for="weight" string="Weight" />
38+
<div>
39+
<field name="weight" class="oe_inline" />
40+
<field
41+
name="weight_uom_name"
42+
nolabel="1"
43+
class="oe_inline"
44+
style="margin-left:5px"
45+
/>
46+
</div>
47+
</group>
48+
</page>
49+
</xpath>
50+
<xpath expr="//div[hasclass('oe_title')]" position="before">
51+
<div class="oe_button_box" name="button_box">
52+
<button
53+
type="object"
54+
name="open_website_url"
55+
class="oe_stat_button"
56+
icon='fa-truck'
57+
string="Tracking"
58+
attrs="{'invisible': ['|','|',('carrier_tracking_ref','=',False),('carrier_id', '=', False),('delivery_type','=','grid')]}"
59+
/>
60+
</div>
61+
</xpath>
62+
</field>
63+
</record>
64+
65+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../delivery_roulier_picking_batch
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import setuptools
2+
3+
setuptools.setup(
4+
setup_requires=['setuptools-odoo'],
5+
odoo_addon=True,
6+
)

0 commit comments

Comments
 (0)
Please sign in to comment.