Compare commits

..

36 Commits

Author SHA1 Message Date
Mihir Kandoi
a60fa59a78 test(accounts): cover an internal transfer invoice that rejects material 2026-09-22 10:12:07 +05:30
Mihir Kandoi
3b8740e455 feat(stock): move rejected material of an internal transfer on an invoice too
An invoice that updates stock moves the same material as a receipt, so it was
left with the fault the receipt no longer has: the rejected material stayed in
the in-transit warehouse and was counted in the rejected warehouse as well.

Its GL entries had no accounting for rejected material at all, which is why the
earlier layers stopped at the receipt. The invoice books the rejected warehouse
now and credits the in-transit warehouse for the accepted and the rejected
material together, so the qty, the value and the entries agree.
2026-09-22 10:12:06 +05:30
Mihir Kandoi
6d1f10ab22 Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 18:21:43 +05:30
Mihir Kandoi
89f8e379e7 test(stock): cover a row that stops rejecting material 2026-09-21 18:21:16 +05:30
Mihir Kandoi
babfa3c13f fix(stock): follow the split when the package of a row is built and reused
Three things went wrong around the package of a row:

A row that accepts nothing got a package of the whole delivered qty, because a
qty of zero reads as no qty at all when the package is copied, and the receipt
was refused. Such a row carries no package of its own now, and the entry for the
in-transit warehouse is built from the package of the rejected material.

A row that stopped rejecting material kept the package of its accepted
warehouse, which the entry for the in-transit warehouse then refused.

A landed cost voucher over a return built a second package for the in-transit
warehouse, so the batch qty counted the returned material twice. The package of
the submitted entry is reused for both kinds of return now.

Rejected material keeps its rate on a receipt alone. A Purchase Invoice that
updates stock books no rejected material, so valuing it there would leave its
ledgers disagreeing.
2026-09-21 18:21:07 +05:30
Mihir Kandoi
00c251c2f1 fix(stock): keep the return rate on the in-transit entry after the merge 2026-09-21 14:36:28 +05:30
Mihir Kandoi
d4d704f336 Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 14:35:42 +05:30
Mihir Kandoi
6f2981cfe5 fix(stock): keep the package of a row on the accepted material
The desk sets the accepted qty of a row from the total of its package, so the
package has to hold the accepted material and nothing else. It held everything
that was received, which made the desk raise the accepted qty to the received
qty as soon as a row was opened.

A row that rejects material now carries the package of its accepted warehouse,
which is also the entry it belongs to, and the entry for the in-transit
warehouse gets a package holding the accepted and the rejected material
together. A row that rejects nothing is unchanged.
2026-09-21 14:35:22 +05:30
Mihir Kandoi
ca1f4de38d Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 14:29:50 +05:30
Mihir Kandoi
fb47d04929 test(stock): cover rejecting serial numbers after the package was built 2026-09-21 14:29:36 +05:30
Mihir Kandoi
a8051b0518 fix(stock): top up the package of a row that rejects material later
The package of a row is built once, when the row first gets one. Rejecting
material afterwards left it holding the accepted material alone, while the entry
for the in-transit warehouse covers everything that was received, so the receipt
could not be submitted.

The package takes in the rejected material when the two disagree, which also
covers a package a user picked by hand for the accepted qty.
2026-09-21 14:29:35 +05:30
Mihir Kandoi
d7140099ca Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 13:17:12 +05:30
Mihir Kandoi
2c55621bae test(stock): mark the rejected package of an internal transfer as rejected 2026-09-21 13:17:07 +05:30
Mihir Kandoi
a1dc88e698 fix(stock): value rejected material that came from an in-transit warehouse
A package of rejected material is valued at zero unless Buying Settings asks
otherwise, because material rejected from a supplier is not paid for. Material
of an internal transfer is different: its value was credited out of the
in-transit warehouse, so refusing it a rate left the difference to be written
off as a divisional loss.

