fix the splitting journal entry if there are discount product
This commit is contained in:
parent
074a918b82
commit
116b132b30
278
.gitignore
vendored
278
.gitignore
vendored
@ -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/
|
||||
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.
|
||||
102
README.md
102
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
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.')
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_pos_payment_method_form_inherit" model="ir.ui.view">
|
||||
<field name="name">pos.payment.method.form.inherit</field>
|
||||
<field name="model">pos.payment.method</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='Payment methods']/group" position="inside">
|
||||
<field name="income_account_id" domain="[('deprecated', '=', False)]"/>
|
||||
<field name="discount_account_id" domain="[('deprecated', '=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_pos_payment_method_form_inherit" model="ir.ui.view">
|
||||
<field name="name">pos.payment.method.form.inherit</field>
|
||||
<field name="model">pos.payment.method</field>
|
||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='Payment methods']/group" position="inside">
|
||||
<field name="income_account_id" domain="[('deprecated', '=', False)]"/>
|
||||
<field name="discount_account_id" domain="[('deprecated', '=', False)]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- No additional views needed for this module -->
|
||||
</odoo>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- No additional views needed for this module -->
|
||||
</odoo>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user