Skip to content

Commit 3daab1a

Browse files
authored
OpenSPP Farmer Registry Demo (#860)
* [FIX] supported countries * [FIX] name of groups * [FIX] view context * [FIX] view context * [FIX] view context * [IMP] generation and add failure logs * [IMP] generation and add failure logs * [FIX] phone and id regex validation * [ADD] farmer specific configuration * [FIX] season creation on generator * [FIX] season creation on generator * [FIX] season creation on generator * [FIX] phone number generation * [IMP] phone number onchange * [FIX] set 100 percent by default * [IMP] test coverage * [FIX] tests * [FIX] tests * [FIX] test * [FIX] tests * [FIX] tests * [FIX] tests * [FIX] tests * [FIX] tests * [FIX] test
1 parent a5fde34 commit 3daab1a

19 files changed

Lines changed: 4489 additions & 434 deletions

spp_base_common/models/res_partner.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from odoo import fields, models
3+
from odoo import api, fields, models
44

55
_logger = logging.getLogger(__name__)
66

@@ -14,3 +14,12 @@ class SPPResPartner(models.Model):
1414
default=lambda self: self.env.company,
1515
required=False,
1616
)
17+
18+
@api.onchange("phone_number_ids")
19+
def phone_number_ids_change(self):
20+
phone = ""
21+
if self.phone_number_ids:
22+
phone = ", ".join(
23+
[p for p in self.phone_number_ids.filtered(lambda rec: not rec.disabled).mapped("phone_no")]
24+
)
25+
self.phone = phone

spp_base_farmer_registry_demo/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"data/chemical_data.xml",
3636
"data/fertilizer_data.xml",
3737
"data/feed_items_data.xml",
38+
"views/demo_data_generator_view.xml",
3839
"views/group_view.xml",
3940
"views/individual_view.xml",
4041
],

spp_base_farmer_registry_demo/models/generate_demo_data.py

Lines changed: 164 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
from faker import Faker
77

8-
from odoo import fields, models
8+
from odoo import _, api, fields, models
9+
from odoo.exceptions import ValidationError
910

1011
_logger = logging.getLogger(__name__)
1112

@@ -16,28 +17,28 @@ class SPPDemoDataGenerator(models.Model):
1617
# Farmer Registry specific fields
1718
percentage_with_farm_details = fields.Integer(
1819
string="% with Farm Details",
19-
default=80,
20+
default=100,
2021
required=True,
2122
help="Percentage of farmers that will have farm details",
2223
)
2324
percentage_with_land_records = fields.Integer(
2425
string="% with Land Records",
25-
default=70,
26+
default=100,
2627
required=True,
2728
help="Percentage of farmers that will have land records",
2829
)
2930
percentage_with_farm_assets = fields.Integer(
30-
string="% with Farm Assets", default=60, required=True, help="Percentage of farmers that will have farm assets"
31+
string="% with Farm Assets", default=100, required=True, help="Percentage of farmers that will have farm assets"
3132
)
3233
percentage_with_agricultural_activities = fields.Integer(
3334
string="% with Agricultural Activities",
34-
default=85,
35+
default=100,
3536
required=True,
3637
help="Percentage of farmers that will have agricultural activities",
3738
)
3839
percentage_with_extension_services = fields.Integer(
3940
string="% with Extension Services",
40-
default=40,
41+
default=100,
4142
required=True,
4243
help="Percentage of farmers that will have extension services",
4344
)
@@ -66,6 +67,34 @@ class SPPDemoDataGenerator(models.Model):
6667
help="Maximum number of agricultural activities per farm",
6768
)
6869

70+
# Season configuration fields
71+
season_name = fields.Char(
72+
string="Season Name",
73+
default=lambda self: f"Demo Season {fields.Date.today().year}",
74+
required=True,
75+
help="Name of the agricultural season to use or create",
76+
)
77+
season_start_date = fields.Date(
78+
string="Season Start Date",
79+
default=lambda self: fields.Date.today().replace(month=1, day=1),
80+
required=True,
81+
help="Start date of the agricultural season",
82+
)
83+
season_end_date = fields.Date(
84+
string="Season End Date",
85+
default=lambda self: fields.Date.today().replace(month=12, day=31),
86+
required=True,
87+
help="End date of the agricultural season",
88+
)
89+
90+
@api.constrains("season_start_date", "season_end_date")
91+
def _check_season_dates(self):
92+
"""Validate that season end date is after start date"""
93+
for record in self:
94+
if record.season_end_date and record.season_start_date:
95+
if record.season_end_date < record.season_start_date:
96+
raise ValidationError(_("Season end date must be after start date"))
97+
6998
# Selection options for farmer registry
7099
FARM_TYPES = [
71100
("crop", "Crop"),
@@ -119,7 +148,21 @@ def generate_demo_data(self):
119148
self._generate_chemical_data(fake)
120149
self._generate_fertilizer_data(fake)
121150
self._generate_feed_items_data(fake)
122-
self._generate_season_data(fake)
151+
152+
# Validate that season can be created/found
153+
season = self._generate_season_data(fake)
154+
if not season:
155+
raise ValidationError(
156+
_(
157+
"Failed to create or find an active agricultural season. "
158+
"Please check the season configuration and try again."
159+
)
160+
)
161+
162+
_logger.info(
163+
f"Demo data generation initialized with season: {season.name} "
164+
f"({season.date_start} to {season.date_end})"
165+
)
123166

124167
result = super().generate_demo_data()
125168
return result
@@ -147,9 +190,11 @@ def get_group_vals(self, fake):
147190
)
148191

