fix the splitting journal entry if there are discount product
This commit is contained in:
parent
074a918b82
commit
116b132b30
80
CONFIGURATION.md
Normal file
80
CONFIGURATION.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Split Pendapatan Payment - Configuration Guide
|
||||
|
||||
## Issue Fixed
|
||||
|
||||
The module had a logic issue where it wasn't properly applying income accounts from payment methods for regular (non-discount) products when a discount product was configured in the POS.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The original code used `elif` logic which meant:
|
||||
- IF discount product exists AND current sale is for discount product → use discount_account_id
|
||||
- ELIF discount product exists BUT current sale is NOT discount → use income_account_id
|
||||
- However, the ELIF was actually inside the discount check, causing it to skip for regular products
|
||||
|
||||
## Solution Applied
|
||||
|
||||
Fixed the logic flow to properly check:
|
||||
1. Is this a discount product sale? → Use `discount_account_id` from payment method (or product default)
|
||||
2. Is this a regular product sale? → Use `income_account_id` from payment method (or product default)
|
||||
3. No discount product configured? → Use `income_account_id` from payment method (or product default)
|
||||
|
||||
## Required Configuration
|
||||
|
||||
To use this module properly, you MUST configure the income accounts in your payment methods:
|
||||
|
||||
### Step 1: Configure Payment Method Income Accounts
|
||||
|
||||
1. Go to **Point of Sale → Configuration → Payment Methods**
|
||||
2. For each payment method (Cash, Bank, Credit Card, etc.), open the form
|
||||
3. Fill in the following fields:
|
||||
- **Income Account**: The account to use for regular product sales with this payment method
|
||||
- **Discount Account**: The account to use for discount product sales with this payment method (if you use POS discounts)
|
||||
|
||||
### Step 2: Important Notes
|
||||
|
||||
- If you don't configure these fields, the module will fall back to using the product's default income account
|
||||
- For discount products, if `discount_account_id` is not set in the payment method, it will use the discount product's configured account
|
||||
- For regular products, if `income_account_id` is not set in the payment method, it will use the product's configured income account (from product or product category)
|
||||
|
||||
### Example Configuration
|
||||
|
||||
**Cash Payment Method:**
|
||||
- Income Account: 4000 - Sales Revenue (Cash)
|
||||
- Discount Account: 4100 - Sales Discounts (Cash)
|
||||
|
||||
**Bank Payment Method:**
|
||||
- Income Account: 4010 - Sales Revenue (Bank)
|
||||
- Discount Account: 4110 - Sales Discounts (Bank)
|
||||
|
||||
**Credit Card Payment Method:**
|
||||
- Income Account: 4020 - Sales Revenue (Credit Card)
|
||||
- Discount Account: 4120 - Sales Discounts (Credit Card)
|
||||
|
||||
## Error Messages
|
||||
|
||||
If you see an error saying "need to define the income account at product level", it means:
|
||||
1. The payment method doesn't have `income_account_id` configured, AND
|
||||
2. The product doesn't have a valid income account defined
|
||||
|
||||
**Solution**: Configure the income account in either:
|
||||
- The payment method form (recommended), OR
|
||||
- The product form or product category
|
||||
|
||||
## Diagnostic Logging
|
||||
|
||||
The module now includes detailed logging to help diagnose issues. Check your Odoo logs for entries starting with `=== SPLIT PENDAPATAN DEBUG ===` to see:
|
||||
- Which payment method is being processed
|
||||
- What accounts are configured
|
||||
- Which account is being used for each sale line
|
||||
|
||||
## Module Behavior
|
||||
|
||||
This module splits income journal entries by payment method. For example, if you have a sale with:
|
||||
- Total: 100,000
|
||||
- Payment: 60,000 Cash + 40,000 Bank
|
||||
|
||||
Instead of one income line of 100,000, you'll get:
|
||||
- Income (Cash): 60,000
|
||||
- Income (Bank): 40,000
|
||||
|
||||
This allows you to track income separately by payment method for better financial reporting.
|
||||
@ -3,115 +3,240 @@ from collections import defaultdict
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PosSession(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
def _prepare_line(self, order_line):
|
||||
"""Override to allow products without income accounts when payment methods have them configured"""
|
||||
def get_income_account(order_line):
|
||||
product = order_line.product_id
|
||||
income_account = product.with_company(order_line.company_id)._get_product_accounts()['income'] or self.config_id.journal_id.default_account_id
|
||||
|
||||
# NEW: If no income account is found on the product, check if payment methods have income accounts configured
|
||||
if not income_account:
|
||||
# First look at payment methods actually used on the order
|
||||
payment_methods_used = order_line.order_id.payment_ids.mapped('payment_method_id').filtered('income_account_id')
|
||||
payment_methods_with_income = payment_methods_used or self.payment_method_ids.filtered('income_account_id')
|
||||
payment_income_account = payment_methods_with_income[:1].income_account_id
|
||||
|
||||
if payment_income_account:
|
||||
_logger.info(
|
||||
"Product '%s' (id:%s) has no income account. "
|
||||
"Using payment method '%s' income account %s as fallback.",
|
||||
product.name, product.id,
|
||||
payment_methods_with_income[:1].name,
|
||||
payment_income_account.display_name
|
||||
)
|
||||
income_account = payment_income_account
|
||||
else:
|
||||
_logger.warning(
|
||||
"Product '%s' (id:%s) has no income account and no payment methods provide an income account.",
|
||||
product.name, product.id
|
||||
)
|
||||
raise UserError(_('Please define income account for this product: "%s" (id:%d).\n'
|
||||
'Or configure the Income Account in your payment methods.',
|
||||
product.name, product.id))
|
||||
|
||||
return order_line.order_id.fiscal_position_id.map_account(income_account)
|
||||
|
||||
company_domain = self.env['account.tax']._check_company_domain(order_line.order_id.company_id)
|
||||
tax_ids = order_line.tax_ids_after_fiscal_position.filtered_domain(company_domain)
|
||||
sign = -1 if order_line.qty >= 0 else 1
|
||||
price = sign * order_line.price_unit * (1 - (order_line.discount or 0.0) / 100.0)
|
||||
check_refund = lambda x: x.qty * x.price_unit < 0
|
||||
is_refund = check_refund(order_line)
|
||||
tax_data = tax_ids.compute_all(price_unit=price, quantity=abs(order_line.qty), currency=self.currency_id, is_refund=is_refund, fixed_multiplicator=sign, include_caba_tags=True)
|
||||
date_order = order_line.order_id.date_order
|
||||
taxes = [{'date_order': date_order, **tax} for tax in tax_data['taxes']]
|
||||
return {
|
||||
'date_order': order_line.order_id.date_order,
|
||||
'income_account_id': get_income_account(order_line).id,
|
||||
'amount': order_line.price_subtotal,
|
||||
'taxes': taxes,
|
||||
'base_tags': tuple(tax_data['base_tags']),
|
||||
}
|
||||
|
||||
def _accumulate_amounts(self, data):
|
||||
# Call the original method to get all the standard accumulations
|
||||
"""Distribute sales amounts per payment method while respecting discount accounts."""
|
||||
data = super(PosSession, self)._accumulate_amounts(data)
|
||||
|
||||
# Get all orders in this session
|
||||
closed_orders = self._get_closed_orders()
|
||||
|
||||
# If no orders, return original data
|
||||
if not closed_orders:
|
||||
return data
|
||||
|
||||
# Get the original sales data
|
||||
sales = data.get('sales', {})
|
||||
if not sales:
|
||||
return data
|
||||
|
||||
# Create new sales data structure split by payment method
|
||||
split_sales = defaultdict(lambda: {'amount': 0.0, 'amount_converted': 0.0, 'tax_amount': 0.0})
|
||||
|
||||
# Get discount product ID from config
|
||||
discount_product_id = self.config_id.discount_product_id.id if self.config_id.discount_product_id else None
|
||||
|
||||
# For each sale entry, we need to distribute it across payment methods
|
||||
# Build per-order breakdown of sales keyed by (account, sign, tax tuple, tags)
|
||||
order_payment_totals = {}
|
||||
order_sales_breakdown = {}
|
||||
for order in closed_orders:
|
||||
order_payment_totals[order.id] = sum(payment.amount for payment in order.payment_ids)
|
||||
if order.is_invoiced:
|
||||
continue
|
||||
|
||||
sale_map = {}
|
||||
for order_line in order.lines:
|
||||
line_vals = self._prepare_line(order_line)
|
||||
sale_key = (
|
||||
line_vals['income_account_id'],
|
||||
-1 if line_vals['amount'] < 0 else 1,
|
||||
tuple((tax['id'], tax['account_id'], tax['tax_repartition_line_id']) for tax in line_vals['taxes']),
|
||||
line_vals['base_tags'],
|
||||
)
|
||||
entry = sale_map.setdefault(sale_key, {
|
||||
'amount': 0.0,
|
||||
'amount_converted': 0.0,
|
||||
'tax_amount': 0.0,
|
||||
'regular_amount': 0.0,
|
||||
'regular_amount_converted': 0.0,
|
||||
'regular_tax_amount': 0.0,
|
||||
'discount_amount': 0.0,
|
||||
'discount_amount_converted': 0.0,
|
||||
'discount_tax_amount': 0.0,
|
||||
})
|
||||
|
||||
prev_amount = entry['amount']
|
||||
prev_amount_converted = entry['amount_converted']
|
||||
updated_amounts = self._update_amounts(
|
||||
{'amount': prev_amount, 'amount_converted': prev_amount_converted},
|
||||
{'amount': line_vals['amount']},
|
||||
order.date_order,
|
||||
round=False,
|
||||
)
|
||||
line_amount_converted = updated_amounts['amount_converted'] - prev_amount_converted
|
||||
line_tax_amount = sum(tax['amount'] for tax in line_vals['taxes'])
|
||||
|
||||
entry['amount'] = updated_amounts['amount']
|
||||
entry['amount_converted'] = updated_amounts['amount_converted']
|
||||
entry['tax_amount'] += line_tax_amount
|
||||
|
||||
is_reward_line = bool(getattr(order_line, 'is_reward_line', False))
|
||||
has_reward = bool(getattr(order_line, 'reward_id', False))
|
||||
is_discount_product = bool(discount_product_id and order_line.product_id.id == discount_product_id)
|
||||
is_discount_line = is_reward_line or has_reward or is_discount_product
|
||||
|
||||
if is_discount_line:
|
||||
entry['discount_amount'] += line_vals['amount']
|
||||
entry['discount_amount_converted'] += line_amount_converted
|
||||
entry['discount_tax_amount'] += line_tax_amount
|
||||
else:
|
||||
entry['regular_amount'] += line_vals['amount']
|
||||
entry['regular_amount_converted'] += line_amount_converted
|
||||
entry['regular_tax_amount'] += line_tax_amount
|
||||
|
||||
if sale_map:
|
||||
order_sales_breakdown[order.id] = sale_map
|
||||
|
||||
split_sales = defaultdict(lambda: {'amount': 0.0, 'amount_converted': 0.0, 'tax_amount': 0.0})
|
||||
|
||||
for sale_key, sale_amounts in sales.items():
|
||||
# Skip if this is a tax key (we only want to split actual sales)
|
||||
if len(sale_key) < 4: # Not a standard sales key
|
||||
if len(sale_key) < 4:
|
||||
continue
|
||||
|
||||
total_amount = sale_amounts['amount']
|
||||
total_amount_converted = sale_amounts['amount_converted']
|
||||
tax_amount = sale_amounts.get('tax_amount', 0.0) # Get tax amount if it exists
|
||||
|
||||
if float_is_zero(total_amount, precision_rounding=self.currency_id.rounding):
|
||||
continue
|
||||
|
||||
# Distribute this sales amount across all orders based on their payment methods
|
||||
total_payment_amount = sum(sum(payment.amount for payment in order.payment_ids) for order in closed_orders)
|
||||
|
||||
if float_is_zero(total_payment_amount, precision_rounding=self.currency_id.rounding):
|
||||
continue
|
||||
|
||||
# Distribute the sales amount across all orders proportionally to their payments
|
||||
for order in closed_orders:
|
||||
if order.is_invoiced:
|
||||
continue # Skip invoiced orders
|
||||
continue
|
||||
|
||||
order_payments = order.payment_ids
|
||||
order_payment_total = sum(payment.amount for payment in order_payments)
|
||||
order_sale_map = order_sales_breakdown.get(order.id)
|
||||
if not order_sale_map:
|
||||
continue
|
||||
|
||||
order_sale_amounts = order_sale_map.get(sale_key)
|
||||
if not order_sale_amounts:
|
||||
continue
|
||||
|
||||
order_payment_total = order_payment_totals.get(order.id, 0.0)
|
||||
if float_is_zero(order_payment_total, precision_rounding=order.currency_id.rounding):
|
||||
continue
|
||||
|
||||
# For each payment in this order, create a split sales entry
|
||||
for payment in order_payments:
|
||||
# Calculate the proportion of this payment relative to all payments
|
||||
payment_proportion = payment.amount / total_payment_amount
|
||||
payment_amount = total_amount * payment_proportion
|
||||
payment_amount_converted = total_amount_converted * payment_proportion
|
||||
payment_tax_amount = tax_amount * payment_proportion
|
||||
|
||||
if float_is_zero(payment_amount, precision_rounding=self.currency_id.rounding):
|
||||
order_amount = order_sale_amounts['amount']
|
||||
order_amount_converted = order_sale_amounts['amount_converted']
|
||||
order_tax_amount = order_sale_amounts['tax_amount']
|
||||
if float_is_zero(order_amount, precision_rounding=self.currency_id.rounding):
|
||||
continue
|
||||
|
||||
# Determine the account to use based on whether this is a discount product
|
||||
income_account_id = sale_key[0] # default account
|
||||
for payment in order.payment_ids:
|
||||
if float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding):
|
||||
continue
|
||||
|
||||
# Check if this sale key corresponds to a discount product
|
||||
# We need to check if the account in the sale_key matches the discount product account
|
||||
if discount_product_id:
|
||||
# Get the discount product's income account
|
||||
discount_product = self.env['product.product'].browse(discount_product_id)
|
||||
discount_account = discount_product._get_product_accounts()['income']
|
||||
if discount_account and discount_account.id == sale_key[0]:
|
||||
# This is a discount product, use discount account if configured
|
||||
payment_proportion = payment.amount / order_payment_total
|
||||
|
||||
net_amount = order_amount * payment_proportion
|
||||
net_amount_converted = order_amount_converted * payment_proportion
|
||||
net_tax_amount = order_tax_amount * payment_proportion
|
||||
|
||||
regular_part_amount = order_sale_amounts['regular_amount'] * payment_proportion
|
||||
discount_part_amount = order_sale_amounts['discount_amount'] * payment_proportion
|
||||
regular_part_amount_converted = order_sale_amounts['regular_amount_converted'] * payment_proportion
|
||||
discount_part_amount_converted = order_sale_amounts['discount_amount_converted'] * payment_proportion
|
||||
regular_part_tax = order_sale_amounts['regular_tax_amount'] * payment_proportion
|
||||
discount_part_tax = order_sale_amounts['discount_tax_amount'] * payment_proportion
|
||||
|
||||
residual_amount = net_amount - (regular_part_amount + discount_part_amount)
|
||||
residual_amount_converted = net_amount_converted - (regular_part_amount_converted + discount_part_amount_converted)
|
||||
residual_tax = net_tax_amount - (regular_part_tax + discount_part_tax)
|
||||
|
||||
if abs(regular_part_amount) >= abs(discount_part_amount):
|
||||
regular_part_amount += residual_amount
|
||||
regular_part_amount_converted += residual_amount_converted
|
||||
regular_part_tax += residual_tax
|
||||
else:
|
||||
discount_part_amount += residual_amount
|
||||
discount_part_amount_converted += residual_amount_converted
|
||||
discount_part_tax += residual_tax
|
||||
|
||||
def add_split_entry(part_amount, part_amount_converted, part_tax_amount, is_discount_part):
|
||||
if float_is_zero(part_amount, precision_rounding=self.currency_id.rounding):
|
||||
return
|
||||
|
||||
target_account_id = sale_key[0]
|
||||
if is_discount_part:
|
||||
if payment.payment_method_id.discount_account_id:
|
||||
income_account_id = payment.payment_method_id.discount_account_id.id
|
||||
target_account_id = payment.payment_method_id.discount_account_id.id
|
||||
elif payment.payment_method_id.income_account_id:
|
||||
# This is a regular product, use income account if configured
|
||||
income_account_id = payment.payment_method_id.income_account_id.id
|
||||
target_account_id = payment.payment_method_id.income_account_id.id
|
||||
_logger.warning(
|
||||
"Payment method %s has no discount account configured; using income account instead for discount lines.",
|
||||
payment.payment_method_id.name,
|
||||
)
|
||||
else:
|
||||
if payment.payment_method_id.income_account_id:
|
||||
target_account_id = payment.payment_method_id.income_account_id.id
|
||||
|
||||
# Ensure we have a valid account ID
|
||||
if not income_account_id:
|
||||
continue
|
||||
if not target_account_id:
|
||||
raise UserError(_(
|
||||
'No income account found for payment method "%s".\n'
|
||||
'Please configure the Income Account in the payment method settings, '
|
||||
'or ensure the product has a valid income account defined.'
|
||||
) % payment.payment_method_id.name)
|
||||
|
||||
# Create a new key that includes the payment method
|
||||
new_sale_key = (
|
||||
# account (use payment method account if specified)
|
||||
income_account_id,
|
||||
# sign (same as original)
|
||||
target_account_id,
|
||||
sale_key[1],
|
||||
# payment method
|
||||
payment.payment_method_id.id,
|
||||
# for taxes (same as original)
|
||||
sale_key[2],
|
||||
# base tags (same as original)
|
||||
sale_key[3],
|
||||
)
|
||||
split_sales[new_sale_key]['amount'] += part_amount
|
||||
split_sales[new_sale_key]['amount_converted'] += part_amount_converted
|
||||
split_sales[new_sale_key]['tax_amount'] += part_tax_amount
|
||||
|
||||
# Update the split sales data
|
||||
split_sales[new_sale_key]['amount'] += payment_amount
|
||||
split_sales[new_sale_key]['amount_converted'] += payment_amount_converted
|
||||
split_sales[new_sale_key]['tax_amount'] += payment_tax_amount
|
||||
add_split_entry(regular_part_amount, regular_part_amount_converted, regular_part_tax, False)
|
||||
add_split_entry(discount_part_amount, discount_part_amount_converted, discount_part_tax, True)
|
||||
|
||||
# Replace the original sales data with our split sales data
|
||||
data['sales'] = split_sales
|
||||
return data
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user