from odoo import models, fields, api from datetime import date from decimal import Decimal, ROUND_HALF_UP from datetime import date, timedelta,datetime from odoo.exceptions import UserError class sos_case_diary(models.Model): _name = 'sos_case_diary' _description = 'Sosaley Case Diary' _rec_name="display_name" _sql_constraints = [ ('unique_customer_casediary', 'unique(customer_name, quote_no)', 'A customer with the same quote no already exists!') ] display_name = fields.Char(string='Display Name', compute='_compute_display_name', store=True) customer_name = fields.Many2one('sos_customers',string="Customer Name") end_customer_name = fields.Char(string="End Customer Name") quote_no = fields.Char(string="Quote/W.O No",help="Fill this if the same customer has different work orders.") line_ids_contacts = fields.One2many(related="customer_name.line_ids_contacts", string="Contact Details",copy=True) customer_city = fields.Char(string="City") correspondence_address = fields.Text(string="Correspondence Address",related="customer_name.correspondence_address") lead_generated_by = fields.Selection([('Self','Self'),('Inside Sales','Inside Sales'),('Expo Directory','Expo Directory'),('Expo Exhibitors','Expo Exhibitors'),('Associations','Associations'),('Website','Website'),('Linkedin','Linkedin'),('Conference','Conference'),('Others','Others')],string='Lead Generated By') lead_generated_by_others = fields.Char(string="Lead Generated By - Others") inside_sales_person = fields.Many2one('res.users', string='Inside Sales person') sales_person = fields.Many2one( 'res.users', string='Sales Executive', default=lambda self: self.env.user, domain=lambda self: [('groups_id', 'in', self.env.ref('sos_inventory.sos_sales_user').ids + self.env.ref('sos_inventory.sos_ce_head').ids)]) currency_id = fields.Many2one( 'res.currency', string='Currency', default=lambda self: self.env['res.currency'].search([('name', '=', 'INR')], limit=1).id or False ) account_start_date = fields.Date(string="Account Start Date") proposal_value = fields.Monetary(currency_field='currency_id',string="Proposal Value(In Lakhs)") quantity = fields.Char(string="Quantity") interested_in = fields.Selection( [ ('products', 'Products'), ('projects', 'Projects') ], string="Interested In",required=True,default="products") project_name = fields.Char(string="Project Name") products = fields.Selection( [ ('BHMS 1.2V', 'BHMS 1.2V'), ('BHMS 2V', 'BHMS 2V'), ('BHMS 12V', 'BHMS 12V'), ('BHMS 48V', 'BHMS 48V'), ('BMS-HV', 'BMS-HV'), ('BMS-LV 100A', 'BMS-LV 100A'), ('BMS-LV 40A', 'BMS-LV 40A'), ('SBMS 55A', 'SBMS 55A'), ('MC 250W', 'MC 250W'), ('HeartTarang', 'HeartTarang') ], string="Products") ce_product_type = fields.Selection( [ ('Sales', 'Sales'), ('Service', 'Service'), ('Spare', 'Spare'), ('Cloud', 'Cloud') ], string="Service Type") line_ids = fields.One2many('sos_case_diary_line', 'ref_id', string="Action") status = fields.Selection([('open', 'Open'), ('close', 'Closed without Order'), ('close_order', 'Closed with Order') ],default='open',string='Status') spenco_status = fields.Selection([('Suspects','Suspects'),('Prospects','Prospects'),('Engaged','Engaged'),('Negotiation','Negotiation'),('Commercial Order','Commercial Order')],string='Current State',compute='_compute_is_latest',store=True) dept_incharge_name = fields.Many2one('res.users', string='Dept-Incharge Approver') dept_incharge_approval_image = fields.Image(related="dept_incharge_name.signature_image",string='Top Management Approval',readonly=True) dept_incharge_approved_on = fields.Datetime(string="Approved On") reporting_to = fields.Many2one('res.users', string='Reporting To') transfer_history_ids = fields.One2many('sos_case_transfer_history', 'case_diary_id', string="Transfer History") po_no= fields.Char(string="PO No") po_copy = fields.Binary(string="PO Copy") po_copy_filename=fields.Char(string="PO DocumentFile Name") order_expected_on = fields.Date(string="Order Expected On") @api.depends('end_customer_name', 'quote_no') def _compute_display_name(self): for rec in self: end_customer = rec.end_customer_name.strip() if rec.end_customer_name else '-' quote = rec.quote_no.strip() if rec.quote_no else '-' rec.display_name = f"{end_customer} / {quote}" def action_transfer_sales_person(self): """ Opens a wizard to transfer sales person """ return { 'type': 'ir.actions.act_window', 'name': 'Transfer Sales Person', 'res_model': 'sos_case_transfer_wizard', 'view_mode': 'form', 'target': 'new', 'context': {'default_case_diary_id': self.id} } def _get_financial_year(self): current_date = date.today() start_year = current_date.year if current_date.month >= 4 else current_date.year - 1 end_year = start_year + 1 return f"FY {start_year}-{end_year}" @api.model def create(self, vals): create_uid = vals.get('create_uid', self.env.uid) create_user = self.env['res.users'].browse(create_uid) vals['reporting_to'] = create_user.reporting_to.id return super(sos_case_diary, self).create(vals) def action_deptincharge_esign_btn(self): fy = self._get_financial_year() if not self.line_ids: raise UserError("No lines found to process.") # Get the last record from line_ids last_record = self.line_ids[-1] if not last_record.status_changed_on: raise UserError("Status changed date is not set on the last line.") month_name = last_record.status_changed_on.strftime('%B').lower() actual_field = f"actual_target_{month_name}" # Get or create the sales achievement record sales_achievement = self.env['sos_sales_achievement_report'].search([ ('financial_year', '=', fy), ('sales_person', '=', last_record.ref_id.sales_person.id) ], limit=1) # Calculate new achieved target if sales_achievement: current_achieved_target = getattr(sales_achievement, actual_field, 0.0) or 0.0 new_achieved_target = current_achieved_target + last_record.current_state_value sales_achievement.write({actual_field: new_achieved_target}) ref_id = sales_achievement.id proposal_value = last_record.current_state_value else: new_record = self.env['sos_sales_achievement_report'].create({ 'financial_year': fy, 'sales_person': last_record.ref_id.sales_person.id, actual_field: last_record.current_state_value }) ref_id = new_record.id proposal_value = last_record.current_state_value # Log brief entry self.env['sos_sales_achievement_report_brief'].create({ 'ref_id': ref_id, 'customer_name': self.customer_name.id, 'action_date': last_record.status_changed_on, 'proposal_value': proposal_value, 'po_no':self.po_no }) # Assign signature self.env['sos_common_scripts'].action_assign_signature( self, 'dept_incharge_name', 'dept_incharge_approved_on' ) @api.onchange('customer_name') def _onchange_customer_name(self): for record in self: primary_record = self.env['sos_customers_line'].search( [('ref_id', '=', record.customer_name.id), ('set_as_primary', '=', True)], order="id desc", limit=1 ) record.customer_city = primary_record.ref_id.customer_city record.correspondence_address = primary_record.ref_id.correspondence_address @api.depends('line_ids') def _compute_is_latest(self): for record in self: latest_record = self.env['sos_case_diary_line'].search( [('ref_id', '=', record.id)], order="id desc", limit=1 ) self.spenco_status = latest_record.spenco_status if latest_record.spenco_status == "Commercial Order": users = self.env['res.users'].browse(self.env.user.id) if users.reporting_to: reporting_user = self.env['res.users'].browse(users.reporting_to.id) reporting_to = reporting_user.login if reporting_to: # Email part body_html = f"""

