@@ -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,120 @@ 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+ move_line = self .env ["account.move.line" ].search (
280+ [
281+ ("partner_id" , "=" , self .partner_id .id ),
282+ ("move_id.invoice_payment_term_id.riba" , "=" , True ),
283+ ("move_id.state" , "=" , "posted" ),
284+ ]
285+ )
286+ move_line = move_line .filtered (
287+ lambda line : any (
288+ inv_line .due_cost_line for inv_line in line .move_id .invoice_line_ids
289+ )
290+ )
291+ move_line = move_line .filtered (lambda line : line .date_maturity is not False )
292+ move_line = move_line .sorted (key = lambda r : r .date_maturity )
293+
294+ previous_date_due = move_line .mapped ("date_maturity" )
295+ all_invoice_date = move_line .mapped ("invoice_date" )
296+
297+ # Compute payment term dates
298+ pterm_list = self .invoice_payment_term_id ._compute_terms (
299+ date_ref = self .invoice_date or self .date or fields .Date .context_today (self ),
300+ currency = self .currency_id ,
301+ company = self .company_id ,
302+ tax_amount = 1 ,
303+ tax_amount_currency = 1 ,
304+ untaxed_amount = 0 ,
305+ sign = 1 if self .is_inbound (include_receipts = True ) else - 1 ,
306+ untaxed_amount_currency = self .amount_untaxed ,
307+ )
308+
309+ policy = self .partner_id .riba_policy_expenses
310+
311+ if policy == "one_per_invoice" :
312+ # One expense per invoice, no date in description
313+ self ._add_riba_expense_line ()
314+ elif policy == "unlimited" :
315+ # One expense for each due date, no checks
316+ for pay_date in pterm_list ["line_ids" ]:
317+ self ._add_riba_expense_line (pay_date ["date" ])
318+ elif policy == "one_a_maturity" :
319+ # One expense per maturity date, skip if date already exists
320+ for pay_date in pterm_list ["line_ids" ]:
321+ if not self .maturity_check (pay_date ["date" ], previous_date_due ):
322+ self ._add_riba_expense_line (pay_date ["date" ])
323+ else :
324+ # Default: one_a_month - one expense per month
325+ if not self .month_check (all_invoice_date ):
326+ pay_date = pterm_list ["line_ids" ][0 ]
327+ self ._add_riba_expense_line (pay_date ["date" ])
328+
329+ # Recompute invoice taxes
330+ self ._sync_dynamic_lines (container = {"records" : self , "self" : self })
331+
213332 def action_post (self ):
214333 for invoice in self :
215- # ---- Add a line with collection fees for each due date only for first due
216- # ---- date of the month
334+ # Check if collection fees should be applied
217335 if (
218336 invoice .move_type != "out_invoice"
219337 or not invoice .invoice_payment_term_id
220338 or not invoice .invoice_payment_term_id .riba
221339 or invoice .invoice_payment_term_id .riba_payment_cost == 0.0
340+ or invoice .partner_id .commercial_partner_id .riba_exclude_expenses
222341 ):
223342 continue
224343 if not invoice .company_id .due_cost_service_id :
225344 raise UserError (
226345 self .env ._ ("Set a Service for Collection Fees in Company Config." )
227346 )
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- )
347+ invoice ._apply_riba_collection_fees ()
254348
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- )
287349 res = super ().action_post ()
288350
289351 # Automatic reconciliation for RiBa credit moves
0 commit comments