Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • coopdevs/comunitats-energetiques/odoo-ce
1 result
Show changes
Showing
with 644 additions and 12 deletions
energy_project_energy_communities/static/description/icon.png

3.63 KiB

from . import models
from . import wizards
from . import controllers
from . import tests
......@@ -9,10 +9,13 @@
"author": "Coopdevs Treball SCCL & Som Energia SCCL",
"website": "https://coopdevs.org",
"category": "Customizations",
"version": "14.0.2.1.0",
"version": "14.0.2.2.0",
"depends": [
"base",
"mail",
"contract",
"contract_variable_quantity",
"contract_invoice_start_end_dates",
"energy_project",
"partner_firstname",
"web_m2x_options",
......@@ -31,6 +34,8 @@
"views/supply_point_assignation_views.xml",
"wizards/selfconsumption_import_wizard_views.xml",
"wizards/distribution_table_import_wizard_views.xml",
"wizards/contract_generation_wizard_views.xml",
"wizards/define_invoicing_mode_wizard_view.xml",
"reports/selfconsumption_reports.xml",
],
}
......@@ -4,3 +4,6 @@ from . import partner
from . import distribution_table
from . import supply_point_assignation
from . import project
from . import contract_line
from . import contract
from . import product
from odoo import fields, models
class Contract(models.Model):
_inherit = "contract.contract"
project_id = fields.Many2one(
"energy_project.project",
ondelete="restrict",
string="Energy Project",
check_company=True,
)
from odoo import api, fields, models
class ContractLine(models.Model):
_inherit = "contract.line"
supply_point_assignation_id = fields.Many2one(
"energy_selfconsumption.supply_point_assignation",
string="Selfconsumption project",
)
from odoo import fields, models
class Product(models.Model):
_inherit = "product.product"
project_id = fields.Many2one(
"energy_project.project",
required=True,
ondelete="restrict",
string="Energy Project",
check_company=True,
)
......@@ -3,6 +3,15 @@ from datetime import datetime
from odoo import _, fields, models
from odoo.exceptions import ValidationError
INVOICING_VALUES = [
("power_acquired", _("Power Acquired")),
("energy_delivered", _("Energy Delivered")),
(
"energy_delivered_variable",
_("Energy Delivered Variable Hourly Coefficient"),
),
]
class Selfconsumption(models.Model):
_name = "energy_selfconsumption.selfconsumption"
......@@ -20,6 +29,13 @@ class Selfconsumption(models.Model):
for record in self:
record.inscription_count = len(record.inscription_ids)
def _compute_contract_count(self):
for record in self:
related_contracts = self.env["contract.contract"].search_count(
[("project_id", "=", record.id)]
)
record.contracts_count = related_contracts
def _compute_report_distribution_table(self):
"""
This compute field gets the distribution table needed to generate the reports.
......@@ -52,7 +68,7 @@ class Selfconsumption(models.Model):
required=True,
default=lambda self: self.env.company.partner_id,
)
power = fields.Float(string="Generation Power (kW)")
power = fields.Float(string="Rated Power (kWn)")
distribution_table_ids = fields.One2many(
"energy_selfconsumption.distribution_table",
"selfconsumption_project_id",
......@@ -68,6 +84,10 @@ class Selfconsumption(models.Model):
"energy_project.inscription", "project_id", readonly=True
)
inscription_count = fields.Integer(compute=_compute_inscription_count)
contracts_count = fields.Integer(compute=_compute_contract_count)
invoicing_mode = fields.Selection(INVOICING_VALUES, string="Invoicing Mode")
product_id = fields.Many2one("product.product", string="Product")
contract_template_id = fields.Many2one("contract.template")
def get_distribution_tables(self):
self.ensure_one()
......@@ -91,6 +111,17 @@ class Selfconsumption(models.Model):
"context": {"create": True, "default_project_id": self.id},
}
def get_contracts(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"name": "Contracts",
"view_mode": "tree,form",
"res_model": "contract.contract",
"domain": [("project_id", "=", self.id)],
"context": {"create": True, "default_project_id": self.id},
}
def distribution_table_state(self, actual_state, new_state):
distribution_table_to_activate = self.distribution_table_ids.filtered(
lambda table: table.state == actual_state
......@@ -107,15 +138,53 @@ class Selfconsumption(models.Model):
self.distribution_table_state("validated", "process")
def activate(self):
"""
Activates the energy self-consumption project, performing various validations.
This method checks for the presence of a valid code, CIL, and rated power
for the project. If all validations pass, it opens a wizard for generating
contracts for the project.
Note:
The change of state for the 'self-consumption' and 'distribution_table'
models is performed in the wizard that gets opened. These state changes
are executed only after the contracts have been successfully generated.
Returns:
dict: A dictionary containing the action details for opening the wizard.
Raises:
ValidationError: If the project does not have a valid Code, CIL, or Rated Power.
"""
for record in self:
if not record.code:
raise ValidationError(_("Project must have a valid Code."))
if not record.cil:
raise ValidationError(_("Project must have a valid CIL."))
if not record.power or record.power <= 0:
raise ValidationError(_("Project must have a valid Generation Power."))
record.write({"state": "active"})
self.distribution_table_state("process", "active")
raise ValidationError(_("Project must have a valid Rated Power."))
return {
"name": _("Generate Contracts"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "energy_selfconsumption.contract_generation.wizard",
"views": [(False, "form")],
"view_id": False,
"target": "new",
"context": {"default_selfconsumption_id": self.id},
}
def set_invoicing_mode(self):
return {
"name": _("Define Invoicing Mode"),
"type": "ir.actions.act_window",
"view_mode": "form",
"res_model": "energy_selfconsumption.define_invoicing_mode.wizard",
"views": [(False, "form")],
"view_id": False,
"target": "new",
"context": {"default_selfconsumption_id": self.id},
}
def action_selfconsumption_import_wizard(self):
self.ensure_one()
......
......@@ -24,9 +24,9 @@ class SupplyPoint(models.Model):
)
partner_id = fields.Many2one(
"res.partner",
string="Partner",
string="Cooperator",
required=True,
help="Partner subscribed to the self-consumption project",
help="Cooperator subscribed to the self-consumption project",
)
company_id = fields.Many2one(
"res.company", default=lambda self: self.env.company, readonly=True
......@@ -58,6 +58,9 @@ class SupplyPoint(models.Model):
)
supplier_id = fields.Many2one("energy_project.supplier", string="Supplier")
@api.onchange("partner_id")
def _onchange_partner_id(self):
self.owner_id = self.partner_id
@api.depends("partner_id", "street")
def _compute_supply_point_name(self):
......@@ -66,4 +69,3 @@ class SupplyPoint(models.Model):
record.name = f"{record.partner_id.name} - {record.street}"
else:
record.name = _("New Supply Point")
......@@ -72,7 +72,7 @@ class SupplyPointAssignation(models.Model):
@api.constrains("supply_point_id")
def constraint_supply_point_id(self):
for record in self:
supply_points = record.distribution_table_id.selfconsumption_project_id.inscription_ids.mapped(
supply_points = record.distribution_table_id.selfconsumption_project_id.project_id.inscription_ids.mapped(
"partner_id.supply_ids"
)
if record.supply_point_id.id not in supply_points.ids:
......
......@@ -9,4 +9,6 @@ access_energy_selfconsumption_distribution_table_admin,energy_selfconsumption.di
access_energy_selfconsumption_supply_point_assignation_admin,energy_selfconsumption.supply_point_assignation.admin,model_energy_selfconsumption_supply_point_assignation,energy_project.group_admin,1,1,1,1
access_energy_selfconsumption_selfconsumption_import_wizard_admin,energy_selfconsumption.selfconsumption_import.wizard.admin,model_energy_selfconsumption_selfconsumption_import_wizard,energy_project.group_admin,1,1,1,1
access_energy_selfconsumption_distribution_table_import_wizard_admin,energy_selfconsumption.distribution_table_import.wizard.admin,model_energy_selfconsumption_distribution_table_import_wizard,energy_project.group_admin,1,1,1,1
access_energy_selfconsumption_contract_generation_wizard_admin,energy_selfconsumption.contract_generation.wizard.admin,model_energy_selfconsumption_contract_generation_wizard,energy_project.group_admin,1,1,1,1
accces_energy_selfconsumption_coefficient_report,energy_selfconsumption.coefficient_report.admin,model_energy_selfconsumption_coefficient_report,energy_project.group_admin,1,1,1,1
access_energy_elfconsumptionn_define_invoicing_mode_admin,energy_selfconsumption.define_invoicing_mode.wizard.admin,model_energy_selfconsumption_define_invoicing_mode_wizard,energy_project.group_admin,1,1,1,1
from . import test_contract_generation_wizard
from datetime import datetime
from odoo.exceptions import ValidationError
from odoo.tests.common import TransactionCase
class TestContractGenerationWizard(TransactionCase):
def setUp(self):
super().setUp()
self.partner = self.env["res.partner"].create({"name": "test partner"})
self.selfconsumption = self.env[
"energy_selfconsumption.selfconsumption"
].create(
{
"name": "test Selfconsumption Project",
"type": self.env.ref(
"energy_selfconsumption.selfconsumption_project_type"
).id,
"code": "ES0397277816188340VL",
"cil": "001ES0397277816188340VL",
"state": "activation",
"power": 100,
"street": "Carrer de Sants, 79",
"zip": "08014",
"city": "Barcelona",
"state_id": self.env.ref("base.state_es_b").id,
"country_id": self.env.ref("base.es").id,
}
)
self.inscription = self.env["energy_project.inscription"].create(
{
"project_id": self.selfconsumption.project_id.id,
"partner_id": self.partner.id,
"effective_date": datetime.today(),
}
)
self.supply_point = self.env["energy_selfconsumption.supply_point"].create(
{
"code": "ES0029542181297829TM",
"street": "C. de Sta. Catalina",
"street2": "55º B",
"zip": "08014",
"city": "Barcelona",
"state_id": self.env.ref("base.state_es_b").id,
"country_id": self.env.ref("base.es").id,
"owner_id": self.partner.id,
"partner_id": self.partner.id,
}
)
self.distribution_table = self.env[
"energy_selfconsumption.distribution_table"
].create(
{
"name": "DT001",
"selfconsumption_project_id": self.selfconsumption.id,
"type": "fixed",
"state": "process",
}
)
self.supply_point_assignation = self.env[
"energy_selfconsumption.supply_point_assignation"
].create(
{
"distribution_table_id": self.distribution_table.id,
"supply_point_id": self.supply_point.id,
"coefficient": 1,
}
)
self.contract_generation_wizard = self.env[
"energy_selfconsumption.contract_generation.wizard"
].create(
{
"price_energy": 0.1,
"recurring_interval": 1,
"recurring_rule_type": "monthly",
"selfconsumption_id": self.selfconsumption.id,
}
)
def test_generation_contracts(self):
res = self.contract_generation_wizard.generate_contracts_button()
self.assertEqual(res, True)
related_contract = self.env["contract.contract"].search(
[("project_id", "=", self.selfconsumption.project_id.id)]
)
contract_line = related_contract[0].contract_line_ids[0]
days_timedelta = (
contract_line.next_period_date_end - contract_line.next_period_date_start
)
expected_quantity = 100 * 1 * (days_timedelta.days + 1)
related_contract[0].recurring_create_invoice()
invoice = related_contract._get_related_invoices()
self.assertEqual(invoice.invoice_line_ids[0].quantity, expected_quantity)
self.assertEqual(invoice.invoice_line_ids[0].price_unit, 0.1)
......@@ -5,8 +5,8 @@
<field name="name">Owners</field>
<field name="res_model">res.partner</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('owner_supply_ids','!=', False)]</field>
<field name="context">{'create':False}</field>
<field name="domain">[('owner_supply_ids', '!=', False)]</field>
<field name="context">{'create': False}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a contact in your address book.
......@@ -22,6 +22,26 @@
action="action_partner_owner_form"
/>
<record id="action_partner_cooperator_form" model="ir.actions.act_window">
<field name="name">Cooperators</field>
<field name="res_model">res.partner</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('member', '=', True)]</field>
<field name="context">{'create': False}</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click to add a contact in your address book.
</p>
</field>
</record>
<menuitem
name="Cooperators"
id="res_partner_cooperators_menu"
parent="energy_selfconsumption.contacts_category_menu"
action="action_partner_cooperator_form"
/>
<record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.buttons</field>
<field name="model">res.partner</field>
......
......@@ -69,6 +69,12 @@
string="Download Partition Coefficient"
attrs="{'invisible':[('state','not in',['activation', 'active'])]}"
/>
<button
type="object"
string="Define Invoicing Mode"
name="set_invoicing_mode"
attrs="{'invisible':[('invoicing_mode', '!=', False)]}"
/>
<field
name="state"
......@@ -106,6 +112,21 @@
widget="statinfo"
/>
</button>
<button
class="oe_stat_button"
type="object"
name="get_contracts"
icon="fa-files-o"
attrs="{'invisible': [('state', '!=', 'active')]}"
>
<field
string="Contracts"
name="contracts_count"
widget="statinfo"
/>
</button>
</div>
<widget
name="web_ribbon"
......@@ -196,6 +217,25 @@
</div>
</group>
</group>
<notebook colspan="4">
<page
string="Invoicing Mode"
name="invoicing_mode"
autofocus="autofocus"
attrs="{'invisible':[('invoicing_mode', '==', False)]}"
>
<group>
<field name="invoicing_mode" readonly="True" />
<field name="product_id" readonly="True" />
<field
name="contract_template_id"
readonly="True"
/>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
......
......@@ -21,7 +21,10 @@
name="partner_id"
options="{'no_create': True}"
/>
<field name="owner_id" />
<field
name="owner_id"
domain="['|', ('member', '=' , False), ('id', '=', partner_id)]"
/>
<field
name="reseller_id"
options="{'no_create': True, 'no_edit':True, 'no_open': True}"
......
from . import selfconsumption_import_wizard
from . import distribution_table_import_wizard
from . import contract_generation_wizard
from . import define_invoicing_mode_wizard
import logging
from odoo import _, fields, models
logger = logging.getLogger(__name__)
class ContractGenerationWizard(models.TransientModel):
_name = "energy_selfconsumption.contract_generation.wizard"
price_energy = fields.Float(string="Price (€/kWn/day)")
recurring_interval = fields.Integer(
default=1,
string="Invoice Every",
help="Invoice every (Days/Week/Month/Year)",
)
recurring_rule_type = fields.Selection(
[
("daily", "Day(s)"),
("weekly", "Week(s)"),
("monthly", "Month(s)"),
("monthlylastday", "Month(s) last day"),
("quarterly", "Quarter(s)"),
("semesterly", "Semester(s)"),
("yearly", "Year(s)"),
],
default="monthly",
string="Recurrence",
help="Specify Interval for automatic invoice generation.",
)
selfconsumption_id = fields.Many2one(
"energy_selfconsumption.selfconsumption", readonly=True
)
def generate_contracts_button(self):
"""
This method generates contracts based on supply point assignations.
It first creates a product and a contract formula. It then
aggregates supply point assignations by partner and owner
to generate the contracts.
In the other hand, if the process was successful, the state of self-consumption
and the distribution_table changes to 'active'.
Returns:
bool: Always True, indicating successful execution.
Raises:
UserWarning: When no accounting journal is found.
SomeException: When no distribution table in process of activation is found.
"""
# Create product
product_id = self.env["product.product"].create(
{
"name": _("Energy Acquired - %s") % (self.selfconsumption_id.name),
"lst_price": self.price_energy,
"company_id": self.env.company.id,
"must_have_dates": True,
}
)
# Create contract formula
formula_contract_id = self.env["contract.line.qty.formula"].create(
{
"name": _("Formula - %s") % (self.selfconsumption_id.name),
"code": """
days_timedelta = line.next_period_date_end - line.next_period_date_start
if days_timedelta:
# Add one so it counts the same day too (month = 29 + 1)
days_between = days_timedelta.days + 1
else:
days_between = 0
result = line.supply_point_assignation_id.distribution_table_id.selfconsumption_project_id.power * line.supply_point_assignation_id.coefficient * days_between
""",
}
)
# Search accounting journal
journal_id = self.env["account.journal"].search(
[("company_id", "=", self.env.company.id), ("type", "=", "sale")], limit=1
)
if not journal_id:
raise UserWarning(_("Accounting Journal not found."))
# Get distribution table
distribution_id = (
self.selfconsumption_id.distribution_table_ids.filtered_domain(
[("state", "=", "process")]
)
)
if not distribution_id:
raise _("There is no distribution table in proces of activation.")
# Build partner list and supply point assignment
partner_list = {}
for supply_point_assignation in distribution_id.supply_point_assignation_ids:
key = (
supply_point_assignation.supply_point_id.partner_id.id,
supply_point_assignation.owner_id.id,
)
if key not in partner_list:
partner_list[key] = {
"parent_id": supply_point_assignation.supply_point_id.partner_id,
"owner_id": supply_point_assignation.owner_id,
"supply_point_assignation_ids": [supply_point_assignation],
}
else:
partner_list[key]["supply_point_assignation_ids"].append(
supply_point_assignation
)
# Create contracts
for partner_data in partner_list.values():
contract_lines = [
(
0,
0,
{
"product_id": product_id.id,
"automatic_price": True,
"company_id": self.env.company.id,
"qty_type": "variable",
"qty_formula_id": formula_contract_id.id,
"name": _(
"""CUPS: %s
Holder: %s
Invoicing period: #START# - #END#"""
)
% (
assignation.supply_point_id.code,
assignation.supply_point_id.owner_id.display_name,
),
"supply_point_assignation_id": assignation.id,
},
)
for assignation in partner_data["supply_point_assignation_ids"]
]
self.env["contract.contract"].create(
{
"name": _("Contract - %s - %s")
% (self.selfconsumption_id.name, partner_data["parent_id"].name),
"partner_id": partner_data["parent_id"].id,
"invoice_partner_id": partner_data["parent_id"].id,
"journal_id": journal_id.id,
"recurring_interval": self.recurring_interval,
"recurring_rule_type": self.recurring_rule_type,
"recurring_invoicing_type": "post-paid",
"date_start": fields.date.today(),
"company_id": self.env.company.id,
"contract_line_ids": contract_lines,
"project_id": self.selfconsumption_id.project_id.id,
}
)
# Update selfconsumption and distribution_table state
self.selfconsumption_id.write({"state": "active"})
self.selfconsumption_id.distribution_table_state("process", "active")
return True
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data>
<record id="contract_generation_wizard_form_view" model="ir.ui.view">
<field
name="name"
>energy_selfconsumption.contract_generation_wizard.form</field>
<field
name="model"
>energy_selfconsumption.contract_generation.wizard</field>
<field name="arch" type="xml">
<form string="Contract Generation">
<sheet>
<group>
<group>
<field name="price_energy" />
</group>
<group>
<field name="recurring_interval" />
<field name="recurring_rule_type" />
</group>
</group>
</sheet>
<footer>
<button
type="object"
name="generate_contracts_button"
>Generate</button>
</footer>
</form>
</field>
</record>
</data>
</odoo>
from odoo import _, fields, models
from odoo.exceptions import UserError
from ..models.selfconsumption import INVOICING_VALUES
class ContractGenerationWizard(models.TransientModel):
_name = "energy_selfconsumption.define_invoicing_mode.wizard"
RULE_TYPE_OPTIONS = [
("daily", _("Day(s)")),
("weekly", _("Week(s)")),
("monthly", _("Month(s)")),
("monthlylastday", _("Month(s) last day")),
("quarterly", _("Quarter(s)")),
("semesterly", _("Semester(s)")),
("yearly", _("Year(s)")),
]
invoicing_mode = fields.Selection(
INVOICING_VALUES,
string="Invoicing Mode",
default="power_acquired",
required=True,
)
price = fields.Float(required=True)
recurrence_interval = fields.Integer(
default=1,
string="Invoice Every",
required=True,
help=_("Invoice every (Days/Week/Month/Year)"),
)
recurring_rule_type = fields.Selection(
RULE_TYPE_OPTIONS,
default="monthlylastday",
string="Recurrence",
required=True,
help=_("Specify Interval for automatic invoice generation."),
)
selfconsumption_id = fields.Many2one(
"energy_selfconsumption.selfconsumption", readonly=True
)
def _prepare_product_values(self):
return {
"name": self.selfconsumption_id.name,
"lst_price": self.price,
"company_id": self.env.company.id,
"project_id": self.selfconsumption_id.project_id.id,
}
def _prepare_formula_values(self, code):
return {
"name": _("Formula - %s") % (self.selfconsumption_id.name),
"code": code,
}
def _prepare_contract_values(self, journal_id, contract_line):
return {
"name": self.selfconsumption_id.name,
"journal_id": journal_id.id,
"company_id": self.env.company.id,
"contract_line_ids": contract_line,
"recurring_interval": self.recurrence_interval,
"recurring_rule_type": self.recurring_rule_type,
}
def save_data_to_selfconsumption(self):
if self.invoicing_mode == "energy_delivered_variable":
raise UserError(_("This invoicing mode is not yet implemented"))
# Create product
product_id = self.env["product.product"].create(self._prepare_product_values())
# Create contract formula
# TODO:Update formula energy_delivered and energy_delivered_variable.
formula_contract_id = None
if self.invoicing_mode == "power_acquired":
code = f"""
days_timedelta = line.next_period_date_end - line.next_period_date_start
if days_timedelta:
# Add one so it counts the same day too (month = 29 + 1)
days_between = days_timedelta.days + 1
else:
days_between = 0
result = line.supply_point_assignation_id.distribution_table_id.selfconsumption_project_id.power * line.supply_point_assignation_id.coefficient * days_between
"""
formula_contract_id = self.env["contract.line.qty.formula"].create(
self._prepare_formula_values(code)
)
elif self.invoicing_mode == "energy_delivered":
code = f"""
days_timedelta = line.next_period_date_end - line.next_period_date_start
if days_timedelta:
# Add one so it counts the same day too (month = 29 + 1)
days_between = days_timedelta.days + 1
else:
days_between = 0
result = line.supply_point_assignation_id.distribution_table_id.selfconsumption_project_id.power * line.supply_point_assignation_id.coefficient * days_between
"""
formula_contract_id = self.env["contract.line.qty.formula"].create(
self._prepare_formula_values(code)
)
elif self.invoicing_mode == "energy_delivered_variable":
code = (
f"""
days_timedelta = line.next_period_date_end - line.next_period_date_start
if days_timedelta:
# Add one so it counts the same day too (month = 29 + 1)
days_between = days_timedelta.days + 1
else:
days_between = 0
result = line.supply_point_assignation_id.distribution_table_id.selfconsumption_project_id.power * line.supply_point_assignation_id.coefficient * days_between
""",
)
formula_contract_id = self.env["contract.line.qty.formula"].create(
self._prepare_formula_values(code)
)
# Search accounting journal
journal_id = self.env["account.journal"].search(
[("company_id", "=", self.env.company.id), ("type", "=", "sale")], limit=1
)
if not journal_id:
raise UserWarning(_("Accounting Journal not found."))
# Create Contract Template
contract_line = [
(
0,
0,
{
"product_id": product_id.id,
"automatic_price": True,
"company_id": self.env.company.id,
"qty_type": "variable",
"qty_formula_id": formula_contract_id.id,
"name": "",
},
)
]
contract_template_id = self.env["contract.template"].create(
self._prepare_contract_values(journal_id, contract_line)
)
self.selfconsumption_id.write(
{
"invoicing_mode": self.invoicing_mode,
"product_id": product_id,
"contract_template_id": contract_template_id,
}
)
return {
"type": "ir.actions.act_window_close",
}