feat: Add configurable accounting entries for 100% discounted Point of Sale orders.
This commit is contained in:
parent
3fed893293
commit
c3ddec84f3
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# Python
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
__pycache__/
|
||||
|
||||
# Odoo
|
||||
*.log
|
||||
.env
|
||||
.venv/
|
||||
venv/
|
||||
filestore/
|
||||
session/
|
||||
24
README.md
24
README.md
@ -10,19 +10,21 @@ This module modifies the loyalty reward discount calculation in POS to apply dis
|
||||
|
||||
3. **Proper Accounting Entries**: Accounting entries are created with tax only on the credit side, ensuring compliance with accounting standards.
|
||||
|
||||
4. **100% Discount Rounding Fix**: Ensures exact zero-total for 100% discounts by generating individual reward lines for each item, preventing tax rounding discrepancies.
|
||||
|
||||
5. **Zero-Value Journal Entries**: Automatically creates journal entries for orders with a 0.00 total (e.g., 100% discount) to track the "Foregone Income" and "Discount Expense". This is configurable via settings.
|
||||
|
||||
## Technical Changes
|
||||
|
||||
### JavaScript Changes
|
||||
|
||||
- Modified `static/src/overrides/models/loyalty.js` to calculate discounts on tax-exclusive amounts
|
||||
- Ensured discount line values are tax-exclusive for display purposes
|
||||
- Modified `static/src/overrides/models/loyalty.js` to calculate discounts on tax-exclusive amounts.
|
||||
- Implemented "Line-by-Line Cancellation" for 100% discounts to fix rounding issues.
|
||||
|
||||
### Backend Changes
|
||||
|
||||
- Created `models/pos_order.py` to handle accounting entries for loyalty discounts
|
||||
- Created `models/loyalty_reward.py` to ensure discount products are properly configured
|
||||
- Created `models/account_move.py` to ensure discount lines in account moves are tax-exclusive
|
||||
- Created `models/pos_session.py` to handle session-level accounting for loyalty discounts
|
||||
- Extended `pos.session` (`models/pos_session.py`) to generate additional journal entry lines for 0-value orders.
|
||||
- Extended `res.config.settings` to allow configuration of Income and Expense accounts for 100% discounts.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -32,7 +34,15 @@ This module modifies the loyalty reward discount calculation in POS to apply dis
|
||||
|
||||
## Configuration
|
||||
|
||||
No additional configuration is required. The module automatically applies the changes to all loyalty programs in POS.
|
||||
### Zero-Value Journal Entries (100% Discount)
|
||||
To enable journal entries for 0.00 total orders:
|
||||
1. Go to **Point of Sale > Configuration > Settings**.
|
||||
2. Scroll to the **100% Discount Accounting** section.
|
||||
3. Select the **Income Account** (Credit) to track the gross sales value.
|
||||
4. Select the **Expense/Discount Account** (Debit) to track the discount cost.
|
||||
5. Save the settings.
|
||||
|
||||
If these accounts are not set, no journal entry will be created for 0.00 total orders.
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@ -1 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
@ -9,7 +9,7 @@
|
||||
""",
|
||||
"depends": ["point_of_sale", "pos_loyalty"],
|
||||
"data": [
|
||||
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
"assets": {
|
||||
"point_of_sale._assets_pos": [
|
||||
|
||||
Binary file not shown.
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from . import pos_config
|
||||
from . import pos_session
|
||||
from . import res_config_settings
|
||||
17
models/pos_config.py
Normal file
17
models/pos_config.py
Normal file
@ -0,0 +1,17 @@
|
||||
from odoo import fields, models
|
||||
|
||||
class PosConfig(models.Model):
|
||||
_inherit = 'pos.config'
|
||||
|
||||
discount_100_income_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='100% Discount Income Account',
|
||||
help='Account used for Income when an order has a 100% discount (0 total).',
|
||||
domain="[('deprecated', '=', False)]"
|
||||
)
|
||||
discount_100_expense_account_id = fields.Many2one(
|
||||
'account.account',
|
||||
string='100% Discount Expense Account',
|
||||
help='Account used for Expense/Discount when an order has a 100% discount (0 total).',
|
||||
domain="[('deprecated', '=', False)]"
|
||||
)
|
||||
79
models/pos_session.py
Normal file
79
models/pos_session.py
Normal file
@ -0,0 +1,79 @@
|
||||
from odoo import models, _
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
class PosSession(models.Model):
|
||||
_inherit = 'pos.session'
|
||||
|
||||
def _create_account_move(self, balancing_account=False, amount_to_balance=0, bank_payment_method_diffs=None):
|
||||
"""
|
||||
Extend _create_account_move to generate additional journal entry lines
|
||||
for 100% discount orders (where total amount is 0).
|
||||
"""
|
||||
# Call super to generate the standard move
|
||||
data = super(PosSession, self)._create_account_move(balancing_account, amount_to_balance, bank_payment_method_diffs)
|
||||
|
||||
if not self.config_id.discount_100_income_account_id or not self.config_id.discount_100_expense_account_id:
|
||||
return data
|
||||
|
||||
# Identify orders with 0 absolute paid amount but non-zero gross amount
|
||||
# We look for orders where amount_total is near zero.
|
||||
# Note: 100% discount orders have amount_total = 0.
|
||||
|
||||
MoveLine = data.get('MoveLine')
|
||||
income_account = self.config_id.discount_100_income_account_id
|
||||
expense_account = self.config_id.discount_100_expense_account_id
|
||||
|
||||
# Helper to convert amount to company currency if needed, similar to Odoo's internals
|
||||
def _get_amounts(amount, date):
|
||||
return self._update_amounts({'amount': 0, 'amount_converted': 0}, {'amount': amount}, date)
|
||||
|
||||
zero_value_moves = []
|
||||
|
||||
for order in self._get_closed_orders():
|
||||
if float_is_zero(order.amount_total, precision_rounding=self.currency_id.rounding):
|
||||
# Calculate gross amount (price before discount logic applied, or sum of positive lines)
|
||||
# Since it's 100% discount, the "Gross Value" we want to record is roughly the sum
|
||||
# of the products' prices *before* they were wiped out.
|
||||
# However, in our new line-by-line strategy, we have:
|
||||
# Item A: $100
|
||||
# Discount A: -$100
|
||||
# Net: $0
|
||||
# We want to record: Credit Income $100, Debit Expense $100.
|
||||
|
||||
# We can sum the positive lines to get the "Sale Value".
|
||||
gross_amount = sum(
|
||||
line.price_subtotal_incl
|
||||
for line in order.lines
|
||||
if line.price_subtotal_incl > 0
|
||||
)
|
||||
|
||||
if float_is_zero(gross_amount, precision_rounding=self.currency_id.rounding):
|
||||
continue
|
||||
|
||||
amounts = _get_amounts(gross_amount, order.date_order)
|
||||
|
||||
# Create Credit Line (Income)
|
||||
# We use _credit_amounts helper logic style manually
|
||||
credit_vals = {
|
||||
'name': _('100%% Discount Income: %s') % order.name,
|
||||
'account_id': income_account.id,
|
||||
'move_id': self.move_id.id,
|
||||
'partner_id': order.partner_id.id or False,
|
||||
}
|
||||
credit_vals.update(self._credit_amounts(credit_vals, amounts['amount'], amounts['amount_converted']))
|
||||
zero_value_moves.append(credit_vals)
|
||||
|
||||
# Create Debit Line (Expense/Discount)
|
||||
debit_vals = {
|
||||
'name': _('100%% Discount Expense: %s') % order.name,
|
||||
'account_id': expense_account.id,
|
||||
'move_id': self.move_id.id,
|
||||
'partner_id': order.partner_id.id or False,
|
||||
}
|
||||
debit_vals.update(self._debit_amounts(debit_vals, amounts['amount'], amounts['amount_converted']))
|
||||
zero_value_moves.append(debit_vals)
|
||||
|
||||
if zero_value_moves:
|
||||
MoveLine.create(zero_value_moves)
|
||||
|
||||
return data
|
||||
13
models/res_config_settings.py
Normal file
13
models/res_config_settings.py
Normal file
@ -0,0 +1,13 @@
|
||||
from odoo import fields, models
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
pos_discount_100_income_account_id = fields.Many2one(
|
||||
related='pos_config_id.discount_100_income_account_id',
|
||||
readonly=False,
|
||||
)
|
||||
pos_discount_100_expense_account_id = fields.Many2one(
|
||||
related='pos_config_id.discount_100_expense_account_id',
|
||||
readonly=False,
|
||||
)
|
||||
22
views/res_config_settings_views.xml
Normal file
22
views/res_config_settings_views.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.pos.loyalty.discount.100</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@id='pos_accounting_section']" position="inside">
|
||||
<setting id="discount_100_account" title="Accounting entries for 100% discount orders" string="100% Discount Accounting">
|
||||
<div class="row">
|
||||
<label string="Income Account" for="pos_discount_100_income_account_id" class="col-lg-3 o_light_label"/>
|
||||
<field name="pos_discount_100_income_account_id"/>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label string="Expense/Discount Account" for="pos_discount_100_expense_account_id" class="col-lg-3 o_light_label"/>
|
||||
<field name="pos_discount_100_expense_account_id"/>
|
||||
</div>
|
||||
</setting>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user