Only a package that belongs to a row with an in-transit warehouse keeps its
rate; an ordinary receipt is unchanged.
2026-09-21 13:17:06 +05:30
Mihir Kandoi
f6de755c83 test(stock): cover returning rejected material of an internal transfer 2026-09-21 12:48:55 +05:30
Mihir Kandoi
7a7e186fb4 fix(stock): put returned rejected material back into the in-transit warehouse
Returning rejected material of an internal transfer left the in-transit
warehouse holding the qty at no value, and wrote the value off: the return has no
delivery note reference, so the entry for the in-transit warehouse got no rate,
and the entry against it was suppressed because rejected material normally
carries no value. On an internal transfer it does, since the value came out of
the in-transit warehouse.

The entry takes the rate of the return now, and the value of the source
warehouse is signed rather than absolute, so a return debits the warehouse the
material goes back into.
2026-09-21 12:48:55 +05:30
Mihir Kandoi
67f63e62c8 Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 12:46:52 +05:30
Mihir Kandoi
8290cd6294 test(stock): cover returning a transfer that rejected batch material 2026-09-21 12:46:14 +05:30
Mihir Kandoi
00ff0684af fix(stock): let the row package hold everything that left the in-transit warehouse
The package of the row covered the accepted material, and the entry for the
in-transit warehouse was given a second package holding the accepted and the
rejected material together. The package of the row then belonged to no entry and
stayed a draft, and a draft package is invisible to a return, so the receipt
could not be returned at all.

The package of the row is sized from the received qty now and validated against
it, which is what left the in-transit warehouse, and the entry uses it directly.
The accepted warehouse takes its share of that package, without the rejected
serial numbers. A return still builds a package of its own, since its material
comes back from two warehouses.
2026-09-21 12:46:14 +05:30
Mihir Kandoi
3213810721 test(stock): cover cancelling a transfer that rejected serial numbers 2026-09-21 12:44:28 +05:30
Mihir Kandoi
88860763c1 fix(stock): return rejected serial numbers to the in-transit warehouse on cancel
Cancelling took the package of the accepted warehouse to reverse an entry that
covered the accepted and the rejected material, so the rejected serial numbers
were left without a warehouse while the bin held their qty. The package of the
submitted entry for that warehouse is used now.

The entry for the source warehouse is also posted after the one for the rejected
warehouse, otherwise the rejected material is put back and taken out again in
the same cancellation.
2026-09-21 12:44:27 +05:30
Mihir Kandoi
794e3050b7 Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 12:40:10 +05:30
Mihir Kandoi
bfcd9a33aa test(stock): cover a converted uom and a landed cost voucher on a rejected transfer 2026-09-21 12:39:52 +05:30
Mihir Kandoi
c13542f8d0 fix(stock): size the in-transit package in stock uom and reuse it on a replay
The package copied for the in-transit warehouse was sized from the qty of the
row, while the row is validated against its qty in stock UOM. A receipt of one
box of twelve was refused, whether or not anything was rejected.

A landed cost voucher and a repost build the entries of a receipt again. The
package holding the accepted and the rejected material was built again with
them, and the batch qty counted both copies, so a voucher over a receipt of ten
left the batch at zero. The package of the submitted entry is reused now.
2026-09-21 12:39:52 +05:30
Mihir Kandoi
d790e47f90 fix(stock): keep the rejected valuation rate on purchase receipts only
The rate of a row with no accepted qty is taken from the rejected qty, which is
what the receipt puts into the rejected warehouse. A Purchase Invoice books no
rejected material, so its rows keep the rate they had.
2026-09-21 11:56:23 +05:30
Mihir Kandoi
032f36b361 Merge branch 'fix/internal-transfer-rejected-serial-batch' into fix/internal-transfer-fully-rejected 2026-09-21 11:55:52 +05:30
Mihir Kandoi
4eb9796c04 fix(stock): keep the package changes on purchase receipts only
A Purchase Invoice that updates stock shares this controller, and its GL entries
have no accounting for rejected material. Resizing its package would let such an
invoice be submitted while the rejected material stayed in the in-transit
warehouse, so the packages of an invoice are left as they were.
2026-09-21 11:55:46 +05:30
Mihir Kandoi
6f5353b280 Merge branch 'fix/internal-transfer-rejected-qty' into fix/internal-transfer-rejected-serial-batch 2026-09-21 11:55:18 +05:30
Mihir Kandoi
423383fb89 fix(stock): keep the source warehouse qty on purchase receipts only
The stock ledger of a Purchase Invoice that updates stock is built by the same
controller, but its GL entries have no accounting for rejected material at all.
Taking the rejected qty out of the in-transit warehouse there would move stock
that the invoice never books, so the two ledgers would disagree.
2026-09-21 11:55:13 +05:30
Mihir Kandoi
4d09a94768 test(stock): cover an internal transfer with every unit rejected 2026-09-21 11:53:04 +05:30
Mihir Kandoi
87c42b0ec0 fix(stock): empty the in-transit warehouse when every unit is rejected
A receipt that rejects the whole qty left the material in the in-transit
warehouse and added it to the rejected warehouse as well, because the entries
for the source warehouse were made only when there was an accepted qty.

