odoo-addon-project-task-stock 18.0.1.0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/project_task_stock/README.rst +146 -0
- odoo/addons/project_task_stock/__init__.py +1 -0
- odoo/addons/project_task_stock/__manifest__.py +24 -0
- odoo/addons/project_task_stock/demo/project_data.xml +28 -0
- odoo/addons/project_task_stock/demo/stock_picking_type_data.xml +11 -0
- odoo/addons/project_task_stock/i18n/es.po +337 -0
- odoo/addons/project_task_stock/i18n/fr.po +337 -0
- odoo/addons/project_task_stock/i18n/hr.po +345 -0
- odoo/addons/project_task_stock/i18n/it.po +344 -0
- odoo/addons/project_task_stock/i18n/project_task_stock.pot +329 -0
- odoo/addons/project_task_stock/i18n/pt_BR.po +345 -0
- odoo/addons/project_task_stock/models/__init__.py +7 -0
- odoo/addons/project_task_stock/models/account_analytic_line.py +21 -0
- odoo/addons/project_task_stock/models/project_project.py +45 -0
- odoo/addons/project_task_stock/models/project_task.py +279 -0
- odoo/addons/project_task_stock/models/stock_move.py +135 -0
- odoo/addons/project_task_stock/models/stock_scrap.py +25 -0
- odoo/addons/project_task_stock/readme/CONFIGURE.md +25 -0
- odoo/addons/project_task_stock/readme/CONTRIBUTORS.md +3 -0
- odoo/addons/project_task_stock/readme/DESCRIPTION.md +1 -0
- odoo/addons/project_task_stock/readme/USAGE.md +21 -0
- odoo/addons/project_task_stock/static/description/icon.png +0 -0
- odoo/addons/project_task_stock/static/description/index.html +485 -0
- odoo/addons/project_task_stock/tests/__init__.py +4 -0
- odoo/addons/project_task_stock/tests/common.py +100 -0
- odoo/addons/project_task_stock/tests/test_project_task_stock.py +350 -0
- odoo/addons/project_task_stock/views/project_project_view.xml +24 -0
- odoo/addons/project_task_stock/views/project_task_type_view.xml +14 -0
- odoo/addons/project_task_stock/views/project_task_view.xml +142 -0
- odoo/addons/project_task_stock/views/stock_move_view.xml +65 -0
- odoo/addons/project_task_stock/views/stock_scrap_views.xml +13 -0
- odoo_addon_project_task_stock-18.0.1.0.0.2.dist-info/METADATA +161 -0
- odoo_addon_project_task_stock-18.0.1.0.0.2.dist-info/RECORD +35 -0
- odoo_addon_project_task_stock-18.0.1.0.0.2.dist-info/WHEEL +5 -0
- odoo_addon_project_task_stock-18.0.1.0.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Copyright 2022-2025 Tecnativa - Víctor Martínez
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
3
|
+
from odoo import _, api, fields, models
|
|
4
|
+
from odoo.exceptions import UserError
|
|
5
|
+
from odoo.tools import float_is_zero
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProjectTask(models.Model):
|
|
9
|
+
_name = "project.task"
|
|
10
|
+
_inherit = ["project.task", "analytic.mixin"]
|
|
11
|
+
|
|
12
|
+
scrap_ids = fields.One2many(
|
|
13
|
+
comodel_name="stock.scrap", inverse_name="task_id", string="Scraps"
|
|
14
|
+
)
|
|
15
|
+
scrap_count = fields.Integer(
|
|
16
|
+
compute="_compute_scrap_move_count", string="Scrap Move"
|
|
17
|
+
)
|
|
18
|
+
move_ids = fields.One2many(
|
|
19
|
+
comodel_name="stock.move",
|
|
20
|
+
inverse_name="raw_material_task_id",
|
|
21
|
+
string="Stock Moves",
|
|
22
|
+
copy=False,
|
|
23
|
+
domain=[("scrapped", "=", False)],
|
|
24
|
+
)
|
|
25
|
+
use_stock_moves = fields.Boolean(related="stage_id.use_stock_moves")
|
|
26
|
+
done_stock_moves = fields.Boolean(related="stage_id.done_stock_moves")
|
|
27
|
+
stock_moves_is_locked = fields.Boolean(default=True)
|
|
28
|
+
allow_moves_action_confirm = fields.Boolean(
|
|
29
|
+
compute="_compute_allow_moves_action_confirm"
|
|
30
|
+
)
|
|
31
|
+
allow_moves_action_assign = fields.Boolean(
|
|
32
|
+
compute="_compute_allow_moves_action_assign"
|
|
33
|
+
)
|
|
34
|
+
stock_state = fields.Selection(
|
|
35
|
+
selection=[
|
|
36
|
+
("pending", "Pending"),
|
|
37
|
+
("confirmed", "Confirmed"),
|
|
38
|
+
("assigned", "Assigned"),
|
|
39
|
+
("done", "Done"),
|
|
40
|
+
("cancel", "Cancel"),
|
|
41
|
+
],
|
|
42
|
+
compute="_compute_stock_state",
|
|
43
|
+
)
|
|
44
|
+
picking_type_id = fields.Many2one(
|
|
45
|
+
comodel_name="stock.picking.type",
|
|
46
|
+
string="Operation Type",
|
|
47
|
+
readonly=False,
|
|
48
|
+
domain="[('company_id', '=?', company_id)]",
|
|
49
|
+
index=True,
|
|
50
|
+
)
|
|
51
|
+
location_id = fields.Many2one(
|
|
52
|
+
comodel_name="stock.location",
|
|
53
|
+
string="Source Location",
|
|
54
|
+
readonly=False,
|
|
55
|
+
index=True,
|
|
56
|
+
check_company=True,
|
|
57
|
+
)
|
|
58
|
+
location_dest_id = fields.Many2one(
|
|
59
|
+
comodel_name="stock.location",
|
|
60
|
+
string="Destination Location",
|
|
61
|
+
readonly=False,
|
|
62
|
+
index=True,
|
|
63
|
+
check_company=True,
|
|
64
|
+
)
|
|
65
|
+
stock_analytic_date = fields.Date()
|
|
66
|
+
unreserve_visible = fields.Boolean(
|
|
67
|
+
string="Allowed to Unreserve Inventory",
|
|
68
|
+
compute="_compute_unreserve_visible",
|
|
69
|
+
help="Technical field to check when we can unreserve",
|
|
70
|
+
)
|
|
71
|
+
stock_analytic_account_id = fields.Many2one(
|
|
72
|
+
comodel_name="account.analytic.account",
|
|
73
|
+
string="Move Analytic Account",
|
|
74
|
+
help="Move created will be assigned to this analytic account",
|
|
75
|
+
)
|
|
76
|
+
stock_analytic_distribution = fields.Json(
|
|
77
|
+
copy=True,
|
|
78
|
+
readonly=False,
|
|
79
|
+
)
|
|
80
|
+
stock_analytic_line_ids = fields.One2many(
|
|
81
|
+
comodel_name="account.analytic.line",
|
|
82
|
+
inverse_name="stock_task_id",
|
|
83
|
+
string="Stock Analytic Lines",
|
|
84
|
+
)
|
|
85
|
+
group_id = fields.Many2one(
|
|
86
|
+
comodel_name="procurement.group",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def _compute_scrap_move_count(self):
|
|
90
|
+
data = self.env["stock.scrap"].read_group(
|
|
91
|
+
[("task_id", "in", self.ids)], ["task_id"], ["task_id"]
|
|
92
|
+
)
|
|
93
|
+
count_data = {item["task_id"][0]: item["task_id_count"] for item in data}
|
|
94
|
+
for item in self:
|
|
95
|
+
item.scrap_count = count_data.get(item.id, 0)
|
|
96
|
+
|
|
97
|
+
@api.depends("move_ids", "move_ids.state")
|
|
98
|
+
def _compute_allow_moves_action_confirm(self):
|
|
99
|
+
for item in self:
|
|
100
|
+
item.allow_moves_action_confirm = any(
|
|
101
|
+
move.state == "draft" for move in item.move_ids
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
@api.depends("move_ids", "move_ids.state")
|
|
105
|
+
def _compute_allow_moves_action_assign(self):
|
|
106
|
+
for item in self:
|
|
107
|
+
item.allow_moves_action_assign = any(
|
|
108
|
+
move.state in ("confirmed", "partially_available")
|
|
109
|
+
for move in item.move_ids
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@api.depends("move_ids", "move_ids.state")
|
|
113
|
+
def _compute_stock_state(self):
|
|
114
|
+
for task in self:
|
|
115
|
+
task.stock_state = "pending"
|
|
116
|
+
if task.move_ids:
|
|
117
|
+
states = task.mapped("move_ids.state")
|
|
118
|
+
for state in ("confirmed", "assigned", "done", "cancel"):
|
|
119
|
+
if state in states:
|
|
120
|
+
task.stock_state = state
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
@api.depends("move_ids", "move_ids.quantity")
|
|
124
|
+
def _compute_unreserve_visible(self):
|
|
125
|
+
for item in self:
|
|
126
|
+
already_reserved = item.mapped("move_ids.move_line_ids")
|
|
127
|
+
any_quantity_done = any(
|
|
128
|
+
[
|
|
129
|
+
m.quantity > 0
|
|
130
|
+
for m in item.move_ids.filtered(lambda x: x.state == "done")
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
item.unreserve_visible = not any_quantity_done and already_reserved
|
|
134
|
+
|
|
135
|
+
@api.onchange("picking_type_id")
|
|
136
|
+
def _onchange_picking_type_id(self):
|
|
137
|
+
self.location_id = self.picking_type_id.default_location_src_id.id
|
|
138
|
+
self.location_dest_id = self.picking_type_id.default_location_dest_id.id
|
|
139
|
+
|
|
140
|
+
def _check_tasks_with_pending_moves(self):
|
|
141
|
+
if self.move_ids and "assigned" in self.mapped("move_ids.state"):
|
|
142
|
+
raise UserError(
|
|
143
|
+
_("It is not possible to change this with reserved movements in tasks.")
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def _update_moves_info(self):
|
|
147
|
+
for item in self:
|
|
148
|
+
item._check_tasks_with_pending_moves()
|
|
149
|
+
picking_type = item.picking_type_id or item.project_id.picking_type_id
|
|
150
|
+
location = item.location_id or item.project_id.location_id
|
|
151
|
+
location_dest = item.location_dest_id or item.project_id.location_dest_id
|
|
152
|
+
moves = item.move_ids.filtered(
|
|
153
|
+
lambda x, location=location, location_dest=location_dest: x.state
|
|
154
|
+
not in ("cancel", "done")
|
|
155
|
+
and (x.location_id != location or x.location_dest_id != location_dest)
|
|
156
|
+
)
|
|
157
|
+
moves.update(
|
|
158
|
+
{
|
|
159
|
+
"warehouse_id": location.warehouse_id.id,
|
|
160
|
+
"location_id": location.id,
|
|
161
|
+
"location_dest_id": location_dest.id,
|
|
162
|
+
"picking_type_id": picking_type.id,
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
self.action_assign()
|
|
166
|
+
|
|
167
|
+
@api.model
|
|
168
|
+
def _prepare_procurement_group_vals(self):
|
|
169
|
+
return {"name": f"Task-ID: {self.id}"}
|
|
170
|
+
|
|
171
|
+
def action_confirm(self):
|
|
172
|
+
self.mapped("move_ids")._action_confirm()
|
|
173
|
+
|
|
174
|
+
def action_assign(self):
|
|
175
|
+
self.action_confirm()
|
|
176
|
+
self.mapped("move_ids")._action_assign()
|
|
177
|
+
|
|
178
|
+
def button_scrap(self):
|
|
179
|
+
self.ensure_one()
|
|
180
|
+
move_items = self.move_ids.filtered(lambda x: x.state not in ("done", "cancel"))
|
|
181
|
+
return {
|
|
182
|
+
"name": _("Scrap"),
|
|
183
|
+
"view_mode": "form",
|
|
184
|
+
"res_model": "stock.scrap",
|
|
185
|
+
"view_id": self.env.ref("stock.stock_scrap_form_view2").id,
|
|
186
|
+
"type": "ir.actions.act_window",
|
|
187
|
+
"context": {
|
|
188
|
+
"default_task_id": self.id,
|
|
189
|
+
"product_ids": move_items.mapped("product_id").ids,
|
|
190
|
+
"default_company_id": self.company_id.id,
|
|
191
|
+
},
|
|
192
|
+
"target": "new",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
def do_unreserve(self):
|
|
196
|
+
for item in self:
|
|
197
|
+
item.move_ids.filtered(
|
|
198
|
+
lambda x: x.state not in ("done", "cancel")
|
|
199
|
+
)._do_unreserve()
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
def button_unreserve(self):
|
|
203
|
+
self.ensure_one()
|
|
204
|
+
self.do_unreserve()
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
def action_cancel(self):
|
|
208
|
+
"""Cancel the stock moves and remove the analytic lines created from
|
|
209
|
+
stock moves when cancelling the task.
|
|
210
|
+
"""
|
|
211
|
+
self.mapped("move_ids.move_line_ids").write({"quantity": 0})
|
|
212
|
+
# Use sudo to avoid error for users with no access to analytic
|
|
213
|
+
self.sudo().stock_analytic_line_ids.unlink()
|
|
214
|
+
self.stock_moves_is_locked = True
|
|
215
|
+
return True
|
|
216
|
+
|
|
217
|
+
def action_toggle_stock_moves_is_locked(self):
|
|
218
|
+
self.ensure_one()
|
|
219
|
+
self.stock_moves_is_locked = not self.stock_moves_is_locked
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
def action_done(self):
|
|
223
|
+
# Filter valid stock moves (avoiding those done and cancelled).
|
|
224
|
+
price_dp = self.env["decimal.precision"].precision_get(
|
|
225
|
+
"Product Unit of Measure"
|
|
226
|
+
)
|
|
227
|
+
moves_to_skip = self.move_ids.filtered(lambda x: x.state in ("done", "cancel"))
|
|
228
|
+
moves_to_do = self.move_ids - moves_to_skip
|
|
229
|
+
for move in moves_to_do.filtered(
|
|
230
|
+
lambda x: float_is_zero(x.quantity, precision_digits=price_dp)
|
|
231
|
+
):
|
|
232
|
+
move.quantity = move.product_uom_qty
|
|
233
|
+
moves_to_do.picking_id.with_context(skip_sanity_check=True).button_validate()
|
|
234
|
+
moves_done = self.move_ids.filtered(lambda x: x.state == "done")
|
|
235
|
+
moves_todo = moves_done - moves_to_skip
|
|
236
|
+
# Use sudo to avoid error for users with no access to analytic
|
|
237
|
+
analytic_line_model = self.env["account.analytic.line"].sudo()
|
|
238
|
+
for move in moves_todo:
|
|
239
|
+
vals = move._prepare_analytic_line_from_task()
|
|
240
|
+
if vals:
|
|
241
|
+
analytic_line_model.create(vals)
|
|
242
|
+
|
|
243
|
+
def action_see_move_scrap(self):
|
|
244
|
+
self.ensure_one()
|
|
245
|
+
action = self.env["ir.actions.actions"]._for_xml_id("stock.action_stock_scrap")
|
|
246
|
+
action["domain"] = [("task_id", "=", self.id)]
|
|
247
|
+
action["context"] = dict(self._context, default_origin=self.name)
|
|
248
|
+
return action
|
|
249
|
+
|
|
250
|
+
def write(self, vals):
|
|
251
|
+
res = super().write(vals)
|
|
252
|
+
if "stage_id" in vals:
|
|
253
|
+
stage = self.env["project.task.type"].browse(vals.get("stage_id"))
|
|
254
|
+
if stage.done_stock_moves:
|
|
255
|
+
# Avoid permissions error if the user does not have access to stock.
|
|
256
|
+
self.sudo().action_assign()
|
|
257
|
+
# Update info
|
|
258
|
+
field_names = ("location_id", "location_dest_id")
|
|
259
|
+
if any(vals.get(field) for field in field_names):
|
|
260
|
+
self._update_moves_info()
|
|
261
|
+
return res
|
|
262
|
+
|
|
263
|
+
def unlink(self):
|
|
264
|
+
# Use sudo to avoid error to users with no access to analytic
|
|
265
|
+
# related to hr_timesheet addon
|
|
266
|
+
return super(ProjectTask, self.sudo()).unlink()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ProjectTaskType(models.Model):
|
|
270
|
+
_inherit = "project.task.type"
|
|
271
|
+
|
|
272
|
+
use_stock_moves = fields.Boolean(
|
|
273
|
+
help="If you mark this check, when a task goes to this state, "
|
|
274
|
+
"it will use stock moves",
|
|
275
|
+
)
|
|
276
|
+
done_stock_moves = fields.Boolean(
|
|
277
|
+
help="If you check this box, when a task is in this state, you will not "
|
|
278
|
+
"be able to add more stock moves but they can be viewed."
|
|
279
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Copyright 2022 Tecnativa - Víctor Martínez
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
3
|
+
from odoo import api, fields, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StockMove(models.Model):
|
|
7
|
+
_inherit = "stock.move"
|
|
8
|
+
|
|
9
|
+
task_id = fields.Many2one(
|
|
10
|
+
comodel_name="project.task",
|
|
11
|
+
string="Related Task",
|
|
12
|
+
check_company=True,
|
|
13
|
+
)
|
|
14
|
+
raw_material_task_id = fields.Many2one(
|
|
15
|
+
comodel_name="project.task", string="Task for material", check_company=True
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@api.onchange("product_id")
|
|
19
|
+
def _onchange_product_id(self):
|
|
20
|
+
"""It is necessary to overwrite the name to prevent set product name
|
|
21
|
+
from being auto-defined."""
|
|
22
|
+
res = super()._onchange_product_id()
|
|
23
|
+
if self.raw_material_task_id:
|
|
24
|
+
self.name = self.raw_material_task_id.name
|
|
25
|
+
return res
|
|
26
|
+
|
|
27
|
+
def _prepare_analytic_line_from_task(self):
|
|
28
|
+
product = self.product_id
|
|
29
|
+
company_id = self.env.company
|
|
30
|
+
task = self.task_id or self.raw_material_task_id
|
|
31
|
+
analytic_account = task.stock_analytic_account_id or task.project_id.account_id
|
|
32
|
+
if not analytic_account:
|
|
33
|
+
return False
|
|
34
|
+
# Apply sudo() in case there is any rule that does not allow access to
|
|
35
|
+
# the analytic account, for example with analytic_hr_department_restriction
|
|
36
|
+
analytic_account = analytic_account.sudo()
|
|
37
|
+
res = {
|
|
38
|
+
"date": (
|
|
39
|
+
task.stock_analytic_date
|
|
40
|
+
or task.project_id.stock_analytic_date
|
|
41
|
+
or fields.date.today()
|
|
42
|
+
),
|
|
43
|
+
"name": task.name + ": " + product.name,
|
|
44
|
+
"unit_amount": self.quantity,
|
|
45
|
+
"account_id": analytic_account.id,
|
|
46
|
+
"user_id": self.env.user.id,
|
|
47
|
+
"product_uom_id": self.product_uom.id,
|
|
48
|
+
"company_id": analytic_account.company_id.id or self.env.company.id,
|
|
49
|
+
"partner_id": task.partner_id.id or task.project_id.partner_id.id or False,
|
|
50
|
+
"stock_task_id": task.id,
|
|
51
|
+
}
|
|
52
|
+
amount_unit = product.with_context(uom=self.product_uom.id)._price_compute(
|
|
53
|
+
"standard_price"
|
|
54
|
+
)[product.id]
|
|
55
|
+
amount = amount_unit * self.quantity or 0.0
|
|
56
|
+
result = round(amount, company_id.currency_id.decimal_places) * -1
|
|
57
|
+
vals = {"amount": result}
|
|
58
|
+
analytic_line_fields = self.env["account.analytic.line"]._fields
|
|
59
|
+
# Extra fields added in account addon
|
|
60
|
+
if "ref" in analytic_line_fields:
|
|
61
|
+
vals["ref"] = task.name
|
|
62
|
+
if "product_id" in analytic_line_fields:
|
|
63
|
+
vals["product_id"] = product.id
|
|
64
|
+
# Prevent incoherence when hr_timesheet addon is installed.
|
|
65
|
+
if "project_id" in analytic_line_fields:
|
|
66
|
+
vals["project_id"] = False
|
|
67
|
+
# distributions
|
|
68
|
+
if task.stock_analytic_distribution:
|
|
69
|
+
new_amount = 0
|
|
70
|
+
for distribution in task.stock_analytic_distribution.values():
|
|
71
|
+
new_amount -= (amount / 100) * distribution
|
|
72
|
+
vals["amount"] = new_amount
|
|
73
|
+
res.update(vals)
|
|
74
|
+
return res
|
|
75
|
+
|
|
76
|
+
@api.model
|
|
77
|
+
def default_get(self, fields_list):
|
|
78
|
+
defaults = super().default_get(fields_list)
|
|
79
|
+
if self.env.context.get("default_raw_material_task_id"):
|
|
80
|
+
task = self.env["project.task"].browse(
|
|
81
|
+
self.env.context.get("default_raw_material_task_id")
|
|
82
|
+
)
|
|
83
|
+
if not task.group_id:
|
|
84
|
+
task.group_id = self.env["procurement.group"].create(
|
|
85
|
+
task._prepare_procurement_group_vals()
|
|
86
|
+
)
|
|
87
|
+
defaults.update(
|
|
88
|
+
{
|
|
89
|
+
"group_id": task.group_id.id,
|
|
90
|
+
"location_id": (
|
|
91
|
+
task.location_id.id or task.project_id.location_id.id
|
|
92
|
+
),
|
|
93
|
+
"location_dest_id": (
|
|
94
|
+
task.location_dest_id.id or task.project_id.location_dest_id.id
|
|
95
|
+
),
|
|
96
|
+
"picking_type_id": (
|
|
97
|
+
task.picking_type_id.id or task.project_id.picking_type_id.id
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
return defaults
|
|
102
|
+
|
|
103
|
+
def action_task_product_forecast_report(self):
|
|
104
|
+
self.ensure_one()
|
|
105
|
+
action = self.product_id.action_product_forecast_report()
|
|
106
|
+
action["context"] = {
|
|
107
|
+
"active_id": self.product_id.id,
|
|
108
|
+
"active_model": "product.product",
|
|
109
|
+
"move_to_match_ids": self.ids,
|
|
110
|
+
}
|
|
111
|
+
warehouse = self.warehouse_id
|
|
112
|
+
if warehouse:
|
|
113
|
+
action["context"]["warehouse"] = warehouse.id
|
|
114
|
+
return action
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class StockMoveLine(models.Model):
|
|
118
|
+
_inherit = "stock.move.line"
|
|
119
|
+
|
|
120
|
+
task_id = fields.Many2one(
|
|
121
|
+
comodel_name="project.task",
|
|
122
|
+
string="Task",
|
|
123
|
+
compute="_compute_task_id",
|
|
124
|
+
store=True,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@api.depends("move_id.raw_material_task_id", "move_id.task_id")
|
|
128
|
+
def _compute_task_id(self):
|
|
129
|
+
for item in self:
|
|
130
|
+
task = (
|
|
131
|
+
item.move_id.raw_material_task_id
|
|
132
|
+
if item.move_id.raw_material_task_id
|
|
133
|
+
else item.move_id.task_id
|
|
134
|
+
)
|
|
135
|
+
item.task_id = task if task else False
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright 2022 Tecnativa - Víctor Martínez
|
|
2
|
+
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
3
|
+
from odoo import api, fields, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StockMove(models.Model):
|
|
7
|
+
_inherit = "stock.scrap"
|
|
8
|
+
|
|
9
|
+
task_id = fields.Many2one(
|
|
10
|
+
comodel_name="project.task", string="Task", check_company=True
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
@api.onchange("task_id")
|
|
14
|
+
def _onchange_task_id(self):
|
|
15
|
+
if self.task_id:
|
|
16
|
+
self.location_id = self.task_id.move_raw_ids.filtered(
|
|
17
|
+
lambda x: x.state not in ("done", "cancel")
|
|
18
|
+
) and (self.task_id.location_src_id or self.task_id.location_dest_id)
|
|
19
|
+
|
|
20
|
+
def _prepare_move_values(self):
|
|
21
|
+
vals = super()._prepare_move_values()
|
|
22
|
+
if self.task_id:
|
|
23
|
+
vals["origin"] = vals["origin"] or self.task_id.name
|
|
24
|
+
vals.update({"raw_material_task_id": self.task_id.id})
|
|
25
|
+
return vals
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
To configure this module, you need to:
|
|
2
|
+
|
|
3
|
+
1. Go to *Inventory -\> Configuration -\> Settings* and check "Storage
|
|
4
|
+
Locations" option.
|
|
5
|
+
|
|
6
|
+
2. Go to *Inventory -\> Configuration -\> Operation types*.
|
|
7
|
+
|
|
8
|
+
3. Create a new operation type with the following options:
|
|
9
|
+
- \`Operation type\`: Task material
|
|
10
|
+
- \`Code\`: TM
|
|
11
|
+
- \`Type of operation\`: Delivery
|
|
12
|
+
- \`Default Source Location\`: WH/Stock
|
|
13
|
+
- \`Default Destination Location\`: WH/Stock/Shelf 1
|
|
14
|
+
|
|
15
|
+
4. Go to *Project -\> Configuration -\> Projects*.
|
|
16
|
+
|
|
17
|
+
5. Create a new project with the following options:
|
|
18
|
+
- \`Name\`: Task material
|
|
19
|
+
- \`Operation type\`: Task material
|
|
20
|
+
|
|
21
|
+
6. Go to *Project -\> Configuration -\> Task Stages* and edit some records.
|
|
22
|
+
- \`In progress\`: Check Use Stock Moves option and add the created
|
|
23
|
+
project.
|
|
24
|
+
- \`Done\`: Check Use Stock Moves option + Done Stock Moves and add
|
|
25
|
+
the created project.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This module allows to consume products directly from a project task.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
1. Go to *Projects -\> Task material (project)* and create a task and
|
|
2
|
+
edit it.
|
|
3
|
+
|
|
4
|
+
2. *Stock Info* is displayed in the *Extra info* tab with the same
|
|
5
|
+
project information, but it can be modified.
|
|
6
|
+
|
|
7
|
+
3. Add some product to *Stock Info* tab and set some initial demand (1
|
|
8
|
+
for example).
|
|
9
|
+
|
|
10
|
+
4. Click on the button *Confirm material* to confirm all moves.
|
|
11
|
+
|
|
12
|
+
5. Change the stage to Done.
|
|
13
|
+
|
|
14
|
+
6. *Stock Info* tab is readonly and some buttons show in header:
|
|
15
|
+
- \`Check availability materials\`: Product availability will be
|
|
16
|
+
checked.
|
|
17
|
+
- \`Transfer Materials\`: Stock moves are confirmed and moved from
|
|
18
|
+
one location to another.
|
|
19
|
+
- \`Unreserve\`: Remove the reservation stock of the products.
|
|
20
|
+
- \`Cancel Materials\`: Set the moves of the products as cancelled.
|
|
21
|
+
- \`Scrap\`: Allows the defined products to be scrapped.
|
|
Binary file
|