Below Commercial Order is waiting for your Acknowledgement

""" subject = f"Commercial Order Received - {self.customer_name.customer_name}" sequence_util = self.env['sos_common_scripts'] sequence_util.send_direct_email(self.env, "sos_case_diary", self.id, reporting_to, subject, body_html) def action_report_pipeline_btn(self, sales_person_id=None, from_date=None, to_date=None): try: domain = [('status', '!=', 'close')] if sales_person_id: domain.append(('sales_person', '=', sales_person_id)) if from_date: domain.append(('order_expected_on', '>=', from_date)) if to_date: domain.append(('order_expected_on', '<=', to_date)) records = self.env['sos_case_diary'].search(domain, order="order_expected_on asc") if records: # Prepare month-year formatted data data_by_month = {} totals_by_month = {} for record in records: order_month_year = record.order_expected_on.strftime('%B %Y') # E.g., "May 2025" if order_month_year not in data_by_month: data_by_month[order_month_year] = [] totals_by_month[order_month_year] = 0.0 data_by_month[order_month_year].append({ 'record_id': record.id, 'quote_no': record.quote_no, 'customer_name': record.customer_name.customer_name, 'end_customer_name':record.end_customer_name, 'customer_city':record.customer_city, 'sales_person': record.sales_person.name, 'order_expected_on': record.order_expected_on, 'proposal_value':record.proposal_value, 'products':record.products }) totals_by_month[order_month_year] += record.proposal_value or 0.0 action = self.env.ref("sos_sales.action_report_pipeline").with_context(landscape=True).report_action( self, data={'data_by_month': data_by_month,'totals_by_month': totals_by_month} ) return action else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'message': "No Records Found", 'type': 'danger', 'sticky': False } } except ValueError as e: print(f"Failed to find report action: {e}") def action_report_action_plan_btn(self, sales_person_id=None, from_date=None, to_date=None): try: domain = [] if sales_person_id: domain.append(('sales_executive', '=', sales_person_id)) if from_date: domain.append(('date', '>=', from_date)) if to_date: domain.append(('date', '<=', to_date)) records = self.env['sos_sales_action_plan'].search(domain, order="date asc") if records: return self.env.ref("sos_sales.action_report_action_plan").with_context(landscape=True).report_action(records) else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'message': "No Records Found", 'type': 'danger', 'sticky': False } } except ValueError as e: print(f"Failed to find report action: {e}") def action_report_spenco_btn(self, sales_person_id=None, from_date=None, to_date=None, customer=None): try: # Filter records based on sales_person_id if provided domain = [('status', '!=', 'close')] if customer: domain.append(('customer_name', '=', customer.id)) if sales_person_id: domain.append(('sales_person', '=', sales_person_id)) if to_date: domain.append(('line_ids.status_changed_on', '<=', to_date)) domain.append(('spenco_status', 'not in', ('Suspects', '', False, None))) records = self.env['sos_case_diary'].search(domain) if records: action = self.env.ref("sos_sales.action_report_spenco").with_context(landscape=True).report_action( self, data={ 'sales_person_id': sales_person_id, 'from_date': from_date, 'to_date': to_date, 'customer_id': customer.id if customer else None, } ) return action else: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'message': "No Records Found", 'type': 'danger', 'sticky': False } } except ValueError as e: print(f"Failed to find report action: {e}") class SosCaseDiaryLine(models.Model): _name = 'sos_case_diary_line' _description = 'Sosaley Case Diary Actions' ref_id = fields.Many2one('sos_case_diary', ondelete="cascade") spenco_status = fields.Selection([ ('Suspects', 'Suspects'), ('Prospects', 'Prospects'), ('Engaged', 'Engaged'), ('Negotiation', 'Negotiation'), ('Commercial Order', 'Commercial Order') ], string='Status') action_plan_date = fields.Date(string="Action Date",required=True) currency_id = fields.Many2one( 'res.currency', string='Currency', default=lambda self: self.env['res.currency'].search([('name', '=', 'INR')], limit=1).id or False ) current_state_value = fields.Monetary(currency_field='currency_id', string="Value (In Lakhs)") action_plan = fields.Text(string="Action Plan") action_type = fields.Selection([ ('Meeting', 'Meeting'), ('Demo', 'Demo'), ('Discussion', 'Discussion'), ('Visit', 'Direct Visit'), ('Negotiation', 'Negotiation'), ('Validation', 'Validation'), ('Proposal', 'Proposal'), ('Email/Call/Video Call', 'Email/Call/Video Call') ], string='Action Type') next_action_date = fields.Date(string="Next Action On") status_changed_on = fields.Date(string="Status Changed On") notes = fields.Text(string="Remarks") @api.model def check_expired_status(self): today = date.today() threshold_date = today - timedelta(days=30) records = self.search([ ('status_changed_on', '<=', threshold_date), ('spenco_status', 'in', ['Prospects', 'Engaged']), ]) if records: sequence_util = self.env['sos_common_scripts'] for record in records: ref = record.ref_id sales_person = ref.sales_person if ref and sales_person: body_html = f"""

