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 trytond.model import ModelView, ModelSQL, fields | 3 from trytond.model import ModelView, ModelSQL, fields |
| 4 from trytond.wizard import Wizard, StateView, Button |
| 5 from trytond.transaction import Transaction |
4 from trytond.pyson import Eval | 6 from trytond.pyson import Eval |
| 7 from trytond.pool import Pool |
5 | 8 |
6 | 9 |
7 class BOM(ModelSQL, ModelView): | 10 class BOM(ModelSQL, ModelView): |
8 "Bill of Material" | 11 "Bill of Material" |
9 _name = 'production.bom' | 12 _name = 'production.bom' |
10 _description = __doc__ | 13 _description = __doc__ |
11 | 14 |
12 name = fields.Char('Name', required=True, translate=True) | 15 name = fields.Char('Name', required=True, translate=True) |
13 active = fields.Boolean('Active', select=1) | 16 active = fields.Boolean('Active', select=1) |
14 inputs = fields.One2Many('production.bom.input', 'bom', 'Inputs') | 17 inputs = fields.One2Many('production.bom.input', 'bom', 'Inputs') |
15 outputs = fields.One2Many('production.bom.output', 'bom', 'Outputs') | 18 outputs = fields.One2Many('production.bom.output', 'bom', 'Outputs') |
16 output_products = fields.Many2Many('production.bom.output', | 19 output_products = fields.Many2Many('production.bom.output', |
17 'bom', 'product', 'Output Products') | 20 'bom', 'product', 'Output Products') |
18 | 21 |
19 def default_active(self): | 22 def default_active(self): |
20 return True | 23 return True |
| 24 |
| 25 def compute_factor(self, bom, product, quantity, uom): |
| 26 ''' |
| 27 Compute factor for an output product |
| 28 ''' |
| 29 uom_obj = Pool().get('product.uom') |
| 30 for output in bom.outputs: |
| 31 if output.product == product: |
| 32 quantity = uom_obj.compute_qty(uom, quantity, |
| 33 output.uom, round=False) |
| 34 return quantity / output.quantity |
| 35 |
| 36 def copy(self, ids, default=None): |
| 37 if default is None: |
| 38 default = {} |
| 39 else: |
| 40 default = default.copy() |
| 41 default['output_products'] = False |
| 42 return super(BOM, self).copy(ids, default=default) |
21 | 43 |
22 BOM() | 44 BOM() |
23 | 45 |
24 | 46 |
25 class BOMInput(ModelSQL, ModelView): | 47 class BOMInput(ModelSQL, ModelView): |
26 "Bill of Material Input" | 48 "Bill of Material Input" |
27 _name = 'production.bom.input' | 49 _name = 'production.bom.input' |
28 _description = __doc__ | 50 _description = __doc__ |
29 _rec_name = 'product' | 51 _rec_name = 'product' |
30 | 52 |
31 bom = fields.Many2One('production.bom', 'BOM', required=True, | 53 bom = fields.Many2One('production.bom', 'BOM', required=True, |
32 select=1) | 54 select=1) |
33 product = fields.Many2One('product.product', 'Product', | 55 product = fields.Many2One('product.product', 'Product', |
34 required=True, domain=[ | 56 required=True, domain=[ |
35 ('type', '!=', 'service'), | 57 ('type', '!=', 'service'), |
36 ], on_change=['product', 'uom']) | 58 ], on_change=['product', 'uom']) |
37 uom_category = fields.Function(fields.Many2One( | 59 uom_category = fields.Function(fields.Many2One( |
38 'product.uom.category', 'Uom Category', | 60 'product.uom.category', 'Uom Category', |
39 on_change_with=['product']), 'get_uom_category') | 61 on_change_with=['product']), 'get_uom_category') |
40 uom = fields.Many2One('product.uom', 'Uom', required=True, | 62 uom = fields.Many2One('product.uom', 'Uom', required=True, |
41 domain=[ | 63 domain=[ |
42 ('category', '=', Eval('uom_category')), | 64 ('category', '=', Eval('uom_category')), |
43 ], depends=['uom_category']) | 65 ], depends=['uom_category']) |
44 unit_digits = fields.Function(fields.Integer('Unit Digits', | 66 unit_digits = fields.Function(fields.Integer('Unit Digits', |
45 on_change_with=['uom']), 'get_unit_digits') | 67 on_change_with=['uom']), 'get_unit_digits') |
46 quantity = fields.Float('Quantity', required=True, | 68 quantity = fields.Float('Quantity', required=True, |
47 digits=(16, Eval('unit_digits', 2)), | 69 digits=(16, Eval('unit_digits', 2)), |
48 depends=['unit_digits']) | 70 depends=['unit_digits']) |
49 | 71 |
| 72 def __init__(self): |
| 73 super(BOMInput, self).__init__() |
| 74 self._sql_constraints = [ |
| 75 ('product_bom_uniq', 'UNIQUE(product, bom)', |
| 76 'product_bom_uniq'), |
| 77 ] |
| 78 self._constraints += [ |
| 79 ('check_bom_recursion', 'recursive_bom'), |
| 80 ] |
| 81 self._error_messages.update({ |
| 82 'product_bom_uniq': 'Product must be unique per BOM!', |
| 83 'recursive_bom': 'You can not create recursive BOMs!', |
| 84 }) |
| 85 |
50 def on_change_product(self, vals): | 86 def on_change_product(self, vals): |
51 product_obj = self.pool.get('product.product') | 87 product_obj = Pool().get('product.product') |
52 | 88 |
53 res = {} | 89 res = {} |
54 if vals.get('product'): | 90 if vals.get('product'): |
55 product = product_obj.browse(vals['product']) | 91 product = product_obj.browse(vals['product']) |
56 uom_ids = [x.id for x in product.default_uom.category.uoms] | 92 uom_ids = [x.id for x in product.default_uom.category.uoms] |
57 if (not vals.get('uom') | 93 if (not vals.get('uom') |
58 or vals.get('uom') not in uom_ids): | 94 or vals.get('uom') not in uom_ids): |
59 res['uom'] = product.default_uom.id | 95 res['uom'] = product.default_uom.id |
60 res['uom.rec_name'] = product.default_uom.rec_name | 96 res['uom.rec_name'] = product.default_uom.rec_name |
61 res['unit_digits'] = product.default_uom.digits | 97 res['unit_digits'] = product.default_uom.digits |
62 else: | 98 else: |
63 res['uom'] = False | 99 res['uom'] = False |
64 res['uom.rec_name'] = '' | 100 res['uom.rec_name'] = '' |
65 res['unit_digits'] = 2 | 101 res['unit_digits'] = 2 |
66 return res | 102 return res |
67 | 103 |
68 def on_change_with_uom_category(self, vals): | 104 def on_change_with_uom_category(self, vals): |
69 product_obj = self.pool.get('product.product') | 105 product_obj = Pool().get('product.product') |
70 if vals.get('product'): | 106 if vals.get('product'): |
71 product = product_obj.browse(vals['product']) | 107 product = product_obj.browse(vals['product']) |
72 return product.default_uom.category.id | 108 return product.default_uom.category.id |
73 return False | 109 return False |
74 | 110 |
75 def get_uom_category(self, ids, name): | 111 def get_uom_category(self, ids, name): |
76 res = {} | 112 res = {} |
77 for input in self.browse(ids): | 113 for input in self.browse(ids): |
78 res[input.id] = input.product.default_uom.category.id | 114 res[input.id] = input.product.default_uom.category.id |
79 return res | 115 return res |
80 | 116 |
81 def on_change_with_unit_digits(self, vals): | 117 def on_change_with_unit_digits(self, vals): |
82 uom_obj = self.pool.get('product.uom') | 118 uom_obj = Pool().get('product.uom') |
83 if vals.get('uom'): | 119 if vals.get('uom'): |
84 uom = uom_obj.browse(vals['uom']) | 120 uom = uom_obj.browse(vals['uom']) |
85 return uom.digits | 121 return uom.digits |
86 return 2 | 122 return 2 |
87 | 123 |
88 def get_unit_digits(self, ids, name): | 124 def get_unit_digits(self, ids, name): |
89 res = {} | 125 res = {} |
90 for input in self.browse(ids): | 126 for input in self.browse(ids): |
91 res[input.id] = input.uom.digits | 127 res[input.id] = input.uom.digits |
92 return res | 128 return res |
93 | 129 |
| 130 def check_bom_recursion(self, ids): |
| 131 ''' |
| 132 Check BOM recursion |
| 133 ''' |
| 134 product_obj = Pool().get('product.product') |
| 135 |
| 136 inputs = self.browse(ids) |
| 137 product_ids = [input.product.id for input in inputs] |
| 138 return product_obj.check_bom_recursion(product_ids) |
| 139 |
| 140 def compute_quantity(self, line, factor): |
| 141 uom_obj = Pool().get('product.uom') |
| 142 return uom_obj.round(line.quantity * factor, line.uom.rounding) |
| 143 |
94 BOMInput() | 144 BOMInput() |
95 | 145 |
96 | 146 |
97 class BOMOutput(BOMInput): | 147 class BOMOutput(BOMInput): |
98 "Bill of Material OutPut" | 148 "Bill of Material Output" |
99 _name = 'production.bom.output' | 149 _name = 'production.bom.output' |
100 _description = __doc__ | 150 _description = __doc__ |
101 | 151 |
102 BOMOutput() | 152 BOMOutput() |
| 153 |
| 154 |
| 155 class BOMTree(ModelView): |
| 156 'BOM Tree' |
| 157 _name = 'production.bom.tree' |
| 158 _description = __doc__ |
| 159 |
| 160 product = fields.Many2One('product.product', 'Product') |
| 161 quantity = fields.Float('Quantity', digits=(16, Eval('unit_digits', 2)), |
| 162 depends=['unit_digits']) |
| 163 uom = fields.Many2One('product.uom', 'Uom') |
| 164 unit_digits = fields.Integer('Unit Digits') |
| 165 childs = fields.One2Many('production.bom.tree', None, 'Childs') |
| 166 |
| 167 def tree(self, product, quantity, uom, bom=None): |
| 168 bom_obj = Pool().get('production.bom') |
| 169 input_obj = Pool().get('production.bom.input') |
| 170 |
| 171 result = [] |
| 172 if bom is None: |
| 173 if not product.boms: |
| 174 return result |
| 175 bom = product.boms[0].bom |
| 176 |
| 177 factor = bom_obj.compute_factor(bom, product, quantity, uom) |
| 178 for input in bom.inputs: |
| 179 quantity = input_obj.compute_quantity(input, factor) |
| 180 childs = self.tree(input.product, quantity, input.uom) |
| 181 values = { |
| 182 'product': input.product.id, |
| 183 'quantity': quantity, |
| 184 'uom': input.uom.id, |
| 185 'unit_digits': input.uom.digits, |
| 186 'childs': childs, |
| 187 } |
| 188 result.append(values) |
| 189 return result |
| 190 |
| 191 BOMTree() |
| 192 |
| 193 |
| 194 class OpenBOMTreeStart(ModelView): |
| 195 'Open BOM Tree' |
| 196 _name = 'production.bom.tree.open.start' |
| 197 |
| 198 quantity = fields.Float('Quantity', required=True, |
| 199 digits=(16, Eval('unit_digits', 2)), depends=['unit_digits']) |
| 200 uom = fields.Many2One('product.uom', 'Unit', required=True, |
| 201 domain=[ |
| 202 ('category', '=', Eval('category')), |
| 203 ], depends=['category']) |
| 204 unit_digits = fields.Integer('Unit Digits', readonly=True, |
| 205 on_change_with=['uom']) |
| 206 category = fields.Many2One('product.uom.category', 'Category', |
| 207 readonly=True) |
| 208 bom = fields.Many2One('product.product-production.bom', |
| 209 'BOM', required=True, domain=[ |
| 210 ('product', '=', Eval('product')), |
| 211 ], depends=['product']) |
| 212 product = fields.Many2One('product.product', 'Product', readonly=True) |
| 213 |
| 214 def on_change_with_unit_digits(self, values): |
| 215 uom_obj = Pool().get('product.uom') |
| 216 if values.get('uom'): |
| 217 uom = uom_obj.browse(values['uom']) |
| 218 return uom.digits |
| 219 return 2 |
| 220 |
| 221 OpenBOMTreeStart() |
| 222 |
| 223 |
| 224 class OpenBOMTreeTree(ModelView): |
| 225 'Open BOM Tree' |
| 226 _name = 'production.bom.tree.open.tree' |
| 227 |
| 228 bom_tree = fields.One2Many('production.bom.tree', None, 'BOM Tree') |
| 229 |
| 230 def tree(self, bom, product, quantity, uom): |
| 231 pool = Pool() |
| 232 tree_obj = pool.get('production.bom.tree') |
| 233 |
| 234 childs = tree_obj.tree(product, quantity, uom, bom=bom) |
| 235 bom_tree = [{ |
| 236 'product': product.id, |
| 237 'quantity': quantity, |
| 238 'uom': uom.id, |
| 239 'unit_digits': uom.digits, |
| 240 'childs': childs, |
| 241 }] |
| 242 return { |
| 243 'bom_tree': bom_tree, |
| 244 } |
| 245 |
| 246 OpenBOMTreeTree() |
| 247 |
| 248 |
| 249 class OpenBOMTree(Wizard): |
| 250 'Open BOM Tree' |
| 251 _name = 'production.bom.tree.open' |
| 252 |
| 253 start = StateView('production.bom.tree.open.start', |
| 254 'production.bom_tree_open_start_view_form', [ |
| 255 Button('Cancel', 'end', 'tryton-cancel'), |
| 256 Button('Ok', 'tree', 'tryton-ok', True), |
| 257 ]) |
| 258 tree = StateView('production.bom.tree.open.tree', |
| 259 'production.bom_tree_open_tree_view_form', [ |
| 260 Button('Change', 'start', 'tryton-go-previous'), |
| 261 Button('Close', 'end', 'tryton-close'), |
| 262 ]) |
| 263 |
| 264 def default_start(self, session, fields): |
| 265 product_obj = Pool().get('product.product') |
| 266 defaults = {} |
| 267 product = product_obj.browse(Transaction().context['active_id']) |
| 268 defaults['category'] = product.default_uom.category.id |
| 269 if session.start.uom: |
| 270 defaults['uom'] = session.start.uom.id |
| 271 defaults['unit_digits'] = session.start.unit_digits |
| 272 else: |
| 273 defaults['uom'] = product.default_uom.id |
| 274 defaults['unit_digits'] = product.default_uom.digits |
| 275 defaults['product'] = product.id |
| 276 if session.start.bom: |
| 277 defaults['bom'] = session.start.bom.id |
| 278 elif product.boms: |
| 279 defaults['bom'] = product.boms[0].id |
| 280 defaults['quantity'] = session.start.quantity |
| 281 return defaults |
| 282 |
| 283 def default_tree(self, session, fields): |
| 284 pool = Pool() |
| 285 bom_tree_obj = pool.get('production.bom.tree.open.tree') |
| 286 return bom_tree_obj.tree(session.start.bom.bom, session.start.product, |
| 287 session.start.quantity, session.start.uom) |
| 288 |
| 289 OpenBOMTree() |
LEFT | RIGHT |