mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-23 13:27:17 +00:00
Compare commits
67 Commits
fix/intern
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
574f345e91 | ||
|
|
9a79253ee1 | ||
|
|
a05229b988 | ||
|
|
a0b7db19d9 | ||
|
|
bc6e5e2060 | ||
|
|
5de2ac1f26 | ||
|
|
a46930e10a | ||
|
|
18093079d9 | ||
|
|
f4979e6650 | ||
|
|
63413f8bfe | ||
|
|
02c04ae167 | ||
|
|
2dcf82a2ab | ||
|
|
ca1a88b53b | ||
|
|
d75b957ce0 | ||
|
|
e9480bd0e1 | ||
|
|
8793ad8264 | ||
|
|
3691ca5b5b | ||
|
|
56f24a6adf | ||
|
|
60f0a736ab | ||
|
|
1ba071d657 | ||
|
|
b3d55db893 | ||
|
|
876bf0b7f7 | ||
|
|
16b1be814c | ||
|
|
ecc643fde0 | ||
|
|
df3f952fac | ||
|
|
726c92f8ae | ||
|
|
c0f13b01de | ||
|
|
23afc786bb | ||
|
|
3bf3bc709f | ||
|
|
102842253b | ||
|
|
e814d13126 | ||
|
|
7f608f31eb | ||
|
|
f990df9deb | ||
|
|
62cb38d36b | ||
|
|
eca99d3df4 | ||
|
|
8d6ee86ba3 | ||
|
|
e3fc1790d4 | ||
|
|
db6dad7ddd | ||
|
|
719b53f2ad | ||
|
|
79fd06a1ba | ||
|
|
5ebee655b7 | ||
|
|
96db1071da | ||
|
|
9f2b05c103 | ||
|
|
9e01992217 | ||
|
|
5ebc52c95c | ||
|
|
86eb9c6204 | ||
|
|
893fc75df4 | ||
|
|
f2279a139f | ||
|
|
30e0382aa9 | ||
|
|
d2b1965cc8 | ||
|
|
a931c7d2f4 | ||
|
|
3c251254b8 | ||
|
|
04c949a662 | ||
|
|
5d00fec1c2 | ||
|
|
bb16dca4f7 | ||
|
|
79fdc8add3 | ||
|
|
6e87694bbe | ||
|
|
6eda8c8c62 | ||
|
|
fb69724cda | ||
|
|
dd72c2688b | ||
|
|
8fd0175b06 | ||
|
|
1e35af3abe | ||
|
|
221baabf9e | ||
|
|
8b59d66f94 | ||
|
|
ff70aa6a33 | ||
|
|
3384c1939b | ||
|
|
14275e4c01 |
13
.github/workflows/linters.yml
vendored
13
.github/workflows/linters.yml
vendored
@@ -23,6 +23,19 @@ jobs:
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
js-unit-tests:
|
||||
name: js unit tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Run JS unit tests
|
||||
run: yarn test:js
|
||||
|
||||
semgrep:
|
||||
name: semgrep
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -22,6 +22,6 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: alyf-de/po-review-action@5928f84d6bc9094f9ad6e2c5780f01c0044b800e # v1.1.1
|
||||
- uses: alyf-de/po-review-action@57fff275f4a0518a2ca55869ec6776fa3813b3d5 # v1.2.0
|
||||
with:
|
||||
hidden-po-files: eo.po
|
||||
|
||||
@@ -47,10 +47,13 @@ class ERPNextAddress(Address):
|
||||
super().on_update()
|
||||
|
||||
address_display = get_address_display(self.as_dict())
|
||||
filters = {"customer_primary_address": self.name}
|
||||
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
|
||||
for customer_name in customers:
|
||||
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
|
||||
customers = frappe.db.get_all(
|
||||
"Customer", filters={"customer_primary_address": self.name}, pluck="name"
|
||||
)
|
||||
for customer in customers:
|
||||
frappe.db.set_value(
|
||||
"Customer", customer, "primary_address", address_display, update_modified=False
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"description": "Heads (or groups) against which Accounting Entries are made and balances are maintained.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/chart-of-accounts",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"properties",
|
||||
@@ -200,7 +201,7 @@
|
||||
"options": "Account Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"icon": "vault",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "folder-tree",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"icon": "scale",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-22 19:13:50.400404",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "field:label",
|
||||
"creation": "2019-05-04 18:13:37.002352",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimensions",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"document_type",
|
||||
@@ -49,6 +50,7 @@
|
||||
"options": "Accounting Dimension Detail"
|
||||
}
|
||||
],
|
||||
"icon": "layers",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:05:56.890002",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "format:{accounting_dimension}-{#####}",
|
||||
"creation": "2020-11-08 18:28:11.906146",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-dimension-filter",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -99,6 +100,7 @@
|
||||
"label": "Fieldname"
|
||||
}
|
||||
],
|
||||
"icon": "funnel",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-08 14:13:22.203011",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "field:period_name",
|
||||
"creation": "2018-04-13 18:50:14.672323",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounting-period",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -77,6 +78,7 @@
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"icon": "calendar-range",
|
||||
"links": [],
|
||||
"modified": "2026-03-09 17:15:33.577217",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"creation": "2013-06-24 15:49:57",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/accounts-settings",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -801,7 +802,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"icon": "settings",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"creation": "2024-10-16 16:57:12.085072",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"icon": "book-open",
|
||||
"is_submittable": 1,
|
||||
"field_order": [
|
||||
"company",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"creation": "2018-04-07 16:59:59.496668",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -100,6 +101,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "landmark",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:39.423431",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2017-05-29 21:35:13.136357",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-account",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account_name",
|
||||
@@ -227,6 +228,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "credit-card",
|
||||
"links": [
|
||||
{
|
||||
"group": "Transactions",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "wallet",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-06-16 22:17:48.007982",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"icon": "credit-card",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.221876",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"icon": "credit-card",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:37.347035",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-check",
|
||||
"icon": "badge-check",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2016-12-17 10:43:35.731631",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-guarantee",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -216,6 +217,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "shield-check",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-25 18:12:10.768835",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "arrow-left-right",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -792,6 +792,7 @@ def create_bulk_payment_entry_and_reconcile(
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
"bank_account",
|
||||
"company",
|
||||
"currency",
|
||||
"unallocated_amount",
|
||||
"date",
|
||||
@@ -826,11 +827,7 @@ def create_bulk_payment_entry_and_reconcile(
|
||||
"paid_from": paid_from,
|
||||
"paid_to": paid_to,
|
||||
"paid_amount": bank_transaction.unallocated_amount,
|
||||
"base_paid_amount": bank_transaction.unallocated_amount,
|
||||
"received_amount": bank_transaction.unallocated_amount,
|
||||
"base_received_amount": bank_transaction.unallocated_amount,
|
||||
"target_exchange_rate": 1,
|
||||
"source_exchange_rate": 1,
|
||||
"reference_date": bank_transaction.date,
|
||||
"posting_date": bank_transaction.date,
|
||||
"reference_no": (bank_transaction.reference_number or bank_transaction.description or "")[
|
||||
@@ -839,6 +836,8 @@ def create_bulk_payment_entry_and_reconcile(
|
||||
}
|
||||
)
|
||||
|
||||
set_multi_currency_amounts(payment_entry_doc)
|
||||
|
||||
payment_entry_doc.insert()
|
||||
payment_entry_doc.submit()
|
||||
|
||||
@@ -877,6 +876,7 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
|
||||
"doctype": "Payment Entry",
|
||||
}
|
||||
)
|
||||
set_multi_currency_amounts(payment_entry)
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
transaction = reconcile_vouchers(
|
||||
@@ -899,6 +899,33 @@ def create_payment_entry_and_reconcile(bank_transaction_name: str | int, payment
|
||||
}
|
||||
|
||||
|
||||
def set_multi_currency_amounts(pe):
|
||||
"""Set real exchange rates when the bank and party accounts differ in currency."""
|
||||
company_currency = frappe.get_cached_value("Company", pe.company, "default_currency")
|
||||
pe.paid_from_account_currency = frappe.get_cached_value("Account", pe.paid_from, "account_currency")
|
||||
pe.paid_to_account_currency = frappe.get_cached_value("Account", pe.paid_to, "account_currency")
|
||||
|
||||
pe.source_exchange_rate = (
|
||||
1.0
|
||||
if pe.paid_from_account_currency == company_currency
|
||||
else get_exchange_rate(pe.paid_from_account_currency, company_currency, pe.posting_date)
|
||||
)
|
||||
pe.target_exchange_rate = (
|
||||
1.0
|
||||
if pe.paid_to_account_currency == company_currency
|
||||
else get_exchange_rate(pe.paid_to_account_currency, company_currency, pe.posting_date)
|
||||
)
|
||||
|
||||
# derive the party-side amount from the authoritative bank-side amount; Payment Entry books any
|
||||
# rounding residual to Exchange Gain/Loss during validation (set_exchange_gain_loss)
|
||||
if pe.payment_type == "Receive" and pe.source_exchange_rate:
|
||||
base_amount = flt(pe.received_amount) * pe.target_exchange_rate
|
||||
pe.paid_amount = flt(base_amount / pe.source_exchange_rate, pe.precision("paid_amount"))
|
||||
elif pe.payment_type == "Pay" and pe.target_exchange_rate:
|
||||
base_amount = flt(pe.paid_amount) * pe.source_exchange_rate
|
||||
pe.received_amount = flt(base_amount / pe.target_exchange_rate, pe.precision("received_amount"))
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["GET"])
|
||||
def search_for_transfer_transaction(transaction_id: str | int):
|
||||
"""
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
# See license.txt
|
||||
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
create_bulk_payment_entry_and_reconcile,
|
||||
create_payment_entry_and_reconcile,
|
||||
get_auto_reconcile_message,
|
||||
get_bank_transactions,
|
||||
get_linked_payments,
|
||||
@@ -16,6 +20,8 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
RATE_METHOD = "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_exchange_rate"
|
||||
|
||||
|
||||
class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
@@ -230,3 +236,117 @@ class TestBankReconciliationTool(ERPNextTestSuite, AccountsTestMixin):
|
||||
self.assertIn("1 Transaction Partially Reconciled", singular)
|
||||
plural, _ = get_auto_reconcile_message(["p1", "p2"], [])
|
||||
self.assertIn("2 Transactions Partially Reconciled", plural)
|
||||
|
||||
def test_multi_currency_pay_converts_and_balances(self):
|
||||
# withdrawal from an INR bank paying a USD supplier; rate 3.0 makes 100/3 non-exact
|
||||
self.enable_multi_currency_setup()
|
||||
pe = self.reconcile_new_payment(
|
||||
self.make_multi_currency_txn(withdrawal=100),
|
||||
payment_type="Pay",
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
party_account=self.creditors_usd,
|
||||
paid_from=self.bank,
|
||||
paid_to=self.creditors_usd,
|
||||
rate=3.0,
|
||||
)
|
||||
self.assertEqual(pe.docstatus, 1) # submits despite the rounding residual
|
||||
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (1.0, 3.0))
|
||||
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33)) # bank side kept, 100/3
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
# Payment Entry auto-books the rounding residual to Exchange Gain/Loss
|
||||
self.assertTrue(pe.deductions[0].is_exchange_gain_loss)
|
||||
self.assertEqual(pe.deductions[0].amount, 0.01) # 100 - 33.33 * 3
|
||||
|
||||
def test_multi_currency_receive_converts_and_balances(self):
|
||||
# deposit into an INR bank from a USD customer; the party side must convert
|
||||
self.enable_multi_currency_setup()
|
||||
pe = self.reconcile_new_payment(
|
||||
self.make_multi_currency_txn(deposit=100),
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
party_account=self.debtors_usd,
|
||||
paid_from=self.debtors_usd,
|
||||
paid_to=self.bank,
|
||||
rate=3.0,
|
||||
)
|
||||
self.assertEqual(pe.docstatus, 1)
|
||||
self.assertEqual((pe.source_exchange_rate, pe.target_exchange_rate), (3.0, 1.0))
|
||||
self.assertEqual((pe.received_amount, pe.paid_amount), (100, 33.33)) # bank side kept, 100/3
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
def test_multi_currency_bulk_pay_converts_and_balances(self):
|
||||
# the bulk path builds the Payment Entry itself, so it must convert too
|
||||
self.enable_multi_currency_setup()
|
||||
txn = self.make_multi_currency_txn(withdrawal=100)
|
||||
with patch(RATE_METHOD, return_value=3.0):
|
||||
result = create_bulk_payment_entry_and_reconcile(
|
||||
[txn.name], "Supplier", self.supplier, self.creditors_usd
|
||||
)
|
||||
|
||||
pe = frappe.get_doc("Payment Entry", result[0]["payment_entry"].name)
|
||||
self.assertEqual(pe.docstatus, 1)
|
||||
self.assertEqual(pe.target_exchange_rate, 3.0)
|
||||
self.assertEqual((pe.paid_amount, pe.received_amount), (100, 33.33))
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
|
||||
def enable_multi_currency_setup(self):
|
||||
# USD party/accounts + a company gain/loss account to absorb rounding residuals
|
||||
self.company_abbr = "_TC"
|
||||
self.create_supplier(supplier_name="_Test Supplier USD", currency="USD")
|
||||
self.create_customer(customer_name="_Test Customer USD", currency="USD")
|
||||
self.create_usd_payable_account()
|
||||
self.create_usd_receivable_account()
|
||||
self.set_party_account("Supplier", self.supplier, self.creditors_usd)
|
||||
if not frappe.db.get_value("Company", self.company, "exchange_gain_loss_account"):
|
||||
frappe.db.set_value(
|
||||
"Company", self.company, "exchange_gain_loss_account", "Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
def set_party_account(self, party_type, party, account):
|
||||
doc = frappe.get_doc(party_type, party)
|
||||
if not any(row.company == self.company for row in doc.accounts):
|
||||
doc.append("accounts", {"company": self.company, "account": account})
|
||||
doc.save()
|
||||
|
||||
def make_multi_currency_txn(self, withdrawal=0, deposit=0):
|
||||
return (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Bank Transaction",
|
||||
"date": today(),
|
||||
"withdrawal": withdrawal,
|
||||
"deposit": deposit,
|
||||
"bank_account": self.bank_account,
|
||||
"currency": "INR",
|
||||
"reference_number": "TEST-FX-REF",
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
def reconcile_new_payment(
|
||||
self, txn, *, payment_type, party_type, party, party_account, paid_from, paid_to, rate
|
||||
):
|
||||
# mimics the /banking frontend, which sends a hardcoded 1:1 rate
|
||||
payment_entry_doc = {
|
||||
"payment_type": payment_type,
|
||||
"company": self.company,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"party_account": party_account,
|
||||
"paid_from": paid_from,
|
||||
"paid_to": paid_to,
|
||||
"paid_amount": txn.unallocated_amount,
|
||||
"received_amount": txn.unallocated_amount,
|
||||
"source_exchange_rate": 1,
|
||||
"target_exchange_rate": 1,
|
||||
"posting_date": today(),
|
||||
"reference_no": f"TEST-FX-{payment_type}",
|
||||
"reference_date": today(),
|
||||
}
|
||||
with patch(RATE_METHOD, return_value=rate):
|
||||
result = create_payment_entry_and_reconcile(txn.name, payment_entry_doc)
|
||||
return frappe.get_doc("Payment Entry", result["payment_entry"].name)
|
||||
|
||||
@@ -225,6 +225,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "file-down",
|
||||
"links": [],
|
||||
"modified": "2026-06-19 14:18:00.000000",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "file-clock",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-07-09 17:55:25.615942",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2018-10-22 18:19:02.784533",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/bank-transaction",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -277,6 +278,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "arrow-left-right",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-11 20:41:15.124085",
|
||||
|
||||
@@ -140,6 +140,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "zap",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-04-11 21:36:26.754667",
|
||||
|
||||
@@ -167,6 +167,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "split",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"label": "Generated"
|
||||
}
|
||||
],
|
||||
"icon": "git-branch",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:39.766063",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2016-05-16 11:42:29.632528",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/budgeting",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -330,6 +331,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "piggy-bank",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "lock",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:44.260440",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"description": "Import Chart of Accounts from a csv file",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/chart-of-accounts-importer",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -47,6 +48,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "file-input",
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "field:bank_name",
|
||||
"creation": "2016-05-04 14:35:00.402544",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cheque-print-template",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"settings",
|
||||
@@ -293,6 +294,7 @@
|
||||
"fieldtype": "HTML"
|
||||
}
|
||||
],
|
||||
"icon": "printer",
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2026-06-08 12:10:35.829531",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"description": "Track separate Income and Expense for product verticals or divisions.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cost-center",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"sb0",
|
||||
@@ -122,7 +123,7 @@
|
||||
"label": "Disabled"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"icon": "chart-pie",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "CC-ALLOC-.#####",
|
||||
"creation": "2022-01-13 20:07:29.871109",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/cost_center_allocation",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -65,6 +66,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "chart-pie",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -5,7 +5,10 @@ frappe.ui.form.on("Coupon Code", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("pricing_rule", function () {
|
||||
return {
|
||||
filters: [["Pricing Rule", "coupon_code_based", "=", "1"]],
|
||||
filters: {
|
||||
coupon_code_based: 1,
|
||||
disable: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2018-01-22 14:34:39.701832",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/coupon-code",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -124,6 +125,7 @@
|
||||
"label": "From External Ecomm Platform"
|
||||
}
|
||||
],
|
||||
"icon": "ticket-percent",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:41.010871",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -42,7 +42,23 @@ class CouponCode(Document):
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
def validate(self):
|
||||
self.validate_from_to_dates("valid_from", "valid_upto")
|
||||
self.validate_pricing_rule()
|
||||
|
||||
if self.coupon_type == "Gift Card":
|
||||
self.maximum_use = 1
|
||||
if not self.customer:
|
||||
frappe.throw(_("Please select the customer."))
|
||||
|
||||
def validate_pricing_rule(self):
|
||||
if not self.pricing_rule or self.from_external_ecomm_platform:
|
||||
return
|
||||
|
||||
# Allow existing coupons to be updated after their pricing rule is disabled.
|
||||
if not (
|
||||
self.has_value_changed("pricing_rule") or self.has_value_changed("from_external_ecomm_platform")
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.db.get_value("Pricing Rule", self.pricing_rule, "disable"):
|
||||
frappe.throw(_("Pricing Rule {0} is disabled").format(frappe.bold(self.pricing_rule)))
|
||||
|
||||
@@ -112,6 +112,43 @@ class TestCouponCode(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
test_create_test_data()
|
||||
|
||||
def test_disabled_pricing_rule_validation(self):
|
||||
coupon = frappe.get_doc("Coupon Code", "SAVE30")
|
||||
rule = frappe.get_doc("Pricing Rule", coupon.pricing_rule)
|
||||
rule.disable = 1
|
||||
rule.save()
|
||||
|
||||
with self.subTest("new coupon cannot select a disabled rule"):
|
||||
new_coupon = frappe.copy_doc(coupon)
|
||||
new_coupon.coupon_name = "Festival Savings"
|
||||
new_coupon.coupon_code = "FESTSAVE"
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
|
||||
new_coupon.insert()
|
||||
|
||||
with self.subTest("existing coupon can retain a disabled rule"):
|
||||
coupon.description = "Offer paused"
|
||||
coupon.save()
|
||||
coupon.reload()
|
||||
self.assertEqual(coupon.description, "Offer paused")
|
||||
self.assertEqual(coupon.pricing_rule, rule.name)
|
||||
|
||||
with self.subTest("existing coupon cannot switch to a disabled rule"):
|
||||
disabled_rule = frappe.copy_doc(rule)
|
||||
disabled_rule.insert()
|
||||
coupon.reload()
|
||||
coupon.pricing_rule = disabled_rule.name
|
||||
with self.assertRaisesRegex(frappe.ValidationError, "is disabled"):
|
||||
coupon.save()
|
||||
coupon.reload()
|
||||
self.assertEqual(coupon.pricing_rule, rule.name)
|
||||
|
||||
def test_cannot_save_coupon_with_reversed_validity_dates(self):
|
||||
coupon = frappe.get_doc("Coupon Code", "SAVE30")
|
||||
coupon.valid_from = "2026-09-17"
|
||||
coupon.valid_upto = "2026-09-02"
|
||||
with self.assertRaises(frappe.exceptions.InvalidDates):
|
||||
coupon.save()
|
||||
|
||||
def test_sales_order_with_coupon_code(self):
|
||||
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"actions": [],
|
||||
"creation": "2022-01-10 13:03:26.237081",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/currency-exchange-settings",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -101,6 +102,7 @@
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"icon": "refresh-cw",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/dunning",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
@@ -398,6 +399,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "bell-ring",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-30 23:18:04.712528",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "bell",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Dunning",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "ACC-ERR-.YYYY.-.#####",
|
||||
"creation": "2018-04-13 18:25:55.943587",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/exchange-rate-revaluation",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -107,6 +108,7 @@
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "arrow-right-left",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:42.951164",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2018-04-13 17:42:43.252224",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/finance-book",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -18,7 +19,7 @@
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-book",
|
||||
"icon": "book",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:42.386104",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2025-08-02 04:44:15.184541",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/financial-report-template",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"template_name",
|
||||
@@ -64,6 +65,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "file-spreadsheet",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-23 01:04:05.797161",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"description": "Represents a Financial Year. All accounting entries and other major transactions are tracked against the Fiscal Year.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/fiscal-year",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"year",
|
||||
@@ -79,7 +80,7 @@
|
||||
"set_only_once": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"icon": "calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:42.509102",
|
||||
|
||||
@@ -377,7 +377,7 @@
|
||||
"precision": "9"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-list",
|
||||
"icon": "book-open",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "ACC-INV-DISC-.YYYY.-.#####",
|
||||
"creation": "2019-03-07 12:01:56.296952",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/invoice_discounting",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -169,6 +170,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "ticket-percent",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-09-09 17:04:59.512294",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2022-01-19 01:09:13.297137",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/item-tax-template",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -56,6 +57,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"icon": "circle-percent",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:43.571355",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"creation": "2022-01-25 10:29:58.717206",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/journal-entry",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"entry_type_and_date",
|
||||
@@ -661,7 +662,7 @@
|
||||
"label": "Custom Remark"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"icon": "notebook-pen",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
|
||||
@@ -253,6 +253,10 @@ def make_reverse_journal_entry(source_name: str, target_doc: str | dict | Docume
|
||||
|
||||
def post_process(source, target) -> None:
|
||||
target.reversal_of = source.name
|
||||
target.naming_series = source.naming_series
|
||||
if source.voucher_type == "Bank Entry":
|
||||
target.cheque_no = source.cheque_no
|
||||
target.cheque_date = source.cheque_date
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Journal Entry",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"creation": "2020-04-09 01:32:51.332301",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/journal-entry-template",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -85,6 +86,7 @@
|
||||
"label": "Multi Currency"
|
||||
}
|
||||
],
|
||||
"icon": "notebook-text",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:58.814734",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"label": "General and Payment Ledger mismatch"
|
||||
}
|
||||
],
|
||||
"icon": "heart-pulse",
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "activity",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "merge",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:02.198009",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
"label": "Discretionary Reason"
|
||||
}
|
||||
],
|
||||
"icon": "star",
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-01 08:51:13.927009",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "field:loyalty_program_name",
|
||||
"creation": "2018-01-23 06:23:05.731431",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/loyalty-program",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -153,6 +154,7 @@
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "gift",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:44.144864",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"creation": "2012-12-04 17:49:20",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/mode-of-payment",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"mode_of_payment",
|
||||
@@ -44,7 +45,7 @@
|
||||
"label": "Enabled"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-credit-card",
|
||||
"icon": "wallet",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"options": "Monthly Distribution Percentage"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bar-chart",
|
||||
"icon": "chart-bar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:44.908490",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"allow_copy": 1,
|
||||
"creation": "2017-08-29 02:22:54.947711",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/opening-invoice-creation-tool",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -88,6 +89,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "file-plus",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-30 23:18:48.691227",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"options": "secondary_role"
|
||||
}
|
||||
],
|
||||
"icon": "link",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:08.607170",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2016-06-01 14:38:51.012597",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-entry",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"type_of_payment",
|
||||
@@ -784,6 +785,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "banknote",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "credit-card",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-14 16:49:55.210352",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"icon": "book-open",
|
||||
"is_submittable": 1,
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2018-07-20 16:43:08.505978",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-order",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -107,6 +108,7 @@
|
||||
"label": "Account"
|
||||
}
|
||||
],
|
||||
"icon": "send",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:10.186727",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"allow_copy": 1,
|
||||
"creation": "2014-07-09 12:04:51.681583",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-reconciliation",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
@@ -235,7 +236,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"icon": "arrow-left-right",
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2015-12-15 22:23:24.745065",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-request",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"payment_request_type",
|
||||
@@ -474,6 +475,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "hand-coins",
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "field:payment_term_name",
|
||||
"creation": "2017-08-10 15:24:54.876365",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-terms",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -115,6 +116,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "calendar-clock",
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:45.693762",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "field:template_name",
|
||||
"creation": "2017-08-10 15:34:28.058054",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/payment-terms-template",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -34,6 +35,7 @@
|
||||
"label": "Allocate Payment Based On Payment Terms"
|
||||
}
|
||||
],
|
||||
"icon": "calendar-clock",
|
||||
"links": [],
|
||||
"modified": "2024-04-07 11:26:42.021585",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "anchor",
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "ACC-PCV-.YYYY.-.#####",
|
||||
"creation": "2013-01-10 16:34:07",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/period-closing-voucher",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"transaction_date",
|
||||
@@ -110,7 +111,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"icon": "folder-lock",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -280,12 +280,13 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
data = self.get_data_for_mapreduce()
|
||||
mapreduce(
|
||||
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.mapper",
|
||||
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.reducer",
|
||||
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.process_date_range",
|
||||
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.aggregate_partial_result",
|
||||
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.summarize_and_post_ledger",
|
||||
data,
|
||||
self.doctype,
|
||||
self.name,
|
||||
f"Closing FY {self.fiscal_year}",
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -330,8 +331,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
def make_gl_entries(self):
|
||||
if frappe.db.estimate_count("GL Entry") > 100_000:
|
||||
frappe.enqueue(
|
||||
process_gl_and_closing_entries,
|
||||
frappe.enqueue_task(
|
||||
method=process_gl_and_closing_entries,
|
||||
doc=self,
|
||||
timeout=1800,
|
||||
)
|
||||
@@ -834,7 +835,7 @@ def get_previous_closed_period_in_current_year(fiscal_year, company):
|
||||
return prev_closed_period_end_date
|
||||
|
||||
|
||||
def mapper(val):
|
||||
def process_date_range(val):
|
||||
start_date = val.from_date
|
||||
end_date = val.to_date
|
||||
pcv = val.pcv
|
||||
@@ -881,7 +882,7 @@ def mapper(val):
|
||||
return res
|
||||
|
||||
|
||||
def reducer(final, partial_res):
|
||||
def aggregate_partial_result(final, partial_res):
|
||||
if final is None:
|
||||
final = []
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.listview_settings["Period Closing Voucher"] = {
|
||||
add_fields: ["gle_processing_status"],
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Draft: "red",
|
||||
Submitted: "blue",
|
||||
Cancelled: "red",
|
||||
};
|
||||
|
||||
const gle_processing_status = {
|
||||
"In Progress": [__("Processing GL Entries"), "blue"],
|
||||
Completed: [__("Period Closed"), "green"],
|
||||
Failed: [__("Period Closing Failed"), "red"],
|
||||
};
|
||||
if (doc.docstatus != 0) {
|
||||
return [
|
||||
gle_processing_status[doc.gle_processing_status][0],
|
||||
gle_processing_status[doc.gle_processing_status][1],
|
||||
"gle_processing_status,=," + doc.gle_processing_status,
|
||||
];
|
||||
}
|
||||
|
||||
return [__(doc.docstatus), status_colors[doc.docstatus], "docstatus,=," + doc.docstatus];
|
||||
},
|
||||
};
|
||||
@@ -254,6 +254,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "book-lock",
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
{
|
||||
|
||||
@@ -1640,7 +1640,7 @@
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"icon": "receipt-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:45.029925",
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "merge",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -9,6 +9,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
@@ -16,7 +17,6 @@ from frappe.utils.scheduler import is_scheduler_inactive
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||
|
||||
|
||||
class POSInvoiceMergeLog(Document):
|
||||
@@ -214,6 +214,8 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
|
||||
|
||||
reversed_rows = get_reversed_rows([doc.return_against for doc in data if doc.is_return])
|
||||
|
||||
for doc in data:
|
||||
old_new_item_map = frappe._dict()
|
||||
old_new_tax_map = frappe._dict()
|
||||
@@ -238,9 +240,13 @@ class POSInvoiceMergeLog(Document):
|
||||
si_item.pos_invoice = doc.name
|
||||
si_item.pos_invoice_item = item.name
|
||||
if doc.is_return:
|
||||
si_item.sales_invoice_item = get_sales_invoice_item_from_consolidated_invoice(
|
||||
doc.return_against, item.pos_invoice_item
|
||||
)
|
||||
reversed_row = reversed_rows.get(item.pos_invoice_item) or frappe._dict()
|
||||
si_item.sales_invoice_item = reversed_row.get("name")
|
||||
# quote the rate of the row being reversed: rounding an invoice-level discount
|
||||
# can leave a return's net rate a minor unit above the sale's, and
|
||||
# validate_returned_items refuses a return priced above its original
|
||||
if si_item.sales_invoice_item:
|
||||
si_item.rate = reversed_row.rate
|
||||
if item.serial_and_batch_bundle:
|
||||
si_item.serial_and_batch_bundle = item.serial_and_batch_bundle
|
||||
items.append(si_item)
|
||||
@@ -432,6 +438,28 @@ class POSInvoiceMergeLog(Document):
|
||||
si.cancel()
|
||||
|
||||
|
||||
def get_reversed_rows(return_against):
|
||||
"""Rows of the consolidated sales these returns reverse, keyed by the POS invoice row."""
|
||||
if not return_against:
|
||||
return {}
|
||||
|
||||
sales_invoice = DocType("Sales Invoice")
|
||||
sales_invoice_item = DocType("Sales Invoice Item")
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(sales_invoice)
|
||||
.from_(sales_invoice_item)
|
||||
.select(sales_invoice_item.name, sales_invoice_item.rate, sales_invoice_item.pos_invoice_item)
|
||||
.where(
|
||||
(sales_invoice.name == sales_invoice_item.parent)
|
||||
& (sales_invoice.is_return == 0)
|
||||
& (sales_invoice_item.pos_invoice.isin(return_against))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return {row.pos_invoice_item: row for row in rows}
|
||||
|
||||
|
||||
def get_all_unconsolidated_invoices():
|
||||
filters = {
|
||||
"consolidated_invoice": ["in", ["", None]],
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.mode_of_payment.test_mode_of_payment import (
|
||||
set_default_account_for_mode_of_payment,
|
||||
@@ -20,6 +23,67 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rounding_method(method):
|
||||
"""System Settings is also cached on frappe.local, so that copy has to go as well."""
|
||||
previous = frappe.db.get_single_value("System Settings", "rounding_method")
|
||||
try:
|
||||
frappe.db.set_single_value("System Settings", "rounding_method", method)
|
||||
frappe.local.system_settings = None
|
||||
yield
|
||||
finally:
|
||||
frappe.db.set_single_value("System Settings", "rounding_method", previous)
|
||||
frappe.local.system_settings = None
|
||||
|
||||
|
||||
def sell_over_the_counter(lines, discount_percentage=0):
|
||||
item_code, qty, rate = lines[0]
|
||||
sale = create_pos_invoice(item_code=item_code, qty=qty, rate=rate, do_not_save=True)
|
||||
for item_code, qty, rate in lines[1:]:
|
||||
sale.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": qty,
|
||||
"rate": rate,
|
||||
"price_list_rate": rate,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
if discount_percentage:
|
||||
sale.apply_discount_on = "Net Total"
|
||||
sale.additional_discount_percentage = discount_percentage
|
||||
|
||||
sale.run_method("calculate_taxes_and_totals")
|
||||
payable = sale.rounded_total or sale.grand_total
|
||||
sale.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": payable})
|
||||
sale.paid_amount = sale.base_paid_amount = payable
|
||||
sale.insert()
|
||||
sale.submit()
|
||||
return sale
|
||||
|
||||
|
||||
def refund_over_the_counter(sale, qty=None):
|
||||
"""Hand back every line of `sale`, `qty` of each when fewer units come back."""
|
||||
note = make_sales_return(sale.name)
|
||||
if qty is not None:
|
||||
for item in note.items:
|
||||
item.qty = qty
|
||||
|
||||
note.run_method("calculate_taxes_and_totals")
|
||||
refundable = note.rounded_total or note.grand_total
|
||||
note.payments[0].amount = refundable
|
||||
for spare in note.payments[1:]:
|
||||
spare.amount = 0
|
||||
note.paid_amount = note.base_paid_amount = refundable
|
||||
note.insert()
|
||||
note.submit()
|
||||
return note
|
||||
|
||||
|
||||
class TestPOSInvoiceMergeLog(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
mode_of_payment = frappe.get_doc("Mode of Payment", "Bank Draft")
|
||||
@@ -478,3 +542,81 @@ class TestPOSInvoiceMergeLog(ERPNextTestSuite):
|
||||
"POS Invoice Merge Log", {"pos_closing_entry": closing_entry.name}, "company"
|
||||
)
|
||||
self.assertEqual(pos_merge_log_company, closing_entry.company)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Selling Settings", {"allow_multiple_items": 1})
|
||||
def test_consolidating_returns_priced_off_a_rounded_invoice_discount(self):
|
||||
"""A return works out its own share of an invoice-level discount, so rounding can leave
|
||||
it a minor unit above the sale's, and validate_returned_items then refuses it.
|
||||
|
||||
Every shape that reaches a consolidated credit note goes through one closing entry:
|
||||
a split landing on a half minor unit, the same item on two rows so the rows can only
|
||||
be paired through sales_invoice_item, fewer units coming back than went out, and — as
|
||||
a control — a sale with no invoice-level discount to split at all.
|
||||
"""
|
||||
for item_code in ("_Test Item", "_Test Item 2"):
|
||||
make_stock_entry(to_warehouse="_Test Warehouse - _TC", item_code=item_code, rate=100, qty=40)
|
||||
|
||||
with rounding_method("Banker's Rounding (legacy)"):
|
||||
tied = sell_over_the_counter(
|
||||
[("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)], discount_percentage=25
|
||||
)
|
||||
repeated = sell_over_the_counter(
|
||||
[("_Test Item", 1, 42.86), ("_Test Item", 1, 57.14)], discount_percentage=25
|
||||
)
|
||||
oversold = sell_over_the_counter(
|
||||
[("_Test Item", 3, 42.86), ("_Test Item 2", 3, 57.14)], discount_percentage=25
|
||||
)
|
||||
undiscounted = sell_over_the_counter([("_Test Item", 1, 42.86), ("_Test Item 2", 1, 57.14)])
|
||||
|
||||
# the sale and the return really do round the split apart
|
||||
self.assertEqual(
|
||||
{item.item_code: item.net_rate for item in tied.items},
|
||||
{"_Test Item": 32.15, "_Test Item 2": 42.85},
|
||||
)
|
||||
returns = [
|
||||
refund_over_the_counter(tied),
|
||||
refund_over_the_counter(repeated),
|
||||
refund_over_the_counter(oversold, qty=-1),
|
||||
refund_over_the_counter(undiscounted),
|
||||
]
|
||||
self.assertEqual(
|
||||
{item.item_code: item.net_rate for item in returns[0].items},
|
||||
{"_Test Item": 32.14, "_Test Item 2": 42.86},
|
||||
)
|
||||
|
||||
self.make_closing_entry()
|
||||
|
||||
for pos_invoice in [tied, repeated, oversold, undiscounted, *returns]:
|
||||
pos_invoice.load_from_db()
|
||||
self.assertTrue(
|
||||
frappe.db.exists("Sales Invoice", pos_invoice.consolidated_invoice),
|
||||
f"{pos_invoice.name} was not consolidated",
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Sales Invoice", pos_invoice.consolidated_invoice, "outstanding_amount"),
|
||||
0,
|
||||
)
|
||||
|
||||
for note in returns:
|
||||
# no returned row may be priced above the row it reverses
|
||||
for row in frappe.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": note.consolidated_invoice},
|
||||
fields=["item_code", "rate", "sales_invoice_item"],
|
||||
):
|
||||
self.assertTrue(row.sales_invoice_item, f"{row.item_code} lost its link to the sale")
|
||||
sold_rate = frappe.db.get_value("Sales Invoice Item", row.sales_invoice_item, "rate")
|
||||
self.assertLessEqual(row.rate, sold_rate)
|
||||
|
||||
# returns for one customer land on a single credit note, which still adds up to
|
||||
# everything handed back over the counter
|
||||
refunded = {}
|
||||
for note in returns:
|
||||
refunded[note.consolidated_invoice] = refunded.get(note.consolidated_invoice, 0) + flt(
|
||||
note.grand_total
|
||||
)
|
||||
for consolidated_name, handed_back in refunded.items():
|
||||
self.assertEqual(
|
||||
flt(frappe.db.get_value("Sales Invoice", consolidated_name, "grand_total"), 2),
|
||||
flt(handed_back, 2),
|
||||
)
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "folder-open",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:16.034296",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "Prompt",
|
||||
"creation": "2013-05-24 12:15:51",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/pos-profile",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
@@ -557,7 +558,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "icon-cog",
|
||||
"icon": "monitor",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 0,
|
||||
"icon": "settings",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-03-16 13:28:19.677217",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2014-02-21 15:02:51",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/pricing-rule",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"applicability_section",
|
||||
@@ -654,7 +655,7 @@
|
||||
"label": "Don't Enforce Free Item Qty"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-gift",
|
||||
"icon": "badge-percent",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-17 12:24:07.553505",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "ACC-PDA-.#####",
|
||||
"creation": "2019-11-04 18:01:23.454775",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/process-deferred-accounting",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -74,6 +75,7 @@
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"icon": "list-clock",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"icon": "scale",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "file-clock",
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "lock",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-05-22 16:46:18.712954",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/process-statement-of-accounts",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -424,6 +425,7 @@
|
||||
"label": "Show Opening Entries"
|
||||
}
|
||||
],
|
||||
"icon": "mail",
|
||||
"links": [],
|
||||
"modified": "2026-06-01 15:37:07.660442",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"options": "Subscription"
|
||||
}
|
||||
],
|
||||
"icon": "repeat",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"autoname": "Prompt",
|
||||
"creation": "2019-02-08 17:10:36.077402",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/promotional-scheme",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -279,6 +280,7 @@
|
||||
"options": "Promotional Scheme Product Discount"
|
||||
}
|
||||
],
|
||||
"icon": "megaphone",
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:48:23.231081",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"creation": "2013-05-21 16:16:39",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/purchase-invoice",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -1692,7 +1693,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"icon": "receipt-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -216,6 +216,11 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
if doc.is_internal_supplier and item.valuation_rate:
|
||||
credit_amount = flt(item.valuation_rate * item.stock_qty)
|
||||
|
||||
rejected_amount = self.make_rejected_warehouse_gl_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, inventory_account_map
|
||||
)
|
||||
credit_amount += rejected_amount
|
||||
|
||||
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -251,6 +256,10 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
)
|
||||
|
||||
else:
|
||||
self.make_rejected_warehouse_gl_entry(
|
||||
gl_entries, item, voucher_wise_stock_value, inventory_account_map
|
||||
)
|
||||
|
||||
if not doc.is_internal_transfer():
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -564,6 +573,49 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
|
||||
return stock_asset_rbnb or item.expense_account
|
||||
|
||||
def make_rejected_warehouse_gl_entry(
|
||||
self, gl_entries, item, voucher_wise_stock_value, inventory_account_map
|
||||
) -> float:
|
||||
"""Book the material the invoice moved into the rejected warehouse.
|
||||
|
||||
An internal transfer carries the value credited out of the in-transit warehouse along with
|
||||
the accepted material, so the entry against it is that warehouse, and the caller credits it
|
||||
for both. On an ordinary invoice the supplier entry already holds the cost.
|
||||
"""
|
||||
doc = self.doc
|
||||
if not (item.rejected_warehouse and flt(item.rejected_qty)):
|
||||
return 0.0
|
||||
|
||||
transfers_rejected_material = doc.is_internal_transfer()
|
||||
|
||||
rejected_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.rejected_warehouse)),
|
||||
item.precision("base_net_amount"),
|
||||
)
|
||||
if not rejected_amount:
|
||||
return 0.0
|
||||
|
||||
rejected_account = doc.get_inventory_account_dict(item, inventory_account_map, "rejected_warehouse")
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": rejected_account["account"],
|
||||
"against": item.expense_account if transfers_rejected_material else doc.supplier,
|
||||
"cost_center": item.cost_center,
|
||||
"project": item.project or doc.project,
|
||||
"remarks": doc.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": rejected_amount,
|
||||
"debit_in_transaction_currency": flt(
|
||||
rejected_amount / doc.conversion_rate, item.precision("net_amount")
|
||||
),
|
||||
},
|
||||
rejected_account["account_currency"],
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
return rejected_amount if transfers_rejected_material else 0.0
|
||||
|
||||
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
|
||||
doc = self.doc
|
||||
net_amt_precision = item.precision("base_net_amount")
|
||||
@@ -577,16 +629,24 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
|
||||
if doc.is_return and doc.update_stock and (doc.is_internal_supplier or not doc.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate:
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
# Material of a transfer goes back at the rate it came in with, the rejected
|
||||
# material along with the accepted.
|
||||
net_rate = (flt(item.qty) + flt(item.rejected_qty)) * item.sales_incoming_rate
|
||||
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
warehouse_debit_amount = flt(
|
||||
voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
# The rejected warehouse carries the rest of what the invoice paid for, and is booked
|
||||
# by its own entry, so it is not a variance.
|
||||
returned_stock_value = warehouse_debit_amount + flt(
|
||||
voucher_wise_stock_value.get((item.name, item.rejected_warehouse)), net_amt_precision
|
||||
)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(returned_stock_value, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_stock_variance_account(item)
|
||||
stock_adjustment_amt = stock_amount - warehouse_debit_amount
|
||||
stock_adjustment_amt = stock_amount - returned_stock_value
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
|
||||
@@ -844,7 +844,9 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
)
|
||||
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0].base_net_amount or 0
|
||||
|
||||
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
|
||||
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name, do_not_save=True)
|
||||
pi.credit_to = "_Test Payable USD - _TC"
|
||||
pi.submit()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Project", project.name, "total_purchase_cost"),
|
||||
existing_purchase_cost + 15000,
|
||||
@@ -856,12 +858,14 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
existing_purchase_cost + 15500,
|
||||
)
|
||||
|
||||
pi1.reload()
|
||||
pi1.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Project", project.name, "total_purchase_cost"),
|
||||
existing_purchase_cost + 15000,
|
||||
)
|
||||
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost
|
||||
@@ -2624,6 +2628,582 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
|
||||
return_pi.submit()
|
||||
self.assertEqual(return_pi.docstatus, 1)
|
||||
|
||||
def test_internal_transfer_invoice_with_rejected_qty(self):
|
||||
"""An invoice that updates stock moves rejected material out of the in-transit warehouse and
|
||||
books it, like a receipt does."""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
get_gl_entries,
|
||||
make_purchase_receipt,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Invoice Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Invoice Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Invoice Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Transfer Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Invoice Internal Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code=item_doc.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
update_stock=1,
|
||||
cost_center="Main - TCP1",
|
||||
debit_to="Debtors - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.items[0].target_warehouse = transit_warehouse
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].warehouse = to_warehouse
|
||||
pi.items[0].qty = 7
|
||||
pi.items[0].rejected_qty = 3
|
||||
pi.items[0].received_qty = 10
|
||||
pi.items[0].rejected_warehouse = rejected_warehouse
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - TCP1"
|
||||
pi.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
|
||||
stock_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
self.assertEqual(stock_qty[transit_warehouse], -10)
|
||||
self.assertEqual(stock_qty[to_warehouse], 7)
|
||||
self.assertEqual(stock_qty[rejected_warehouse], 3)
|
||||
|
||||
booked_value = {}
|
||||
for entry in get_gl_entries("Purchase Invoice", pi.name, skip_cancelled=True):
|
||||
booked_value.setdefault(entry.account, 0)
|
||||
booked_value[entry.account] += flt(entry.debit) - flt(entry.credit)
|
||||
|
||||
self.assertEqual(flt(sum(booked_value.values()), 2), 0)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, transit_warehouse)], -1000)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, to_warehouse)], 700)
|
||||
self.assertEqual(booked_value[get_inventory_account(company, rejected_warehouse)], 300)
|
||||
|
||||
def test_internal_transfer_invoice_with_rejected_batch_qty(self):
|
||||
"""Batch material rejected on a stock updating internal transfer invoice gets its own package."""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
make_purchase_receipt,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Batch Invoice Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Batch Invoice Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Batch Invoice Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Batch Invoice Transfer Rejected", company=company)
|
||||
|
||||
item = make_item(
|
||||
"Test Invoice Internal Transfer Batch Item",
|
||||
{
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TIITB-.####",
|
||||
},
|
||||
)
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code=item.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
update_stock=1,
|
||||
cost_center="Main - TCP1",
|
||||
debit_to="Debtors - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.items[0].target_warehouse = transit_warehouse
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].warehouse = to_warehouse
|
||||
pi.items[0].qty = 7
|
||||
pi.items[0].rejected_qty = 3
|
||||
pi.items[0].received_qty = 10
|
||||
pi.items[0].rejected_warehouse = rejected_warehouse
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - TCP1"
|
||||
pi.submit()
|
||||
|
||||
row = pi.items[0]
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.serial_and_batch_bundle, "total_qty"), 7
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("Serial and Batch Bundle", row.rejected_serial_and_batch_bundle, "total_qty"),
|
||||
3,
|
||||
)
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
moved_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
moved_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertEqual(moved_qty[transit_warehouse], -10)
|
||||
self.assertEqual(moved_qty[to_warehouse], 7)
|
||||
self.assertEqual(moved_qty[rejected_warehouse], 3)
|
||||
self.assertEqual(flt(moved_value[transit_warehouse]), -1000)
|
||||
self.assertEqual(flt(moved_value[to_warehouse]), 700)
|
||||
self.assertEqual(flt(moved_value[rejected_warehouse]), 300)
|
||||
|
||||
def test_internal_transfer_invoice_with_every_unit_rejected(self):
|
||||
"""An invoice may reject a whole row, and the in-transit warehouse is emptied all the same."""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
make_purchase_receipt,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Rejected Invoice From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Rejected Invoice Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Rejected Invoice To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Rejected Invoice Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Fully Rejected Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code=item_doc.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
update_stock=1,
|
||||
cost_center="Main - TCP1",
|
||||
debit_to="Debtors - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.items[0].target_warehouse = transit_warehouse
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].warehouse = to_warehouse
|
||||
pi.items[0].qty = 0
|
||||
pi.items[0].rejected_qty = 10
|
||||
pi.items[0].received_qty = 10
|
||||
pi.items[0].rejected_warehouse = rejected_warehouse
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - TCP1"
|
||||
pi.submit()
|
||||
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty", "stock_value_difference"],
|
||||
)
|
||||
moved_qty = {d.warehouse: d.actual_qty for d in sl_entries}
|
||||
moved_value = {d.warehouse: d.stock_value_difference for d in sl_entries}
|
||||
|
||||
self.assertNotIn(to_warehouse, moved_qty)
|
||||
self.assertEqual(moved_qty[transit_warehouse], -10)
|
||||
self.assertEqual(moved_qty[rejected_warehouse], 10)
|
||||
self.assertEqual(flt(moved_value[transit_warehouse]), -1000)
|
||||
self.assertEqual(flt(moved_value[rejected_warehouse]), 1000)
|
||||
|
||||
def test_return_of_an_internal_transfer_invoice_that_rejected_everything(self):
|
||||
"""Material rejected in full goes back to the in-transit warehouse at the rate it came in
|
||||
with, and the entries say the same as the stock."""
|
||||
from erpnext.accounts.doctype.sales_invoice.mapper import make_inter_company_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
make_purchase_receipt,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Returned Transfer From", company=company)
|
||||
transit_warehouse = create_warehouse("_Test Returned Transfer Transit", company=company)
|
||||
to_warehouse = create_warehouse("_Test Returned Transfer To", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Returned Transfer Rejected", company=company)
|
||||
|
||||
item_doc = create_item("Test Returned Fully Rejected Transfer Item")
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name, company=company, warehouse=from_warehouse, qty=10, rate=100
|
||||
)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company=company,
|
||||
customer="_Test Internal Customer 2",
|
||||
item_code=item_doc.name,
|
||||
qty=10,
|
||||
rate=100,
|
||||
warehouse=from_warehouse,
|
||||
update_stock=1,
|
||||
cost_center="Main - TCP1",
|
||||
debit_to="Debtors - TCP1",
|
||||
income_account="Sales - TCP1",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.items[0].target_warehouse = transit_warehouse
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].warehouse = to_warehouse
|
||||
pi.items[0].qty = 0
|
||||
pi.items[0].rejected_qty = 10
|
||||
pi.items[0].received_qty = 10
|
||||
pi.items[0].rejected_warehouse = rejected_warehouse
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - TCP1"
|
||||
pi.submit()
|
||||
|
||||
returned = make_return_doc("Purchase Invoice", pi.name)
|
||||
returned.update_stock = 1
|
||||
returned.submit()
|
||||
|
||||
moved = {
|
||||
d.warehouse: flt(d.stock_value_difference)
|
||||
for d in frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": returned.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "stock_value_difference"],
|
||||
)
|
||||
}
|
||||
self.assertEqual(moved[transit_warehouse], 1000)
|
||||
self.assertEqual(moved[rejected_warehouse], -1000)
|
||||
|
||||
booked = {}
|
||||
for entry in frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": returned.name, "is_cancelled": 0},
|
||||
fields=["account", "debit", "credit"],
|
||||
):
|
||||
booked.setdefault(entry.account, 0)
|
||||
booked[entry.account] += flt(entry.debit) - flt(entry.credit)
|
||||
|
||||
self.assertEqual(flt(sum(booked.values()), 2), 0)
|
||||
self.assertEqual(booked[get_inventory_account(company, transit_warehouse)], 1000)
|
||||
self.assertEqual(booked[get_inventory_account(company, rejected_warehouse)], -1000)
|
||||
|
||||
def test_stock_updating_invoice_rejects_every_unit_of_a_row(self):
|
||||
"""A row of a stock updating invoice may be rejected in full."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
rejected_warehouse = create_warehouse("_Test Fully Rejected Invoice Warehouse", company=company)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company=company,
|
||||
item_code=item,
|
||||
warehouse="Stores - TCP1",
|
||||
qty=0,
|
||||
rejected_qty=10,
|
||||
received_qty=10,
|
||||
rate=100,
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
update_stock=1,
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
do_not_save=True,
|
||||
)
|
||||
pi.submit()
|
||||
|
||||
moved_qty = {
|
||||
d.warehouse: d.actual_qty
|
||||
for d in frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "actual_qty"],
|
||||
)
|
||||
}
|
||||
self.assertEqual(moved_qty, {rejected_warehouse: 10})
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"bill_for_rejected_quantity_in_purchase_invoice": 1,
|
||||
"set_valuation_rate_for_rejected_materials": 1,
|
||||
},
|
||||
)
|
||||
def test_stock_updating_invoice_bills_the_rejected_quantity(self):
|
||||
"""With the rejected quantity billed and valued, the invoice pays for every unit received and
|
||||
the stock it moves matches the entries it books."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Billed Rejected Warehouse", company=company)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=item,
|
||||
company=company,
|
||||
warehouse="Stores - TCP1",
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
cost_center="Main - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
expense_account="_Test Account Cost for Goods Sold - TCP1",
|
||||
update_stock=1,
|
||||
received_qty=10,
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
self.assertEqual(pi.items[0].amount, 1000)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
stock_value = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "stock_value_difference"],
|
||||
)
|
||||
by_warehouse = {d.warehouse: d.stock_value_difference for d in stock_value}
|
||||
|
||||
self.assertEqual(by_warehouse["Stores - TCP1"], 600)
|
||||
self.assertEqual(by_warehouse[rejected_warehouse], 400)
|
||||
|
||||
booked = frappe.get_all(
|
||||
"GL Entry", filters={"voucher_no": pi.name, "is_cancelled": 0}, fields=["debit"]
|
||||
)
|
||||
self.assertEqual(sum(flt(d.debit) for d in booked), 1000)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"bill_for_rejected_quantity_in_purchase_invoice": 1,
|
||||
"set_valuation_rate_for_rejected_materials": 1,
|
||||
},
|
||||
)
|
||||
def test_rejected_material_is_reposted_after_the_setting_changes(self):
|
||||
"""The entries an invoice books follow the stock it moved, so they can be built again once
|
||||
the settings have moved on."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Repost Rejected", company=company)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company=company,
|
||||
item_code=item,
|
||||
warehouse="Stores - TCP1",
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
received_qty=10,
|
||||
rate=100,
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
update_stock=1,
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice", 0)
|
||||
frappe.db.set_single_value("Buying Settings", "set_valuation_rate_for_rejected_materials", 0)
|
||||
|
||||
rebuilt = pi.get_gl_entries()
|
||||
rejected_account = get_inventory_account(company, rejected_warehouse)
|
||||
|
||||
self.assertEqual(
|
||||
flt(sum(flt(entry.get("debit")) - flt(entry.get("credit")) for entry in rebuilt), 2), 0
|
||||
)
|
||||
self.assertEqual(
|
||||
flt(
|
||||
sum(flt(entry.get("debit")) for entry in rebuilt if entry.get("account") == rejected_account)
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"bill_for_rejected_quantity_in_purchase_invoice": 1,
|
||||
"set_valuation_rate_for_rejected_materials": 1,
|
||||
},
|
||||
)
|
||||
def test_return_without_a_reference_books_both_warehouses(self):
|
||||
"""A return that stands on its own gives back the rejected material too, and books it once."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
accepted_warehouse = create_warehouse("_Test Invoice Return Accepted", company=company)
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Return Rejected", company=company)
|
||||
|
||||
def make_invoice(sign):
|
||||
return make_purchase_invoice(
|
||||
company=company,
|
||||
item_code=item,
|
||||
warehouse=accepted_warehouse,
|
||||
qty=6 * sign,
|
||||
rejected_qty=4 * sign,
|
||||
received_qty=10 * sign,
|
||||
rate=100,
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
update_stock=1,
|
||||
is_return=1 if sign < 0 else 0,
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
)
|
||||
|
||||
make_invoice(1)
|
||||
returned = make_invoice(-1)
|
||||
|
||||
booked = {}
|
||||
for entry in frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": returned.name, "is_cancelled": 0},
|
||||
fields=["account", "debit", "credit"],
|
||||
):
|
||||
booked.setdefault(entry.account, 0)
|
||||
booked[entry.account] += flt(entry.debit) - flt(entry.credit)
|
||||
|
||||
self.assertEqual(flt(sum(booked.values()), 2), 0)
|
||||
self.assertEqual(booked[get_inventory_account(company, accepted_warehouse)], -600)
|
||||
self.assertEqual(booked[get_inventory_account(company, rejected_warehouse)], -400)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"bill_for_rejected_quantity_in_purchase_invoice": 1,
|
||||
"set_valuation_rate_for_rejected_materials": 1,
|
||||
},
|
||||
)
|
||||
def test_discount_on_an_invoice_that_bills_the_rejected_quantity(self):
|
||||
"""A discount is spread over every unit the invoice pays for, not the accepted ones alone."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Discount Rejected", company=company)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company=company,
|
||||
item_code=item,
|
||||
warehouse="Stores - TCP1",
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
received_qty=10,
|
||||
rate=100,
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
update_stock=1,
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
cost_center="Main - TCP1",
|
||||
do_not_save=True,
|
||||
)
|
||||
pi.apply_discount_on = "Net Total"
|
||||
pi.additional_discount_percentage = 10
|
||||
pi.submit()
|
||||
|
||||
self.assertEqual(pi.items[0].amount, 1000)
|
||||
self.assertEqual(pi.items[0].net_rate, 90)
|
||||
self.assertEqual(pi.grand_total, 900)
|
||||
self.assertEqual(frappe.db.get_value("Item", item, "last_purchase_rate"), 90)
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Buying Settings",
|
||||
{
|
||||
"set_valuation_rate_for_rejected_materials": 1,
|
||||
"bill_for_rejected_quantity_in_purchase_invoice": 0,
|
||||
},
|
||||
)
|
||||
def test_rejected_material_is_not_valued_on_a_stock_updating_invoice(self):
|
||||
"""An invoice that does not bill the rejected quantity has nothing to pay for that material,
|
||||
so it carries no cost and the stock the invoice moves matches the entries it books."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
item = make_item(properties={"is_stock_item": 1, "valuation_method": "FIFO"}).name
|
||||
rejected_warehouse = create_warehouse("_Test Invoice Rejected Warehouse", company=company)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=item,
|
||||
company=company,
|
||||
warehouse="Stores - TCP1",
|
||||
rejected_warehouse=rejected_warehouse,
|
||||
cost_center="Main - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
expense_account="_Test Account Cost for Goods Sold - TCP1",
|
||||
update_stock=1,
|
||||
received_qty=10,
|
||||
qty=6,
|
||||
rejected_qty=4,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
self.assertEqual(pi.items[0].amount, 600)
|
||||
|
||||
stock_value = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_no": pi.name, "is_cancelled": 0},
|
||||
fields=["warehouse", "stock_value_difference"],
|
||||
)
|
||||
by_warehouse = {d.warehouse: d.stock_value_difference for d in stock_value}
|
||||
|
||||
self.assertEqual(by_warehouse["Stores - TCP1"], 600)
|
||||
self.assertEqual(by_warehouse[rejected_warehouse], 0)
|
||||
|
||||
booked = frappe.get_all(
|
||||
"GL Entry", filters={"voucher_no": pi.name, "is_cancelled": 0}, fields=["debit"]
|
||||
)
|
||||
self.assertEqual(sum(flt(d.debit) for d in booked), sum(by_warehouse.values()))
|
||||
|
||||
def test_purchase_invoice_with_use_serial_batch_field_for_rejected_qty(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain a list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\", etc.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/purchase-taxes-and-charges-template",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
@@ -74,7 +75,7 @@
|
||||
"options": "Tax Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"icon": "badge-percent",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:47.506282",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"creation": "2023-07-04 13:07:32.923675",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/repost-accounting-ledger",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -101,6 +102,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "list-restart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"label": "Repost Error Log"
|
||||
}
|
||||
],
|
||||
"icon": "list-restart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2022-01-25 10:29:57.771398",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/sales-invoice",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"customer_section",
|
||||
@@ -2395,7 +2396,7 @@
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-file-text",
|
||||
"icon": "receipt-text",
|
||||
"idx": 181,
|
||||
"is_submittable": 1,
|
||||
"links": [
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"description": "Standard tax template that can be applied to all Sales Transactions. This template can contain a list of tax heads and also other expense/income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/sales-taxes-and-charges-template",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
@@ -76,7 +77,7 @@
|
||||
"options": "Tax Category"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"icon": "badge-percent",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:48.797423",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "ACC-SHT-.YYYY.-.#####",
|
||||
"creation": "2017-12-25 17:18:03.143726",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/share-transfer",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -190,6 +191,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "arrow-right-left",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:40.003178",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"icon": "tag",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:40.190914",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2017-12-25 16:50:53.878430",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/shareholder",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -112,6 +113,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "user-star",
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:40.356171",
|
||||
"modified_by": "Administrator",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"creation": "2013-06-25 11:48:03",
|
||||
"description": "Specify conditions to calculate shipping amount",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/shipping-rule",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"label",
|
||||
@@ -146,7 +147,7 @@
|
||||
"options": "Project"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"icon": "truck",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-21 23:11:49.532098",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"autoname": "ACC-SUB-.YYYY.-.#####",
|
||||
"creation": "2017-07-18 17:50:43.967266",
|
||||
"doctype": "DocType",
|
||||
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/subscription",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
@@ -299,6 +300,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"icon": "calendar-sync",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user