The SPENCO status of the following case diary has been downgraded due to no activity in past 30 Days.

""" sequence_util.send_direct_email( self.env, "sos_case_diary", ref.id, sales_person.login, "SPENCO Status Downgrade", body_html, "ramachandran.r@sosaley.in" ) for rec in records: original_status = rec.spenco_status if rec.spenco_status == 'Prospects': rec.write({'spenco_status': 'Suspects'}) elif rec.spenco_status == 'Engaged': rec.write({'spenco_status': 'Prospects'}) @api.onchange('spenco_status') def _onchange_spenco_status(self): if self.spenco_status and self.ref_id: domain = [ ('ref_id', '=', self.ref_id.id), ('spenco_status', '=', self.spenco_status) ] if self.id: domain.append(('id', '!=', self.id)) old_record = self.env['sos_case_diary_line'].search(domain, order="status_changed_on desc", limit=1) if old_record and old_record.status_changed_on: self.status_changed_on = old_record.status_changed_on else: self.status_changed_on = fields.Date.today() # Set today's date for the first record def write(self, vals): if not self.env.context.get('prevent_recursion'): result = super(SosCaseDiaryLine, self).write(vals) self.with_context(prevent_recursion=True)._update_current_state_values() return result return super(SosCaseDiaryLine, self).write(vals) def _update_current_state_values(self): """ Ensures that the most recent entry keeps its value, and previous entries are set to 0. """ for record in self: lines = self.search([('ref_id', '=', record.ref_id.id)], order='id desc') if lines: # Keep the most recent entry's value, reset all others first_entry = lines[0] # The latest entry retains its value for line in lines[1:]: line.with_context(prevent_recursion=True).write({'current_state_value': 0.00}) @api.model def create(self, vals): is_first_record = False if 'spenco_status' in vals and 'ref_id' in vals: last_record = self.env['sos_case_diary_line'].search([ ('ref_id', '=', vals['ref_id']) ], order="id desc", limit=1) if last_record: previous_spenco_status = last_record.spenco_status previous_state_value = last_record.current_state_value else: previous_spenco_status = 'Suspects' previous_state_value = 0.00 is_first_record = True else: previous_spenco_status = 'Suspects' previous_state_value = 0.00 is_first_record = True record = super(SosCaseDiaryLine, self).create(vals) if record.spenco_status: sales_executive = record.ref_id.sales_person action_plan_date = record.action_plan_date #week wise week_number = record._compute_week_number(action_plan_date) record.update_summary( previous_spenco_status = previous_spenco_status, previous_state_value=previous_state_value, action_plan_date=action_plan_date, sales_executive=sales_executive, field_name='week_number', field_value=week_number, model_name='sos_spenco_summary_week_wise', is_first_record=is_first_record ) #month wise month_number = record._compute_month_number(action_plan_date) record.update_summary( previous_spenco_status = previous_spenco_status, previous_state_value=previous_state_value, action_plan_date=action_plan_date, sales_executive=sales_executive, field_name='month_number', field_value=month_number, model_name='sos_spenco_summary_month_wise', is_first_record=is_first_record ) #quarter wise quarter_number = record._compute_quarter_number(action_plan_date) record.update_summary( previous_spenco_status = previous_spenco_status, previous_state_value=previous_state_value, action_plan_date=action_plan_date, sales_executive=sales_executive, field_name='quarter_number', field_value=quarter_number, model_name='sos_spenco_summary_quarter_wise', is_first_record=is_first_record ) #year wise year_number = record._compute_year_number(action_plan_date) record.update_summary( previous_spenco_status = previous_spenco_status, previous_state_value=previous_state_value, action_plan_date=action_plan_date, sales_executive=sales_executive, field_name='year', field_value=year_number, model_name='sos_spenco_summary_year_wise', is_first_record=is_first_record ) return record def _compute_year_number(self,action_plan_date): month_number = action_plan_date.strftime('%B') month = action_plan_date.month year = action_plan_date.year year_number = f"{year}-{year + 1}" if month >= 4 else f"{year - 1}-{year}" return year_number def _compute_quarter_number(self,action_plan_date): month_number = action_plan_date.strftime('%B') month = action_plan_date.month quarter_number = ( "Q1" if 4 <= month <= 6 else "Q2" if 7 <= month <= 9 else "Q3" if 10 <= month <= 12 else "Q4" ) return quarter_number def _compute_month_number(self,action_plan_date): month_number = action_plan_date.strftime('%B') return month_number def _compute_week_number(self,action_plan_date): start_of_week = action_plan_date - timedelta(days=action_plan_date.weekday()) end_of_week = start_of_week + timedelta(days=5) return f"{start_of_week.strftime('%b %d')} - {end_of_week.strftime('%b %d')}" def _compute_previous_week_number(self, action_plan_date): prev_week_date = action_plan_date - timedelta(days=7) start_of_week = prev_week_date - timedelta(days=prev_week_date.weekday()) end_of_week = start_of_week + timedelta(days=5) return f"{start_of_week.strftime('%b %d')} - {end_of_week.strftime('%b %d')}" def _compute_previous_month_number(self, action_date): prev_month_date = (action_date.replace(day=1) - timedelta(days=1)) return prev_month_date.strftime('%B') def _compute_previous_quarter_number(self, action_date): month = action_date.month year = action_date.year if 4 <= month <= 6: return "Q4" elif 7 <= month <= 9: return "Q1" elif 10 <= month <= 12: return "Q2" else: # Jan-Mar return "Q3" def _compute_previous_year_number(self, action_date): month = action_date.month year = action_date.year return f"{year - 1}-{year}" if month >= 4 else f"{year - 2}-{year - 1}" def update_summary(self, previous_spenco_status, previous_state_value, action_plan_date, sales_executive, field_name, field_value, model_name, is_first_record=False): SummaryModel = self.env[model_name] current_spenco_status = self.spenco_status def update_all_status(SummaryModel, sales_executive_id, field_name, prev_field_value): for all_status in ['Suspects', 'Prospects', 'Engaged', 'Negotiation', 'Commercial Order']: status_record = SummaryModel.search([ ('sales_executive', '=', sales_executive_id), (field_name, '=', prev_field_value), ('spenco_status', '=', all_status) ], limit=1) if status_record: closing_count_status = ( (status_record.opening_cases_count or 0) - (status_record.downgrade_cases_count or 0) - (status_record.upgrade_cases_count or 0) + (status_record.new_cases_count or 0) ) closing_value_status = ( (status_record.opening_cases_value or 0.0) - (status_record.downgrade_cases_value or 0.0) - (status_record.upgrade_cases_value or 0.0) + (status_record.new_cases_value or 0.0) ) status_record.write({ 'closing_cases_count': closing_count_status, 'closing_cases_value': closing_value_status, }) # Compute previous field value once prev_field_value = None if field_name == 'week_number': prev_field_value = self._compute_previous_week_number(action_plan_date) elif field_name == 'month_number': prev_field_value = self._compute_previous_month_number(action_plan_date) elif field_name == 'quarter_number': prev_field_value = self._compute_previous_quarter_number(action_plan_date) elif field_name == 'year': prev_field_value = self._compute_previous_year_number(action_plan_date) # Update all status closing fields from previous period update_all_status(SummaryModel, sales_executive.id, field_name, prev_field_value) def get_or_create_status_record(status): record = SummaryModel.search([ ('sales_executive', '=', sales_executive.id), (field_name, '=', field_value), ('spenco_status', '=', status) ], limit=1) if not record: old_record = SummaryModel.search([ ('sales_executive', '=', sales_executive.id), (field_name, '=', prev_field_value), ('spenco_status', '=', status) ], limit=1) if prev_field_value else None vals = { 'sales_executive': sales_executive.id, field_name: field_value, 'spenco_status': status, 'entered_date': action_plan_date, } if old_record: closing_count = ( (old_record.opening_cases_count or 0) - (old_record.downgrade_cases_count or 0) - (old_record.upgrade_cases_count or 0) + (old_record.new_cases_count or 0) ) closing_value = ( (old_record.opening_cases_value or 0.0) - (old_record.downgrade_cases_value or 0.0) - (old_record.upgrade_cases_value or 0.0) + (old_record.new_cases_value or 0.0) ) old_record.write({ 'closing_cases_count': closing_count, 'closing_cases_value': closing_value, }) vals.update({ 'opening_cases_count': closing_count, 'opening_cases_value': closing_value, }) record = SummaryModel.create(vals) return record # Now process previous/current statuses prev_component = get_or_create_status_record(previous_spenco_status) current_component = get_or_create_status_record(current_spenco_status) if is_first_record: current_component.write({ 'new_cases_count': max(0, (current_component.new_cases_count or 0)) + 1, 'new_cases_value': max(0.0, (current_component.new_cases_value or 0.0)) + (self.current_state_value or 0.0), }) return True if previous_spenco_status != current_spenco_status: spenco_order = ['Suspects', 'Prospects', 'Engaged', 'Negotiation', 'Commercial Order'] try: prev_index = spenco_order.index(previous_spenco_status) curr_index = spenco_order.index(current_spenco_status) except ValueError: return True # Skip if invalid if curr_index > prev_index: # Upgrade prev_component.write({ 'upgrade_cases_count': max(0, (prev_component.upgrade_cases_count or 0)) + 1, 'upgrade_cases_value': max(0.0, (prev_component.upgrade_cases_value or 0.0)) + (previous_state_value or 0.0), }) current_component.write({ 'new_cases_count': max(0, (current_component.new_cases_count or 0)) + 1, 'new_cases_value': max(0.0, (current_component.new_cases_value or 0.0)) + (self.current_state_value or 0.0), }) elif curr_index < prev_index: # Downgrade prev_component.write({ 'downgrade_cases_count': max(0, (prev_component.downgrade_cases_count or 0)) + 1, 'downgrade_cases_value': max(0.0, (prev_component.downgrade_cases_value or 0.0)) + (previous_state_value or 0.0), }) current_component.write({ 'new_cases_count': max(0, (current_component.new_cases_count or 0)) + 1, 'new_cases_value': max(0.0, (current_component.new_cases_value or 0.0)) + (self.current_state_value or 0.0), }) return True class SosCaseTransferHistory(models.Model): _name = 'sos_case_transfer_history' _description = 'Case Transfer History' _order = 'id desc' case_diary_id = fields.Many2one('sos_case_diary', string="Case Diary", required=True, ondelete='cascade') previous_sales_person = fields.Many2one('res.users', string="Transferred From", readonly=True) new_sales_person = fields.Many2one('res.users', string="Transferred To", readonly=True) transfer_date = fields.Datetime(string="Transfer Date", default=fields.Datetime.now, readonly=True) status = fields.Text(string="Status") top_management_name = fields.Many2one('res.users', string='Top Management Approver') top_management_approval_image = fields.Image(related="top_management_name.signature_image",string='Top Management Approval',readonly=True) top_management_approved_on = fields.Datetime(string="Approved On") def action_top_esign_btn(self): self.status = "Approved" self.case_diary_id.sales_person = self.new_sales_person self.case_diary_id.customer_name.responsible = self.new_sales_person #Customers change sequence_util = self.env['sos_common_scripts'] sequence_util.action_assign_signature( self, 'top_management_name', 'top_management_approved_on', 'sos_inventory.sos_management_user' ) return {'type': 'ir.actions.act_window_close'}