484 lines
23 KiB
Python
484 lines
23 KiB
Python
# -*- 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
|