# -*- coding: utf-8 -*- from odoo import models, fields, api from odoo.exceptions import UserError from math import ceil class SOS_Budget_Plan(models.Model): _name = 'sos_budget_plan' _description = 'Budget Plan' name= fields.Char(string="Plan Name") sfg_option = fields.Boolean('Semi-Finished Goods') fg_option = fields.Boolean('Finished Goods',default=True) fg_name = fields.Many2one('sos_fg', string='FG Name') sfg_name = fields.Many2one('sos_sfg', string="SFG Name") quantity = fields.Integer(string="Quantity",required=True) @api.onchange('sfg_option') def _onchange_sfg_option(self): if self.sfg_option: self.fg_option = False @api.onchange('fg_option') def _onchange_fg_option(self): if self.fg_option: self.sfg_option = False def create_budget_plan(self): self.ensure_one() if not self.quantity: raise UserError("Quantity must be greater than zero.") if not (self.fg_option or getattr(self, 'sfg_option', False)): raise UserError("Select Finished Goods or SFG option.") # --- Accumulator keyed by product/component id --- # { prod_id: {'name': str, 'needed': float, 'inhand': float, 'price': float} } agg = {} def _accumulate(line, needed_qty): """Accumulate needed qty for a line's primary component.""" prod = line.primary_component_id if not prod: return pid = prod.id rec = agg.get(pid) if rec: rec['needed'] += float(needed_qty or 0.0) else: agg[pid] = { 'name': prod.part_no or prod.name or '—', 'needed': float(needed_qty or 0.0), 'inhand': float(prod.inhand_stock_qty or 0.0), # take once per product 'price': float(prod.unit_price or 0.0), 'pack': float(getattr(prod, 'std_packing_qty', 0.0) or 0.0), } label_name = "" # what we'll display in the report header if self.fg_option: # -------- FG → explode SFGs + add direct materials -------- product_bom = self.env['sos_fg_bom'].search([ ('fg_name', '=', self.fg_name.id), ('is_primary', '=', True) ], limit=1) if not product_bom: raise UserError("BOM for product is Missing") label_name = self.fg_name.display_name # 1) FG BOM lines that reference SFG BOMs fg_sfg_lines = self.env['sos_fg_bom_line'].search([('bom_id', '=', product_bom.id)]) for sfg_line in fg_sfg_lines: sfg_bom = sfg_line.sfg_bom_id # Many2one to SFG BOM if not sfg_bom: continue # Required SFG qty = (FG qty * SFG-per-FG) - SFG in-hand (clamped ≥ 0) inhand_sfg = float((sfg_bom.name and sfg_bom.name.inhand_stock_qty) or 0.0) requested = float(self.quantity or 0.0) * float(sfg_line.quantity or 0.0) required_sfg_qty = max(requested - inhand_sfg, 0.0) if not required_sfg_qty: continue # Explode SFG BOM to components for the required SFG qty for comp in sfg_bom.sfg_bom_line_ids: needed_qty = float(comp.quantity or 0.0) * required_sfg_qty _accumulate(comp, needed_qty) # 2) Direct materials under the FG BOM direct_material_lines = self.env['sos_sfg_bom_line'].search([('fg_bom_id', '=', product_bom.id)]) for mat in direct_material_lines: needed_qty = float(mat.quantity or 0.0) * float(self.quantity or 0.0) _accumulate(mat, needed_qty) else: # -------- SFG-only path -------- if not getattr(self, 'sfg_option', False): raise UserError("SFG option must be selected.") sfg_bom = self.env['sos_sfg_bom'].search([('name', '=', self.sfg_name.id)], limit=1) if not sfg_bom: raise UserError("SFG BOM is missing.") label_name = self.sfg_name.display_name # Required SFG qty = requested - in-hand (clamped ≥ 0) inhand_sfg = float((sfg_bom.name and sfg_bom.name.inhand_stock_qty) or 0.0) requested = float(self.quantity or 0.0) required_sfg_qty = max(requested - inhand_sfg, 0.0) if required_sfg_qty: for comp in sfg_bom.sfg_bom_line_ids: needed_qty = float(comp.quantity or 0.0) * required_sfg_qty _accumulate(comp, needed_qty) # ---- Build final list from aggregator (merge duplicates; consider inhand once) ---- materials = [] for rec in agg.values(): raw_to_purchase = max(rec['needed'] - rec['inhand'], 0.0) if raw_to_purchase <= 0: continue pack = float(getattr(rec, 'pack', 0.0) or rec.get('pack') or 0.0) if pack > 0: pack_count = ceil(raw_to_purchase / pack) to_purchase = pack_count * pack else: pack_count = 0 to_purchase = raw_to_purchase materials.append({ 'material_name': rec['name'], 'needed_quantity': rec['needed'], 'inhand_quantity': rec['inhand'], 'actual_needed': raw_to_purchase, 'std_packing_qty': pack, 'packs_to_buy': pack_count, 'to_purchase': to_purchase, 'price': rec['price'], 'cost': round(to_purchase * rec['price'], 2), }) if not materials: raise UserError("No materials need to be purchased.") # Optional ordering: most to purchase first materials.sort(key=lambda r: (-r['to_purchase'], r['material_name'])) total_cost = sum(m['cost'] for m in materials) action = self.env.ref('sos_inventory.action_material_budget_summary') return action.report_action( self, data={ 'fg_name': label_name, # shown as header label 'total_quantity': self.quantity, 'materials': materials, 'total_cost': total_cost, 'currency_symbol': self.env.company.currency_id.symbol or '', } )