mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-05 02:33:04 +00:00
Compare commits
33 Commits
mergify/bp
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866688931b | ||
|
|
f0d1887e6e | ||
|
|
e3af2f9302 | ||
|
|
15c381701d | ||
|
|
35f523e2dd | ||
|
|
a19fd5d6b5 | ||
|
|
25bd2bd4e7 | ||
|
|
387f2b5d01 | ||
|
|
78cd25de04 | ||
|
|
431dc2e5f1 | ||
|
|
7098602dcc | ||
|
|
32b56ac505 | ||
|
|
9f1bdba9a7 | ||
|
|
c630226846 | ||
|
|
d5b49cd66e | ||
|
|
b9c9b76f5b | ||
|
|
da0e3b5882 | ||
|
|
3d4198494b | ||
|
|
e6b4799b1a | ||
|
|
49eb2366cd | ||
|
|
fb2a4e5f98 | ||
|
|
0ce7917648 | ||
|
|
41038979ec | ||
|
|
88b6779062 | ||
|
|
b5f784612d | ||
|
|
457424f7a4 | ||
|
|
25ee3695f0 | ||
|
|
ff205da810 | ||
|
|
2980171007 | ||
|
|
c6c4815e8d | ||
|
|
82a85818c2 | ||
|
|
57a2be6b56 | ||
|
|
47f54a4725 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.112.0"
|
||||
__version__ = "15.119.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"account_number": "11530"
|
||||
},
|
||||
"account_number": "115",
|
||||
"is_group": 1
|
||||
"is_group": 1,
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Trade Receivables": {
|
||||
"Trade Debtors": {
|
||||
@@ -529,6 +530,13 @@
|
||||
"account_number": "630",
|
||||
"is_group": 1
|
||||
},
|
||||
"Accrued Manufacturing Expenses": {
|
||||
"Accrued Expenses - Manufacturing": {
|
||||
"account_number": "63510"
|
||||
},
|
||||
"account_number": "635",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "63",
|
||||
"is_group": 1
|
||||
},
|
||||
@@ -814,4 +822,4 @@
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,8 +1301,14 @@ class PaymentEntry(AccountsController):
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
add_regional_gl_entries(gl_entries, self)
|
||||
self.set_transaction_currency_and_rate_in_gl_map(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
|
||||
for gle in gl_entries:
|
||||
gle.setdefault("transaction_currency", self.transaction_currency)
|
||||
gle.setdefault("transaction_exchange_rate", self.transaction_exchange_rate)
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
|
||||
@@ -1046,14 +1046,17 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
gle.credit_in_account_currency,
|
||||
gle.debit_in_transaction_currency,
|
||||
gle.credit_in_transaction_currency,
|
||||
gle.transaction_currency,
|
||||
gle.transaction_exchange_rate,
|
||||
)
|
||||
.orderby(gle.account)
|
||||
.where(gle.voucher_no == payment_entry.name)
|
||||
.run()
|
||||
)
|
||||
# transaction currency/rate come from the paid-from USD account (company currency is INR)
|
||||
expected_gl_entries = (
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0),
|
||||
(paid_from, 0.0, 8440.0, 0.0, 100.0, 0.0, 100.0, "USD", 84.4),
|
||||
("_Test Payable USD - _TC", 8440.0, 0.0, 100.0, 0.0, 100.0, 0.0, "USD", 84.4),
|
||||
)
|
||||
self.assertEqual(gl_entries, expected_gl_entries)
|
||||
|
||||
|
||||
@@ -2436,6 +2436,86 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(flt(pr.allocation[0].get("difference_amount")), -5000.0)
|
||||
pr.reconcile()
|
||||
|
||||
def test_foreign_currency_reverse_payment_entry_gain_for_supplier(self):
|
||||
transaction_date = nowdate()
|
||||
self.supplier = "_Test Supplier USD"
|
||||
amount = 100
|
||||
department = frappe.db.get_value("Department", {"company": self.company, "is_group": 0}, "name")
|
||||
|
||||
# Pay USD 100 at an exchange rate of 90.
|
||||
pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
pe.payment_type = "Pay"
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_from_account_currency = "INR"
|
||||
pe.target_exchange_rate = 90
|
||||
pe.paid_amount = 90 * amount
|
||||
pe.received_amount = amount
|
||||
pe.paid_to = self.creditors_usd
|
||||
pe.paid_to_account_currency = "USD"
|
||||
pe.department = department
|
||||
pe = pe.save().submit()
|
||||
|
||||
# Receive USD 100 from the supplier at an exchange rate of 100.
|
||||
reverse_pe = self.create_payment_entry(amount=amount, posting_date=transaction_date)
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.paid_from = self.creditors_usd
|
||||
reverse_pe.paid_from_account_currency = "USD"
|
||||
reverse_pe.source_exchange_rate = 100
|
||||
reverse_pe.paid_amount = amount
|
||||
reverse_pe.received_amount = 100 * amount
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.paid_to_account_currency = "INR"
|
||||
reverse_pe.department = department
|
||||
reverse_pe = reverse_pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.party = self.supplier
|
||||
pr.receivable_payable_account = self.creditors_usd
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
for row in pr.allocation:
|
||||
row.department = department
|
||||
|
||||
self.assertEqual(flt(pr.allocation[0].difference_amount), 1000)
|
||||
pr.reconcile()
|
||||
|
||||
gain_loss_journal = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"reference_type": reverse_pe.doctype,
|
||||
"reference_name": reverse_pe.name,
|
||||
"party": self.supplier,
|
||||
"docstatus": 1,
|
||||
},
|
||||
"parent",
|
||||
)
|
||||
party_row = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": gain_loss_journal, "party": self.supplier},
|
||||
["debit", "credit"],
|
||||
as_dict=True,
|
||||
)
|
||||
self.assertEqual(flt(party_row.debit), 1000)
|
||||
self.assertEqual(flt(party_row.credit), 0)
|
||||
|
||||
party_gl_entries = frappe.get_all(
|
||||
"GL Entry",
|
||||
filters={
|
||||
"voucher_no": ["in", [pe.name, reverse_pe.name, gain_loss_journal]],
|
||||
"account": self.creditors_usd,
|
||||
"party": self.supplier,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
fields=["debit", "credit"],
|
||||
)
|
||||
self.assertEqual(flt(sum(row.debit - row.credit for row in party_gl_entries)), 0)
|
||||
|
||||
def test_foreign_currency_reverse_journal_entry_against_journal_entry_for_customer(self):
|
||||
transaction_date = nowdate()
|
||||
customer = self.customer3
|
||||
|
||||
@@ -1869,7 +1869,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def is_payable_account(self, reference_doctype, account):
|
||||
if reference_doctype == "Purchase Invoice" or (
|
||||
reference_doctype == "Journal Entry"
|
||||
reference_doctype in ("Journal Entry", "Payment Entry")
|
||||
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
||||
):
|
||||
return True
|
||||
|
||||
@@ -5,7 +5,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, nowtime, today
|
||||
from frappe.utils import add_days, add_to_date, flt, nowtime, today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
@@ -1512,3 +1512,186 @@ class TestSerialandBatchBundleLogic(FrappeTestCase):
|
||||
|
||||
self.assertNotIn(bundles[1], bundle_wise_serial_nos)
|
||||
self.assertEqual(bundle_wise_serial_nos[bundles[0]], [serial_no])
|
||||
|
||||
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||
def test_batchwise_valuation_for_same_posting_datetime_entries(self):
|
||||
# an inward at a different rate and multiple outward rows with the same
|
||||
# item and warehouse share the same posting datetime, the tie-breaking
|
||||
# must include the same-timestamp entries which are already part of the
|
||||
# ledger and must not let the outward rows count each other
|
||||
item_code = make_item(
|
||||
"Test Batchwise Same Posting Datetime Item 1",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBSPD-ITEM1-.#####",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
self.assertTrue(frappe.db.get_value("Batch", batch_no, "use_batchwise_valuation"))
|
||||
|
||||
# same posting datetime as the outward rows below, at a different rate
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=20,
|
||||
rate=250,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
issue = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
source=warehouse,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
do_not_save=True,
|
||||
)
|
||||
|
||||
for qty in [3, 4]:
|
||||
issue.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"s_warehouse": warehouse,
|
||||
"qty": qty,
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
issue.save()
|
||||
issue.submit()
|
||||
|
||||
# (10 * 100 + 20 * 250) / 30 = 200
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=200.0, balance_value=4200.0)
|
||||
|
||||
# backdated receipt reposts the same posting datetime cluster
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -4),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
# (20 * 100 + 20 * 250) / 40 = 175
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=175.0, balance_value=5425.0)
|
||||
|
||||
@change_settings("Stock Settings", {"auto_create_serial_and_batch_bundle_for_outward": 1})
|
||||
def test_batchwise_valuation_when_bundle_created_before_the_sle(self):
|
||||
# a bundle can be created (drafted) much before / after its SLE, the
|
||||
# tie-breaking for the same posting datetime entries must follow the
|
||||
# SLE creation and not the bundle creation
|
||||
item_code = make_item(
|
||||
"Test Batchwise Same Posting Datetime Item 2",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TBSPD-ITEM2-.#####",
|
||||
"valuation_method": "FIFO",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
receipt = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
target=warehouse,
|
||||
posting_date=add_days(today(), -5),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
batch_no = get_batch_from_bundle(receipt.items[0].serial_and_batch_bundle)
|
||||
|
||||
# inward at a different rate, same posting datetime as the outward below
|
||||
inward = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=200,
|
||||
target=warehouse,
|
||||
batch_no=batch_no,
|
||||
use_serial_batch_fields=1,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
outward = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
source=warehouse,
|
||||
posting_date=add_days(today(), -3),
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
|
||||
# simulate the inward's bundle drafted after the outward's SLE, the
|
||||
# bundle creation timeline no longer matches the SLE creation timeline
|
||||
outward_sle_creation = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": outward.name, "is_cancelled": 0},
|
||||
"creation",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Bundle",
|
||||
inward.items[0].serial_and_batch_bundle,
|
||||
"creation",
|
||||
add_to_date(outward_sle_creation, minutes=30),
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
repost = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": add_days(today(), -6),
|
||||
"posting_time": "00:00:00",
|
||||
"allow_negative_stock": 1,
|
||||
}
|
||||
)
|
||||
|
||||
repost.submit()
|
||||
|
||||
# (10 * 100 + 10 * 200) / 20 = 150, the inward precedes the outward as
|
||||
# per the SLE creation even though its bundle was created afterwards
|
||||
self.assert_batchwise_outgoing_rate(item_code, outgoing_rate=150.0, balance_value=1500.0)
|
||||
|
||||
def assert_batchwise_outgoing_rate(self, item_code, outgoing_rate, balance_value):
|
||||
sl_entries = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"item_code": item_code, "is_cancelled": 0},
|
||||
fields=["actual_qty", "stock_value_difference", "stock_value"],
|
||||
order_by="posting_datetime, creation",
|
||||
)
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.actual_qty > 0:
|
||||
continue
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference, 2), flt(sle.actual_qty * outgoing_rate, 2))
|
||||
|
||||
self.assertEqual(flt(sl_entries[-1].stock_value, 2), flt(balance_value, 2))
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe.utils import today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_account
|
||||
from erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison import (
|
||||
create_reposting_entries,
|
||||
execute,
|
||||
@@ -55,3 +57,22 @@ class TestStockAndAccountValueComparison(FrappeTestCase):
|
||||
filters={"based_on": "Item and Warehouse", "item_code": item},
|
||||
)
|
||||
self.assertFalse(item_wh_rivs, "Purchase vouchers must not be reposted Item-and-Warehouse based")
|
||||
|
||||
def test_child_account_override_excluded_from_group_account(self):
|
||||
# A group warehouse carries an inventory account; a child (e.g. Goods-in-Transit) can override
|
||||
# it with its own account. get_warehouses_based_on_account must return only warehouses whose
|
||||
# effective account matches, excluding the overriding child.
|
||||
group = create_warehouse("_Test SAVC Group WH", {"is_group": 1}, company=PI_COMPANY)
|
||||
group_account = frappe.get_value("Warehouse", group, "account")
|
||||
|
||||
inheriting = create_warehouse(
|
||||
"_Test SAVC Inherit WH", {"parent_warehouse": group, "account": group_account}, company=PI_COMPANY
|
||||
)
|
||||
overriding = create_warehouse(
|
||||
"_Test SAVC Transit WH", {"parent_warehouse": group}, company=PI_COMPANY
|
||||
)
|
||||
|
||||
warehouses = get_warehouses_based_on_account(group_account, PI_COMPANY)
|
||||
|
||||
self.assertIn(inheriting, warehouses)
|
||||
self.assertNotIn(overriding, warehouses)
|
||||
|
||||
@@ -829,14 +829,43 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
parent = frappe.qb.DocType("Serial and Batch Bundle")
|
||||
child = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
sle_creation = self.sle.creation if self.sle.get("name") else None
|
||||
if not self.sle.get("name") and self.sle.get("serial_and_batch_bundle"):
|
||||
sle_creation = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"serial_and_batch_bundle": self.sle.serial_and_batch_bundle, "is_cancelled": 0},
|
||||
"creation",
|
||||
)
|
||||
|
||||
timestamp_condition = ""
|
||||
if self.sle.posting_datetime:
|
||||
timestamp_condition = parent.posting_datetime < self.sle.posting_datetime
|
||||
|
||||
if self.sle.creation:
|
||||
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & (
|
||||
parent.creation < self.sle.creation
|
||||
sle_table = frappe.qb.DocType("Stock Ledger Entry")
|
||||
if sle_creation:
|
||||
# bundle creation and SLE creation are different timelines (a
|
||||
# bundle can be created much before its SLE), so break the tie
|
||||
# using the creation of the bundle's own SLE
|
||||
tie_condition = ExistsCriterion(
|
||||
frappe.qb.from_(sle_table)
|
||||
.select(sle_table.name)
|
||||
.where(
|
||||
(sle_table.serial_and_batch_bundle == parent.name)
|
||||
& (sle_table.is_cancelled == 0)
|
||||
& (sle_table.creation < sle_creation)
|
||||
)
|
||||
)
|
||||
else:
|
||||
# the current entry is not yet in the ledger and will get the
|
||||
# latest creation, so the same-timestamp entries which are
|
||||
# already in the ledger precede it
|
||||
tie_condition = ExistsCriterion(
|
||||
frappe.qb.from_(sle_table)
|
||||
.select(sle_table.name)
|
||||
.where((sle_table.serial_and_batch_bundle == parent.name) & (sle_table.is_cancelled == 0))
|
||||
)
|
||||
|
||||
timestamp_condition |= (parent.posting_datetime == self.sle.posting_datetime) & tie_condition
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent)
|
||||
|
||||
Reference in New Issue
Block a user