They are made from the qty that leaves the source warehouse now, so a row with
no accepted qty is posted like any other. Such a row also had no valuation rate,
since the rate of an internal transfer is taken from the accepted qty alone, and
the rejected warehouse was then debited without a matching credit.

The one gate that guarded the entries for a row replaces two nested ones, so the
body of the loop moves out by one level. Read the diff with whitespace ignored.
2026-09-21 11:53:03 +05:30
Mihir Kandoi
8b277f4b47 test(stock): cover rejected serial and batch material on an internal transfer 2026-09-21 11:48:22 +05:30
Mihir Kandoi
ab0d052fa2 fix(stock): let rejected serial and batch material leave the in-transit warehouse
A serial or batch item rejected on an internal transfer could not be received at
all: the package for the in-transit warehouse was copied from the delivery note
at its full qty, and the receipt was then refused because that package did not
match the accepted qty.

The package is copied at the accepted qty now. The comparison that decides
whether to resize compares absolute quantities, because the package of a
delivery note is an outgoing one and carries a negative total. Rejected serial
numbers are kept out of that package.

The outgoing entry for the in-transit warehouse gets a package of its own,
holding the accepted and the rejected material, so the qty and the package agree
on everything that leaves the warehouse. On cancellation the accepted warehouse
takes the package of the submitted entry, since the row package belongs to no
entry in this case.
2026-09-21 11:48:22 +05:30
Mihir Kandoi
fb905f9d6f style(stock): shorten the source warehouse qty docstring 2026-09-21 11:48:12 +05:30
Mihir Kandoi
8921f160d1 test(stock): cover rejected qty on an internal transfer receipt 2026-09-19 13:21:16 +05:30
Mihir Kandoi
3eef93e792 fix(stock): deduct rejected qty from the in-transit warehouse
On an internal transfer the Purchase Receipt only took the accepted qty out of
the in-transit warehouse, while the rejected qty was booked into the rejected
warehouse. The rejected material stayed in transit and was counted twice.

The rejected material also carried stock value, because an internal-transfer
receipt anchors every inward entry to the delivery note rate, but the rejected
warehouse got no GL entry unless "Set valuation rate for rejected Materials" was
enabled. That left the stock value and the account value out of sync.

The outgoing entry now covers accepted plus rejected qty, and the rejected
warehouse is booked on an internal transfer whatever that setting says. Rows
tracked by a serial and batch package are unchanged: their qty comes from the
package, which still holds the accepted material only.
2026-09-19 13:21:16 +05:30
382 changed files with 2384 additions and 6338 deletions

View File

@@ -22,6 +22,6 @@ jobs:
pull-requests: write
steps:
- uses: alyf-de/po-review-action@57fff275f4a0518a2ca55869ec6776fa3813b3d5 # v1.2.0
- uses: alyf-de/po-review-action@5928f84d6bc9094f9ad6e2c5780f01c0044b800e # v1.1.1
with:
hidden-po-files: eo.po

View File

@@ -47,13 +47,10 @@ class ERPNextAddress(Address):
super().on_update()
address_display = get_address_display(self.as_dict())
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
)
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)
@frappe.whitelist()

View File

@@ -7,7 +7,6 @@
"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",
@@ -201,7 +200,7 @@
"options": "Account Category"
}
],
"icon": "vault",
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],

