Skip to content

Commit 8296765

Browse files
committed
Merge PR #863 into 14.0
Signed-off-by jbaudoux
2 parents aaee65a + 8ca839b commit 8296765

17 files changed

+418
-0
lines changed

delivery_purchase_label/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright 2024 Camptocamp SA
2+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
3+
{
4+
"name": "Delivery Purchase Label",
5+
"summary": "Allows printing carrier delivery labels for a dropshipping vendor.",
6+
"version": "14.0.1.0.0",
7+
"category": "Delivery",
8+
"website": "https://github.com/OCA/delivery-carrier",
9+
"author": "Camptocamp, BCIM, Odoo Community Association (OCA)",
10+
"maintainers": ["TDu", "jbaudoux"],
11+
"installable": True,
12+
"license": "AGPL-3",
13+
"depends": ["delivery", "purchase"],
14+
"data": [
15+
"data/stock_picking_type.xml",
16+
"views/delivery_carrier.xml",
17+
"views/purchase_order_views.xml",
18+
],
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<odoo noupdate="1">
2+
<record id="picking_type_send_label" model="stock.picking.type">
3+
<field name="name">Dropship Carrier Label</field>
4+
<field name="code">internal</field>
5+
<field name="sequence_code">LBL</field>
6+
<field name="default_location_dest_id" ref="stock.stock_location_suppliers" />
7+
<field name="default_location_src_id" ref="stock.stock_location_suppliers" />
8+
</record>
9+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from . import delivery_carrier
2+
from . import mail_compose_message
3+
from . import purchase_order
4+
from . import stock_picking
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright 2024 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3+
4+
from odoo import fields, models
5+
6+
7+
class DeliveryCarrier(models.Model):
8+
_inherit = "delivery.carrier"
9+
10+
purchase_label_picking_type = fields.Many2one(
11+
string="Operation Type For Purchase Label", comodel_name="stock.picking.type"
12+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3+
4+
from odoo import api, models
5+
6+
7+
class MailComposer(models.TransientModel):
8+
_inherit = "mail.compose.message"
9+
10+
@api.model
11+
def generate_email_for_composer(self, template_id, res_ids, fields):
12+
res = super().generate_email_for_composer(template_id, res_ids, fields)
13+
if self.model != "purchase.order":
14+
return res
15+
if (
16+
template_id
17+
in self.env["purchase.order"]._mail_templates_to_not_attach_labels()
18+
):
19+
return
20+
purchase_orders = self.env["purchase.order"].browse(res_ids)
21+
for order in purchase_orders:
22+
# Add the labels generated when sending the order by email
23+
label_picking = order.delivery_label_picking_id
24+
if not label_picking:
25+
continue
26+
attachments = self.env["ir.attachment"].search(
27+
[
28+
("res_model", "=", label_picking._name),
29+
("res_id", "=", label_picking.id),
30+
]
31+
)
32+
if attachments:
33+
attachment_ids = (
34+
res[order.id].get("attachment_ids", []) + attachments.ids
35+
)
36+
res[order.id]["attachment_ids"] = attachment_ids
37+
return res
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
# Copyright 2024 Camptocamp SA
2+
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
3+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
4+
5+
from odoo import SUPERUSER_ID, _, api, fields, models
6+
from odoo.exceptions import UserError
7+
8+
9+
class SameDeliveryLabelGenerated(Exception):
10+
pass
11+
12+
13+
class PurchaseOrder(models.Model):
14+
_inherit = "purchase.order"
15+
16+
vendor_label_carrier_id = fields.Many2one(
17+
"delivery.carrier",
18+
"Vendor Label Carrier",
19+
domain=[("purchase_label_picking_type", "!=", False)],
20+
)
21+
delivery_label_picking_id = fields.Many2one(
22+
"stock.picking",
23+
"Delivery Label Picking",
24+
store=True,
25+
readonly=True,
26+
)
27+
28+
@api.model
29+
def _states_to_generate_delivery_label(self):
30+
"""Labels will be (re)generated only if the PO is in one of these states."""
31+
return ["draft", "sent"]
32+
33+
@api.model
34+
def _mail_templates_to_not_attach_labels(self):
35+
return self.env.ref("purchase.email_template_edi_purchase_reminder").ids
36+
37+
def action_rfq_send(self):
38+
self.ensure_one()
39+
self._generate_purchase_delivery_label()
40+
return super().action_rfq_send()
41+
42+
def button_cancel(self):
43+
self._cancel_purchase_delivery_label_picking()
44+
return super().button_cancel()
45+
46+
def _is_valid_for_vendor_labels(self):
47+
self.ensure_one()
48+
if self.state not in self._states_to_generate_delivery_label():
49+
return False
50+
if not self.dest_address_id:
51+
return False
52+
if not any(
53+
product.type in ["product", "consu"]
54+
for product in self.order_line.product_id
55+
):
56+
return False
57+
if not self.vendor_label_carrier_id.purchase_label_picking_type:
58+
return False
59+
return True
60+
61+
def _are_delivery_label_equivalent(self, pick_1, pick_2):
62+
if len(pick_1.move_lines) != len(pick_2.move_lines):
63+
return False
64+
for move_old, move_new in zip(pick_1.move_lines, pick_2.move_lines):
65+
if (
66+
move_old.product_id != move_new.product_id
67+
or move_old.product_uom_qty != move_new.product_uom_qty
68+
):
69+
return False
70+
return True
71+
72+
def _generate_purchase_delivery_label(self):
73+
"""Create a transfer to generate the carrier labels."""
74+
self.ensure_one()
75+
if not self._is_valid_for_vendor_labels():
76+
return
77+
if not self.partner_id.property_stock_supplier.id:
78+
raise UserError(
79+
_(
80+
"You must set a Vendor Location for this partner %s",
81+
self.partner_id.name,
82+
)
83+
)
84+
carrier = self.vendor_label_carrier_id
85+
order = self.with_company(self.company_id)
86+
new_label_picking = None
87+
try:
88+
with self.env.cr.savepoint():
89+
# Using a savepoint for generating the transfer for the label
90+
# and keep the new one only if it is different
91+
new_label_picking = order._create_purchase_delivery_label_picking(
92+
carrier
93+
)
94+
if self._are_delivery_label_equivalent(
95+
new_label_picking, order.delivery_label_picking_id
96+
):
97+
new_label_picking = None
98+
raise SameDeliveryLabelGenerated
99+
except SameDeliveryLabelGenerated:
100+
pass
101+
if not new_label_picking:
102+
return
103+
if order.delivery_label_picking_id:
104+
order._cancel_purchase_delivery_label_picking()
105+
order.delivery_label_picking_id = new_label_picking
106+
new_label_picking.message_post_with_view(
107+
"mail.message_origin_link",
108+
values={"self": new_label_picking, "origin": self},
109+
subtype_id=self.env.ref("mail.mt_note").id,
110+
)
111+
112+
def _create_purchase_delivery_label_picking(self, carrier):
113+
self.ensure_one()
114+
values = self._get_purchase_delivery_label_picking_value(carrier)
115+
picking = self.env["stock.picking"].with_user(SUPERUSER_ID).create(values)
116+
moves = self.order_line._create_stock_moves(picking)
117+
moves.location_id = picking.location_id
118+
moves.location_dest_id = picking.location_dest_id
119+
# Remove the link on the sale and purchase
120+
# To not impact the delivered quantity on them
121+
picking.sale_id = False
122+
moves.sale_line_id = False
123+
moves.purchase_line_id = False
124+
picking.action_assign()
125+
for move in picking.move_lines:
126+
move.quantity_done = move.product_uom_qty
127+
picking._action_done()
128+
return picking
129+
130+
def _cancel_purchase_delivery_label_picking(self):
131+
delivery_label_pickings = self.delivery_label_picking_id
132+
for picking in delivery_label_pickings:
133+
picking.cancel_shipment()
134+
# Using wirte to by pass internal checks, not a problem
135+
# because this is a fake move (Vendor to Vendor)
136+
delivery_label_pickings.move_line_ids.write({"state": "cancel"})
137+
delivery_label_pickings.move_lines.write({"state": "cancel"})
138+
delivery_label_pickings.write({"state": "cancel"})
139+
140+
def _get_purchase_delivery_label_picking_value(self, carrier):
141+
return {
142+
"picking_type_id": carrier.purchase_label_picking_type.id,
143+
"partner_id": self.dest_address_id.id,
144+
"user_id": False,
145+
"date": fields.Datetime.now(),
146+
"origin": " - ".join(
147+
[origin for origin in [self.origin, self.name] if origin]
148+
),
149+
"location_dest_id": self.partner_id.property_stock_supplier.id,
150+
"location_id": self.partner_id.property_stock_supplier.id,
151+
"company_id": self.company_id.id,
152+
"carrier_id": carrier.id,
153+
"delivery_label_purchase_id": self.id,
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Copyright 2024 Michael Tietz (MT Software) <mtietz@mt-software.de>
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3+
from odoo import fields, models
4+
5+
6+
class StockPicking(models.Model):
7+
_inherit = "stock.picking"
8+
9+
delivery_label_purchase_id = fields.Many2one(
10+
"purchase.order", string="Delivery label purchase order", ondelete="cascade"
11+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
On the partner form a new field `Purchase Delivery Method` allows to select
2+
the carrier that will be used by the supplier to send the goods.
3+
4+
In "Inventory > Configuration > Delivery > Shipping Methods", a new field
5+
`Operation Type for Sending Labels`
6+
allows to select the operation type to use for the stock transfer that will
7+
generate the required labels.
8+
If that field is not set, there will be no changes to the normal flow.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
* Thierry Ducrest <thierry.ducrest@camptocamp.com>
2+
* Michael Tietz (MT Software) <mtietz@mt-software.de>
3+
4+
Design
5+
~~~~~~
6+
7+
* Jacques-Etienne Baudoux <je@bcim.be>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Print delivery labels for dropshipping purchase order.
2+
Useful when the vendor that will process the order does not have the
3+
capabilities to print the transporter labels needed for the delivery.
4+
5+
This is done by creating and processing a stock transfer with the source
6+
and destination location set as the vendor location. And the carrier set.
7+
8+
The transporter being used is the one set on the `Purchase Delivery Method` of the
9+
vendor in the "Sales & Purchase" tab.
10+
11+
A new operation type is created by the module and used for the transfer.
12+
All attachment (should be only the labels) on the transfer are then attached
13+
to the email send to the vendor (rfq or po).
14+
When the purchase order is still in draft the labels are regenerated everytime
15+
the email is sent.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import test_delivery_purchase_label
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Copyright 2024 Camptocamp SA
2+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
3+
4+
from odoo.tests.common import SavepointCase
5+
6+
7+
class TestDeliveryPurchaseLabel(SavepointCase):
8+
@classmethod
9+
def setUpClass(cls):
10+
super().setUpClass()
11+
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
12+
# Mocking to avoid a NotImplementedError
13+
cls.env["delivery.carrier"]._patch_method(
14+
"base_on_rule_cancel_shipment", lambda x, y: True
15+
)
16+
cls.picking_type = cls.env.ref(
17+
"delivery_purchase_label.picking_type_send_label"
18+
)
19+
cls.supplier = cls.env.ref("base.res_partner_1")
20+
cls.customer = cls.env.ref("base.res_partner_2")
21+
cls.carrier = cls.env.ref("delivery.delivery_carrier")
22+
cls.carrier.purchase_label_picking_type = cls.picking_type
23+
24+
cls.product = cls.env.ref("product.product_product_5")
25+
cls.order = cls.env["purchase.order"].create(
26+
{
27+
"partner_id": cls.supplier.id,
28+
"dest_address_id": cls.customer.id,
29+
"vendor_label_carrier_id": cls.carrier.id,
30+
"order_line": [
31+
(
32+
0,
33+
0,
34+
{
35+
"name": cls.product.name,
36+
"product_id": cls.product.id,
37+
"product_qty": 5.0,
38+
"product_uom": cls.product.uom_id.id,
39+
"price_unit": 10,
40+
},
41+
)
42+
],
43+
}
44+
)
45+
cls.order.order_line._onchange_quantity()
46+
47+
def _create_fake_label_attachment(self, linked_to):
48+
return self.env["ir.attachment"].create(
49+
{
50+
"name": "Fake Label Pdf",
51+
"datas": "bWlncmF0aW9uIHRlc3Q=",
52+
"res_model": linked_to._name,
53+
"res_id": linked_to.id,
54+
}
55+
)
56+
57+
def test_transfer_label_generated(self):
58+
self.order._generate_purchase_delivery_label()
59+
label_picking = self.order.delivery_label_picking_id
60+
self.assertEqual(label_picking.picking_type_id, self.picking_type)
61+
self.assertEqual(label_picking.state, "done")
62+
# Generating a second time with no changes on the purchase
63+
# Does not change the picking label
64+
self.order._generate_purchase_delivery_label()
65+
self.assertEqual(label_picking, self.order.delivery_label_picking_id)
66+
self.assertEqual(label_picking.delivery_label_purchase_id, self.order)
67+
# Changing the PO
68+
self.order.order_line[0].product_qty = 10
69+
self.order._generate_purchase_delivery_label()
70+
self.assertTrue(label_picking.state == "cancel")
71+
self.assertTrue(label_picking != self.order.delivery_label_picking_id)
72+
self.assertEqual(
73+
self.order.delivery_label_picking_id.delivery_label_purchase_id, self.order
74+
)
75+
76+
def test_transfer_label_not_generated(self):
77+
self.carrier.purchase_label_picking_type = False
78+
self.order._generate_purchase_delivery_label()
79+
self.assertFalse(self.order.picking_ids)
80+
81+
def test_add_label_attachment_to_email(self):
82+
self.order._generate_purchase_delivery_label()
83+
pdf_label = self._create_fake_label_attachment(
84+
self.order.delivery_label_picking_id
85+
)
86+
vals = {
87+
"partner_ids": [(6, 0, self.order.partner_id.ids)],
88+
"model": self.order._name,
89+
"res_id": self.order.id,
90+
}
91+
wiz = self.env["mail.compose.message"].create(vals)
92+
template = self.env.ref("purchase.email_template_edi_purchase")
93+
res = wiz.generate_email_for_composer(template.id, self.order.ids, ["subject"])
94+
self.assertTrue(res[self.order.id].get("attachment_ids"))
95+
self.assertEqual(res[self.order.id].get("attachment_ids"), pdf_label.ids)
96+
97+
def test_cancel_purchase_order(self):
98+
self.order._generate_purchase_delivery_label()
99+
self.order.button_cancel()
100+
self.assertTrue(self.order.delivery_label_picking_id.state == "cancel")

0 commit comments

Comments
 (0)