@@ -173,17 +173,39 @@ def _onchange_riba_partner_bank_id(self):
173173 bank_ids = allowed_banks
174174 self .riba_partner_bank_id = bank_ids [0 ] if bank_ids else None
175175
176- def month_check (self , invoice_date_due , all_date_due ):
176+ def month_check (self , all_invoice_date ):
177177 """
178- :param invoice_date_due: first due date of invoice
179- :param all_date_due : list of due dates for partner
180- :return: True if month of invoice_date_due is in a list of all_date_due
178+ Check if collection fees should be applied based on invoice date month.
179+ :param all_invoice_date : list of invoice dates for partner
180+ :return: True if month of current invoice date is already in all_invoice_date
181181 """
182- for d in all_date_due :
183- if invoice_date_due .month == d .month and invoice_date_due .year == d .year :
182+ self .ensure_one ()
183+ date = self .invoice_date or self .date or fields .Date .context_today (self )
184+ current_invoice_month = date .strftime ("%Y-%m" )
185+ for d in all_invoice_date :
186+ if d and current_invoice_month == d .strftime ("%Y-%m" ):
184187 return True
185188 return False
186189
190+ def maturity_check (self , invoice_date_due , all_date_due ):
191+ """
192+ Check if expenses should be applied based on exact maturity date.
193+ Used for 'one_a_maturity' policy.
194+ :param invoice_date_due: due date of current invoice
195+ :param all_date_due: list of existing due dates for partner
196+ :return: True if invoice_date_due already exists in all_date_due
197+ Example:
198+ - Invoice 1: Oct -> Dec (60 days) -> expenses YES
199+ - Invoice 2: Nov -> Dec (30 days) -> expenses NO (Dec already exists)
200+ - Invoice with 30/60 days: 2 different dates -> 2 expenses
201+ """
202+ self .ensure_one ()
203+ if self .partner_id .riba_policy_expenses == "one_a_maturity" :
204+ for d in all_date_due :
205+ if invoice_date_due == d :
206+ return True
207+ return False
208+
187209 def _post (self , soft = True ):
188210 inv_riba_no_bank = self .filtered (
189211 lambda x : x .is_riba_payment
@@ -210,80 +232,128 @@ def _post(self, soft=True):
210232 )
211233 return super ()._post (soft = soft )
212234
235+ def _get_riba_expense_line_vals (self , pay_date = None ):
236+ """
237+ Prepare values for RiBa collection fees invoice line.
238+ :param pay_date: optional date for the expense description
239+ :return: dict with invoice line values
240+ """
241+ self .ensure_one ()
242+ service_prod = self .company_id .due_cost_service_id
243+ account = service_prod .product_tmpl_id .get_product_accounts (
244+ self .fiscal_position_id
245+ )["income" ]
246+ line_vals = {
247+ "partner_id" : self .partner_id .id ,
248+ "product_id" : service_prod .id ,
249+ "move_id" : self .id ,
250+ "price_unit" : self .invoice_payment_term_id .riba_payment_cost ,
251+ "due_cost_line" : True ,
252+ "account_id" : account .id ,
253+ "sequence" : 9999 ,
254+ }
255+ if pay_date :
256+ line_vals ["name" ] = self .env ._ ("{line_name} for {month}-{year}" ).format (
257+ line_name = service_prod .name ,
258+ month = pay_date .month ,
259+ year = pay_date .year ,
260+ )
261+ if self .company_id .due_cost_service_id .taxes_id :
262+ tax = self .fiscal_position_id .map_tax (service_prod .taxes_id )
263+ line_vals ["tax_ids" ] = [(4 , tax .id )]
264+ return line_vals
265+
266+ def _add_riba_expense_line (self , pay_date = None ):
267+ """Add a RiBa collection fees line to the invoice."""
268+ self .ensure_one ()
269+ line_vals = self ._get_riba_expense_line_vals (pay_date )
270+ self .write ({"invoice_line_ids" : [(0 , 0 , line_vals )]})
271+
272+ def _apply_riba_collection_fees (self ):
273+ """
274+ Apply collection fees based on partner's riba_policy_expenses.
275+ """
276+ self .ensure_one ()
277+
278+ # Get existing move lines with RiBa expenses for this partner.
279+ # Use commercial_partner_id because receivable lines store that instead
280+ # of the invoice partner (which may be a contact). Include draft state
281+ # so that invoices being posted in the same batch are also considered.
282+ move_line = self .env ["account.move.line" ].search (
283+ [
284+ (
285+ "partner_id" ,
286+ "=" ,
287+ self .partner_id .commercial_partner_id .id ,
288+ ),
289+ ("move_id.invoice_payment_term_id.riba" , "=" , True ),
290+ ("move_id.state" , "in" , ("posted" , "draft" )),
291+ ("move_id.id" , "!=" , self .id ),
292+ ]
293+ )
294+ move_line = move_line .filtered (
295+ lambda line : any (
296+ inv_line .due_cost_line for inv_line in line .move_id .invoice_line_ids
297+ )
298+ )
299+ move_line = move_line .filtered (lambda line : line .date_maturity is not False )
300+ move_line = move_line .sorted (key = lambda r : r .date_maturity )
301+
302+ previous_date_due = move_line .mapped ("date_maturity" )
303+ all_invoice_date = move_line .mapped ("invoice_date" )
304+
305+ # Compute payment term dates
306+ pterm_list = self .invoice_payment_term_id ._compute_terms (
307+ date_ref = self .invoice_date or self .date or fields .Date .context_today (self ),
308+ currency = self .currency_id ,
309+ company = self .company_id ,
310+ tax_amount = 1 ,
311+ tax_amount_currency = 1 ,
312+ untaxed_amount = 0 ,
313+ sign = 1 if self .is_inbound (include_receipts = True ) else - 1 ,
314+ untaxed_amount_currency = self .amount_untaxed ,
315+ )
316+
317+ policy = self .partner_id .riba_policy_expenses
318+
319+ if policy == "one_per_invoice" :
320+ # One expense per invoice, no date in description
321+ self ._add_riba_expense_line ()
322+ elif policy == "unlimited" :
323+ # One expense for each due date, no checks
324+ for pay_date in pterm_list ["line_ids" ]:
325+ self ._add_riba_expense_line (pay_date ["date" ])
326+ elif policy == "one_a_maturity" :
327+ # One expense per maturity date, skip if date already exists
328+ for pay_date in pterm_list ["line_ids" ]:
329+ if not self .maturity_check (pay_date ["date" ], previous_date_due ):
330+ self ._add_riba_expense_line (pay_date ["date" ])
331+ else :
332+ # Default: one_a_month - one expense per month
333+ if not self .month_check (all_invoice_date ):
334+ pay_date = pterm_list ["line_ids" ][0 ]
335+ self ._add_riba_expense_line (pay_date ["date" ])
336+
337+ # Recompute invoice taxes
338+ self ._sync_dynamic_lines (container = {"records" : self , "self" : self })
339+
213340 def action_post (self ):
214341 for invoice in self :
215- # ---- Add a line with collection fees for each due date only for first due
216- # ---- date of the month
342+ # Check if collection fees should be applied
217343 if (
218344 invoice .move_type != "out_invoice"
219345 or not invoice .invoice_payment_term_id
220346 or not invoice .invoice_payment_term_id .riba
221347 or invoice .invoice_payment_term_id .riba_payment_cost == 0.0
348+ or invoice .partner_id .commercial_partner_id .riba_exclude_expenses
222349 ):
223350 continue
224351 if not invoice .company_id .due_cost_service_id :
225352 raise UserError (
226353 self .env ._ ("Set a Service for Collection Fees in Company Config." )
227354 )
228- # ---- Apply Collection Fees on invoice only on first due date of the month
229- # ---- Get Date of first due date
230- move_line = self .env ["account.move.line" ].search (
231- [("partner_id" , "=" , invoice .partner_id .id )]
232- )
233- if not any (line .due_cost_line for line in move_line ):
234- move_line = self .env ["account.move.line" ]
235- # ---- Filtered recordset with date_maturity
236- move_line = move_line .filtered (lambda line : line .date_maturity is not False )
237- # ---- Sorted
238- move_line = move_line .sorted (key = lambda r : r .date_maturity )
239- # ---- Get date
240- previous_date_due = move_line .mapped ("date_maturity" )
241- pterm = self .env ["account.payment.term" ].browse (
242- self .invoice_payment_term_id .id
243- )
244- pterm_list = pterm ._compute_terms (
245- date_ref = self .invoice_date ,
246- currency = self .currency_id ,
247- company = self .company_id ,
248- tax_amount = 1 ,
249- tax_amount_currency = 1 ,
250- untaxed_amount = 0 ,
251- untaxed_amount_currency = 0 ,
252- sign = 1 ,
253- )
355+ invoice ._apply_riba_collection_fees ()
254356
255- for pay_date in pterm_list ["line_ids" ]:
256- if not self .month_check (pay_date ["date" ], previous_date_due ):
257- # ---- Get Line values for service product
258- service_prod = invoice .company_id .due_cost_service_id
259- account = service_prod .product_tmpl_id .get_product_accounts (
260- invoice .fiscal_position_id
261- )["income" ]
262- line_vals = {
263- "partner_id" : invoice .partner_id .id ,
264- "product_id" : service_prod .id ,
265- "move_id" : invoice .id ,
266- "price_unit" : (
267- invoice .invoice_payment_term_id .riba_payment_cost
268- ),
269- "due_cost_line" : True ,
270- "name" : self .env ._ ("{line_name} for {month}-{year}" ).format (
271- line_name = service_prod .name ,
272- month = pay_date ["date" ].month ,
273- year = pay_date ["date" ].year ,
274- ),
275- "account_id" : account .id ,
276- "sequence" : 9999 ,
277- }
278- # ---- Update Line Value with tax if is set on product
279- if invoice .company_id .due_cost_service_id .taxes_id :
280- tax = invoice .fiscal_position_id .map_tax (service_prod .taxes_id )
281- line_vals .update ({"tax_ids" : [(4 , tax .id )]})
282- invoice .write ({"invoice_line_ids" : [(0 , 0 , line_vals )]})
283- # ---- recompute invoice taxes
284- invoice ._sync_dynamic_lines (
285- container = {"records" : invoice , "self" : invoice }
286- )
287357 res = super ().action_post ()
288358
289359 # Automatic reconciliation for RiBa credit moves
0 commit comments