diff --git a/.gitignore b/.gitignore
index 71f52ea..a6db1f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,140 +1,140 @@
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-pip-wheel-metadata/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-build/
-temp/
-
-# PyBuilder
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-.python-version
-
-# pipenv
-Pipfile.lock
-
-# poetry
-poetry.lock
-
-# PEP 582; used by python-next
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# VS Code settings
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+build/
+temp/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+Pipfile.lock
+
+# poetry
+poetry.lock
+
+# PEP 582; used by python-next
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# VS Code settings
.vscode/
\ No newline at end of file
diff --git a/CONFIGURATION.md b/CONFIGURATION.md
new file mode 100644
index 0000000..e44976c
--- /dev/null
+++ b/CONFIGURATION.md
@@ -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.
\ No newline at end of file
diff --git a/README.md b/README.md
index 31deecd..dd1250b 100644
--- a/README.md
+++ b/README.md
@@ -1,51 +1,51 @@
-# Split Pendapatan Payment
-
-This module modifies Point of Sale transactions to split income/pendapatan journal entries per payment method used in the transaction.
-
-## Overview
-
-In standard Odoo Point of Sale, all sales income is recorded in a single journal entry line regardless of the payment methods used. This module changes that behavior by creating separate income lines for each payment method used in the transactions.
-
-For example, if a session has sales paid with both cash and credit card, instead of creating one income line for all sales, the module will create separate income lines:
-- Sales - Cash
-- Sales - Credit Card
-
-## Features
-
-- Splits income journal entries by payment method
-- Maintains proper accounting reconciliation
-- Works with all payment methods (cash, bank transfers, credit cards, etc.)
-- Preserves tax calculations and reporting
-- Allows linking specific income accounts to payment methods
-
-## Configuration
-
-### Payment Method Configuration
-1. Go to Point of Sale > Configuration > Payment Methods
-2. Edit a payment method
-3. In the Accounting section, set the "Income Account" field to specify which account should be used for sales paid with this method
-4. If left empty, the default product income account will be used
-
-## How it works
-
-The module inherits from the `pos.session` model and overrides two key methods:
-
-1. `_accumulate_amounts()` - Modifies how sales amounts are accumulated to include payment method information and use specific income accounts
-2. `_get_sale_vals()` - Customizes the journal entry line descriptions to include payment method names
-
-The module also extends the `pos.payment.method` model to add an "Income Account" field that allows you to specify which account should be used for sales paid with each payment method.
-
-## Installation
-
-1. Place the module in your Odoo addons directory
-2. Update the Apps list in Odoo
-3. Install the "Split Pendapatan Payment" module
-
-## Compatibility
-
-This module is designed for Odoo 17.0 and is compatible with the standard Point of Sale module.
-
-## Limitations
-
-- Only affects non-invoiced POS orders (invoiced orders follow standard Odoo accounting)
-- May require adjustments if used with other POS customization modules that also modify journal entries
+# Split Pendapatan Payment
+
+This module modifies Point of Sale transactions to split income/pendapatan journal entries per payment method used in the transaction.
+
+## Overview
+
+In standard Odoo Point of Sale, all sales income is recorded in a single journal entry line regardless of the payment methods used. This module changes that behavior by creating separate income lines for each payment method used in the transactions.
+
+For example, if a session has sales paid with both cash and credit card, instead of creating one income line for all sales, the module will create separate income lines:
+- Sales - Cash
+- Sales - Credit Card
+
+## Features
+
+- Splits income journal entries by payment method
+- Maintains proper accounting reconciliation
+- Works with all payment methods (cash, bank transfers, credit cards, etc.)
+- Preserves tax calculations and reporting
+- Allows linking specific income accounts to payment methods
+
+## Configuration
+
+### Payment Method Configuration
+1. Go to Point of Sale > Configuration > Payment Methods
+2. Edit a payment method
+3. In the Accounting section, set the "Income Account" field to specify which account should be used for sales paid with this method
+4. If left empty, the default product income account will be used
+
+## How it works
+
+The module inherits from the `pos.session` model and overrides two key methods:
+
+1. `_accumulate_amounts()` - Modifies how sales amounts are accumulated to include payment method information and use specific income accounts
+2. `_get_sale_vals()` - Customizes the journal entry line descriptions to include payment method names
+
+The module also extends the `pos.payment.method` model to add an "Income Account" field that allows you to specify which account should be used for sales paid with each payment method.
+
+## Installation
+
+1. Place the module in your Odoo addons directory
+2. Update the Apps list in Odoo
+3. Install the "Split Pendapatan Payment" module
+
+## Compatibility
+
+This module is designed for Odoo 17.0 and is compatible with the standard Point of Sale module.
+
+## Limitations
+
+- Only affects non-invoiced POS orders (invoiced orders follow standard Odoo accounting)
+- May require adjustments if used with other POS customization modules that also modify journal entries
diff --git a/__init__.py b/__init__.py
index cde864b..03c48c1 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,3 +1,3 @@
-# -*- coding: utf-8 -*-
-
-from . import models
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/__manifest__.py b/__manifest__.py
index 660442f..b316548 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -1,37 +1,37 @@
-# -*- coding: utf-8 -*-
-{
- 'name': "Split Pendapatan Payment",
-
- 'summary': "Split income journal entries per payment method in Point of Sale transactions",
-
- 'description': """
- This module modifies Point of Sale transactions to split income/pendapatan
- journal entries per payment method used in the transaction. Instead of
- creating a single income line for all sales, separate income lines are
- created for each payment method.
- """,
-
- 'author': "Suherdy Yacob",
-
- # Categories can be used to filter modules in modules listing
- 'category': 'Point of Sale',
- 'version': '17.0.1.0.0',
-
- # any module necessary for this one to work correctly
- 'depends': ['point_of_sale', 'pos_discount'],
-
- # always loaded
- 'data': [
- # 'security/ir.model.access.csv',
- 'views/pos_session_views.xml',
- 'views/pos_payment_method_views.xml',
- ],
- # only loaded in demonstration mode
- 'demo': [
- # 'demo/demo.xml',
- ],
- 'installable': True,
- 'application': False,
- 'auto_install': False,
- 'license': 'LGPL-3',
-}
+# -*- coding: utf-8 -*-
+{
+ 'name': "Split Pendapatan Payment",
+
+ 'summary': "Split income journal entries per payment method in Point of Sale transactions",
+
+ 'description': """
+ This module modifies Point of Sale transactions to split income/pendapatan
+ journal entries per payment method used in the transaction. Instead of
+ creating a single income line for all sales, separate income lines are
+ created for each payment method.
+ """,
+
+ 'author': "Suherdy Yacob",
+
+ # Categories can be used to filter modules in modules listing
+ 'category': 'Point of Sale',
+ 'version': '17.0.1.0.0',
+
+ # any module necessary for this one to work correctly
+ 'depends': ['point_of_sale', 'pos_discount'],
+
+ # always loaded
+ 'data': [
+ # 'security/ir.model.access.csv',
+ 'views/pos_session_views.xml',
+ 'views/pos_payment_method_views.xml',
+ ],
+ # only loaded in demonstration mode
+ 'demo': [
+ # 'demo/demo.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+ 'license': 'LGPL-3',
+}
diff --git a/models/__init__.py b/models/__init__.py
index f27bfcb..12f93ab 100644
--- a/models/__init__.py
+++ b/models/__init__.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
-
-from . import pos_session
-from . import pos_payment_method
+# -*- coding: utf-8 -*-
+
+from . import pos_session
+from . import pos_payment_method
diff --git a/models/pos_payment_method.py b/models/pos_payment_method.py
index fec32d2..6868e25 100644
--- a/models/pos_payment_method.py
+++ b/models/pos_payment_method.py
@@ -1,20 +1,20 @@
-# -*- coding: utf-8 -*-
-from odoo import models, fields, api
-
-
-class PosPaymentMethod(models.Model):
- _inherit = 'pos.payment.method'
-
- income_account_id = fields.Many2one(
- 'account.account',
- string='Income Account',
- domain=[('deprecated', '=', False)],
- help='Account used for income lines when splitting by payment method. '
- 'If empty, the default income account from the product will be used.')
-
- discount_account_id = fields.Many2one(
- 'account.account',
- string='Discount Account',
- domain=[('deprecated', '=', False)],
- help='Account used for discount product lines when splitting by payment method. '
- 'If empty, the default discount account from the product will be used.')
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api
+
+
+class PosPaymentMethod(models.Model):
+ _inherit = 'pos.payment.method'
+
+ income_account_id = fields.Many2one(
+ 'account.account',
+ string='Income Account',
+ domain=[('deprecated', '=', False)],
+ help='Account used for income lines when splitting by payment method. '
+ 'If empty, the default income account from the product will be used.')
+
+ discount_account_id = fields.Many2one(
+ 'account.account',
+ string='Discount Account',
+ domain=[('deprecated', '=', False)],
+ help='Account used for discount product lines when splitting by payment method. '
+ 'If empty, the default discount account from the product will be used.')
diff --git a/models/pos_session.py b/models/pos_session.py
index c6684bc..a229a9c 100644
--- a/models/pos_session.py
+++ b/models/pos_session.py
@@ -1,155 +1,280 @@
-# -*- coding: utf-8 -*-
-from collections import defaultdict
-from odoo import models, fields, api, _
-from odoo.exceptions import UserError
-from odoo.tools import float_is_zero, float_compare
-
-
-class PosSession(models.Model):
- _inherit = 'pos.session'
-
- def _accumulate_amounts(self, data):
- # Call the original method to get all the standard accumulations
- 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
- 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
- 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
-
- order_payments = order.payment_ids
- order_payment_total = sum(payment.amount for payment in order_payments)
-
- 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):
- continue
-
- # Determine the account to use based on whether this is a discount product
- income_account_id = sale_key[0] # default account
-
- # 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
- if payment.payment_method_id.discount_account_id:
- income_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
-
- # Ensure we have a valid account ID
- if not income_account_id:
- continue
-
- # 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)
- 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],
- )
-
- # 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
-
- # Replace the original sales data with our split sales data
- data['sales'] = split_sales
- return data
-
- def _get_sale_vals(self, key, amount, amount_converted):
- """ Override to add payment method information to the sales line description """
- # Check if this key includes payment method information
- if len(key) >= 5 and isinstance(key[2], int): # Has payment method ID
- account_id, sign, payment_method_id, tax_keys, base_tag_ids = key
- # Try to get the payment method name
- try:
- payment_method = self.env['pos.payment.method'].browse(payment_method_id)
- payment_method_name = payment_method.name
- except:
- payment_method_name = "Unknown Payment"
- else:
- # Original format
- account_id, sign, tax_keys, base_tag_ids = key
- payment_method_name = None
-
- tax_ids = set(tax[0] for tax in tax_keys) if tax_keys else set()
- applied_taxes = self.env['account.tax'].browse(tax_ids)
- title = _('Sales') if sign == 1 else _('Refund')
-
- # Create name with payment method information
- if payment_method_name:
- name = _('%s - %s', title, payment_method_name)
- if applied_taxes:
- name = _('%s with %s - %s', title, ', '.join([tax.name for tax in applied_taxes]), payment_method_name)
- else:
- name = _('%s untaxed', title)
- if applied_taxes:
- name = _('%s with %s', title, ', '.join([tax.name for tax in applied_taxes]))
-
- partial_vals = {
- 'name': name,
- 'account_id': account_id,
- 'move_id': self.move_id.id,
- 'tax_ids': [(6, 0, tax_ids)],
- 'tax_tag_ids': [(6, 0, base_tag_ids)] if base_tag_ids else [],
- }
- return self._credit_amounts(partial_vals, amount, amount_converted)
+# -*- coding: utf-8 -*-
+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):
+ """Distribute sales amounts per payment method while respecting discount accounts."""
+ data = super(PosSession, self)._accumulate_amounts(data)
+
+ closed_orders = self._get_closed_orders()
+ if not closed_orders:
+ return data
+
+ sales = data.get('sales', {})
+ if not sales:
+ return data
+
+ discount_product_id = self.config_id.discount_product_id.id if self.config_id.discount_product_id else None
+
+ # 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():
+ if len(sale_key) < 4:
+ continue
+
+ total_amount = sale_amounts['amount']
+ if float_is_zero(total_amount, precision_rounding=self.currency_id.rounding):
+ continue
+
+ for order in closed_orders:
+ if order.is_invoiced:
+ continue
+
+ 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
+
+ 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
+
+ for payment in order.payment_ids:
+ if float_is_zero(payment.amount, precision_rounding=order.currency_id.rounding):
+ continue
+
+ 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:
+ target_account_id = payment.payment_method_id.discount_account_id.id
+ elif payment.payment_method_id.income_account_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
+
+ 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)
+
+ new_sale_key = (
+ target_account_id,
+ sale_key[1],
+ payment.payment_method_id.id,
+ sale_key[2],
+ 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
+
+ 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)
+
+ data['sales'] = split_sales
+ return data
+
+ def _get_sale_vals(self, key, amount, amount_converted):
+ """ Override to add payment method information to the sales line description """
+ # Check if this key includes payment method information
+ if len(key) >= 5 and isinstance(key[2], int): # Has payment method ID
+ account_id, sign, payment_method_id, tax_keys, base_tag_ids = key
+ # Try to get the payment method name
+ try:
+ payment_method = self.env['pos.payment.method'].browse(payment_method_id)
+ payment_method_name = payment_method.name
+ except:
+ payment_method_name = "Unknown Payment"
+ else:
+ # Original format
+ account_id, sign, tax_keys, base_tag_ids = key
+ payment_method_name = None
+
+ tax_ids = set(tax[0] for tax in tax_keys) if tax_keys else set()
+ applied_taxes = self.env['account.tax'].browse(tax_ids)
+ title = _('Sales') if sign == 1 else _('Refund')
+
+ # Create name with payment method information
+ if payment_method_name:
+ name = _('%s - %s', title, payment_method_name)
+ if applied_taxes:
+ name = _('%s with %s - %s', title, ', '.join([tax.name for tax in applied_taxes]), payment_method_name)
+ else:
+ name = _('%s untaxed', title)
+ if applied_taxes:
+ name = _('%s with %s', title, ', '.join([tax.name for tax in applied_taxes]))
+
+ partial_vals = {
+ 'name': name,
+ 'account_id': account_id,
+ 'move_id': self.move_id.id,
+ 'tax_ids': [(6, 0, tax_ids)],
+ 'tax_tag_ids': [(6, 0, base_tag_ids)] if base_tag_ids else [],
+ }
+ return self._credit_amounts(partial_vals, amount, amount_converted)
diff --git a/views/pos_payment_method_views.xml b/views/pos_payment_method_views.xml
index 233e18d..7716363 100644
--- a/views/pos_payment_method_views.xml
+++ b/views/pos_payment_method_views.xml
@@ -1,14 +1,14 @@
-
-
-
- pos.payment.method.form.inherit
- pos.payment.method
-
-
-
-
-
-
-
-
-
+
+
+
+ pos.payment.method.form.inherit
+ pos.payment.method
+
+
+
+
+
+
+
+
+
diff --git a/views/pos_session_views.xml b/views/pos_session_views.xml
index 8b82d66..6c61f5e 100644
--- a/views/pos_session_views.xml
+++ b/views/pos_session_views.xml
@@ -1,4 +1,4 @@
-
-
-
-
+
+
+
+