View File

@@ -40,7 +40,6 @@
}
],
"grid_page_length": 50,
"icon": "folder-tree",
"index_web_pages_for_search": 1,
"links": [
{

View File

@@ -148,7 +148,7 @@
"precision": "9"
}
],
"icon": "scale",
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2025-08-22 19:13:50.400404",

View File

@@ -3,7 +3,6 @@
"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",
@@ -50,7 +49,6 @@
"options": "Accounting Dimension Detail"
}
],
"icon": "layers",
"links": [],
"modified": "2024-03-27 13:05:56.890002",
"modified_by": "Administrator",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -100,7 +99,6 @@
"label": "Fieldname"
}
],
"icon": "funnel",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-08 14:13:22.203011",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -78,7 +77,6 @@
"options": "Role"
}
],
"icon": "calendar-range",
"links": [],
"modified": "2026-03-09 17:15:33.577217",
"modified_by": "Administrator",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -802,7 +801,7 @@
}
],
"grid_page_length": 50,
"icon": "settings",
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,

View File

@@ -4,7 +4,6 @@
"creation": "2024-10-16 16:57:12.085072",
"doctype": "DocType",
"engine": "InnoDB",
"icon": "book-open",
"is_submittable": 1,
"field_order": [
"company",

View File

@@ -6,7 +6,6 @@
"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": [
@@ -101,7 +100,6 @@
"read_only": 1
}
],
"icon": "landmark",
"links": [],
"modified": "2026-08-21 23:11:39.423431",
"modified_by": "Administrator",

View File

@@ -5,7 +5,6 @@
"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",
@@ -228,7 +227,6 @@
}
],
"grid_page_length": 50,
"icon": "credit-card",
"links": [
{
"group": "Transactions",

View File

@@ -44,7 +44,6 @@
}
],
"grid_page_length": 50,
"icon": "wallet",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-06-16 22:17:48.007982",

View File

@@ -18,7 +18,6 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.221876",
"modified_by": "Administrator",

View File

@@ -18,7 +18,6 @@
"unique": 1
}
],
"icon": "credit-card",
"links": [],
"modified": "2024-03-27 13:06:37.347035",
"modified_by": "Administrator",

View File

@@ -87,7 +87,7 @@
}
],
"hide_toolbar": 1,
"icon": "badge-check",
"icon": "fa fa-check",
"idx": 1,
"issingle": 1,
"links": [],

View File

@@ -5,7 +5,6 @@
"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": [
@@ -217,7 +216,6 @@
}
],
"grid_page_length": 50,
"icon": "shield-check",
"is_submittable": 1,
"links": [],
"modified": "2026-05-25 18:12:10.768835",

View File

