Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Farm season #730

Open
wants to merge 3 commits into
base: 17.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions spp_farmer_registry_base/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
],
"external_dependencies": {"python": ["shapely", "geojson", "simplejson", "pyproj"]},
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"views/farm_season_view.xml",
"data/kind_data.xml",
"data/id_data.xml",
"views/res_partner.xml",
Expand Down
1 change: 1 addition & 0 deletions spp_farmer_registry_base/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
from . import farm
from . import land_record
from . import base_import
from . import farm_season
34 changes: 33 additions & 1 deletion spp_farmer_registry_base/models/agricultural_activity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from odoo import api, fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class AgriculturalActivity(models.Model):
Expand Down Expand Up @@ -35,7 +36,38 @@ class AgriculturalActivity(models.Model):
"spp.farm.species", ondelete="restrict", string="Species", domain="[('species_type', '=', activity_type)]"
)

season_id = fields.Many2one(
"spp.farm.season",
string="Agricultural Season",
domain="[('state', '=', 'active')]",
)

@api.onchange("crop_farm_id")
def _onchange_farm_id(self):
for rec in self:
rec.land_id = False

@api.constrains("season_id")
def _check_season_state(self):
for record in self:
if record.season_id and record.season_id.state == "closed":
raise ValidationError(_("Cannot modify activities in closed seasons"))

@api.model
def default_get(self, fields_list):
"""Set default season to most recent active season"""
res = super().default_get(fields_list)
if "season_id" in fields_list:
active_season = self.env["spp.farm.season"].search(
[("state", "=", "active")], order="date_start desc", limit=1
)
if active_season:
res["season_id"] = active_season.id
return res

def write(self, vals):
"""Prevent modifications in closed seasons"""
for record in self:
if record.season_id and record.season_id.state == "closed":
raise ValidationError(_("Cannot modify activities in closed seasons"))
return super().write(vals)
258 changes: 258 additions & 0 deletions spp_farmer_registry_base/models/farm_season.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class SPPFarmSeason(models.Model):
_name = "spp.farm.season"
_description = "Agricultural Season"
_order = "date_start desc"

name = fields.Char(
string="Season Name",
required=True,
index=True,
)
description = fields.Text(
string="Description",
)
date_start = fields.Date(
string="Start Date",
required=True,
index=True,
)
date_end = fields.Date(
string="End Date",
required=True,
index=True,
)
active = fields.Boolean(
default=True,
)
state = fields.Selection(
[("draft", "Draft"), ("active", "Active"), ("closed", "Closed")],
string="Status",
default="draft",
required=True,
index=True,
)
activity_ids = fields.One2many(
"spp.farm.activity",
"season_id",
string="Agricultural Activities",
)
activity_count = fields.Integer(
string="Activities",
compute="_compute_activity_count",
store=True,
)

can_edit = fields.Boolean(
string="Can Edit",
compute="_compute_access_rights",
)
can_activate = fields.Boolean(
string="Can Activate",
compute="_compute_access_rights",
)
can_close = fields.Boolean(
string="Can Close",
compute="_compute_access_rights",
)

allow_overlap = fields.Boolean(
string="Allow Overlap with Other Seasons",
default=False,
help="If checked, this season can overlap with other active seasons",
)

activity_type = fields.Selection(
related="activity_ids.activity_type",
store=False,
readonly=True,
)

@api.depends("state")
def _compute_access_rights(self):
"""Compute access rights based on user and state"""
is_manager = self.env.user.has_group("spp_farmer_registry_base.group_spp_farm_manager")
for record in self:
record.can_edit = is_manager and record.state != "closed"
record.can_activate = is_manager and record.state == "draft"
record.can_close = is_manager and record.state == "active"

@api.depends("activity_ids")
def _compute_activity_count(self):
for record in self:
record.activity_count = len(record.activity_ids)

@api.constrains("date_start", "date_end")
def _check_dates(self):
for record in self:
if record.date_end < record.date_start:
raise ValidationError(_("End date must be after start date"))

def action_activate(self):
"""Activate the season with proper security checks"""
self.ensure_one()
if not self.can_activate:
raise ValidationError(_("You don't have permission to activate seasons"))
if self.state != "draft":
raise ValidationError(_("Only draft seasons can be activated"))
self.write({"state": "active"})