149192
# Specific Head Farmer details on Group
193+
farmer_family_name = fake.last_name()
150194
group_vals.update(
151195
{
152-
"farmer_family_name": fake.last_name(),
196+
"farmer_family_name": farmer_family_name,
197+
"name": farmer_family_name,
153198
"farmer_given_name": fake.first_name(),
154199
"farmer_addtnl_name": fake.first_name() if random.choice([True, False]) else None,
155200
"farmer_mobile_tel": self.generate_phone_number(fake),
@@ -541,7 +586,7 @@ def _generate_land_records(self, fake, group):
541586
"""Generate land records for a farm"""
542587
num_parcels = random.randint(1, self.max_land_parcels_per_farm)
543588

544-
for _ in range(num_parcels):
589+
for _i in range(num_parcels):
545590
land_vals = self._get_land_record_vals(fake, group)
546591
land_record = self.env["spp.land.record"].create(land_vals)
547592

@@ -598,7 +643,7 @@ def _generate_farm_assets(self, fake, group):
598643
"""Generate farm assets and machinery"""
599644
num_assets = random.randint(1, self.max_assets_per_farm)
600645

601-
for _ in range(num_assets):
646+
for _i in range(num_assets):
602647
# Generate farm assets
603648
asset_vals = self._get_farm_asset_vals(fake, group)
604649
self.env["spp.farm.asset"].create(asset_vals)
@@ -629,20 +674,32 @@ def _get_machinery_vals(self, fake, group):
629674

630675
def _generate_agricultural_activities(self, fake, group):
631676
"""Generate agricultural activities"""
677+
# Ensure season is available
678+
season_id = self._get_random_season()
679+
if not season_id:
680+
_logger.warning(f"No active season available for group {group.id}. Skipping agricultural activities.")
681+
return
682+
683+
season = self.env["spp.farm.season"].browse(season_id)
684+
_logger.debug(f"Generating agricultural activities for group {group.id} using season: {season.name}")
685+
632686
num_activities = random.randint(1, self.max_activities_per_farm)
633687

634-
for _ in range(num_activities):
635-
activity_vals = self._get_agricultural_activity_vals(fake, group)
636-
self.env["spp.farm.activity"].create(activity_vals)
688+
for _i in range(num_activities):
689+
activity_vals = self._get_agricultural_activity_vals(fake, group, season_id)
690+
try:
691+
self.env["spp.farm.activity"].create(activity_vals)
692+
except Exception as e:
693+
_logger.error(f"Failed to create agricultural activity for group {group.id}: {str(e)}")
637694

638-
def _get_agricultural_activity_vals(self, fake, group):
695+
def _get_agricultural_activity_vals(self, fake, group, season_id):
639696
"""Get agricultural activity values"""
640697
activity_type = random.choice(self.ACTIVITY_TYPES)
641698

642699
vals = {
643700
"activity_type": activity_type[0],
644701
"purpose": random.choice(self.PRODUCTION_PURPOSES)[0],
645-
"season_id": self._get_random_season(),
702+
"season_id": season_id,
646703
}
647704

648705
# Set the appropriate farm field based on activity type
@@ -708,7 +765,7 @@ def _generate_extension_services(self, fake, group):
708765
"""Generate farm extension services"""
709766
num_services = random.randint(1, 3)
710767

711-
for _ in range(num_services):
768+
for _i in range(num_services):
712769
extension_vals = self._get_extension_service_vals(fake, group)
713770
self.env["spp.farm.extension"].create(extension_vals)
714771

@@ -824,29 +881,81 @@ def _generate_feed_items_data(self, fake):
824881
self.env["spp.feed.items"].create(feed_info)
825882

826883
def _generate_season_data(self, fake):
827-
"""Generate agricultural season data for the demo"""
828-
current_year = fields.Date.today().year
829-
seasons = [
830-
{
831-
"name": f"Season {current_year}",
832-
"description": f"Main agricultural season for {current_year}",
833-
"date_start": fields.Date.today().replace(month=1, day=1),
834-
"date_end": fields.Date.today().replace(month=12, day=31),
835-
"state": "active",
836-
},
837-
{
838-
"name": f"Season {current_year - 1}",
839-
"description": f"Previous agricultural season for {current_year - 1}",
840-
"date_start": fields.Date.today().replace(year=current_year - 1, month=1, day=1),
841-
"date_end": fields.Date.today().replace(year=current_year - 1, month=12, day=31),
842-
"state": "closed",
843-
},
844-
]
884+
"""Generate or find agricultural season data for the demo"""
885+
try:
886+
# Search for existing season matching name, dates, and active state
887+
existing_season = self.env["spp.farm.season"].search(
888+
[
889+
("name", "=", self.season_name),
890+
("date_start", "=", self.season_start_date),
891+
("date_end", "=", self.season_end_date),
892+
("state", "=", "active"),
893+
],
894+
limit=1,
895+
)
845896

846-
for season_info in seasons:
847-
existing = self.env["spp.farm.season"].search([("name", "=", season_info["name"])])
848-
if not existing:
849-
self.env["spp.farm.season"].create(season_info)
897+
if existing_season:
898+
_logger.info(f"Using existing active season: {existing_season.name} (ID: {existing_season.id})")
899+
return existing_season
900+
901+
# Search for season with same name but different dates or status
902+
existing_season_by_name = self.env["spp.farm.season"].search(
903+
[
904+
("name", "=", self.season_name),
905+
],
906+
limit=1,
907+
)
908+
909+
if existing_season_by_name:
910+
# Update existing season if it's not closed
911+
if existing_season_by_name.state != "closed":
912+
_logger.info(f"Updating existing season: {existing_season_by_name.name}")
913+
existing_season_by_name.write(
914+
{
915+
"date_start": self.season_start_date,
916+
"date_end": self.season_end_date,
917+
}
918+
)
919+
if existing_season_by_name.state == "draft":
920+
existing_season_by_name.action_activate()
921+
_logger.info(
922+
f"Season updated and activated: {existing_season_by_name.name} "
923+
f"(ID: {existing_season_by_name.id})"
924+
)
925+
return existing_season_by_name
926+
else:
927+
_logger.warning(
928+
f"Season '{self.season_name}' exists but is closed. Creating new season with modified name."
929+
)
930+
# Create new season with modified name
931+
season_vals = {
932+
"name": f"{self.season_name} (Demo)",
933+
"description": f"Demo agricultural season created on {fields.Date.today()}",
934+
"date_start": self.season_start_date,
935+
"date_end": self.season_end_date,
936+
"state": "draft",
937+
}
938+
new_season = self.env["spp.farm.season"].create(season_vals)
939+
new_season.action_activate()
940+
_logger.info(f"Created and activated new season: {new_season.name} (ID: {new_season.id})")
941+
return new_season
942+
943+
# Create new season if none exists
944+
season_vals = {
945+
"name": self.season_name,
946+
"description": f"Demo agricultural season created on {fields.Date.today()}",
947+
"date_start": self.season_start_date,
948+
"date_end": self.season_end_date,
949+
"state": "draft",
950+
}
951+
new_season = self.env["spp.farm.season"].create(season_vals)
952+
new_season.action_activate()
953+
_logger.info(f"Created and activated new season: {new_season.name} (ID: {new_season.id})")
954+
return new_season
955+
956+
except Exception as e:
957+
_logger.error(f"Failed to generate/find season: {str(e)}")
958+
raise ValidationError(_(f"Failed to create or find agricultural season: {str(e)}")) from e
850959

851960
def _get_random_species(self, species_type):
852961
"""Get a random species of the specified type"""
@@ -880,8 +989,21 @@ def _get_random_feed_items(self, limit=3):
880989
return [(6, 0, [])]
881990

882991
def _get_random_season(self):
883-
"""Get a random active season"""
884-
seasons = self.env["spp.farm.season"].search([("state", "=", "active")])
885-
if seasons:
886-
return random.choice(seasons).id
992+
"""Get the configured active season, creating it if necessary"""
993+
try:
994+
# Get faker locale safely
995+
faker_locale = "en_US"
996+
if hasattr(self, "locale_origin") and self.locale_origin:
997+
faker_locale = self.locale_origin.faker_locale or "en_US"
998+
999+
fake = Faker(faker_locale)
1000+
season = self._generate_season_data(fake)
1001+
1002+
if season:
1003+
_logger.debug(f"Using season: {season.name} (ID: {season.id})")
1004+
return season.id
1005+
except Exception as e:
1006+
_logger.error(f"Exception while getting season: {str(e)}", exc_info=True)
1007+
1008+
_logger.error("Failed to generate/find active season! Agricultural activities cannot be created.")
8871009
return None
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
from . import test_farm
2+
from . import test_generate_demo_data
3+
from . import test_farmer
4+
from . import test_farm_details
5+
from . import test_agricultural_activity
6+
from . import test_land_record
7+
from . import test_farm_asset

0 commit comments

Comments
 (0)