@@ -116,7 +116,6 @@
}
],
"hide_toolbar": 1,
"icon": "arrow-left-right",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -792,7 +792,6 @@ def create_bulk_payment_entry_and_reconcile(
"deposit",
"withdrawal",
"bank_account",
"company",
"currency",
"unallocated_amount",
"date",
@@ -827,7 +826,11 @@ 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 "")[
@@ -836,8 +839,6 @@ def create_bulk_payment_entry_and_reconcile(
}
)
set_multi_currency_amounts(payment_entry_doc)
payment_entry_doc.insert()
payment_entry_doc.submit()
@@ -876,7 +877,6 @@ 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,33 +899,6 @@ 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):
"""

View File

@@ -2,16 +2,12 @@
# 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,
@@ -20,8 +16,6 @@ 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):
@@ -236,117 +230,3 @@ 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)

View File

@@ -225,7 +225,6 @@
}
],
"hide_toolbar": 1,
"icon": "file-down",
"links": [],
"modified": "2026-06-19 14:18:00.000000",
"modified_by": "Administrator",

View File

@@ -188,7 +188,6 @@
}
],
"grid_page_length": 50,
"icon": "file-clock",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-07-09 17:55:25.615942",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -278,7 +277,6 @@
}
],
"grid_page_length": 50,
"icon": "arrow-left-right",
"is_submittable": 1,
"links": [],
"modified": "2026-04-11 20:41:15.124085",

View File

@@ -140,7 +140,6 @@
}
],
"grid_page_length": 50,
"icon": "zap",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-04-11 21:36:26.754667",

View File

@@ -167,7 +167,6 @@
}
],
"hide_toolbar": 1,
"icon": "split",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -68,7 +68,6 @@
"label": "Generated"
}
],
"icon": "git-branch",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:06:39.766063",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -331,7 +330,6 @@
}
],
"grid_page_length": 50,
"icon": "piggy-bank",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -122,7 +122,6 @@
"read_only": 1
}
],
"icon": "lock",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:06:44.260440",

View File

@@ -5,7 +5,6 @@
"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": [
@@ -48,7 +47,6 @@
}
],
"hide_toolbar": 1,
"icon": "file-input",
"in_create": 1,
"issingle": 1,
"links": [],

View File

@@ -4,7 +4,6 @@
"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",
@@ -294,7 +293,6 @@
"fieldtype": "HTML"
}
],
"icon": "printer",
"links": [],
"max_attachments": 1,
"modified": "2026-06-08 12:10:35.829531",

View File

@@ -7,7 +7,6 @@
"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",
@@ -123,7 +122,7 @@
"label": "Disabled"
}
],
"icon": "chart-pie",
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],

View File

@@ -4,7 +4,6 @@
"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": [
@@ -66,7 +65,6 @@
"read_only": 1
}
],
"icon": "chart-pie",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -5,10 +5,7 @@ frappe.ui.form.on("Coupon Code", {
setup: function (frm) {
frm.set_query("pricing_rule", function () {
return {
filters: {
coupon_code_based: 1,
disable: 0,
},
filters: [["Pricing Rule", "coupon_code_based", "=", "1"]],
};
});
},

View File

@@ -5,7 +5,6 @@
"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": [
@@ -125,7 +124,6 @@
"label": "From External Ecomm Platform"
}
],
"icon": "ticket-percent",
"links": [],
"modified": "2026-08-21 23:11:41.010871",
"modified_by": "Administrator",

View File

@@ -42,23 +42,7 @@ 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)))

View File

@@ -112,43 +112,6 @@ 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)

View File

@@ -2,7 +2,6 @@
"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": [
@@ -102,7 +101,6 @@
"label": "Use HTTP Protocol"
}
],
"icon": "refresh-cw",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -5,7 +5,6 @@
"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",
@@ -399,7 +398,6 @@
"fieldtype": "Column Break"
}
],
"icon": "bell-ring",
"is_submittable": 1,
"links": [],
"modified": "2026-05-30 23:18:04.712528",

View File

@@ -101,7 +101,6 @@
"fieldtype": "Column Break"
}
],
"icon": "bell",
"links": [
{
"link_doctype": "Dunning",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -108,7 +107,6 @@
"precision": "9"
}
],
"icon": "arrow-right-left",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:09:42.951164",

View File

@@ -5,7 +5,6 @@
"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": [
@@ -19,7 +18,7 @@
"unique": 1
}
],
"icon": "book",
"icon": "fa fa-book",
"links": [],
"modified": "2026-08-21 23:11:42.386104",
"modified_by": "Administrator",

View File

@@ -3,7 +3,6 @@
"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",
@@ -65,7 +64,6 @@
}
],
"grid_page_length": 50,
"icon": "file-spreadsheet",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-02-23 01:04:05.797161",

View File

@@ -6,7 +6,6 @@
"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",
@@ -80,7 +79,7 @@
"set_only_once": 1
}
],
"icon": "calendar",
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
"modified": "2026-08-21 23:11:42.509102",

View File

@@ -377,7 +377,7 @@
"precision": "9"
}
],
"icon": "book-open",
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
"links": [],

View File

@@ -5,7 +5,6 @@
"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": [
@@ -170,7 +169,6 @@
"read_only": 1
}
],
"icon": "ticket-percent",
"is_submittable": 1,
"links": [],
"modified": "2026-09-09 17:04:59.512294",

View File

@@ -5,7 +5,6 @@
"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": [
@@ -57,7 +56,6 @@
"fieldtype": "Section Break"
}
],
"icon": "circle-percent",
"links": [],
"modified": "2026-08-21 23:11:43.571355",
"modified_by": "Administrator",

View File

@@ -6,7 +6,6 @@
"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",
@@ -662,7 +661,7 @@
"label": "Custom Remark"
}
],
"icon": "notebook-pen",
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [

View File

@@ -253,10 +253,6 @@ 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",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -86,7 +85,6 @@
"label": "Multi Currency"
}
],
"icon": "notebook-text",
"links": [],
"modified": "2024-03-27 13:09:58.814734",
"modified_by": "Administrator",

View File

@@ -40,7 +40,6 @@
"label": "General and Payment Ledger mismatch"
}
],
"icon": "heart-pulse",
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],

View File

@@ -57,7 +57,6 @@
}
],
"hide_toolbar": 1,
"icon": "activity",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -92,7 +92,6 @@
}
],
"hide_toolbar": 1,
"icon": "merge",
"links": [],
"modified": "2024-03-27 13:10:02.198009",
"modified_by": "Administrator",

View File

@@ -97,7 +97,6 @@
"label": "Discretionary Reason"
}
],
"icon": "star",
"in_create": 1,
"links": [],
"modified": "2024-07-01 08:51:13.927009",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -154,7 +153,6 @@
"options": "Project"
}
],
"icon": "gift",
"links": [],
"modified": "2026-08-21 23:11:44.144864",
"modified_by": "Administrator",

View File

@@ -6,7 +6,6 @@
"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",
@@ -45,7 +44,7 @@
"label": "Enabled"
}
],
"icon": "wallet",
"icon": "fa fa-credit-card",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],

View File

@@ -43,7 +43,7 @@
"options": "Monthly Distribution Percentage"
}
],
"icon": "chart-bar",
"icon": "fa fa-bar-chart",
"idx": 1,
"links": [],
"modified": "2026-08-21 23:11:44.908490",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -89,7 +88,6 @@
}
],
"hide_toolbar": 1,
"icon": "file-plus",
"issingle": 1,
"links": [],
"modified": "2026-05-30 23:18:48.691227",

View File

@@ -50,7 +50,6 @@
"options": "secondary_role"
}
],
"icon": "link",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-27 13:10:08.607170",

View File

@@ -5,7 +5,6 @@
"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",
@@ -785,7 +784,6 @@
}
],
"grid_page_length": 50,
"icon": "banknote",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [

View File

@@ -84,7 +84,6 @@
"reqd": 1
}
],
"icon": "credit-card",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-07-14 16:49:55.210352",

View File

@@ -5,7 +5,6 @@
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"icon": "book-open",
"is_submittable": 1,
"field_order": [
"posting_date",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -108,7 +107,6 @@
"label": "Account"
}
],
"icon": "send",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:10.186727",

View File

@@ -3,7 +3,6 @@
"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",
@@ -236,7 +235,7 @@
}
],
"hide_toolbar": 1,
"icon": "arrow-left-right",
"icon": "icon-resize-horizontal",
"is_virtual": 1,
"issingle": 1,
"links": [],

View File

@@ -3,7 +3,6 @@
"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",
@@ -475,7 +474,6 @@
}
],
"grid_page_length": 50,
"icon": "hand-coins",
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,

View File

@@ -5,7 +5,6 @@
"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": [
@@ -116,7 +115,6 @@
"fieldtype": "Column Break"
}
],
"icon": "calendar-clock",
"links": [],
"modified": "2026-08-21 23:11:45.693762",
"modified_by": "Administrator",

View File

@@ -5,7 +5,6 @@
"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": [
@@ -35,7 +34,6 @@
"label": "Allocate Payment Based On Payment Terms"
}
],
"icon": "calendar-clock",
"links": [],
"modified": "2024-04-07 11:26:42.021585",
"modified_by": "Administrator",

View File

@@ -20,7 +20,6 @@
}
],
"grid_page_length": 50,
"icon": "anchor",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],

View File

@@ -3,7 +3,6 @@
"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",
@@ -111,7 +110,7 @@
"reqd": 1
}
],
"icon": "folder-lock",
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],

View File

@@ -280,13 +280,12 @@ class PeriodClosingVoucher(AccountsController):
data = self.get_data_for_mapreduce()
mapreduce(
"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.mapper",
"erpnext.accounts.doctype.period_closing_voucher.period_closing_voucher.reducer",
"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):
@@ -331,8 +330,8 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
if frappe.db.estimate_count("GL Entry") > 100_000:
frappe.enqueue_task(
method=process_gl_and_closing_entries,
frappe.enqueue(
process_gl_and_closing_entries,
doc=self,
timeout=1800,
)
@@ -835,7 +834,7 @@ def get_previous_closed_period_in_current_year(fiscal_year, company):
return prev_closed_period_end_date
def process_date_range(val):
def mapper(val):
start_date = val.from_date
end_date = val.to_date
pcv = val.pcv
@@ -882,7 +881,7 @@ def process_date_range(val):
return res
def aggregate_partial_result(final, partial_res):
def reducer(final, partial_res):
if final is None:
final = []

View File

@@ -1,28 +0,0 @@
// 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];
},
};

View File

@@ -254,7 +254,6 @@
}
],
"grid_page_length": 50,
"icon": "book-lock",
"is_submittable": 1,
"links": [
{

View File

@@ -1640,7 +1640,7 @@
"print_hide": 1
}
],
"icon": "receipt-text",
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2026-08-21 23:11:45.029925",

View File

@@ -126,7 +126,6 @@
"reqd": 1
}
],
"icon": "merge",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -129,7 +129,6 @@
"reqd": 1
}
],
"icon": "folder-open",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:16.034296",

View File

@@ -4,7 +4,6 @@
"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",
@@ -558,7 +557,7 @@
}
],
"grid_page_length": 50,
"icon": "monitor",
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [

View File

@@ -51,7 +51,6 @@
}
],
"hide_toolbar": 0,
"icon": "settings",
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:19.677217",

View File

@@ -5,7 +5,6 @@
"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",
@@ -655,7 +654,7 @@
"label": "Don't Enforce Free Item Qty"
}
],
"icon": "badge-percent",
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2026-02-17 12:24:07.553505",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -75,7 +74,6 @@
"reqd": 1
}
],
"icon": "list-clock",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -154,7 +154,6 @@
"reqd": 0
}
],
"icon": "scale",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -106,7 +106,6 @@
"read_only": 1
}
],
"icon": "file-clock",
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],

View File

@@ -80,7 +80,6 @@
}
],
"grid_page_length": 50,
"icon": "lock",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -4,7 +4,6 @@
"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": [
@@ -425,7 +424,6 @@
"label": "Show Opening Entries"
}
],
"icon": "mail",
"links": [],
"modified": "2026-06-01 15:37:07.660442",
"modified_by": "Administrator",

View File

@@ -35,7 +35,6 @@
"options": "Subscription"
}
],
"icon": "repeat",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -5,7 +5,6 @@
"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": [
@@ -280,7 +279,6 @@
"options": "Promotional Scheme Product Discount"
}
],
"icon": "megaphone",
"links": [],
"modified": "2025-08-20 11:48:23.231081",
"modified_by": "Administrator",

View File

@@ -6,7 +6,6 @@
"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": [
@@ -1693,7 +1692,7 @@
}
],
"grid_page_length": 50,
"icon": "receipt-text",
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],

View File

@@ -256,10 +256,6 @@ 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(
@@ -576,18 +572,12 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
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.
"""
"""Book the rejected material of an internal transfer, whose value was credited out of the
in-transit warehouse along with the accepted material."""
doc = self.doc
if not (item.rejected_warehouse and flt(item.rejected_qty)):
if not (doc.is_internal_transfer() and flt(item.rejected_qty) and item.rejected_warehouse):
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"),
@@ -595,26 +585,23 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
if not rejected_amount:
return 0.0
rejected_account = doc.get_inventory_account_dict(item, inventory_account_map, "rejected_warehouse")
_inv_dict = 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,
"account": _inv_dict["account"],
"against": item.expense_account,
"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"],
_inv_dict["account_currency"],
item=item,
)
)
return rejected_amount if transfers_rejected_material else 0.0
return rejected_amount
def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency):
doc = self.doc
@@ -629,24 +616,16 @@ 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:
# 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
net_rate = item.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
)
# 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):
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
cost_of_goods_sold_account = self.get_stock_variance_account(item)
stock_adjustment_amt = stock_amount - returned_stock_value
stock_adjustment_amt = stock_amount - warehouse_debit_amount
gl_entries.append(
self.get_gl_dict(

View File

@@ -844,9 +844,7 @@ 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, do_not_save=True)
pi.credit_to = "_Test Payable USD - _TC"
pi.submit()
pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
self.assertEqual(
frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000,
@@ -858,14 +856,12 @@ 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
@@ -2232,6 +2228,81 @@ class TestPurchaseInvoice(ERPNextTestSuite, StockTestMixin):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 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_adjust_incoming_rate_for_rejected_item(self):
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
@@ -2628,582 +2699,6 @@ 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

View File

@@ -6,7 +6,6 @@
"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",
@@ -75,7 +74,7 @@
"options": "Tax Category"
}
],
"icon": "badge-percent",
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2026-08-21 23:11:47.506282",

