GOLEM/golem_member/models/golem_member.py

473 lines
19 KiB
Python

# -*- coding: utf-8 -*-
#
# Copyright 2016-2021 Fabien Bourgeois <fabien@yaltik.com>
# Copyright 2018 Youssef El Ouahby <youssef@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" GOLEM Members """
import logging
from re import compile as rcompile
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
_LOGGER = logging.getLogger(__name__)
street_number = rcompile(r'^(\d-?(bis)*(ter)*,?\s*)+')
def get_root_area(area_id):
""" Get root area """
if not area_id.parent_id:
return area_id
return get_root_area(area_id.parent_id)
def is_sub_area(area_id, parent_id):
""" Check if parent is sub area """
if parent_id.parent_id.id == area_id.id:
return True
if not parent_id.parent_id:
return False
return is_sub_area(area_id, parent_id.parent_id)
class PartnerArea(models.Model):
""" Partner Area """
_name = 'golem.partner.area'
_description = 'Partner Area'
_order = 'sequence asc, name asc'
_sql_constraints = [('golem_partner_area_uniq',
'UNIQUE (name)',
_('This patner area has already been used.'))]
name = fields.Char(required=True, index=True)
sequence = fields.Integer()
area_street_ids = fields.One2many('golem.partner.area.street', 'area_id',
string='Street list')
parent_id = fields.Many2one('golem.partner.area', string='Parent Territory',
domain="[('id', '!=', id)]")
root_id = fields.Many2one('golem.partner.area', compute='_compute_root_id',
string='Root area')
@api.depends('parent_id')
def _compute_root_id(self):
""" Compute root_id """
for area in self:
area.root_id = get_root_area(area)
@api.constrains('parent_id')
def check_parent_id(self):
""" Check if parent is sub area """
for area in self:
if is_sub_area(area, area.parent_id):
err = _('The parent area is a sub area of the current area, '
'please change it.')
raise ValidationError(err)
class GolemPartnerAreaStreet(models.Model):
""" GOLEM Partner Area Street Management """
_name = 'golem.partner.area.street'
_description = 'GOLEM Partner Area Street'
name = fields.Char(required=True)
area_id = fields.Many2one('golem.partner.area', required=True, string='Area',
index=True, auto_join=True, ondelete='set null')
class ResPartner(models.Model):
""" GOLEM Member partner adaptations """
_inherit = 'res.partner'
age = fields.Integer(compute='_compute_age', search='_search_age')
@api.depends('birthdate_date')
def _compute_age(self):
for contact in self:
if contact.birthdate_date:
age = relativedelta(date.today(),
fields.Date.from_string(contact.birthdate_date))
contact.age = age.years
def _search_age(self, operator, value):
""" Age search function """
if operator not in ('=', '!=', '<', '<=', '>', '>='):
err = _('Unsupported operator for age search')
raise NotImplementedError(err)
today = date.today()
birthdate_date = today - timedelta(days=365.25 * value)
if operator in ('=', '!='):
birthdate_date = today - timedelta(days=365.25 * value)
max_birthdate_date = today - timedelta(days=365.25 * (value + 1))
if operator == '=':
return ['&', ('birthdate_date', '>', max_birthdate_date),
('birthdate_date', '<=', birthdate_date)]
else:
return ['|', ('birthdate_date', '<=', max_birthdate_date),
('birthdate_date', '>', birthdate_date)]
else:
if operator == '>':
operator = '<'
elif operator == '>=':
operator = '<='
elif operator == '<':
operator = '>'
else:
operator = '>='
return [('birthdate_date', operator, birthdate_date)]
@api.model
def _get_default_nationality_id(self):
return self.env.ref('base.main_company').country_id
nationality_id = fields.Many2one('res.country', 'Nationality',
auto_join=True,
default=_get_default_nationality_id)
area_id = fields.Many2one(
'golem.partner.area', index=True, auto_join=True, string='Area',
help="Area, quarter... for statistics and activity price."
)
area_from_street = fields.Boolean(store=False, default=False)
country_id = fields.Many2one(default=_get_default_nationality_id)
# Gender overwriting
gender = fields.Selection([('male', _('Male')),
('female', _('Female')),
('not_disclosed', _('Not Disclosed'))],
default='not_disclosed')
member_id = fields.One2many('golem.member', 'partner_id', 'Service user',
readonly=True)
is_service_user = fields.Boolean(compute='_compute_is_service_user')
member_number = fields.Char(related='member_id.number')
@api.depends('member_id')
def _compute_is_service_user(self):
""" Computes is member """
for partner in self:
partner.is_service_user = len(partner.member_id) > 0
@api.multi
def view_member(self):
""" Go to member form """
self.ensure_one()
return {'type': 'ir.actions.act_window',
'res_model': 'golem.member',
'view_mode': 'form',
'res_id': self[0].member_id.id if self[0].member_id else False}
@api.multi
def create_golem_member(self):
""" Member creation from partner form """
self.ensure_one()
gm_obj = self.env['golem.member']
gm_obj.create({'partner_id': self[0].id})
@api.constrains('street')
def save_street(self):
""" Save street if no exist """
for member in self:
if member.street and not member.area_from_street:
mstreet = member.street.strip()
mstreet = street_number.sub(u'', mstreet).strip()
street_id = self.env['golem.partner.area.street'].search(
[('name', 'ilike', mstreet)]
)
if not street_id:
self.env['golem.partner.area.street'].create(
{'name': mstreet, 'area_id': member.area_id.id}
)
class GolemMembershipInvoice(models.TransientModel):
""" GOLEM Membership Invoice adaptations """
_inherit = 'golem.membership.invoice'
@api.multi
def membership_invoice(self):
""" Extend invoice generation with number generation """
self.ensure_one()
res = super(GolemMembershipInvoice, self).membership_invoice()
if self.partner_id.member_id:
self.partner_id.member_id.generate_number()
return res
class GolemMember(models.Model):
""" GOLEM Member model """
_name = 'golem.member'
_description = 'GOLEM Member'
_inherit = 'mail.thread'
_inherits = {'res.partner': 'partner_id'}
_sql_constraints = [('golem_member_number_manual_uniq',
'UNIQUE (number_manual)',
_('This member number has already been used.'))]
partner_id = fields.Many2one('res.partner', required=True, index=True,
ondelete='cascade')
@api.model
def default_season(self):
""" Get default season """
domain = [('is_default', '=', True)]
return self.env['golem.season'].search(domain, limit=1)
number_name = fields.Char('Member computed name', compute='_compute_number_name')
number = fields.Char('Member number', store=True, readonly=True)
number_manual = fields.Char('Manual number', size=50, index=True,
help='Manual number overwriting automatic '
'numbering')
pictures_agreement = fields.Boolean('Pictures agreement?')
electronic_processing_agreement = fields.Boolean('Electronic Processing Agreement?',
default=True)
opt_out_sms = fields.Boolean('Out of SMS campaigns?',
help='If this field has been checked, it '
'tells that the user refuses to receive SMS')
season_ids = fields.Many2many('golem.season', string='Seasons',
required=True, default=default_season,
auto_join=True, ondelete='restrict')
is_default = fields.Boolean('Default season?',
compute='_compute_is_default', store=True)
is_number_manual = fields.Boolean('Is number manual?', store=False,
compute='_compute_is_number_manual')
@api.onchange('country_id')
def onchange_country_domain_state(self):
""" On country change : adapts state domain """
member = self[0]
if member.country_id:
return {
'domain': {'state_id': [('country_id', '=', member.country_id.id)]}
}
return {'domain': {'state_id': []}}
@api.depends('number', 'name')
def _compute_number_name(self):
""" Computes a name composed with number and name """
for member in self:
vals = []
if member.number:
vals.append(member.number)
if member.name:
vals.append(member.name)
member.number_name = u' - '.join(vals)
@api.depends('season_ids')
def _compute_is_default(self):
""" Computes is current according to seasons """
default_s = self.default_season()
for member in self:
member.is_default = default_s in member.season_ids
@api.depends('number')
def _compute_is_number_manual(self):
conf = self.env['ir.config_parameter']
is_num_man = (conf.get_param('golem_numberconfig_isautomatic') == '0')
self.update({'is_number_manual': is_num_man})
@api.onchange('street')
def onchange_street(self):
""" Area auto assignement """
for member in self:
mstreet = member.street.strip() if member.street else False
if mstreet and not member.area_id:
mstreet = street_number.sub('', mstreet).strip()
street_id = self.env['golem.partner.area.street'].search(
[('name', 'ilike', mstreet)], limit=1
)
if street_id:
member.area_id = street_id.area_id
member.area_from_street = True
@api.multi
def generate_number_perseason(self):
""" Number generation in case of per season configuration """
res = None
conf = self.env['ir.config_parameter']
member_number_obj = self.env['golem.member.number']
for member in self:
for season in member.season_ids:
domain = ['&',
('member_id', '=', member.id),
('season_id', '=', season.id)]
member_num = member_number_obj.search(domain)
if not member_num:
season.write({'member_counter': season.member_counter})
pkey = 'golem_numberconfig_prefix'
pfx = conf.get_param(pkey, '')
number = u'{}{}'.format(pfx, unicode(season.member_counter))
data = {'member_id': member.id,
'season_id': season.id,
'number': number}
member_num = member_number_obj.create(data)
season.member_counter += 1
if season.is_default:
res = member_num.number
return res
@api.multi
def generate_number_global(self):
""" Number generation in case of global configuration """
self.ensure_one()
conf = self.env['ir.config_parameter']
domain = ['&',
('member_id', '=', self[0].id),
('season_id', '=', None)]
member_number_obj = self.env['golem.member.number']
member_num = member_number_obj.search(domain)
if not member_num:
last = int(conf.get_param('golem_number_counter', 1))
pfx = conf.get_param('golem_numberconfig_prefix', '')
number = pfx + str(last)
data = {'member_id': self[0].id,
'season_id': None,
'number': number}
member_num = member_number_obj.create(data)
last += 1
conf.set_param('golem_number_counter', str(last))
else:
member_num = member_num[0]
return member_num.number
@api.multi
def generate_number(self):
""" Computes number according to pre-existing number and chosen
seasons """
conf = self.env['ir.config_parameter']
isauto = conf.get_param('golem_numberconfig_isautomatic') == '1'
isperseason = conf.get_param('golem_numberconfig_isperseason') == '1'
isfornew = conf.get_param('golem_numberconfig_isfornewmembersonly') == '1'
for member in self.filtered(lambda m: m.membership_state != 'none'):
if not isauto or (isfornew and member.number_manual):
member.number = member.number_manual
else:
if isperseason:
member.number = member.generate_number_perseason()
else:
member.number = member.generate_number_global()
@api.multi
def write(self, vals):
""" Number generation after updates """
res = super(GolemMember, self).write(vals)
if 'season_ids' in vals or 'number_manual' in vals:
self.generate_number()
return res
class GolemMemberNumber(models.Model):
""" GOLEM Member Numbers """
_name = 'golem.member.number'
_description = 'GOLEM Member Numbers'
name = fields.Char(compute='_compute_name')
member_id = fields.Many2one('golem.member', string='Member', index=True,
required=True, ondelete='cascade',
auto_join=True)
season_id = fields.Many2one('golem.season', string='Season', index=True,
auto_join=True)
number = fields.Char(index=True, readonly=True)
@api.depends('season_id')
def _compute_name(self):
for number in self:
number.name = number.season_id.name
class GolemNumberConfig(models.TransientModel):
""" Configuration for number computing """
_name = 'golem.member.numberconfig'
_description = 'Configuration for number computing'
@api.model
def _default_is_automatic(self):
conf = self.env['ir.config_parameter']
return conf.get_param('golem_numberconfig_isautomatic', '1')
@api.model
def _default_is_per_season(self):
conf = self.env['ir.config_parameter']
return conf.get_param('golem_numberconfig_isperseason', '0')
@api.model
def _default_prefix(self):
conf = self.env['ir.config_parameter']
return conf.get_param('golem_numberconfig_prefix', '')
is_automatic = fields.Selection([('1', _('Yes')), ('0', _('No'))],
string='Computed automatically?',
default=_default_is_automatic)
is_per_season = fields.Selection([('1', _('Yes')), ('0', _('No'))],
string='Per season number?',
default=_default_is_per_season)
prefix = fields.Char('Optional prefix', default=_default_prefix)
number_from = fields.Integer('First number', default=1,
help='Number starting from, default to 1')
@api.multi
def apply_config(self):
""" Apply new configuration """
self.ensure_one()
conf = self.env['ir.config_parameter']
conf.set_param('golem_numberconfig_isautomatic', self.is_automatic)
conf.set_param('golem_numberconfig_isperseason', self.is_per_season)
conf.set_param('golem_numberconfig_prefix', self.prefix or '')
if self.number_from:
_LOGGER.warning('New number_from %s', self.number_from)
conf.set_param('golem_number_counter', unicode(self.number_from))
self.env['golem.season'].search([]).write({
'member_counter': self.number_from
})
@api.multi
def apply_nocompute(self):
""" Apply new configuration only for new members (keep old numbers) """
self.ensure_one()
self.apply_config()
conf = self.env['ir.config_parameter']
conf.set_param('golem_numberconfig_isfornewmembersonly', '1')
@api.multi
def apply_recompute(self):
""" Recomputes all member numbers according to new configuration """
self.ensure_one()
self.apply_config()
conf = self.env['ir.config_parameter']
conf.set_param('golem_numberconfig_isfornewmembersonly', '0')
self.env['golem.member.number'].search([]).unlink()
self.env['golem.season'].search([]).write({
'member_counter': int(self.number_from)
})
member_obj = self.env['golem.member']
member_obj.search([('membership_state', '=', 'none')]).write({'number': False})
member_obj.search([('membership_state', '!=', 'none')]).generate_number()
return {'type': 'ir.actions.client', 'tag': 'reload'}
class MergePartnerAutomatic(models.TransientModel):
""" Merge Partner Automatic adaptations """
_inherit = 'base.partner.merge.automatic.wizard'
@api.multi
def action_merge(self):
""" Merge adaptations : warn if there is a member """
for merge in self:
for partner in merge.partner_ids:
if partner.member_id:
emsg = _('GOLEM Members merge has not been implemented yet. '
'Please only merge partners, not members, or delete '
'GOLEM Members manually before merging.')
raise UserError(emsg)
return super(MergePartnerAutomatic, self).action_merge()