def action_close(self):
"""Close the season with proper security checks"""
self.ensure_one()
if not self.can_close:
raise ValidationError(_("You don't have permission to close seasons"))
if self.state != "active":
raise ValidationError(_("Only active seasons can be closed"))
self.write({"state": "closed"})

def action_draft(self):
"""Reset season to draft state with proper security checks"""
self.ensure_one()
if not self.can_edit:
raise ValidationError(_("You don't have permission to modify this season"))
if self.state == "closed":
raise ValidationError(_("Closed seasons cannot be reopened"))
if self.activity_ids:
raise ValidationError(_("Cannot reset to draft when activities exist"))
self.write({"state": "draft"})

@api.constrains("state")
def _check_state_transition(self):
"""Validate state transitions"""
for record in self:
if record.state == "closed":
# Check for ongoing activities
ongoing = self.env["spp.farm.activity"].search_count([("season_id", "=", record.id)])
if ongoing:
raise ValidationError(
_("Cannot close season with ongoing activities. " "Please complete or cancel them first.")
)

@api.constrains("date_start", "date_end", "state")
def _check_overlapping_active_seasons(self):
"""Prevent overlapping active seasons unless explicitly allowed"""
for record in self:
if record.state == "active":
overlapping = self.search(
[
("id", "!=", record.id),
("state", "=", "active"),
"|",
"&",
("date_start", "<=", record.date_start),
("date_end", ">=", record.date_start),
"&",
("date_start", "<=", record.date_end),
("date_end", ">=", record.date_end),
]
)
if overlapping and not self.allow_overlap:
raise ValidationError(
_(
"This season overlaps with existing active seasons: %s. "
"If this is intended, please use the 'Allow Overlap' option."
)
% ", ".join(overlapping.mapped("name"))
)

def toggle_active(self):
"""Override to prevent archiving closed seasons with activities"""
for record in self:
if not record.active and record.state == "closed":
if record.activity_ids:
raise ValidationError(_("Cannot reactivate closed season with existing activities"))
return super().toggle_active()

@api.model
def _get_current_season(self):
"""Helper method to get current active season"""
today = fields.Date.today()
current_season = self.search(
[
("state", "=", "active"),
("date_start", "<=", today),
("date_end", ">=", today),
],
limit=1,
order="date_start desc",
)
return current_season

def copy(self, default=None):
"""Override copy to handle unique constraints and naming"""
self.ensure_one()
default = dict(default or {})

# Handle name uniqueness
if "name" not in default:
default["name"] = _("%s (Copy)") % self.name

# Reset state and dates
default.update(
{
"state": "draft",
"activity_ids": [],
"date_start": fields.Date.today(),
"date_end": fields.Date.today(),
}
)

return super().copy(default)

def write(self, vals):
"""Override write to implement state-based access control"""
for record in self:
if not record.can_edit and (set(vals.keys()) - {"message_ids", "message_follower_ids"}):
raise ValidationError(_("You don't have permission to modify this season"))
return super().write(vals)

@api.model
def create(self, vals):
"""Override create to implement creation access control"""
if not self.env.user.has_group("spp_farmer_registry_base.group_spp_farm_manager"):
raise ValidationError(_("Only managers can create seasons"))
return super().create(vals)

def unlink(self):
"""Override unlink to implement deletion access control"""
for record in self:
if not record.can_edit:
raise ValidationError(_("You don't have permission to delete this season"))
if record.state == "closed":
raise ValidationError(_("Closed seasons cannot be deleted"))
return super().unlink()

def _compute_display_name(self):
"""Modern approach to custom display names"""
for record in self:
record.display_name = f"{record.name} ({record.date_start} to {record.date_end})"

def name_get(self):
"""Custom name display including dates"""
result = []
for record in self:
name = f"{record.name} ({record.date_start.strftime('%Y-%m-%d')} to {record.date_end.strftime('%Y-%m-%d')})"
result.append((record.id, name))
return result

