Skip to content

Commit

Permalink
T1657 - Fix invoice generation for Sub Proposal (1/3) (#239)
Browse files Browse the repository at this point in the history
* might have fixed the bug, still need to test. Changed logic of how invoices are generated, specially in the case where we pass a specific conteact to generate the invoices of.

* handle case where more than one open invoice found,might remove this later

* cleanups

* trying to make code clearer, not fully satisfied...

* removed comment

* ruff-format

* lines too long

* more cleanups

* pre-commit... again...

* clean

* PR suggestions

* improved handling of grouped contracts

* rebased branch and fixed mapping over properties

* moved comment

* removed code specific to sub proposal (moved to compassion-module), cleanups

* raise error instead of logging when more than one open invoice found

* ruff-format, should all be good now, hopefully...
  • Loading branch information
Prazn authored Aug 22, 2024
1 parent 5fdc6cc commit e3b0e5c
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 42 deletions.
107 changes: 69 additions & 38 deletions recurring_contract/models/contract_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ def open_invoices(self):
"context": {"search_default_unpaid": 1},
}

def button_generate_invoices(self):
def button_generate_invoices(self, contract_id=None):
"""Immediately generate invoices for the contract group."""
invoicer = (
self.with_context({"async_mode": False})
.with_company(self.active_contract_ids[0].company_id)
.generate_invoices()
.generate_invoices(contract_id)
)
if invoicer.invoice_ids:
notification_type = "success"
Expand All @@ -224,20 +224,20 @@ def button_generate_invoices(self):
##########################################################################
# PRIVATE METHODS #
##########################################################################
def generate_invoices(self):
def generate_invoices(self, contract_id=None):
"""By default, launch asynchronous job to perform the task.
Context value async_mode set to False can force to perform
the task immediately.
"""
invoicer = self.env["recurring.invoicer"].create({})
if self.env.context.get("async_mode", True):
for group in self:
group.with_delay()._generate_invoices(invoicer)
group.with_delay()._generate_invoices(invoicer, contract_id)
else:
self._generate_invoices(invoicer)
self._generate_invoices(invoicer, contract_id)
return invoicer

def _generate_invoices(self, invoicer):
def _generate_invoices(self, invoicer, contract_id=None):
"""Checks all contracts and generate invoices if needed.
Create an invoice per contract group per date.
"""
Expand All @@ -248,6 +248,13 @@ def _generate_invoices(self, invoicer):
# Set to track processed invoices to avoid duplication
processed_invoices = set()

contract = None
if contract_id:
contract = self.env["recurring.contract"].browse(contract_id)
if not contract:
_logger.error(f"The contract with the id {contract_id} does not exist.")
return False

for group in self:
# Calculate the initial invoicing date and starting offset
invoicing_date, starting_offset = group._calculate_start_date_and_offset()
Expand All @@ -262,7 +269,9 @@ def _generate_invoices(self, invoicer):
)

# Check if invoice generation should be skipped for this date
if group._should_skip_invoice_generation(current_invoicing_date):
if group._should_skip_invoice_generation(
current_invoicing_date, contract
):
continue

# Create a unique key for the invoice to track it
Expand All @@ -271,7 +280,9 @@ def _generate_invoices(self, invoicer):
# Check if the invoice for this key has already been processed
if invoice_key not in processed_invoices:
# Process invoice generation if not already processed
group._process_invoice_generation(invoicer, current_invoicing_date)
group._process_invoice_generation(
invoicer, current_invoicing_date, contract
)
# Add the invoice key to the set of processed invoices
processed_invoices.add(invoice_key)

Expand Down Expand Up @@ -304,47 +315,60 @@ def _calculate_start_date_and_offset(self):
offset = 1
return start_date, offset

def _should_skip_invoice_generation(self, invoicing_date):
def _should_skip_invoice_generation(self, invoicing_date, contract=None):
"""In such cases, we should skip the invoice generation:
- There is already an invoice for this due date which has been cancelled or
edited.
- Contract group suspension.
- A specific contract is given and an invoice for this due date already exists
and isn't cancelled.
"""
self.ensure_one()
dangling_invoices = self.env["account.move"].search(
[
"|",
"&",

if contract:
search_filter = [
("state", "!=", "cancel"),
("invoice_date_due", "=", invoicing_date),
("partner_id", "=", self.partner_id.id),
("move_type", "=", "out_invoice"),
("line_ids.contract_id", "=", contract.id),
(
"line_ids.product_id",
"in",
contract.product_ids.ids,
),
]
else:
search_filter = [
("invoice_date_due", "=", invoicing_date),
("partner_id", "=", self.partner_id.id),
"&",
"&",
"&",
"&",
("move_type", "=", "out_invoice"),
("state", "=", "cancel"),
("payment_state", "not in", ["paid", "not_paid"]),
("line_ids.contract_id", "in", self.active_contract_ids.ids),
(
"line_ids.product_id",
"in",
self.active_contract_ids.mapped("product_ids").ids,
),
"|",
("payment_state", "not in", ["paid", "not_paid"]),
("state", "=", "cancel"),
]
)
# Check for contract group suspension

existing_invoices = self.env["account.move"].search_count(search_filter)

is_suspended = (
self.invoice_suspended_until
and self.invoice_suspended_until > invoicing_date
)

return bool(dangling_invoices) or is_suspended
return bool(existing_invoices) or is_suspended

def _process_invoice_generation(self, invoicer, invoicing_date):
def _process_invoice_generation(self, invoicer, invoicing_date, contract=None):
self.ensure_one()
active_contracts = self.active_contract_ids
open_invoices = active_contracts.mapped("open_invoice_ids").filtered(
lambda i: i.invoice_date_due >= invoicing_date
and i.invoice_date_due.year == invoicing_date.year
)

# invoice already open we complete the move lines
Expand All @@ -355,6 +379,9 @@ def _process_invoice_generation(self, invoicer, invoicing_date):
lambda m: getattr(m.invoice_date_due, self.recurring_unit)
== current_rec_unit_date
)
if len(open_invoice) > 1:
raise ValueError(f"Found more than one open invoice on {invoicing_date}.")

