diff --git a/sos_sales/models/__pycache__/sos_proposal_boq.cpython-310.pyc b/sos_sales/models/__pycache__/sos_proposal_boq.cpython-310.pyc index 9b8f470..bc5e6a9 100644 Binary files a/sos_sales/models/__pycache__/sos_proposal_boq.cpython-310.pyc and b/sos_sales/models/__pycache__/sos_proposal_boq.cpython-310.pyc differ diff --git a/sos_sales/models/__pycache__/sos_sales_achievement_report.cpython-310.pyc b/sos_sales/models/__pycache__/sos_sales_achievement_report.cpython-310.pyc index 7b98b7b..8e69670 100644 Binary files a/sos_sales/models/__pycache__/sos_sales_achievement_report.cpython-310.pyc and b/sos_sales/models/__pycache__/sos_sales_achievement_report.cpython-310.pyc differ diff --git a/sos_sales/models/sos_proposal_boq.py b/sos_sales/models/sos_proposal_boq.py index a87954c..bb898ef 100755 --- a/sos_sales/models/sos_proposal_boq.py +++ b/sos_sales/models/sos_proposal_boq.py @@ -1,5 +1,7 @@ from odoo import models, fields, api import math +from collections import defaultdict +from odoo.tools.misc import format_amount class Battery_Installation_Requirement(models.Model): _name = 'sos_proposal_boq' @@ -100,6 +102,14 @@ class Battery_Installation_Requirement(models.Model): line_ids_fg_ups13 = fields.One2many('sos_proposal_boq_fg', 'ref_id', string="FG UPS 13",compute='_compute_line_ids_by_ups',store=False) line_ids_fg_ups14 = fields.One2many('sos_proposal_boq_fg', 'ref_id', string="FG UPS 14",compute='_compute_line_ids_by_ups',store=False) line_ids_fg_ups15 = fields.One2many('sos_proposal_boq_fg', 'ref_id', string="FG UPS 15",compute='_compute_line_ids_by_ups',store=False) + merged_fg_html = fields.Html(string="FG (Merged)", compute="_compute_merged_fg_html", sanitize=False) + merged_sfg_html = fields.Html(string="SFG (Merged)", compute="_compute_merged_sfg_html", sanitize=False) + merged_material_html = fields.Html(string="Materials (Merged)", compute="_compute_merged_material_html", sanitize=False) + merged_installation_kit_html = fields.Html(string="InstallationKit (Merged)", compute="_compute_merged_installation_kit_html", sanitize=False) + merged_miscellaneous_html = fields.Html(string="Miscellaneous (Merged)", compute="_compute_merged_miscellaneous_html", sanitize=False) + merged_spare_html = fields.Html(string="Spare (Merged)", compute="_compute_merged_spare_html", sanitize=False) + + #SFG Fields line_ids_sfg_ups1 = fields.One2many('sos_proposal_boq_sfg', 'ref_id', string="SFG UPS 1",compute='_compute_line_ids_by_ups',store=False) @@ -120,21 +130,21 @@ class Battery_Installation_Requirement(models.Model): #Material Fields - line_ids_material_ups1 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 1",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups2 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 2",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups3 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 3",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups4 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 4",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups5 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 5",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups6 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 6",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups7 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 7",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups8 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 8",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups9 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 9",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups10 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 10",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups11 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 11",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups12 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 12",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups13 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 13",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups14 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 14",compute='_compute_line_ids_by_ups',store=False) - line_ids_material_ups15 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 15",compute='_compute_line_ids_by_ups',store=False) + line_ids_material_ups1 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 1",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups2 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 2",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups3 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 3",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups4 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 4",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups5 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 5",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups6 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 6",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups7 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 7",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups8 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 8",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups9 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 9",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups10 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 10",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups11 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 11",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups12 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 12",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups13 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 13",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups14 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 14",compute='_compute_line_ids_by_ups',store=False,readonly=False) + line_ids_material_ups15 = fields.One2many('sos_proposal_boq_material', 'ref_id', string="Material UPS 15",compute='_compute_line_ids_by_ups',store=False,readonly=False) #Installation Kit Fields @@ -206,6 +216,454 @@ class Battery_Installation_Requirement(models.Model): line_ids_spare_ups13 = fields.One2many('sos_proposal_line_spare_ups13','ref_id', string="Spare UPS 13") line_ids_spare_ups14 = fields.One2many('sos_proposal_line_spare_ups14','ref_id', string="Spare UPS 14") line_ids_spare_ups15 = fields.One2many('sos_proposal_line_spare_ups15','ref_id', string="Spare UPS 15") + def _compute_merged_spare_html(self): + for rec in self: + # Collect lines from ups1..ups15 (skip models that don't exist) + models = [f'sos_proposal_line_spare_ups{i}' for i in range(1, 16)] + + agg = defaultdict(float) # (component_id, uom, currency_id, unit_price) -> qty + name_map, curr_map, price_map = {}, {}, {} + + for model in models: + if model not in self.env: + continue + for l in self.env[model].search([('ref_id', '=', rec.id)]): + unit_price = l.unit_price or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.component_id.id, l.uom, curr_id, unit_price) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + # Use part number (fallback to component name if needed) + name_map[key] = (getattr(l.component_id, 'part_no', False) or l.component_id.name or '') + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = unit_price + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_spare_html = "No Material items." + continue + + def fmt_price(amount, currency): + return format_amount(self.env, amount, currency) if currency else f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{key[1] or ''}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: + rec.merged_spare_html = "No Material items." + continue + + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_spare_html = f""" + + + + + + + + + + + {rows_html} +
Material NameUoMUnit PriceTotal QtyTotal Price
+ """ + def _compute_merged_miscellaneous_html(self): + for rec in self: + lines = self.env['sos_proposal_miscellaneous_items'].search([('ref_id', '=', rec.id)]) + + # Group by (component, currency, cost) + agg = defaultdict(float) + name_map = {} + curr_map = {} + price_map = {} + + for l in lines: + cost = l.cost or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.name, curr_id, cost) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + name_map[key] = l.name or '' + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = cost + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_miscellaneous_html = "No Miscellaneous items." + continue + + def fmt_price(amount, currency): + if currency: + return format_amount(self.env, amount, currency) + return f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: # Skip rows with qty = 0 + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: # If all rows were skipped + rec.merged_miscellaneous_html = "No Miscellaneous items." + continue + + # Add grand total row + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_miscellaneous_html = f""" + + + + + + + + + + {rows_html} +
NameUnit PriceTotal QtyTotal Price
+ """ + def _compute_merged_installation_kit_html(self): + for rec in self: + lines = self.env['sos_proposal_line_material_installation'].search([('ref_id', '=', rec.id)]) + + # Group by (component, uom, currency, unit_price) + agg = defaultdict(float) + name_map = {} + curr_map = {} + price_map = {} + + for l in lines: + unit_price = l.unit_price or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.component_id.id, l.uom, curr_id, unit_price) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + name_map[key] = l.component_id.part_no or '' + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = unit_price + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_installation_kit_html = "No Material items." + continue + + def fmt_price(amount, currency): + if currency: + return format_amount(self.env, amount, currency) + return f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: # Skip rows with qty = 0 + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{key[1] or ''}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: # If all rows were skipped + rec.merged_installation_kit_html = "No Material items." + continue + + # Add grand total row + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_installation_kit_html = f""" + + + + + + + + + + + {rows_html} +
Material NameUoMUnit PriceTotal QtyTotal Price
+ """ + def _compute_merged_material_html(self): + for rec in self: + lines = self.env['sos_proposal_boq_material'].search([('ref_id', '=', rec.id)]) + + # Group by (component, uom, currency, unit_price) + agg = defaultdict(float) + name_map = {} + curr_map = {} + price_map = {} + + for l in lines: + unit_price = l.unit_price or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.component_id.id, l.uom, curr_id, unit_price) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + name_map[key] = l.component_id.part_no or '' + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = unit_price + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_material_html = "No Material items." + continue + + def fmt_price(amount, currency): + if currency: + return format_amount(self.env, amount, currency) + return f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: # Skip rows with qty = 0 + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{key[1] or ''}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: # If all rows were skipped + rec.merged_material_html = "No Material items." + continue + + # Add grand total row + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_material_html = f""" + + + + + + + + + + + {rows_html} +
Material NameUoMUnit PriceTotal QtyTotal Price
+ """ + def _compute_merged_sfg_html(self): + for rec in self: + lines = self.env['sos_proposal_boq_sfg'].search([('ref_id', '=', rec.id)]) + + # Group by (component, uom, currency, unit_price) + agg = defaultdict(float) + name_map = {} + curr_map = {} + price_map = {} + + for l in lines: + unit_price = l.unit_price or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.component_id.id, l.uom, curr_id, unit_price) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + name_map[key] = l.component_id.name or '' + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = unit_price + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_sfg_html = "No SFG items." + continue + + def fmt_price(amount, currency): + if currency: + return format_amount(self.env, amount, currency) + return f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: # Skip rows with qty = 0 + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{key[1] or ''}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: # If all rows were skipped + rec.merged_sfg_html = "No SFG items." + continue + + # Add grand total row + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_sfg_html = f""" + + + + + + + + + + + {rows_html} +
SFG NameUoMUnit PriceTotal QtyTotal Price
+ """ + def _compute_merged_fg_html(self): + for rec in self: + lines = self.env['sos_proposal_boq_fg'].search([('ref_id', '=', rec.id)]) + + # Group by (component, uom, currency, unit_price) + agg = defaultdict(float) + name_map = {} + curr_map = {} + price_map = {} + + for l in lines: + unit_price = l.unit_price or 0.0 + curr_id = l.currency_id.id if l.currency_id else False + key = (l.component_id.id, l.uom, curr_id, unit_price) + + agg[key] += float(l.quantity or 0) + if key not in name_map: + name_map[key] = l.component_id.name or '' + curr_map[key] = l.currency_id if l.currency_id else False + price_map[key] = unit_price + + rows_sorted = sorted(agg.items(), key=lambda it: name_map[it[0]].lower()) + if not rows_sorted: + rec.merged_fg_html = "No FG items." + continue + + def fmt_price(amount, currency): + if currency: + return format_amount(self.env, amount, currency) + return f"{amount:.2f}" + + total_sum = 0.0 + rows_html = "" + for key, qty in rows_sorted: + if not qty: # Skip rows with qty = 0 + continue + total_price = qty * price_map[key] + total_sum += total_price + rows_html += ( + f"" + f"{name_map[key]}" + f"{key[1] or ''}" + f"{fmt_price(price_map[key], curr_map[key])}" + f"{qty:.2f}" + f"{fmt_price(total_price, curr_map[key])}" + f"" + ) + + if not rows_html: # If all rows were skipped + rec.merged_fg_html = "No FG items." + continue + + # Add grand total row + currency_for_total = curr_map[rows_sorted[0][0]] if rows_sorted else False + rows_html += ( + f"" + f"Grand Total:" + f"{fmt_price(total_sum, currency_for_total)}" + f"" + ) + + rec.merged_fg_html = f""" + + + + + + + + + + + {rows_html} +
FG NameUoMUnit PriceTotal QtyTotal Price
+ """ + @api.model def create(self, vals): res = super().create(vals) @@ -662,10 +1120,22 @@ class Battery_Installation_Requirement(models.Model): record.total_fg_cost = sum(line.total_price for line in record.line_ids_fg) def action_ce_esign_btn(self): body_html = f""" -