View File

@@ -4,7 +4,6 @@
"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": [
@@ -102,7 +101,6 @@
"read_only": 1
}
],
"icon": "list-restart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -95,7 +95,6 @@
"label": "Repost Error Log"
}
],
"icon": "list-restart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],

View File

@@ -4,7 +4,6 @@
"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",
@@ -2396,7 +2395,7 @@
}
],
"grid_page_length": 50,
"icon": "receipt-text",
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [

View File

@@ -6,7 +6,6 @@
"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",
@@ -77,7 +76,7 @@
"options": "Tax Category"
}
],
"icon": "badge-percent",
"icon": "fa fa-money",
"idx": 1,
"links": [],
"modified": "2026-08-21 23:11:48.797423",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -191,7 +190,6 @@
"fieldtype": "Column Break"
}
],
"icon": "arrow-right-left",
"is_submittable": 1,
"links": [],
"modified": "2024-03-27 13:10:40.003178",

View File

@@ -24,7 +24,6 @@
"label": "Description"
}
],
"icon": "tag",
"links": [],
"modified": "2024-03-27 13:10:40.190914",
"modified_by": "Administrator",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -113,7 +112,6 @@
"read_only": 1
}
],
"icon": "user-star",
"links": [],
"modified": "2024-03-27 13:10:40.356171",
"modified_by": "Administrator",

