Slink/sos_inventory/models/sos_budget_plan.py

159 lines
6.6 KiB
Python
Executable File

# -*- 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 '',
}
)