Below Proposal is waiting for your Updation

+

Below BOQ is waiting for your updation.

+

Customer Name: {self.customer_name or ''}

+

Location: {self.location or ''}

+

Number of Batteries: {self.number_of_batteries or ''}

""" + sequence_util = self.env['sos_common_scripts'] - sequence_util.send_group_email(self.env,'sos_proposal_boq',self.id,"deenalaura.m@sosaley.in","Proposal System - BOQ Submitted",body_html,'sos_inventory.sos_finance_user') + sequence_util.send_group_email( + self.env, + 'sos_proposal_boq', + self.id, + "deenalaura.m@sosaley.in", + f"Proposal System - BOQ Submitted for {self.customer_name}", + body_html, + 'sos_inventory.sos_finance_user' + ) return sequence_util.action_assign_signature( self, 'boq_submitted_by_name', diff --git a/sos_sales/models/sos_sales_achievement_report.py b/sos_sales/models/sos_sales_achievement_report.py index 25b26ca..7bb5410 100755 --- a/sos_sales/models/sos_sales_achievement_report.py +++ b/sos_sales/models/sos_sales_achievement_report.py @@ -694,16 +694,31 @@ class SOS_Sales_Achievement_Report_Brief(models.Model): report.write({ new_field_billed: (getattr(report, new_field_billed, 0.0) or 0.0) + new_billed_amount }) - # Optionally create billing collection entry (if needed) - if new_billed_amount > 0: + # Optionally create billing collection entry (if needed + domain = [ + ('ref_id', '=', report.id), + ('sales_person', '=', report.sales_person.id), + ('customer_name', '=', vals.get('customer_name', rec.customer_name.id)) + ] + + existing = self.env['sos_billing_collection'].search(domain, limit=1) + + if not existing: self.env['sos_billing_collection'].create({ 'ref_id': report.id, 'customer_name': vals.get('customer_name', rec.customer_name.id), 'sales_person': report.sales_person.id, 'action_status': 'Billed', 'date_of_action': new_billed_date, + 'po_no':vals.get('po_no'), 'value': new_billed_amount }) + else: + existing.write({ + 'value': new_billed_amount, + 'po_no':vals.get('po_no'), + 'date_of_action': new_billed_date + }) return super(SOS_Sales_Achievement_Report_Brief, self).write(vals) @@ -748,7 +763,8 @@ class SOS_Sales_Achievement_Report_Brief(models.Model): 'sales_person': report.sales_person.id, 'action_status': 'Billed', 'date_of_action': billed_date, - 'value': billed_value + 'value': billed_value, + 'po_no':vals.get('po_no') }) new_record = super(SOS_Sales_Achievement_Report_Brief, self).create(vals) return new_record diff --git a/sos_sales/views/sos_proposal_boq_view.xml b/sos_sales/views/sos_proposal_boq_view.xml index 28e00a0..f70bcc1 100755 --- a/sos_sales/views/sos_proposal_boq_view.xml +++ b/sos_sales/views/sos_proposal_boq_view.xml @@ -40,6 +40,7 @@ +

Finished Goods

@@ -1654,115 +1655,38 @@
- - - -

- + - - + +
No of PersonsCost
No of PersonsCost per Day
Man Month( 1 to 2 Yrs)
Man Month( 2 to 3 Yrs)
Man Month( 2 to 3 Yrs)
Man Month( 3 to 5 Yrs)
Man Month(Manager)
Total