# -*- coding: utf-8 -*- from dateutil.relativedelta import relativedelta from odoo import models, fields, api, _ from odoo.exceptions import UserError from odoo.tools import date_utils from dateutil.relativedelta import relativedelta class ConfirmBenefitExpense(models.Model): _name = 'confirm.benefit.expense' _description = 'Confirm Benefit Expense' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = "family_expense_seq desc" # region [Default Methods] def _default_start_date(self): today = fields.Date.today() start_date = date_utils.start_of(today, 'month') return start_date def _default_end_date(self): today = fields.Date.today() end_date = date_utils.end_of(today, 'month') return end_date # endregion [Default Methods] family_expense_seq = fields.Char(string="Number", copy=False, readonly=True, default=lambda x: _('New')) state = fields.Selection(selection=[ ('draft', 'Draft'), ('calculated', 'Calculated'), ('assistant_general_manager', 'Waiting For The Assistant General Manager'), ('accounting_approve', 'Accounting Approve'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ], string='Status', default='draft', required=True, copy=False, tracking=True) name = fields.Char(string="Name", states={'confirm': [('readonly', True)]}, copy=False) date = fields.Date(string="Date", default=fields.Date.context_today, required=False, states={'confirm': [('readonly', True)]}) start_date = fields.Date(string="Start Date", default=_default_start_date, required=True) end_date = fields.Date(string="End Date", required=True, default=_default_end_date) family_ids = fields.Many2many(comodel_name='grant.benefit', relation='benefit_expense_grant_rel', column1='expense_id', column2='family_id', string='Families', states={'confirm': [('readonly', True)]}, copy=False) benefit_expense_line_ids = fields.One2many(comodel_name='benefit.expense.line', inverse_name='confirm_expense_id', string='Benefit Expense Lines') othaim_line_ids = fields.One2many(comodel_name='benefit.expense.line', inverse_name='confirm_expense_id', string='Othaim Lines', domain=[('meal_card', '=', True)]) cash_expense = fields.Boolean(string='Include Cash Expense', default=True, states={'confirm': [('readonly', True)]}) meal_expense = fields.Boolean(string='Include Meal Expense', default=True, states={'confirm': [('readonly', True)]}) cloth_expense = fields.Boolean(string='Include Clothing Expense', default=True, states={'confirm': [('readonly', True)]}) payment_order_id = fields.Many2one('payment.orders', string='Payment Order', ondelete="set null", copy=False) move_id = fields.Many2one('account.move') available_payment_method_line_ids = fields.Many2many(comodel_name='account.payment.method.line') family_monthly_income = fields.Float(string="Total Monthly Income", compute='_get_family_monthly_values', store=True) family_monthly_meals = fields.Float(string="Total Monthly Meals", compute='_get_family_monthly_values', store=True) family_monthly_clotting = fields.Float(string="Total Monthly Clotting", compute='_get_family_monthly_values', store=True) family_monthly_othaime = fields.Float(string="Total Othaim", compute='_get_family_monthly_values', store=True) family_monthly_total = fields.Float(string="Total", compute='_get_family_monthly_values', store=True) branch_custom_ids = fields.Many2many(comodel_name='branch.settings', relation='confirm_benefit_expense_branch_rel', column1='expense_id', column2='branch_id', string="Branches", domain="[('has_employees', '=', True)]") family_domain_ids = fields.Many2many(comodel_name='grant.benefit', compute='_compute_domain_ids') company_id = fields.Many2one('res.company', default=lambda self: self.env.company) currency_id = fields.Many2one(comodel_name='res.currency', string="Company Currency", related='company_id.currency_id') payment_state = fields.Selection(string='Payment State', selection=[ ('none', 'None'), ('waiting', 'Waiting Payment'), ('done', 'Done Payment'), ], copy=False, compute="_compute_payment_move_state", store=True) move_state = fields.Selection(string='Move State', selection=[ ('none', 'None'), ('waiting', 'Waiting Payment'), ('done', 'Done Payment'), ], copy=False, compute="_compute_payment_move_state", store=True) family_count_expense = fields.Integer(string="Family Count (Monthly Expense)", compute='_get_family_monthly_values', store=True) family_count_othaim = fields.Integer(string="Family Count (Othaim)", compute='_get_family_monthly_values', store=True) member_count_expense = fields.Integer(string="Member Count (Monthly Expense)", compute='_get_family_monthly_values', store=True, ) member_count_othaim = fields.Integer(string="Member Count (Othaim)", compute='_get_family_monthly_values', store=True, ) is_return_calculation = fields.Boolean(string="Return Calculation Mode", default=False, help="Enable to calculate returned amounts instead of regular monthly expense." ) return_expense_line_ids = fields.One2many(comodel_name='benefit.expense.line', inverse_name='return_confirm_id', string="Selected Return Lines", domain="[('is_return', '=', True)]", ) line_domain_ids = fields.Many2many(comodel_name='benefit.expense.line', compute='_compute_domain_ids', string="Return Line Domain", ) has_draft_return_lines = fields.Boolean(string="Has Draft Return Lines", compute='_compute_has_draft_return_lines', ) @api.depends('return_expense_line_ids') def _compute_has_draft_return_lines(self): for rec in self: if rec.return_expense_line_ids: draft_lines = rec.return_expense_line_ids.filtered(lambda l: l.state == 'draft') rec.has_draft_return_lines = bool(draft_lines) else: rec.has_draft_return_lines = False @api.depends('payment_order_id', 'payment_order_id.state', 'move_id', 'move_id.state') def _compute_payment_move_state(self): for rec in self: payment_state = 'none' move_state = 'none' if rec.payment_order_id: if rec.payment_order_id.state == "done": payment_state = "done" else: payment_state = "waiting" if rec.move_id: if rec.move_id.state == "posted": move_state = "done" else: move_state = "waiting" rec.move_state = move_state rec.payment_state = payment_state if rec.move_state == 'done' and rec.payment_state == 'done': rec.state = 'confirm' def _get_month_count(self): self.ensure_one() if not self.start_date or not self.end_date: return 1 start = fields.Date.from_string(self.start_date) end = fields.Date.from_string(self.end_date) diff = relativedelta(end, start) months = diff.years * 12 + diff.months + 1 return max(1, months) @api.model def create(self, vals): res = super(ConfirmBenefitExpense, self).create(vals) if not res.family_ids: raise UserError(_('Select Family')) if not res.family_expense_seq or res.family_expense_seq == _('New'): res.family_expense_seq = self.env['ir.sequence'].sudo().next_by_code('family.expense.sequence') or _('New') return res def _update_benefit_expense_lines(self): self.ensure_one() month_count = self._get_month_count() for line in self.benefit_expense_line_ids: family = line.family_id income, meals, clotting, othaim = 0, 0, 0, 0 if not family: continue monthly_meals = 0.0 if family.meal_card else family.family_monthly_meals othaime = family.family_monthly_meals if family.meal_card else 0.0 if self.cash_expense: income = family.family_monthly_income * month_count if self.meal_expense: meals = monthly_meals * month_count othaim = othaime * month_count if self.cloth_expense: clotting = family.family_monthly_clotting * month_count vals = { 'branch_id': family.branch_custom_id.id, 'family_category_id': family.benefit_category_id.id, 'meal_card': family.meal_card, 'benefit_member_count': family.benefit_member_count, 'start_date': self.start_date, 'end_date': self.end_date, 'family_monthly_income': income, 'family_monthly_meals': meals, 'family_monthly_clotting': clotting, 'family_monthly_othaime': othaim, } line.write(vals) def _calculate_return_lines(self): self.ensure_one() return_lines = self.return_expense_line_ids if not return_lines: raise UserError(_("Please select at least one return line to calculate.")) if return_lines.filtered(lambda l: l.state == 'waiting_processing'): raise UserError( _("You cannot calculate because some return lines are still in 'Waiting Processing' state.")) self.benefit_expense_line_ids.unlink() lines = [] for line in return_lines: lines.append((0, 0, { 'family_id': line.family_id.id, 'branch_id': line.branch_id.id, 'family_category_id': line.family_category_id.id, 'meal_card': line.meal_card, 'benefit_member_count': line.benefit_member_count, 'start_date': self.start_date, 'end_date': self.end_date, 'family_monthly_income': line.family_monthly_income, 'family_monthly_meals': line.family_monthly_meals, 'family_monthly_clotting': line.family_monthly_clotting, 'family_monthly_othaime': 0, })) self.benefit_expense_line_ids = lines def action_calculate(self): for rec in self: if rec.state != 'draft': raise UserError(_("You can only calculate in draft state.")) families = rec.family_ids if not families: raise UserError(_("Please select at least one family to calculate.")) if not rec.cash_expense and not rec.meal_expense and not rec.cloth_expense: raise UserError(_("At least one expense type should be selected.")) if rec.is_return_calculation: rec._calculate_return_lines() else: rec.benefit_expense_line_ids.unlink() lines = [] for fam in families: vals = { 'confirm_expense_id': rec.id, 'family_id': fam.id, 'start_date': rec.start_date, 'end_date': rec.end_date, } lines.append((0, 0, vals)) rec.write({'benefit_expense_line_ids': lines}) rec._update_benefit_expense_lines() rec.state = 'calculated' def action_recalculate(self): for rec in self: if rec.state != 'calculated': raise UserError(_("You can only recalculate when status is 'Calculated'.")) if rec.is_return_calculation: rec._calculate_return_lines() else: rec._update_benefit_expense_lines() @api.depends('is_return_calculation', 'branch_custom_ids', 'start_date', 'end_date') def _compute_domain_ids(self): for rec in self: Line = self.env['benefit.expense.line'] validation_setting = self.env["family.validation.setting"].search([], limit=1) if rec.is_return_calculation: domain = [ ('start_date', '<=', rec.end_date), ('end_date', '>=', rec.start_date), ('is_return', '=', True), ('return_reason_id', '!=', False), ('return_confirm_id', '=', False), ] if rec.branch_custom_ids: domain.append(('branch_id', 'in', rec.branch_custom_ids.ids)) return_lines = Line.search(domain) rec.line_domain_ids = return_lines rec.family_domain_ids = return_lines.mapped('family_id') else: # Define base domain for family selection base_domain = ['|', ('state', '=', 'second_approve'), '&', ('state', 'not in', ('temporary_suspended', 'suspended_second_approve')), ('action_type', '=', 'suspended')] if rec.branch_custom_ids: base_domain.append(('branch_custom_id', 'in', rec.branch_custom_ids.ids)) min_income = validation_setting.benefit_category_ids.mapped('mini_income_amount') max_income = validation_setting.benefit_category_ids.mapped('max_income_amount') benefit_category_ids = validation_setting.benefit_category_ids base_domain.extend([('member_income', '>=', min(min_income)), ('member_income', '<=', max(max_income))]) if benefit_category_ids: base_domain.extend([('benefit_category_id', 'in', benefit_category_ids.ids)]) else: base_domain.extend([('benefit_category_id', '!=', False)]) if rec.start_date and rec.end_date: conflicting_records = self.search([ ('id', '!=', rec._origin.id), ('start_date', '<=', rec.end_date), ('end_date', '>=', rec.start_date), ]) if conflicting_records: conflicting_family_ids = conflicting_records.mapped('family_ids').ids base_domain.append(('id', 'not in', conflicting_family_ids)) rec.family_domain_ids = self.env['grant.benefit'].search(base_domain) rec.line_domain_ids = self.env['benefit.expense.line'].browse([]) @api.onchange('branch_custom_ids') def _onchange_branch_custom_ids(self): if self.branch_custom_ids: allowed_families = self.env['grant.benefit'].search([ ('id', 'in', self.family_ids.ids), ('branch_custom_id', 'in', self.branch_custom_ids.ids), ]) self.family_ids = [(6, 0, allowed_families.ids)] else: self.family_ids = [(5, 0, 0)] def unlink(self): for rec in self: if rec.state not in ['draft']: raise UserError(_('This record can only be deleted in draft state.')) return super(ConfirmBenefitExpense, self).unlink() @api.depends('benefit_expense_line_ids') def _get_family_monthly_values(self): for rec in self: lines = rec.benefit_expense_line_ids rec.family_monthly_income = sum(lines.mapped('family_monthly_income')) rec.family_monthly_meals = sum(lines.mapped('family_monthly_meals')) rec.family_monthly_clotting = sum(lines.mapped('family_monthly_clotting')) rec.family_monthly_othaime = sum(lines.mapped('family_monthly_othaime')) rec.family_monthly_total = rec.family_monthly_income + rec.family_monthly_meals + rec.family_monthly_clotting othaim_lines = lines.filtered('meal_card') rec.family_count_expense = len(lines.mapped('family_id')) rec.member_count_expense = sum(lines.mapped('benefit_member_count')) rec.family_count_othaim = len(othaim_lines.mapped('family_id')) rec.member_count_othaim = sum(othaim_lines.mapped('benefit_member_count')) def action_assistant_manager(self): for family in self.family_ids: if self.end_date and family.last_disbursement_date: if self.end_date > family.last_disbursement_date: family.last_disbursement_date = self.end_date else: family.last_disbursement_date = self.end_date self.state = 'assistant_general_manager' def action_accounting_approve(self): self.sudo().action_accounting_transfer() self.sudo().state = 'accounting_approve' def action_send_to_researcher(self): for rec in self: if not rec.is_return_calculation: raise UserError(_("This action is only available for return calculations.")) return_lines = rec.return_expense_line_ids.filtered(lambda l: l.state == 'draft') if not return_lines: raise UserError(_("No return lines in draft state to send to specialist.")) lines_without_researcher = self.env['benefit.expense.line'] for line in return_lines: family = line.family_id researcher = family.researcher_id if hasattr(family, 'researcher_id') else False if not researcher: lines_without_researcher |= line continue line.write({ 'researcher_id': researcher.id, 'state': 'waiting_processing' }) if lines_without_researcher: families_without_specialist = lines_without_researcher.mapped('family_id.name') raise UserError(_( "The following families do not have an assigned researcher:\n%s\n\n" "Please assign researchers to these families before sending." ) % ", ".join(families_without_specialist)) def action_cancel(self): self.state = 'cancel' def action_reset_to_draft(self): self.payment_order_id.unlink() self.move_id.unlink() self.benefit_expense_line_ids.unlink() self.state = 'draft' def action_reset_to_calculated(self): self.ensure_one() return { 'name': _('Reason for Return'), 'type': 'ir.actions.act_window', 'res_model': 'reason.for.return.wizard', 'view_mode': 'form', 'target': 'new', } def action_open_related_move_records(self): moves = self.move_id.ids return { 'name': _('Vendor Bills'), 'type': 'ir.actions.act_window', 'res_model': 'account.move', 'view_mode': 'tree,form', 'domain': [('id', 'in', moves)], } def action_open_related_payment_orders(self): payment_orders = self.payment_order_id.ids return { 'name': _('Payment Orders'), 'type': 'ir.actions.act_window', 'res_model': 'payment.orders', 'view_mode': 'tree,form', 'domain': [('id', 'in', payment_orders)], } def action_accounting_transfer(self): for rec in self: validation_setting = self.env["family.validation.setting"].search([], limit=1) lines = rec.benefit_expense_line_ids if not lines: raise UserError(_("Please make sure you have benefit expense lines.")) families = lines.mapped('family_id') invalid_families = families.filtered( lambda f: f.state != 'second_approve' or (f.state in ('waiting_approve', 'first_approve') and f.action_type == 'suspended') ) if invalid_families: raise UserError(_( "Some selected benefits are not in valid state or are suspended:\n%s" ) % ", ".join(invalid_families.mapped('name'))) if not validation_setting.cash_expense_account_id or not validation_setting.meal_expense_account_id or not validation_setting.clothing_expense_account_id: raise UserError(_("Please configure the expense accounts in the validation settings.")) credit_account_id = validation_setting.account_id.id if not credit_account_id: raise UserError(_("Please select credit account.")) # todo if have paymnet or move dont create again # Create Payment Order for Benefit Expense payment_order = self.env['payment.orders'].create({ 'state': 'draft', 'accountant_id': validation_setting.accountant_id.id, 'benefit_expense_line_ids': [(6, 0, rec.benefit_expense_line_ids.ids)], 'type': 'benefit_expense', }) rec.payment_order_id = payment_order if not rec.is_return_calculation: # Create Vendor Bill for Meal Card Invoice(othaime) account_id = validation_setting.meal_expense_account_id invoice_lines = [] if lines.filtered(lambda l: l.meal_card): for line in lines.filtered(lambda l: l.meal_card): family = line.family_id invoice_lines.append((0, 0, { 'name': f'{family.name}/{family.code}', 'account_id': account_id.id, 'quantity': 1, 'benefit_family_id': family.id, 'price_unit': line.family_monthly_othaime, 'family_confirm_id': rec.id, 'analytic_account_id': family.branch_family_id.branch.analytic_account_id.id })) invoice_vals = { 'move_type': 'in_invoice', 'partner_id': validation_setting.meal_partner_id.id, 'invoice_date': rec.date, 'family_confirm_id': rec.id, 'benefit_family_ids': [(6, 0, rec.benefit_expense_line_ids.mapped('family_id').ids)], 'journal_id': validation_setting.journal_id.id, 'invoice_line_ids': invoice_lines, 'ref': rec.name, } invoice = self.env['account.move'].create(invoice_vals) rec.move_id = invoice return True