LEFT | RIGHT |
1 #This file is part of Tryton. The COPYRIGHT file at the top level of | 1 #This file is part of Tryton. The COPYRIGHT file at the top level of |
2 #this repository contains the full copyright notices and license terms. | 2 #this repository contains the full copyright notices and license terms. |
| 3 from decimal import Decimal |
| 4 |
3 from trytond.model import ModelView, ModelSQL, Workflow, fields | 5 from trytond.model import ModelView, ModelSQL, Workflow, fields |
4 from trytond.wizard import Wizard, StateTransition, StateView, Button | 6 from trytond.wizard import Wizard, StateTransition, StateView, Button |
5 from trytond.pyson import Eval, Bool, If, Id | 7 from trytond.pyson import Eval, Bool, If, Id |
6 from trytond.pool import Pool | 8 from trytond.pool import Pool |
7 from trytond.transaction import Transaction | 9 from trytond.transaction import Transaction |
8 | 10 |
9 BOM_CHANGES = ['bom', 'product', 'quantity', 'uom', 'warehouse', 'location', | 11 BOM_CHANGES = ['bom', 'product', 'quantity', 'uom', 'warehouse', 'location', |
10 'company', 'inputs', 'outputs'] | 12 'company', 'inputs', 'outputs'] |
11 | 13 |
12 | 14 |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
89 on_change_with=['uom']), 'get_unit_digits') | 91 on_change_with=['uom']), 'get_unit_digits') |
90 quantity = fields.Float('Quantity', | 92 quantity = fields.Float('Quantity', |
91 digits=(16, Eval('unit_digits', 2)), | 93 digits=(16, Eval('unit_digits', 2)), |
92 states={ | 94 states={ |
93 'readonly': ~Eval('state').in_(['request', 'draft']), | 95 'readonly': ~Eval('state').in_(['request', 'draft']), |
94 'required': Bool(Eval('bom')), | 96 'required': Bool(Eval('bom')), |
95 'invisible': ~Eval('bom'), | 97 'invisible': ~Eval('bom'), |
96 }, | 98 }, |
97 on_change=BOM_CHANGES, | 99 on_change=BOM_CHANGES, |
98 depends=['unit_digits']) | 100 depends=['unit_digits']) |
| 101 cost = fields.Function(fields.Numeric('Cost', digits=(16, 4), |
| 102 readonly=True, on_change_with=['inputs']), 'get_cost') |
99 inputs = fields.One2Many('stock.move', 'production_input', 'Inputs', | 103 inputs = fields.One2Many('stock.move', 'production_input', 'Inputs', |
100 domain=[ | 104 domain=[ |
101 ('from_location', 'child_of', [Eval('warehouse')], 'parent'), | 105 ('from_location', 'child_of', [Eval('warehouse')], 'parent'), |
102 ('to_location', '=', Eval('location')), | 106 ('to_location', '=', Eval('location')), |
103 ], | 107 ], |
104 states={ | 108 states={ |
105 'readonly': ~Eval('state').in_(['request', 'draft', 'waiting']), | 109 'readonly': (~Eval('state').in_(['request', 'draft', 'waiting']) |
| 110 | ~Eval('location')), |
106 }, | 111 }, |
107 depends=['warehouse', 'location']) | 112 depends=['warehouse', 'location']) |
108 outputs = fields.One2Many('stock.move', 'production_output', 'Outputs', | 113 outputs = fields.One2Many('stock.move', 'production_output', 'Outputs', |
109 domain=[ | 114 domain=[ |
110 ('from_location', '=', Eval('location')), | 115 ('from_location', '=', Eval('location')), |
111 ('to_location', 'child_of', [Eval('warehouse')], 'parent'), | 116 ('to_location', 'child_of', [Eval('warehouse')], 'parent'), |
112 ], | 117 ], |
113 states={ | 118 states={ |
114 'readonly': Eval('state').in_(['done', 'cancel']), | 119 'readonly': (Eval('state').in_(['done', 'cancel']) |
| 120 | ~Eval('location')), |
115 }, | 121 }, |
116 depends=['warehouse', 'location']) | 122 depends=['warehouse', 'location']) |
117 state = fields.Selection([ | 123 state = fields.Selection([ |
118 ('request', 'Request'), | 124 ('request', 'Request'), |
119 ('draft', 'Draft'), | 125 ('draft', 'Draft'), |
120 ('waiting', 'Waiting'), | 126 ('waiting', 'Waiting'), |
121 ('assigned', 'Assigned'), | 127 ('assigned', 'Assigned'), |
122 ('running', 'Running'), | 128 ('running', 'Running'), |
123 ('done', 'Done'), | 129 ('done', 'Done'), |
124 ('cancel', 'Canceled'), | 130 ('cancel', 'Canceled'), |
125 ], 'State', readonly=True) | 131 ], 'State', readonly=True) |
126 | 132 |
127 def __init__(self): | 133 def __init__(self): |
128 super(Production, self).__init__() | 134 super(Production, self).__init__() |
| 135 self._constraints += [ |
| 136 ('check_cost', 'missing_cost'), |
| 137 ] |
| 138 self._error_messages.update({ |
| 139 'missing_cost': 'It misses some cost on the outputs!', |
| 140 }) |
129 self._transitions |= set(( | 141 self._transitions |= set(( |
130 ('request', 'draft'), | 142 ('request', 'draft'), |
131 ('draft', 'waiting'), | 143 ('draft', 'waiting'), |
132 ('waiting', 'assigned'), | 144 ('waiting', 'assigned'), |
133 ('assigned', 'running'), | 145 ('assigned', 'running'), |
134 ('running', 'done'), | 146 ('running', 'done'), |
135 ('running', 'waiting'), | 147 ('running', 'waiting'), |
136 ('assigned', 'waiting'), | 148 ('assigned', 'waiting'), |
137 ('waiting', 'waiting'), | 149 ('waiting', 'waiting'), |
138 ('waiting', 'draft'), | 150 ('waiting', 'draft'), |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
176 }) | 188 }) |
177 | 189 |
178 def default_state(self): | 190 def default_state(self): |
179 return 'draft' | 191 return 'draft' |
180 | 192 |
181 def default_warehouse(self): | 193 def default_warehouse(self): |
182 location_obj = Pool().get('stock.location') | 194 location_obj = Pool().get('stock.location') |
183 location_ids = location_obj.search(self.warehouse.domain) | 195 location_ids = location_obj.search(self.warehouse.domain) |
184 if len(location_ids) == 1: | 196 if len(location_ids) == 1: |
185 return location_ids[0] | 197 return location_ids[0] |
186 return False | |
187 | 198 |
188 def default_location(self): | 199 def default_location(self): |
189 location_obj = Pool().get('stock.location') | 200 location_obj = Pool().get('stock.location') |
190 warehouse_id = self.default_warehouse() | 201 warehouse_id = self.default_warehouse() |
191 if warehouse_id: | 202 if warehouse_id: |
192 warehouse = location_obj.browse(warehouse_id) | 203 warehouse = location_obj.browse(warehouse_id) |
193 return warehouse.production_location.id | 204 return warehouse.production_location.id |
194 return False | |
195 | 205 |
196 def default_company(self): | 206 def default_company(self): |
197 return Transaction().context.get('company') or False | 207 return Transaction().context.get('company') |
198 | 208 |
199 def _move_values(self, from_location, to_location, company, bom_io, | 209 def _move_values(self, from_location, to_location, company, product, uom, |
200 quantity): | 210 quantity): |
201 values = { | 211 values = { |
202 'product': bom_io.product.id, | 212 'product': product.id, |
203 'product.rec_name': bom_io.product.rec_name, | 213 'uom': uom.id, |
204 'uom': bom_io.uom.id, | |
205 'uom.rec_name': bom_io.uom.rec_name, | |
206 'quantity': quantity, | 214 'quantity': quantity, |
207 'from_location': None, | 215 'from_location': None, |
208 'to_location': None, | 216 'to_location': None, |
209 'company': None, | 217 'company': None, |
210 'state': 'draft', | 218 'state': 'draft', |
211 } | 219 } |
| 220 values['currency'] = company.currency.id if company else None |
212 if from_location: | 221 if from_location: |
213 values['from_location'] = from_location.id | 222 values['from_location'] = from_location.id |
| 223 if to_location: |
| 224 values['to_location'] = to_location.id |
| 225 if company: |
| 226 values['company'] = company.id |
| 227 return values |
| 228 |
| 229 def _explode_move_values(self, from_location, to_location, company, |
| 230 bom_io, quantity): |
| 231 pool = Pool() |
| 232 move_obj = pool.get('stock.move') |
| 233 |
| 234 values = self._move_values(from_location, to_location, company, |
| 235 bom_io.product, bom_io.uom, quantity) |
| 236 values['product.rec_name'] = bom_io.product.rec_name |
| 237 values['uom.rec_name'] = bom_io.uom.rec_name |
| 238 values['unit_price_required'] = \ |
| 239 move_obj.on_change_with_unit_price_required({ |
| 240 'from_location': (from_location.id if from_location else |
| 241 None), |
| 242 'to_location': to_location.id if to_location else None, |
| 243 }) |
| 244 if from_location: |
214 values['from_location.rec_name'] = from_location.rec_name | 245 values['from_location.rec_name'] = from_location.rec_name |
215 if to_location: | 246 if to_location: |
216 values['to_location'] = to_location.id | |
217 values['to_location.rec_name'] = to_location.rec_name | 247 values['to_location.rec_name'] = to_location.rec_name |
218 if company: | 248 if company: |
219 values['company'] = company.id | |
220 values['company.rec_name'] = company.rec_name | 249 values['company.rec_name'] = company.rec_name |
221 return values | 250 return values |
222 | 251 |
223 def explode_bom(self, values): | 252 def explode_bom(self, values): |
224 pool = Pool() | 253 pool = Pool() |
225 bom_obj = pool.get('production.bom') | 254 bom_obj = pool.get('production.bom') |
226 product_obj = pool.get('product.product') | 255 product_obj = pool.get('product.product') |
227 uom_obj = pool.get('product.uom') | 256 uom_obj = pool.get('product.uom') |
228 input_obj = pool.get('production.bom.input') | 257 input_obj = pool.get('production.bom.input') |
229 output_obj = pool.get('production.bom.output') | 258 output_obj = pool.get('production.bom.output') |
230 location_obj = pool.get('stock.location') | 259 location_obj = pool.get('stock.location') |
231 company_obj = pool.get('company.company') | 260 company_obj = pool.get('company.company') |
232 | 261 |
233 if not (values.get('bom') | 262 if not (values.get('bom') |
234 and values.get('product') | 263 and values.get('product') |
235 and values.get('uom')): | 264 and values.get('uom')): |
236 return {} | 265 return {} |
237 inputs = { | 266 inputs = { |
238 'remove': [r['id'] for r in values.get('inputs') or []], | 267 'remove': [r['id'] for r in values.get('inputs') or []], |
239 'add': [], | 268 'add': [], |
240 } | 269 } |
241 outputs = { | 270 outputs = { |
242 'remove': [r['id'] for r in values.get('outputs') or []], | 271 'remove': [r['id'] for r in values.get('outputs') or []], |
243 'add': [], | 272 'add': [], |
244 } | 273 } |
245 changes = { | 274 changes = { |
246 'inputs': inputs, | 275 'inputs': inputs, |
247 'outputs': outputs, | 276 'outputs': outputs, |
| 277 'cost': Decimal(0), |
248 } | 278 } |
249 | 279 |
250 bom = bom_obj.browse(values['bom']) | 280 bom = bom_obj.browse(values['bom']) |
251 product = product_obj.browse(values['product']) | 281 product = product_obj.browse(values['product']) |
252 quantity = values.get('quantity') or 0 | 282 quantity = values.get('quantity') or 0 |
253 uom = uom_obj.browse(values['uom']) | 283 uom = uom_obj.browse(values['uom']) |
254 if values.get('warehouse'): | 284 if values.get('warehouse'): |
255 warehouse = location_obj.browse(values['warehouse']) | 285 warehouse = location_obj.browse(values['warehouse']) |
256 storage_location = warehouse.storage_location | 286 storage_location = warehouse.storage_location |
257 else: | 287 else: |
258 storage_location = None | 288 storage_location = None |
259 if values.get('location'): | 289 if values.get('location'): |
260 location = location_obj.browse(values['location']) | 290 location = location_obj.browse(values['location']) |
261 else: | 291 else: |
262 location = None | 292 location = None |
263 if values.get('company'): | 293 if values.get('company'): |
264 company = company_obj.browse(values['company']) | 294 company = company_obj.browse(values['company']) |
265 else: | 295 else: |
266 company = None | 296 company = None |
267 | 297 |
268 factor = bom_obj.compute_factor(bom, product, quantity, uom) | 298 factor = bom_obj.compute_factor(bom, product, quantity, uom) |
269 for input_ in bom.inputs: | 299 for input_ in bom.inputs: |
270 quantity = input_obj.compute_quantity(input_, factor) | 300 quantity = input_obj.compute_quantity(input_, factor) |
271 values = self._move_values(storage_location, location, company, | 301 values = self._explode_move_values(storage_location, location, |
272 input_, quantity) | 302 company, input_, quantity) |
273 if values: | 303 if values: |
274 inputs['add'].append(values) | 304 inputs['add'].append(values) |
| 305 quantity = uom_obj.compute_qty(input_.uom, quantity, |
| 306 input_.product.default_uom) |
| 307 changes['cost'] += (Decimal(str(quantity)) * |
| 308 input_.product.cost_price) |
275 | 309 |
276 for output in bom.outputs: | 310 for output in bom.outputs: |
277 quantity = output_obj.compute_quantity(output, factor) | 311 quantity = output_obj.compute_quantity(output, factor) |
278 values = self._move_values(location, storage_location, company, | 312 values = self._explode_move_values(location, storage_location, |
279 output, quantity) | 313 company, output, quantity) |
280 if values: | 314 if values: |
| 315 values['unit_price'] = Decimal(0) |
| 316 if output.product.id == values.get('product') and quantity: |
| 317 values['unit_price'] = (changes['cost'] / |
| 318 Decimal(str(quantity))) |
281 outputs['add'].append(values) | 319 outputs['add'].append(values) |
282 return changes | 320 return changes |
283 | 321 |
284 def on_change_warehouse(self, values): | 322 def on_change_warehouse(self, values): |
285 location_obj = Pool().get('stock.location') | 323 location_obj = Pool().get('stock.location') |
286 changes = { | 324 changes = { |
287 'location': False, | 325 'location': None, |
288 } | 326 } |
289 if values.get('warehouse'): | 327 if values.get('warehouse'): |
290 warehouse = location_obj.browse(values['warehouse']) | 328 warehouse = location_obj.browse(values['warehouse']) |
291 changes['location'] = warehouse.production_location.id | 329 changes['location'] = warehouse.production_location.id |
292 return changes | 330 return changes |
293 | 331 |
294 def on_change_product(self, values): | 332 def on_change_product(self, values): |
295 product_obj = Pool().get('product.product') | 333 product_obj = Pool().get('product.product') |
296 | 334 |
297 result = {} | 335 result = {} |
298 if values.get('product'): | 336 if values.get('product'): |
299 product = product_obj.browse(values['product']) | 337 product = product_obj.browse(values['product']) |
300 uom_ids = [x.id for x in product.default_uom.category.uoms] | 338 uom_ids = [x.id for x in product.default_uom.category.uoms] |
301 if (not values.get('uom') | 339 if (not values.get('uom') |
302 or values.get('uom') not in uom_ids): | 340 or values.get('uom') not in uom_ids): |
303 result['uom'] = product.default_uom.id | 341 result['uom'] = product.default_uom.id |
304 result['uom.rec_name'] = product.default_uom.rec_name | 342 result['uom.rec_name'] = product.default_uom.rec_name |
305 result['unit_digits'] = product.default_uom.digits | 343 result['unit_digits'] = product.default_uom.digits |
306 else: | 344 else: |
307 result['uom'] = False | 345 result['uom'] = None |
308 result['uom.rec_name'] = '' | 346 result['uom.rec_name'] = '' |
309 result['unit_digits'] = 2 | 347 result['unit_digits'] = 2 |
310 | 348 |
311 values = values.copy() | 349 values = values.copy() |
312 values['uom'] = result['uom'] | 350 values['uom'] = result['uom'] |
313 result.update(self.explode_bom(values)) | 351 result.update(self.explode_bom(values)) |
314 return result | 352 return result |
315 | 353 |
316 def on_change_with_uom_category(self, values): | 354 def on_change_with_uom_category(self, values): |
317 product_obj = Pool().get('product.product') | 355 product_obj = Pool().get('product.product') |
318 if values.get('product'): | 356 if values.get('product'): |
319 product = product_obj.browse(values['product']) | 357 product = product_obj.browse(values['product']) |
320 return product.default_uom.category.id | 358 return product.default_uom.category.id |
321 return False | |
322 | 359 |
323 def get_uom_category(self, ids, name): | 360 def get_uom_category(self, ids, name): |
324 res = {} | 361 res = {} |
325 for input in self.browse(ids): | 362 for production in self.browse(ids): |
326 res[input.id] = input.product.default_uom.category.id | 363 if production.product: |
| 364 res[production.id] = production.product.default_uom.category.id |
| 365 else: |
| 366 res[production.id] = None |
327 return res | 367 return res |
328 | 368 |
329 def on_change_with_unit_digits(self, values): | 369 def on_change_with_unit_digits(self, values): |
330 uom_obj = Pool().get('product.uom') | 370 uom_obj = Pool().get('product.uom') |
331 if values.get('uom'): | 371 if values.get('uom'): |
332 uom = uom_obj.browse(values['uom']) | 372 uom = uom_obj.browse(values['uom']) |
333 return uom.digits | 373 return uom.digits |
334 return 2 | 374 return 2 |
335 | 375 |
336 def get_unit_digits(self, ids, name): | 376 def get_unit_digits(self, ids, name): |
337 digits = {} | 377 digits = {} |
338 for production in self.browse(ids): | 378 for production in self.browse(ids): |
339 digits[production.id] = production.uom.digits | 379 if production.uom: |
| 380 digits[production.id] = production.uom.digits |
| 381 else: |
| 382 digits[production.id] = 2 |
340 return digits | 383 return digits |
341 | 384 |
342 def on_change_bom(self, values): | 385 def on_change_bom(self, values): |
343 return self.explode_bom(values) | 386 return self.explode_bom(values) |
344 | 387 |
345 def on_change_uom(self, values): | 388 def on_change_uom(self, values): |
346 return self.explode_bom(values) | 389 return self.explode_bom(values) |
347 | 390 |
348 def on_change_quantity(self, values): | 391 def on_change_quantity(self, values): |
349 return self.explode_bom(values) | 392 return self.explode_bom(values) |
| 393 |
| 394 def get_cost(self, ids, name): |
| 395 costs = {} |
| 396 for production in self.browse(ids): |
| 397 costs[production.id] = Decimal(0) |
| 398 for input_ in production.inputs: |
| 399 if input_.cost_price is not None: |
| 400 cost_price = input_.cost_price |
| 401 else: |
| 402 cost_price = input_.product.cost_price |
| 403 costs[production.id] += (Decimal(str(input_.internal_quantity)) |
| 404 * cost_price) |
| 405 return costs |
| 406 |
| 407 def on_change_with_cost(self, values): |
| 408 pool = Pool() |
| 409 product_obj = pool.get('product.product') |
| 410 uom_obj = pool.get('product.uom') |
| 411 |
| 412 cost = Decimal(0) |
| 413 if not values.get('inputs'): |
| 414 return cost |
| 415 |
| 416 product_ids = list(set(r['product'] for r in values['inputs'] if |
| 417 r['product'] is not None)) |
| 418 id2product = dict((p.id, p) for p in product_obj.browse(product_ids)) |
| 419 |
| 420 uom_ids = list(set(r['uom'] for r in values['inputs'])) |
| 421 id2uom = dict((u.id, u) for u in uom_obj.browse(uom_ids)) |
| 422 |
| 423 for input_ in values['inputs']: |
| 424 if (input_['product'] is None |
| 425 or input_['uom'] is None |
| 426 or input_['quantity'] is None): |
| 427 continue |
| 428 product = id2product[input_['product']] |
| 429 quantity = uom_obj.compute_qty(id2uom[input_['uom']], |
| 430 input_['quantity'], product.default_uom) |
| 431 cost += Decimal(str(quantity)) * product.cost_price |
| 432 return cost |
| 433 |
| 434 def set_moves(self, production): |
| 435 pool = Pool() |
| 436 bom_obj = pool.get('production.bom') |
| 437 move_obj = pool.get('stock.move') |
| 438 input_obj = pool.get('production.bom.input') |
| 439 output_obj = pool.get('production.bom.output') |
| 440 |
| 441 storage_location = production.warehouse.storage_location |
| 442 location = production.location |
| 443 company = production.company |
| 444 |
| 445 if not production.bom: |
| 446 if production.product: |
| 447 product = production.product |
| 448 values = self._move_values(location, storage_location, company, |
| 449 product, product.default_uom) |
| 450 if values: |
| 451 values['production_output'] = production.id |
| 452 move_obj.create(values) |
| 453 return |
| 454 |
| 455 factor = bom_obj.compute_factor(production.bom, production.product, |
| 456 production.quantity, production.uom) |
| 457 cost = Decimal(0) |
| 458 for input_ in production.bom.inputs: |
| 459 quantity = input_obj.compute_quantity(input_, factor) |
| 460 product = input_.product |
| 461 values = self._move_values(storage_location, location, company, |
| 462 product, product.default_uom, quantity) |
| 463 if values: |
| 464 values['production_input'] = production.id |
| 465 move_obj.create(values) |
| 466 cost += Decimal(str(quantity)) * product.cost_price |
| 467 |
| 468 for output in production.bom.outputs: |
| 469 quantity = output_obj.compute_quantity(output, factor) |
| 470 product = output.product |
| 471 values = self._move_values(location, storage_location, company, |
| 472 product, product.default_uom, quantity) |
| 473 if values: |
| 474 values['production_output'] = production.id |
| 475 if product == production.product: |
| 476 values['unit_price'] = cost / Decimal(str(quantity)) |
| 477 move_obj.create(values) |
| 478 self._set_move_planned_date([production.id]) |
| 479 |
| 480 def check_cost(self, ids): |
| 481 pool = Pool() |
| 482 currency_obj = pool.get('currency.currency') |
| 483 |
| 484 for production in self.browse(ids): |
| 485 if production.state != 'done': |
| 486 continue |
| 487 cost_price = Decimal(0) |
| 488 for output in production.outputs: |
| 489 cost_price += (Decimal(str(output.quantity)) |
| 490 * output.unit_price) |
| 491 if not currency_obj.is_zero(production.company.currency, |
| 492 production.cost - cost_price): |
| 493 return False |
| 494 return True |
350 | 495 |
351 def create(self, values): | 496 def create(self, values): |
352 sequence_obj = Pool().get('ir.sequence') | 497 sequence_obj = Pool().get('ir.sequence') |
353 config_obj = Pool().get('production.configuration') | 498 config_obj = Pool().get('production.configuration') |
354 | 499 |
355 values = values.copy() | 500 values = values.copy() |
356 config = config_obj.browse(1) | 501 config = config_obj.browse(1) |
357 values['code'] = sequence_obj.get_id(config.production_sequence.id) | 502 values['code'] = sequence_obj.get_id(config.production_sequence.id) |
358 production_id = super(Production, self).create(values) | 503 production_id = super(Production, self).create(values) |
359 self._set_move_planned_date(production_id) | 504 self._set_move_planned_date(production_id) |
(...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
526 return 'failed' | 671 return 'failed' |
527 | 672 |
528 def transition_force(self, session): | 673 def transition_force(self, session): |
529 pool = Pool() | 674 pool = Pool() |
530 production_obj = pool.get('production') | 675 production_obj = pool.get('production') |
531 | 676 |
532 production_obj.assign_force([Transaction().context['active_id']]) | 677 production_obj.assign_force([Transaction().context['active_id']]) |
533 return 'end' | 678 return 'end' |
534 | 679 |
535 Assign() | 680 Assign() |
LEFT | RIGHT |