def action_view_activities(self):
"""Open the activities related to this season"""
self.ensure_one()
action = {
"name": _("Season Activities"),
"type": "ir.actions.act_window",
"res_model": "spp.farm.activity",
"view_mode": "tree,form",
"domain": [("season_id", "=", self.id)],
"context": {"default_season_id": self.id},
}
return action

_sql_constraints = [
("name_uniq", "unique(name)", "Season name must be unique!"),
("date_check", "CHECK(date_end >= date_start)", "End date must be after start date"),
]
6 changes: 6 additions & 0 deletions spp_farmer_registry_base/security/ir.model.access.csv
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,9 @@ spp_farmer_registrar_access,Temporary Model for Farmer Registrar Access,spp_farm
spp_farm_chemical_registrar_access,Farm Chemical Interventions Types Registrar Access,spp_farmer_registry_base.model_spp_farm_chemical,g2p_registry_base.group_g2p_registrar,1,1,1,1
spp_fertilizer_registrar_access,Fertilizer Interventions Types Registrar Access,spp_farmer_registry_base.model_spp_fertilizer,g2p_registry_base.group_g2p_registrar,1,1,1,1
spp_feed_items_registrar_access,Feed Items Types Registrar Access,spp_farmer_registry_base.model_spp_feed_items,g2p_registry_base.group_g2p_registrar,1,1,1,1

access_spp_farm_season_user,spp.farm.season.user,spp_farmer_registry_base.model_spp_farm_season,spp_farmer_registry_base.group_spp_farm_user,1,0,0,0
access_spp_farm_activity_user,spp.farm.activity.user,spp_farmer_registry_base.model_spp_farm_activity,spp_farmer_registry_base.group_spp_farm_user,1,0,0,0

access_spp_farm_season_manager,spp.farm.season.manager,spp_farmer_registry_base.model_spp_farm_season,spp_farmer_registry_base.group_spp_farm_manager,1,1,1,1
access_spp_farm_activity_manager,spp.farm.activity.manager,spp_farmer_registry_base.model_spp_farm_activity,spp_farmer_registry_base.group_spp_farm_manager,1,1,1,1
66 changes: 66 additions & 0 deletions spp_farmer_registry_base/security/security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="0">
<!-- Category -->
<record id="module_category_spp_farm" model="ir.module.category">
<field name="name">Farm Management</field>
<field name="sequence">20</field>
</record>

<!-- Groups -->
<record id="group_spp_farm_user" model="res.groups">
<field name="name">Farm User</field>
<field name="category_id" ref="module_category_spp_farm" />
<field name="implied_ids" eval="[(4, ref('base.group_user'))]" />
</record>

<record id="group_spp_farm_manager" model="res.groups">
<field name="name">Farm Manager</field>
<field name="category_id" ref="module_category_spp_farm" />
<field name="implied_ids" eval="[(4, ref('group_spp_farm_user'))]" />
<field name="users" eval="[(4, ref('base.user_admin'))]" />
</record>
</data>

<!-- Record Rules -->
<data noupdate="1">
<!-- Season Rules -->
<record id="rule_spp_farm_season_user" model="ir.rule">
<field name="name">Farm Season: User Access</field>
<field name="model_id" ref="model_spp_farm_season" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_spp_farm_user'))]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="False" />
<field name="perm_create" eval="False" />
<field name="perm_unlink" eval="False" />
</record>

<record id="rule_spp_farm_season_manager" model="ir.rule">
<field name="name">Farm Season: Manager Access</field>
<field name="model_id" ref="model_spp_farm_season" />
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('group_spp_farm_manager'))]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
</record>

<!-- Activity Rules -->
<record id="rule_spp_farm_activity_season_closed" model="ir.rule">
<field name="name">Farm Activity: Closed Season Protection</field>
<field name="model_id" ref="model_spp_farm_activity" />
<field name="domain_force">[
'|',
('season_id', '=', False),
('season_id.state', '!=', 'closed')
]</field>
<field name="groups" eval="[(4, ref('group_spp_farm_user'))]" />
<field name="perm_read" eval="True" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
</record>
</data>
</odoo>
1 change: 1 addition & 0 deletions spp_farmer_registry_base/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import test_farm
from . import test_farm_season
Loading
Loading