Skip to content

Commit 5a482ff

Browse files
[IMP] purchase_reception_status_line: add over-received status and propagation logic for orders and lines
1 parent ece439e commit 5a482ff

File tree

6 files changed

+187
-35
lines changed

6 files changed

+187
-35
lines changed

purchase_reception_status_line/__manifest__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"maintainers": ["DavidJForgeFlow"],
1212
"website": "https://github.com/OCA/purchase-workflow",
1313
"depends": ["purchase_reception_status"],
14-
"data": ["views/purchase_order.xml"],
14+
"data": [
15+
"views/purchase_order.xml",
16+
"views/purchase_order_line_views.xml",
17+
],
1518
"installable": True,
1619
}

purchase_reception_status_line/models/purchase_order.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,42 @@
11
# Copyright 2024 ForgeFlow (http://www.akretion.com/)
22
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
33

4-
from odoo import api, models
4+
from odoo import api, fields, models
55

66

77
class PurchaseOrder(models.Model):
88
_inherit = "purchase.order"
99

10+
force_received = fields.Boolean(
11+
compute="_compute_force_received",
12+
inverse="_inverse_force_received",
13+
store=True,
14+
tracking=True,
15+
help="If true, the order is marked forced only when all lines are fully received"
16+
" and at least one line was manually forced.",
17+
)
18+
19+
@api.depends("order_line.reception_status", "order_line.force_received")
20+
def _compute_force_received(self):
21+
for po in self:
22+
all_received = all(
23+
line.reception_status == "received" for line in po.order_line
24+
)
25+
any_forced = any(line.force_received for line in po.order_line)
26+
po.force_received = all_received and any_forced
27+
28+
def _inverse_force_received(self):
29+
for po in self:
30+
if po.force_received:
31+
to_force = po.order_line.filtered(
32+
lambda l: l.reception_status != "received"
33+
)
34+
to_force.write({"force_received": True})
35+
else:
36+
forced_lines = po.order_line.filtered(lambda l: l.force_received)
37+
forced_lines.write({"force_received": False})
38+
forced_lines._compute_reception_status()
39+
1040
@api.depends(
1141
"state",
1242
"force_received",

purchase_reception_status_line/models/purchase_order_line.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,40 @@
66

77

88
class PurchaseOrderLine(models.Model):
9-
_inherit = "purchase.order.line"
9+
_name = "purchase.order.line"
10+
_inherit = ["purchase.order.line", "mail.thread", "mail.activity.mixin"]
1011

1112
reception_status = fields.Selection(
1213
[
1314
("no", "Nothing Received"),
1415
("partial", "Partially Received"),
1516
("received", "Fully Received"),
17+
("over", "Over Received"),
1618
],
1719
compute="_compute_reception_status",
1820
store=True,
1921
)
2022
force_received = fields.Boolean(
23+
string="Closed by Buyer",
2124
readonly=False,
2225
states={"draft": [("readonly", True)]},
23-
compute="_compute_force_received",
2426
store=True,
2527
copy=False,
2628
help="If true, the reception status will be forced to Fully Received, "
2729
"even if some quantities are not fully received. ",
30+
tracking=True,
2831
)
2932

30-
@api.depends("order_id.force_received")
31-
def _compute_force_received(self):
32-
prec = self.env["decimal.precision"].precision_get("Product Unit of Measure")
33+
def action_commute_force_received(self):
3334
for rec in self:
34-
if (
35-
rec.order_id.force_received
36-
and not float_compare(
37-
rec.qty_received, rec.product_qty, precision_digits=prec
38-
)
39-
>= 0
40-
):
41-
rec.force_received = True
35+
rec.force_received = not rec.force_received
36+
rec._compute_reception_status()
4237

4338
@api.depends(
4439
"state",
4540
"force_received",
4641
"qty_received",
4742
"product_qty",
48-
"order_id.force_received",
4943
)
5044
def _compute_reception_status(self):
5145
prec = self.env["decimal.precision"].precision_get("Product Unit of Measure")
@@ -54,15 +48,21 @@ def _compute_reception_status(self):
5448
if line.order_id.state in ("purchase", "done"):
5549
if line.force_received:
5650
status = "received"
57-
elif (
58-
float_compare(
59-
line.qty_received, line.product_qty, precision_digits=prec
60-
)
61-
>= 0
62-
):
63-
status = "received"
64-
elif float_compare(line.qty_received, 0, precision_digits=prec) > 0:
65-
status = "partial"
66-
line.reception_status = (
67-
status if not line.order_id.force_received else "received"
68-
)
51+
else:
52+
if (
53+
float_compare(
54+
line.qty_received, line.product_qty, precision_digits=prec
55+
)
56+
> 0
57+
):
58+
status = "over"
59+
elif (
60+
float_compare(
61+
line.qty_received, line.product_qty, precision_digits=prec
62+
)
63+
>= 0
64+
):
65+
status = "received"
66+
elif float_compare(line.qty_received, 0, precision_digits=prec) > 0:
67+
status = "partial"
68+
line.reception_status = status

purchase_reception_status_line/static/description/index.html

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,10 @@
88

99
/*
1010
:Author: David Goodger ([email protected])
11-
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
11+
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
1212
:Copyright: This stylesheet has been placed in the public domain.
1313
1414
Default cascading style sheet for the HTML output of Docutils.
15-
Despite the name, some widely supported CSS2 features are used.
1615
1716
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
1817
customize this style sheet.
@@ -275,7 +274,7 @@
275274
margin-left: 2em ;
276275
margin-right: 2em }
277276

278-
pre.code .ln { color: gray; } /* line numbers */
277+
pre.code .ln { color: grey; } /* line numbers */
279278
pre.code, code { background-color: #eeeeee }
280279
pre.code .comment, code .comment { color: #5C6576 }
281280
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
@@ -301,7 +300,7 @@
301300
span.pre {
302301
white-space: pre }
303302

304-
span.problematic, pre.problematic {
303+
span.problematic {
305304
color: red }
306305

307306
span.section-subtitle {
@@ -424,9 +423,7 @@ <h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
424423
<div class="section" id="maintainers">
425424
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
426425
<p>This module is maintained by the OCA.</p>
427-
<a class="reference external image-reference" href="https://odoo-community.org">
428-
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
429-
</a>
426+
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
430427
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
431428
mission is to support the collaborative development of Odoo features and
432429
promote its widespread use.</p>

purchase_reception_status_line/tests/test_purchase_receipt_status_line.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,53 @@ def test_02_order_with_lines(self):
7474
self.assertEqual(self.order.order_line[0].reception_status, "received")
7575
self.assertEqual(self.order.order_line[1].reception_status, "received")
7676
self.assertEqual(self.order.order_line[1].force_received, True)
77+
78+
def test_03_over_received_status(self):
79+
over_line = self.env["purchase.order.line"].create(
80+
{
81+
"order_id": self.order.id,
82+
"name": self.product.name,
83+
"product_id": self.product.id,
84+
"product_qty": 5.0,
85+
"qty_received_manual": 7.0,
86+
}
87+
)
88+
self.order.button_confirm()
89+
self.assertEqual(over_line.reception_status, "over")
90+
over_line.force_received = True
91+
self.assertEqual(over_line.reception_status, "received")
92+
93+
def test_04_order_force_logic_and_propagation(self):
94+
l1 = self.env["purchase.order.line"].create(
95+
{
96+
"order_id": self.order.id,
97+
"name": self.product.name,
98+
"product_id": self.product.id,
99+
"product_qty": 4.0,
100+
"qty_received_manual": 0.0,
101+
}
102+
)
103+
l2 = self.env["purchase.order.line"].create(
104+
{
105+
"order_id": self.order.id,
106+
"name": self.product.name,
107+
"product_id": self.product.id,
108+
"product_qty": 3.0,
109+
"qty_received_manual": 0.0,
110+
}
111+
)
112+
self.order.button_confirm()
113+
self.assertTrue(l1.reception_status == "no")
114+
self.assertTrue(l2.reception_status == "no")
115+
self.assertFalse(self.order.force_received)
116+
l1.force_received = True
117+
self.assertFalse(self.order.force_received)
118+
l2.force_received = True
119+
self.assertTrue(self.order.force_received)
120+
l1.force_received = False
121+
self.assertFalse(self.order.force_received)
122+
self.assertTrue(l2.force_received)
123+
self.order.force_received = True
124+
self.assertTrue(all(line.force_received for line in self.order.order_line))
125+
self.order.force_received = False
126+
self.assertFalse(any(line.force_received for line in self.order.order_line))
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<odoo>
3+
<record id="purchase_order_line_tree" model="ir.ui.view">
4+
<field name="name">purchase.order.line.tree - aust_purchase</field>
5+
<field name="model">purchase.order.line</field>
6+
<field name="inherit_id" ref="purchase.purchase_order_line_tree" />
7+
<field name="arch" type="xml">
8+
<field name="product_uom" position="after">
9+
<field name="reception_status" optional="hide" />
10+
<field
11+
name="force_received"
12+
groups="purchase.group_purchase_manager"
13+
optional="hide"
14+
/>
15+
<field name="state" invisible="1" />
16+
<button
17+
name="action_commute_force_received"
18+
type="object"
19+
string="Force Receive"
20+
attrs="{'invisible': ['|','|',('state', 'not in', ['purchase', 'done']),('force_received', '=', True),('reception_status','=','received')]}"
21+
/>
22+
</field>
23+
</field>
24+
</record>
25+
26+
<record id="purchase_order_line_form2" model="ir.ui.view">
27+
<field name="name">purchase.order.line.form - aust_purchase</field>
28+
<field name="model">purchase.order.line</field>
29+
<field name="inherit_id" ref="purchase.purchase_order_line_form2" />
30+
<field name="arch" type="xml">
31+
<sheet position="after">
32+
<div class="oe_chatter">
33+
<field name="message_follower_ids" groups="base.group_user" />
34+
<field name="activity_ids" />
35+
<field name="message_ids" />
36+
</div>
37+
</sheet>
38+
</field>
39+
</record>
40+
41+
<record id="purchase_order_line_search_filters" model="ir.ui.view">
42+
<field name="name">purchase.order.line.search.reception_status.filters</field>
43+
<field name="model">purchase.order.line</field>
44+
<field name="inherit_id" ref="purchase.purchase_order_line_search" />
45+
<field name="arch" type="xml">
46+
<xpath expr="//filter[@name='hide_cancelled']" position="after">
47+
<separator string="Reception Status" />
48+
<filter
49+
name="filter_no"
50+
string="Nothing Received"
51+
domain="[('reception_status','=','no'),('state','in',['done','purchase'])]"
52+
/>
53+
<filter
54+
name="filter_partial"
55+
string="Partially Received"
56+
domain="[('reception_status','=','partial'),('state','in',['done','purchase'])]"
57+
/>
58+
<filter
59+
name="filter_received"
60+
string="Fully Received"
61+
domain="[('reception_status','=','received'),('state','in',['done','purchase'])]"
62+
/>
63+
<filter
64+
name="filter_over"
65+
string="Over Received"
66+
domain="[('reception_status','=','over'),('state','in',['done','purchase'])]"
67+
/>
68+
<separator/>
69+
</xpath>
70+
</field>
71+
</record>
72+
</odoo>

0 commit comments

Comments
 (0)