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
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
build/
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
lib/
|
||||||
lib64/
|
lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
wheels/
|
wheels/
|
||||||
pip-wheel-metadata/
|
pip-wheel-metadata/
|
||||||
share/python-wheels/
|
share/python-wheels/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
MANIFEST
|
MANIFEST
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# 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.
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
*.manifest
|
*.manifest
|
||||||
*.spec
|
*.spec
|
||||||
|
|
||||||
# Installer logs
|
# Installer logs
|
||||||
pip-log.txt
|
pip-log.txt
|
||||||
pip-delete-this-directory.txt
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
# Unit test / coverage reports
|
# Unit test / coverage reports
|
||||||
htmlcov/
|
htmlcov/
|
||||||
.tox/
|
.tox/
|
||||||
.nox/
|
.nox/
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
.cache
|
.cache
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
*.py,cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
cover/
|
cover/
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
*.mo
|
*.mo
|
||||||
*.pot
|
*.pot
|
||||||
|
|
||||||
# Django stuff:
|
# Django stuff:
|
||||||
*.log
|
*.log
|
||||||
local_settings.py
|
local_settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
db.sqlite3-journal
|
||||||
|
|
||||||
# Flask stuff:
|
# Flask stuff:
|
||||||
instance/
|
instance/
|
||||||
.webassets-cache
|
.webassets-cache
|
||||||
|
|
||||||
# Scrapy stuff:
|
# Scrapy stuff:
|
||||||
.scrapy
|
.scrapy
|
||||||
|
|
||||||
# Sphinx documentation
|
# Sphinx documentation
|
||||||
docs/_build/
|
docs/_build/
|
||||||
build/
|
build/
|
||||||
temp/
|
temp/
|
||||||
|
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
# Jupyter Notebook
|
# Jupyter Notebook
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
# IPython
|
# IPython
|
||||||
profile_default/
|
profile_default/
|
||||||
ipython_config.py
|
ipython_config.py
|
||||||
|
|
||||||
# pyenv
|
# pyenv
|
||||||
.python-version
|
.python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
Pipfile.lock
|
Pipfile.lock
|
||||||
|
|
||||||
# poetry
|
# poetry
|
||||||
poetry.lock
|
poetry.lock
|
||||||
|
|
||||||
# PEP 582; used by python-next
|
# PEP 582; used by python-next
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
# Celery stuff
|
# Celery stuff
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
celerybeat.pid
|
celerybeat.pid
|
||||||
|
|
||||||
# SageMath parsed files
|
# SageMath parsed files
|
||||||
*.sage.py
|
*.sage.py
|
||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
.spyproject
|
.spyproject
|
||||||
|
|
||||||
# Rope project settings
|
# Rope project settings
|
||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
# mkdocs documentation
|
# mkdocs documentation
|
||||||
/site
|
/site
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
.dmypy.json
|
.dmypy.json
|
||||||
dmypy.json
|
dmypy.json
|
||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
# pytype static type analyzer
|
# pytype static type analyzer
|
||||||
.pytype/
|
.pytype/
|
||||||
|
|
||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
|
||||||
# VS Code settings
|
# VS Code settings
|
||||||
.vscode/
|
.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
|
# Split Pendapatan Payment
|
||||||
|
|
||||||
This module modifies Point of Sale transactions to split income/pendapatan journal entries per payment method used in the transaction.
|
This module modifies Point of Sale transactions to split income/pendapatan journal entries per payment method used in the transaction.
|
||||||
|
|
||||||
## Overview
|
## 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.
|
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:
|
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 - Cash
|
||||||
- Sales - Credit Card
|
- Sales - Credit Card
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Splits income journal entries by payment method
|
- Splits income journal entries by payment method
|
||||||
- Maintains proper accounting reconciliation
|
- Maintains proper accounting reconciliation
|
||||||
- Works with all payment methods (cash, bank transfers, credit cards, etc.)
|
- Works with all payment methods (cash, bank transfers, credit cards, etc.)
|
||||||
- Preserves tax calculations and reporting
|
- Preserves tax calculations and reporting
|
||||||
- Allows linking specific income accounts to payment methods
|
- Allows linking specific income accounts to payment methods
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
### Payment Method Configuration
|
### Payment Method Configuration
|
||||||
1. Go to Point of Sale > Configuration > Payment Methods
|
1. Go to Point of Sale > Configuration > Payment Methods
|
||||||
2. Edit a payment method
|
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
|
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
|
4. If left empty, the default product income account will be used
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The module inherits from the `pos.session` model and overrides two key methods:
|
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
|
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
|
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.
|
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
|
## Installation
|
||||||
|
|
||||||
1. Place the module in your Odoo addons directory
|
1. Place the module in your Odoo addons directory
|
||||||
2. Update the Apps list in Odoo
|
2. Update the Apps list in Odoo
|
||||||
3. Install the "Split Pendapatan Payment" module
|
3. Install the "Split Pendapatan Payment" module
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
This module is designed for Odoo 17.0 and is compatible with the standard Point of Sale module.
|
This module is designed for Odoo 17.0 and is compatible with the standard Point of Sale module.
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
- Only affects non-invoiced POS orders (invoiced orders follow standard Odoo accounting)
|
- 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
|
- May require adjustments if used with other POS customization modules that also modify journal entries
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|||||||
@ -1,37 +1,37 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': "Split Pendapatan Payment",
|
'name': "Split Pendapatan Payment",
|
||||||
|
|
||||||
'summary': "Split income journal entries per payment method in Point of Sale transactions",
|
'summary': "Split income journal entries per payment method in Point of Sale transactions",
|
||||||
|
|
||||||
'description': """
|
'description': """
|
||||||
This module modifies Point of Sale transactions to split income/pendapatan
|
This module modifies Point of Sale transactions to split income/pendapatan
|
||||||
journal entries per payment method used in the transaction. Instead of
|
journal entries per payment method used in the transaction. Instead of
|
||||||
creating a single income line for all sales, separate income lines are
|
creating a single income line for all sales, separate income lines are
|
||||||
created for each payment method.
|
created for each payment method.
|
||||||
""",
|
""",
|
||||||
|
|
||||||
'author': "Suherdy Yacob",
|
'author': "Suherdy Yacob",
|
||||||
|
|
||||||
# Categories can be used to filter modules in modules listing
|
# Categories can be used to filter modules in modules listing
|
||||||
'category': 'Point of Sale',
|
'category': 'Point of Sale',
|
||||||
'version': '17.0.1.0.0',
|
'version': '17.0.1.0.0',
|
||||||
|
|
||||||
# any module necessary for this one to work correctly
|
# any module necessary for this one to work correctly
|
||||||
'depends': ['point_of_sale', 'pos_discount'],
|
'depends': ['point_of_sale', 'pos_discount'],
|
||||||
|
|
||||||
# always loaded
|
# always loaded
|
||||||
'data': [
|
'data': [
|
||||||
# 'security/ir.model.access.csv',
|
# 'security/ir.model.access.csv',
|
||||||
'views/pos_session_views.xml',
|
'views/pos_session_views.xml',
|
||||||
'views/pos_payment_method_views.xml',
|
'views/pos_payment_method_views.xml',
|
||||||
],
|
],
|
||||||
# only loaded in demonstration mode
|
# only loaded in demonstration mode
|
||||||
'demo': [
|
'demo': [
|
||||||
# 'demo/demo.xml',
|
# 'demo/demo.xml',
|
||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
'license': 'LGPL-3',
|
'license': 'LGPL-3',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import pos_session
|
from . import pos_session
|
||||||
from . import pos_payment_method
|
from . import pos_payment_method
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
class PosPaymentMethod(models.Model):
|
class PosPaymentMethod(models.Model):
|
||||||
_inherit = 'pos.payment.method'
|
_inherit = 'pos.payment.method'
|
||||||
|
|
||||||
income_account_id = fields.Many2one(
|
income_account_id = fields.Many2one(
|
||||||
'account.account',
|
'account.account',
|
||||||
string='Income Account',
|
string='Income Account',
|
||||||
domain=[('deprecated', '=', False)],
|
domain=[('deprecated', '=', False)],
|
||||||
help='Account used for income lines when splitting by payment method. '
|
help='Account used for income lines when splitting by payment method. '
|
||||||
'If empty, the default income account from the product will be used.')
|
'If empty, the default income account from the product will be used.')
|
||||||
|
|
||||||
discount_account_id = fields.Many2one(
|
discount_account_id = fields.Many2one(
|
||||||
'account.account',
|
'account.account',
|
||||||
string='Discount Account',
|
string='Discount Account',
|
||||||
domain=[('deprecated', '=', False)],
|
domain=[('deprecated', '=', False)],
|
||||||
help='Account used for discount product lines when splitting by payment method. '
|
help='Account used for discount product lines when splitting by payment method. '
|
||||||
'If empty, the default discount account from the product will be used.')
|
'If empty, the default discount account from the product will be used.')
|
||||||
|
|||||||
@ -1,155 +1,280 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
from odoo.exceptions import UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.tools import float_is_zero, float_compare
|
from odoo.tools import float_is_zero, float_compare
|
||||||
|
import logging
|
||||||
|
|
||||||
class PosSession(models.Model):
|
_logger = logging.getLogger(__name__)
|
||||||
_inherit = 'pos.session'
|
|
||||||
|
|
||||||
def _accumulate_amounts(self, data):
|
class PosSession(models.Model):
|
||||||
# Call the original method to get all the standard accumulations
|
_inherit = 'pos.session'
|
||||||
data = super(PosSession, self)._accumulate_amounts(data)
|
|
||||||
|
def _prepare_line(self, order_line):
|
||||||
# Get all orders in this session
|
"""Override to allow products without income accounts when payment methods have them configured"""
|
||||||
closed_orders = self._get_closed_orders()
|
def get_income_account(order_line):
|
||||||
|
product = order_line.product_id
|
||||||
# If no orders, return original data
|
income_account = product.with_company(order_line.company_id)._get_product_accounts()['income'] or self.config_id.journal_id.default_account_id
|
||||||
if not closed_orders:
|
|
||||||
return data
|
# NEW: If no income account is found on the product, check if payment methods have income accounts configured
|
||||||
|
if not income_account:
|
||||||
# Get the original sales data
|
# First look at payment methods actually used on the order
|
||||||
sales = data.get('sales', {})
|
payment_methods_used = order_line.order_id.payment_ids.mapped('payment_method_id').filtered('income_account_id')
|
||||||
if not sales:
|
payment_methods_with_income = payment_methods_used or self.payment_method_ids.filtered('income_account_id')
|
||||||
return data
|
payment_income_account = payment_methods_with_income[:1].income_account_id
|
||||||
|
|
||||||
# Create new sales data structure split by payment method
|
if payment_income_account:
|
||||||
split_sales = defaultdict(lambda: {'amount': 0.0, 'amount_converted': 0.0, 'tax_amount': 0.0})
|
_logger.info(
|
||||||
|
"Product '%s' (id:%s) has no income account. "
|
||||||
# Get discount product ID from config
|
"Using payment method '%s' income account %s as fallback.",
|
||||||
discount_product_id = self.config_id.discount_product_id.id if self.config_id.discount_product_id else None
|
product.name, product.id,
|
||||||
|
payment_methods_with_income[:1].name,
|
||||||
# For each sale entry, we need to distribute it across payment methods
|
payment_income_account.display_name
|
||||||
for sale_key, sale_amounts in sales.items():
|
)
|
||||||
# Skip if this is a tax key (we only want to split actual sales)
|
income_account = payment_income_account
|
||||||
if len(sale_key) < 4: # Not a standard sales key
|
else:
|
||||||
continue
|
_logger.warning(
|
||||||
|
"Product '%s' (id:%s) has no income account and no payment methods provide an income account.",
|
||||||
total_amount = sale_amounts['amount']
|
product.name, product.id
|
||||||
total_amount_converted = sale_amounts['amount_converted']
|
)
|
||||||
tax_amount = sale_amounts.get('tax_amount', 0.0) # Get tax amount if it exists
|
raise UserError(_('Please define income account for this product: "%s" (id:%d).\n'
|
||||||
|
'Or configure the Income Account in your payment methods.',
|
||||||
if float_is_zero(total_amount, precision_rounding=self.currency_id.rounding):
|
product.name, product.id))
|
||||||
continue
|
|
||||||
|
return order_line.order_id.fiscal_position_id.map_account(income_account)
|
||||||
# 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)
|
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)
|
||||||
if float_is_zero(total_payment_amount, precision_rounding=self.currency_id.rounding):
|
sign = -1 if order_line.qty >= 0 else 1
|
||||||
continue
|
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
|
||||||
# Distribute the sales amount across all orders proportionally to their payments
|
is_refund = check_refund(order_line)
|
||||||
for order in closed_orders:
|
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)
|
||||||
if order.is_invoiced:
|
date_order = order_line.order_id.date_order
|
||||||
continue # Skip invoiced orders
|
taxes = [{'date_order': date_order, **tax} for tax in tax_data['taxes']]
|
||||||
|
return {
|
||||||
order_payments = order.payment_ids
|
'date_order': order_line.order_id.date_order,
|
||||||
order_payment_total = sum(payment.amount for payment in order_payments)
|
'income_account_id': get_income_account(order_line).id,
|
||||||
|
'amount': order_line.price_subtotal,
|
||||||
if float_is_zero(order_payment_total, precision_rounding=order.currency_id.rounding):
|
'taxes': taxes,
|
||||||
continue
|
'base_tags': tuple(tax_data['base_tags']),
|
||||||
|
}
|
||||||
# For each payment in this order, create a split sales entry
|
|
||||||
for payment in order_payments:
|
def _accumulate_amounts(self, data):
|
||||||
# Calculate the proportion of this payment relative to all payments
|
"""Distribute sales amounts per payment method while respecting discount accounts."""
|
||||||
payment_proportion = payment.amount / total_payment_amount
|
data = super(PosSession, self)._accumulate_amounts(data)
|
||||||
payment_amount = total_amount * payment_proportion
|
|
||||||
payment_amount_converted = total_amount_converted * payment_proportion
|
closed_orders = self._get_closed_orders()
|
||||||
payment_tax_amount = tax_amount * payment_proportion
|
if not closed_orders:
|
||||||
|
return data
|
||||||
if float_is_zero(payment_amount, precision_rounding=self.currency_id.rounding):
|
|
||||||
continue
|
sales = data.get('sales', {})
|
||||||
|
if not sales:
|
||||||
# Determine the account to use based on whether this is a discount product
|
return data
|
||||||
income_account_id = sale_key[0] # default account
|
|
||||||
|
discount_product_id = self.config_id.discount_product_id.id if self.config_id.discount_product_id else None
|
||||||
# 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
|
# Build per-order breakdown of sales keyed by (account, sign, tax tuple, tags)
|
||||||
if discount_product_id:
|
order_payment_totals = {}
|
||||||
# Get the discount product's income account
|
order_sales_breakdown = {}
|
||||||
discount_product = self.env['product.product'].browse(discount_product_id)
|
for order in closed_orders:
|
||||||
discount_account = discount_product._get_product_accounts()['income']
|
order_payment_totals[order.id] = sum(payment.amount for payment in order.payment_ids)
|
||||||
if discount_account and discount_account.id == sale_key[0]:
|
if order.is_invoiced:
|
||||||
# This is a discount product, use discount account if configured
|
continue
|
||||||
if payment.payment_method_id.discount_account_id:
|
|
||||||
income_account_id = payment.payment_method_id.discount_account_id.id
|
sale_map = {}
|
||||||
elif payment.payment_method_id.income_account_id:
|
for order_line in order.lines:
|
||||||
# This is a regular product, use income account if configured
|
line_vals = self._prepare_line(order_line)
|
||||||
income_account_id = payment.payment_method_id.income_account_id.id
|
sale_key = (
|
||||||
|
line_vals['income_account_id'],
|
||||||
# Ensure we have a valid account ID
|
-1 if line_vals['amount'] < 0 else 1,
|
||||||
if not income_account_id:
|
tuple((tax['id'], tax['account_id'], tax['tax_repartition_line_id']) for tax in line_vals['taxes']),
|
||||||
continue
|
line_vals['base_tags'],
|
||||||
|
)
|
||||||
# Create a new key that includes the payment method
|
entry = sale_map.setdefault(sale_key, {
|
||||||
new_sale_key = (
|
'amount': 0.0,
|
||||||
# account (use payment method account if specified)
|
'amount_converted': 0.0,
|
||||||
income_account_id,
|
'tax_amount': 0.0,
|
||||||
# sign (same as original)
|
'regular_amount': 0.0,
|
||||||
sale_key[1],
|
'regular_amount_converted': 0.0,
|
||||||
# payment method
|
'regular_tax_amount': 0.0,
|
||||||
payment.payment_method_id.id,
|
'discount_amount': 0.0,
|
||||||
# for taxes (same as original)
|
'discount_amount_converted': 0.0,
|
||||||
sale_key[2],
|
'discount_tax_amount': 0.0,
|
||||||
# base tags (same as original)
|
})
|
||||||
sale_key[3],
|
|
||||||
)
|
prev_amount = entry['amount']
|
||||||
|
prev_amount_converted = entry['amount_converted']
|
||||||
# Update the split sales data
|
updated_amounts = self._update_amounts(
|
||||||
split_sales[new_sale_key]['amount'] += payment_amount
|
{'amount': prev_amount, 'amount_converted': prev_amount_converted},
|
||||||
split_sales[new_sale_key]['amount_converted'] += payment_amount_converted
|
{'amount': line_vals['amount']},
|
||||||
split_sales[new_sale_key]['tax_amount'] += payment_tax_amount
|
order.date_order,
|
||||||
|
round=False,
|
||||||
# Replace the original sales data with our split sales data
|
)
|
||||||
data['sales'] = split_sales
|
line_amount_converted = updated_amounts['amount_converted'] - prev_amount_converted
|
||||||
return data
|
line_tax_amount = sum(tax['amount'] for tax in line_vals['taxes'])
|
||||||
|
|
||||||
def _get_sale_vals(self, key, amount, amount_converted):
|
entry['amount'] = updated_amounts['amount']
|
||||||
""" Override to add payment method information to the sales line description """
|
entry['amount_converted'] = updated_amounts['amount_converted']
|
||||||
# Check if this key includes payment method information
|
entry['tax_amount'] += line_tax_amount
|
||||||
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
|
is_reward_line = bool(getattr(order_line, 'is_reward_line', False))
|
||||||
# Try to get the payment method name
|
has_reward = bool(getattr(order_line, 'reward_id', False))
|
||||||
try:
|
is_discount_product = bool(discount_product_id and order_line.product_id.id == discount_product_id)
|
||||||
payment_method = self.env['pos.payment.method'].browse(payment_method_id)
|
is_discount_line = is_reward_line or has_reward or is_discount_product
|
||||||
payment_method_name = payment_method.name
|
|
||||||
except:
|
if is_discount_line:
|
||||||
payment_method_name = "Unknown Payment"
|
entry['discount_amount'] += line_vals['amount']
|
||||||
else:
|
entry['discount_amount_converted'] += line_amount_converted
|
||||||
# Original format
|
entry['discount_tax_amount'] += line_tax_amount
|
||||||
account_id, sign, tax_keys, base_tag_ids = key
|
else:
|
||||||
payment_method_name = None
|
entry['regular_amount'] += line_vals['amount']
|
||||||
|
entry['regular_amount_converted'] += line_amount_converted
|
||||||
tax_ids = set(tax[0] for tax in tax_keys) if tax_keys else set()
|
entry['regular_tax_amount'] += line_tax_amount
|
||||||
applied_taxes = self.env['account.tax'].browse(tax_ids)
|
|
||||||
title = _('Sales') if sign == 1 else _('Refund')
|
if sale_map:
|
||||||
|
order_sales_breakdown[order.id] = sale_map
|
||||||
# Create name with payment method information
|
|
||||||
if payment_method_name:
|
split_sales = defaultdict(lambda: {'amount': 0.0, 'amount_converted': 0.0, 'tax_amount': 0.0})
|
||||||
name = _('%s - %s', title, payment_method_name)
|
|
||||||
if applied_taxes:
|
for sale_key, sale_amounts in sales.items():
|
||||||
name = _('%s with %s - %s', title, ', '.join([tax.name for tax in applied_taxes]), payment_method_name)
|
if len(sale_key) < 4:
|
||||||
else:
|
continue
|
||||||
name = _('%s untaxed', title)
|
|
||||||
if applied_taxes:
|
total_amount = sale_amounts['amount']
|
||||||
name = _('%s with %s', title, ', '.join([tax.name for tax in applied_taxes]))
|
if float_is_zero(total_amount, precision_rounding=self.currency_id.rounding):
|
||||||
|
continue
|
||||||
partial_vals = {
|
|
||||||
'name': name,
|
for order in closed_orders:
|
||||||
'account_id': account_id,
|
if order.is_invoiced:
|
||||||
'move_id': self.move_id.id,
|
continue
|
||||||
'tax_ids': [(6, 0, tax_ids)],
|
|
||||||
'tax_tag_ids': [(6, 0, base_tag_ids)] if base_tag_ids else [],
|
order_sale_map = order_sales_breakdown.get(order.id)
|
||||||
}
|
if not order_sale_map:
|
||||||
return self._credit_amounts(partial_vals, amount, amount_converted)
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_pos_payment_method_form_inherit" model="ir.ui.view">
|
<record id="view_pos_payment_method_form_inherit" model="ir.ui.view">
|
||||||
<field name="name">pos.payment.method.form.inherit</field>
|
<field name="name">pos.payment.method.form.inherit</field>
|
||||||
<field name="model">pos.payment.method</field>
|
<field name="model">pos.payment.method</field>
|
||||||
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
<field name="inherit_id" ref="point_of_sale.pos_payment_method_view_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//group[@name='Payment methods']/group" position="inside">
|
<xpath expr="//group[@name='Payment methods']/group" position="inside">
|
||||||
<field name="income_account_id" domain="[('deprecated', '=', False)]"/>
|
<field name="income_account_id" domain="[('deprecated', '=', False)]"/>
|
||||||
<field name="discount_account_id" domain="[('deprecated', '=', False)]"/>
|
<field name="discount_account_id" domain="[('deprecated', '=', False)]"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- No additional views needed for this module -->
|
<!-- No additional views needed for this module -->
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user