159 lines
6.6 KiB
Python
Executable File
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 '',
|
|
}
|
|
) |