View File

@@ -5,7 +5,6 @@
"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",
@@ -147,7 +146,7 @@
"options": "Project"
}
],
"icon": "truck",
"icon": "fa fa-truck",
"idx": 1,
"links": [],
"modified": "2026-08-21 23:11:49.532098",

View File

@@ -3,7 +3,6 @@
"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": [
@@ -300,7 +299,6 @@
"fieldtype": "Section Break"
}
],
"icon": "calendar-sync",
"index_web_pages_for_search": 1,
"links": [
{

View File

@@ -4,7 +4,6 @@
"autoname": "field:plan_name",
"creation": "2018-02-24 11:31:23.066506",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/subscription-plan",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -149,7 +148,6 @@
"label": "Product Price ID"
}
],
"icon": "calendar-sync",
"links": [],
"modified": "2024-03-27 13:10:47.998597",
"modified_by": "Administrator",

View File

@@ -2,7 +2,6 @@
"actions": [],
"creation": "2018-02-26 06:13:37.910139",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/subscription-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -33,7 +32,6 @@
],
"grid_page_length": 50,
"hide_toolbar": 0,
"icon": "settings",
"issingle": 1,
"links": [],
"modified": "2026-03-16 13:28:20.485964",

View File

@@ -4,7 +4,6 @@
"autoname": "field:title",
"creation": "2022-01-19 01:09:28.920486",
"doctype": "DocType",
"documentation": "https://docs.frappe.io/erpnext/user/manual/en/tax-category",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -28,7 +27,6 @@
"label": "Disabled"
}
],
"icon": "badge-percent",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-08-21 23:11:50.740962",

Some files were not shown because too many files have changed in this diff Show More