if open_invoice:
# Retrieve account_move_line already existing for this contract
acc_move_line_curr_contr = open_invoice.mapped("invoice_line_ids").filtered(
Expand Down Expand Up @@ -406,7 +433,12 @@ def _process_invoice_generation(self, invoicer, invoicing_date):
)
else:
# Building invoices data
inv_data = self._build_invoice_gen_data(invoicing_date, invoicer)
contracts = self.active_contract_ids

if contract is not None:
contracts = contract

inv_data = self._build_invoice_gen_data(invoicing_date, invoicer, contracts)
# Creating the actual invoice
_logger.info(f"Generating invoice : {inv_data}")
invoice = self.env["account.move"].create(inv_data)
Expand All @@ -420,7 +452,9 @@ def _process_invoice_generation(self, invoicer, invoicing_date):
)
invoice.unlink()

def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False):
def _build_invoice_gen_data(
self, invoicing_date, invoicer, contracts, gift_wizard=False
):
"""Setup a dict with data passed to invoice.create.
If any custom data is wanted in invoice from contract group, just
inherit this method.
Expand All @@ -432,22 +466,22 @@ def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False):
.search(
[
("date", "=", invoicing_date),
("contract_id", "in", self.active_contract_ids.ids),
("contract_id", "in", contracts.ids),
(
"product_id",
"in",
self.active_contract_ids.mapped("product_ids").ids,
contracts.mapped("product_ids").ids,
),
("payment_state", "=", "paid"),
]
)
.mapped("contract_id.contract_line_ids")
)
# we use the first contract because the information we retrieve has to be shared
# between all the contracts of the list
contract = self.active_contract_ids[0]
company_id = contract.company_id.id
partner_id = self._get_partner_for_contract(contract).id
# we use the first contract because the information we retrieve
# has to be shared between all the contracts of the list
reference_contract = contracts[0]
company_id = reference_contract.company_id.id
partner_id = self._get_partner_for_contract(reference_contract).id
journal = self.env["account.journal"].search(
[("type", "=", "sale"), ("company_id", "=", company_id)], limit=1
)
Expand All @@ -457,10 +491,10 @@ def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False):
"move_type": "out_invoice",
"partner_id": partner_id,
"journal_id": journal.id,
"currency_id": contract.pricelist_id.currency_id.id,
"currency_id": reference_contract.pricelist_id.currency_id.id,
"invoice_date": invoicing_date, # Accountant date
"recurring_invoicer_id": invoicer.id,
"pricelist_id": contract.pricelist_id.id,
"pricelist_id": reference_contract.pricelist_id.id,
"payment_mode_id": self.payment_mode_id.id,
"company_id": company_id,
# Field for the invoice_due_date to be automatically calculated
Expand All @@ -485,10 +519,7 @@ def _build_invoice_gen_data(self, invoicing_date, invoicer, gift_wizard=False):
invoicing_date=invoicing_date, contract_line=cl
),
)
for cl in (
self.mapped("active_contract_ids.contract_line_ids")
- already_paid_cl
)
for cl in (contracts.mapped("contract_line_ids") - already_paid_cl)
if cl
],
"narration": "\n".join(
Expand Down
7 changes: 5 additions & 2 deletions recurring_contract/models/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,11 @@ def update_open_invoices(self, updt_val):
):
if updt_val.get(invoice.name):
val_to_updt = updt_val[invoice.name]
if 'partner_id' in val_to_updt and val_to_updt['partner_id'] == invoice.partner_id.id:
del val_to_updt['partner_id']
if (
"partner_id" in val_to_updt
and val_to_updt["partner_id"] == invoice.partner_id.id
):
del val_to_updt["partner_id"]
if not val_to_updt:
continue
# In case we modify the amount we want to test if the amount is zero
Expand Down
4 changes: 2 additions & 2 deletions recurring_contract/models/recurring_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,14 +371,14 @@ def unlink(self):
# PUBLIC METHODS #
##########################################################################
def button_generate_invoices(self):
return self.mapped("group_id").button_generate_invoices()
return self.mapped("group_id").button_generate_invoices(self.id)

def generate_invoices(self):
"""By default, launch asynchronous job to perform the task.
Context value async_mode set to False can force to perform
the task immediately.
"""
self.mapped("group_id").generate_invoices()
self.mapped("group_id").generate_invoices(self.id)

def cancel_contract_invoices(self):
"""By default, launch asynchronous job to perform the task.
Expand Down

0 comments on commit e3b0e5c

Please sign in to comment.