mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-01 16:54:36 +00:00
Compare commits
139 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
481610d400 | ||
|
|
829a42d679 | ||
|
|
c442dc6b8e | ||
|
|
fff8f8b83f | ||
|
|
c9467f4c1f | ||
|
|
eed7eba695 | ||
|
|
4646f11829 | ||
|
|
872c150ea7 | ||
|
|
a26f0e9973 | ||
|
|
dc6d3cca70 | ||
|
|
9af2df3810 | ||
|
|
84447caac7 | ||
|
|
5434432cfc | ||
|
|
2193395072 | ||
|
|
73821ac647 | ||
|
|
2e323cc126 | ||
|
|
95a66b8d8f | ||
|
|
62f12861f1 | ||
|
|
77bdf18c23 | ||
|
|
097eae478d | ||
|
|
8f02b88119 | ||
|
|
a91fb260e1 | ||
|
|
91105344bb | ||
|
|
6895efe577 | ||
|
|
2a890f9061 | ||
|
|
b2d361b495 | ||
|
|
d51cf281c3 | ||
|
|
3574d11946 | ||
|
|
37b8715096 | ||
|
|
e8aae5018a | ||
|
|
a4b9dda4b2 | ||
|
|
a5a0c0cb07 | ||
|
|
14ae3edc1d | ||
|
|
5e33961448 | ||
|
|
2db15d7375 | ||
|
|
53496ed79f | ||
|
|
4536e8bc47 | ||
|
|
36f65fc592 | ||
|
|
c237f3c4c4 | ||
|
|
cf78f9702c | ||
|
|
de0b8c07f6 | ||
|
|
4dd06b69a1 | ||
|
|
b6f352a024 | ||
|
|
a3a40febf3 | ||
|
|
6bfc8a0152 | ||
|
|
5113b0063e | ||
|
|
9e77a0245a | ||
|
|
7c37d4c71a | ||
|
|
873962a109 | ||
|
|
f0542613e0 | ||
|
|
77892f4e24 | ||
|
|
4bd7ce3e0b | ||
|
|
e7b17e05b0 | ||
|
|
f5069524f3 | ||
|
|
202ebbe140 | ||
|
|
3691a500eb | ||
|
|
f4032b64c9 | ||
|
|
2913785bac | ||
|
|
3a57ef4c5f | ||
|
|
610f74ca62 | ||
|
|
012248bd12 | ||
|
|
ed185a6171 | ||
|
|
3912c688ac | ||
|
|
c7fb6e5f61 | ||
|
|
22b283d4a8 | ||
|
|
56b4e79305 | ||
|
|
e8cfb4d602 | ||
|
|
c87f2d2f7c | ||
|
|
ec00b2cf48 | ||
|
|
c55f8e3388 | ||
|
|
4951d01102 | ||
|
|
a2d5024d3f | ||
|
|
ef7a83c354 | ||
|
|
810bc65e30 | ||
|
|
cfb5a6592c | ||
|
|
9afbd43f05 | ||
|
|
6fda0e8e1b | ||
|
|
9db7f7b033 | ||
|
|
82b2046805 | ||
|
|
6cb9ae2035 | ||
|
|
dda4002da4 | ||
|
|
91bff147e0 | ||
|
|
b3c9697b7c | ||
|
|
7648d8db80 | ||
|
|
578f9b61c6 | ||
|
|
23c5edaf08 | ||
|
|
35c10fa97f | ||
|
|
10025a2c8d | ||
|
|
813b699d5e | ||
|
|
94b09bb836 | ||
|
|
ff14d72a46 | ||
|
|
0c2d7f2d9a | ||
|
|
094411f5f0 | ||
|
|
c0b5f7c8eb | ||
|
|
73eab91631 | ||
|
|
2f4f2a8eec | ||
|
|
6a2210e46a | ||
|
|
820dcd79bb | ||
|
|
97205ce355 | ||
|
|
8207b12f11 | ||
|
|
09946c7ea7 | ||
|
|
15aeec8a2d | ||
|
|
29fe7bea6d | ||
|
|
c58987ba38 | ||
|
|
ef93c96f43 | ||
|
|
c29b95e306 | ||
|
|
e6438b293d | ||
|
|
18a75fef9a | ||
|
|
12d5e247c5 | ||
|
|
72cc42997b | ||
|
|
3a02800fbc | ||
|
|
6cf248578f | ||
|
|
5186872b9b | ||
|
|
54842ea0b3 | ||
|
|
a95a0524d9 | ||
|
|
dffd5f291d | ||
|
|
164b417136 | ||
|
|
8412938442 | ||
|
|
30ad25d86c | ||
|
|
651e853156 | ||
|
|
c2fdef4d5b | ||
|
|
678f08898a | ||
|
|
c79088f46a | ||
|
|
d5896cd8a0 | ||
|
|
c5dedab304 | ||
|
|
cf34219c71 | ||
|
|
752f93c630 | ||
|
|
d14e386e18 | ||
|
|
9f687eee4e | ||
|
|
c98ab15a44 | ||
|
|
333c7577eb | ||
|
|
ea7c82c6a3 | ||
|
|
54815b44e7 | ||
|
|
ed568fb01d | ||
|
|
a520d06c3f | ||
|
|
47a9325177 | ||
|
|
db319f95f1 | ||
|
|
450b3f338e | ||
|
|
003653c572 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.72.1"
|
||||
__version__ = "14.74.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-20 09:37:47.650347",
|
||||
"modified": "2024-01-22 12:10:10.151819",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -36,8 +36,12 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
refresh: function (frm) {
|
||||
frm.disable_save();
|
||||
frm.add_custom_button(__("Get Payment Entries"), () => frm.trigger("get_payment_entries"));
|
||||
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "primary");
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "primary");
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () => frm.trigger("update_clearance_date"));
|
||||
frm.change_custom_button_type(__("Get Payment Entries"), null, "default");
|
||||
frm.change_custom_button_type(__("Update Clearance Date"), null, "primary");
|
||||
}
|
||||
},
|
||||
|
||||
update_clearance_date: function (frm) {
|
||||
@@ -45,13 +49,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
method: "update_clearance_date",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "primary");
|
||||
frm.change_custom_button_type("Update Clearance Date", null, "default");
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -60,17 +58,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
return frappe.call({
|
||||
method: "get_payment_entries",
|
||||
doc: frm.doc,
|
||||
callback: function (r, rt) {
|
||||
frm.refresh_field("payment_entries");
|
||||
|
||||
if (frm.doc.payment_entries.length) {
|
||||
frm.add_custom_button(__("Update Clearance Date"), () =>
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type("Get Payment Entries", null, "default");
|
||||
frm.change_custom_button_type("Update Clearance Date", null, "primary");
|
||||
}
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, getdate
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -210,8 +210,11 @@ class BankClearance(Document):
|
||||
|
||||
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
|
||||
d.idx, d.clearance_date, d.cheque_date
|
||||
_("Row #{0}: For {1} Clearance date {2} cannot be before Cheque Date {3}").format(
|
||||
d.idx,
|
||||
get_link_to_form(d.payment_document, d.payment_entry),
|
||||
d.clearance_date,
|
||||
d.cheque_date,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Main Cost Center 2",
|
||||
"Main Cost Center 3",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
"Sub Cost Center 3",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
@@ -36,7 +38,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
"Cash - _TC", "Sales - _TC", 100, cost_center="Main Cost Center 1 - _TC", submit=True
|
||||
)
|
||||
|
||||
expected_values = [["Sub Cost Center 1 - _TC", 0.0, 60], ["Sub Cost Center 2 - _TC", 0.0, 40]]
|
||||
@@ -120,7 +122,7 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
def test_valid_from_based_on_existing_gle(self):
|
||||
# GLE posted against Sub Cost Center 1 on today
|
||||
jv = make_journal_entry(
|
||||
"_Test Cash - _TC",
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 1 - _TC",
|
||||
@@ -141,6 +143,53 @@ class TestCostCenterAllocation(unittest.TestCase):
|
||||
|
||||
jv.cancel()
|
||||
|
||||
def test_multiple_cost_center_allocation_on_same_main_cost_center(self):
|
||||
coa1 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 30, "Sub Cost Center 2 - _TC": 30, "Sub Cost Center 3 - _TC": 40},
|
||||
valid_from=add_days(today(), -5),
|
||||
)
|
||||
|
||||
coa2 = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 3 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50},
|
||||
valid_from=add_days(today(), -1),
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"Cash - _TC",
|
||||
"Sales - _TC",
|
||||
100,
|
||||
cost_center="Main Cost Center 3 - _TC",
|
||||
posting_date=today(),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
expected_values = {"Sub Cost Center 1 - _TC": 50, "Sub Cost Center 2 - _TC": 50}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
gl_entries = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.cost_center, gle.debit, gle.credit)
|
||||
.where(gle.voucher_type == "Journal Entry")
|
||||
.where(gle.voucher_no == jv.name)
|
||||
.where(gle.account == "Sales - _TC")
|
||||
.orderby(gle.cost_center)
|
||||
).run(as_dict=1)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertTrue(gle.cost_center in expected_values)
|
||||
self.assertEqual(gle.debit, 0)
|
||||
self.assertEqual(gle.credit, expected_values[gle.cost_center])
|
||||
|
||||
coa1.cancel()
|
||||
coa2.cancel()
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def create_cost_center_allocation(
|
||||
company,
|
||||
|
||||
@@ -23,6 +23,11 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ["in", account_types],
|
||||
@@ -77,6 +82,9 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
if (frm.doc.party_type == "Shareholder") {
|
||||
account_types.push("Equity");
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ["in", account_types],
|
||||
@@ -242,7 +250,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
set_dynamic_labels: function(frm) {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company)?.default_currency: "";
|
||||
|
||||
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
|
||||
@@ -326,6 +334,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.customer_query"
|
||||
}
|
||||
} else if (frm.doc.party_type == "Shareholder") {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -503,7 +517,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company)?.default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -1563,7 +1563,7 @@ def get_outstanding_reference_documents(args):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
if args.get("party_type") != "Employee" and not args.get("voucher_no"):
|
||||
if args.get("party_type") != "Employee":
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
|
||||
@@ -1365,6 +1365,79 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps))
|
||||
self.assertEqual(out_str, expected_out_str)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1})
|
||||
def test_delete_linked_exchange_gain_loss_journal(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=83.970000000,
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# create a payment entry for the invoice
|
||||
pe = get_payment_entry("Sales Invoice", si.name)
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 100
|
||||
pe.source_exchange_rate = 90
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 2710,
|
||||
},
|
||||
)
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{"reference_type": pe.doctype, "reference_name": pe.name, "docstatus": 1},
|
||||
pluck="parent",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
|
||||
# check cancellation of payment entry and journal entry
|
||||
pe.cancel()
|
||||
self.assertTrue(pe.docstatus == 2)
|
||||
self.assertTrue(frappe.db.get_value("Journal Entry", {"name": jv[0]}, "docstatus") == 2)
|
||||
|
||||
# check deletion of payment entry and journal entry
|
||||
pe.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, pe.doctype, pe.name)
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, "Journal Entry", jv[0])
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -19,7 +19,7 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
)
|
||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.accounts.utils import get_account_currency, get_currency_precision
|
||||
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
|
||||
from erpnext.utilities import payment_app_import_guard
|
||||
|
||||
@@ -520,7 +520,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
return flt(grand_total, get_currency_precision())
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
inv.save()
|
||||
|
||||
self.assertEqual(inv.net_total, 4298.25)
|
||||
self.assertEqual(inv.net_total, 4298.24)
|
||||
self.assertEqual(inv.grand_total, 4900.00)
|
||||
|
||||
def test_tax_calculation_with_multiple_items(self):
|
||||
|
||||
@@ -340,7 +340,7 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc("Sales Invoice", inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.status, "Return")
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.002)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -419,7 +419,8 @@
|
||||
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate"
|
||||
"label": "Rate",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -647,7 +648,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-17 13:16:34.496704",
|
||||
"modified": "2024-09-16 18:14:51.314765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -709,4 +710,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,17 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_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.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
class TestPricingRule(unittest.TestCase):
|
||||
class TestPricingRule(FrappeTestCase):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
@@ -1099,6 +1102,69 @@ class TestPricingRule(unittest.TestCase):
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
def test_ignore_pricing_rule_for_credit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
selling=1,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
|
||||
item = si.items[0]
|
||||
si.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 80)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
credit_note = make_return_doc(si.doctype, si.name)
|
||||
credit_note.save()
|
||||
self.assertEqual(credit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(credit_note.pricing_rules, [])
|
||||
self.assertEqual(credit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(credit_note.items[0].rate, 80)
|
||||
self.assertEqual(credit_note.items[0].pricing_rules, None)
|
||||
|
||||
credit_note.delete()
|
||||
si.cancel()
|
||||
|
||||
def test_ignore_pricing_rule_for_debit_note(self):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule")
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=20,
|
||||
buying=1,
|
||||
priority=1,
|
||||
title="_Test Pricing Rule",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_submit=True, supplier="_Test Supplier 1", qty=1)
|
||||
item = pi.items[0]
|
||||
pi.submit()
|
||||
self.assertEqual(item.discount_percentage, 20)
|
||||
self.assertEqual(item.rate, 40)
|
||||
|
||||
# change discount on pricing rule
|
||||
pricing_rule.discount_percentage = 30
|
||||
pricing_rule.save()
|
||||
|
||||
# create debit note from purchase invoice
|
||||
debit_note = make_return_doc(pi.doctype, pi.name)
|
||||
debit_note.save()
|
||||
|
||||
self.assertEqual(debit_note.ignore_pricing_rule, 1)
|
||||
self.assertEqual(debit_note.pricing_rules, [])
|
||||
self.assertEqual(debit_note.items[0].discount_percentage, 20)
|
||||
self.assertEqual(debit_note.items[0].rate, 40)
|
||||
self.assertEqual(debit_note.items[0].pricing_rules, None)
|
||||
|
||||
debit_note.delete()
|
||||
pi.cancel()
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -485,7 +485,7 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules, row_item):
|
||||
continue
|
||||
|
||||
stock_qty = row.get("qty") * (row.get("conversion_factor") or 1.0)
|
||||
amount = stock_qty * (row.get("price_list_rate") or row.get("rate"))
|
||||
amount = stock_qty * (flt(row.get("price_list_rate")) or flt(row.get("rate")))
|
||||
pricing_rules = filter_pricing_rules_for_qty_amount(stock_qty, amount, pricing_rules, row)
|
||||
|
||||
if pricing_rules and pricing_rules[0]:
|
||||
|
||||
@@ -20,6 +20,17 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("default_advance_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
is_group: 0,
|
||||
account_type: doc.party_type == "Customer" ? "Receivable" : "Payable",
|
||||
root_type: doc.party_type == "Customer" ? "Liability" : "Asset",
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -102,6 +113,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company(frm) {
|
||||
frm.set_value("party", "");
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
},
|
||||
party_type(frm) {
|
||||
frm.set_value("party", "");
|
||||
@@ -109,6 +121,7 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
|
||||
party(frm) {
|
||||
frm.set_value("receivable_payable_account", "");
|
||||
frm.set_value("default_advance_account", "");
|
||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
@@ -116,10 +129,16 @@ frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
include_advance: 1,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
if (typeof r.message === "string") {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
} else if (Array.isArray(r.message)) {
|
||||
frm.set_value("receivable_payable_account", r.message[0]);
|
||||
frm.set_value("default_advance_account", r.message[1]);
|
||||
}
|
||||
}
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"column_break_io6c",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"filter_section",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
@@ -141,12 +142,23 @@
|
||||
{
|
||||
"fieldname": "section_break_a8yx",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party",
|
||||
"description": "Only 'Payment Entries' made against this advance account are supported.",
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/advance-in-separate-party-account",
|
||||
"fieldname": "default_advance_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-11 10:56:51.699137",
|
||||
"modified": "2024-08-27 14:48:56.715320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
@@ -180,4 +192,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ def get_pr_instance(doc: str):
|
||||
"party_type",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"default_advance_account",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
|
||||
@@ -1265,6 +1265,7 @@
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -1610,7 +1611,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified": "2024-09-11 12:59:19.130593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -369,6 +369,21 @@ class PurchaseInvoice(BuyingController):
|
||||
item.expense_account = stock_not_billed_account
|
||||
elif item.is_fixed_asset:
|
||||
account = None
|
||||
if not item.pr_detail and item.po_detail:
|
||||
receipt_item = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
{
|
||||
"purchase_order": item.purchase_order,
|
||||
"purchase_order_item": item.po_detail,
|
||||
"docstatus": 1,
|
||||
},
|
||||
["name", "parent"],
|
||||
as_dict=1,
|
||||
)
|
||||
if receipt_item:
|
||||
item.pr_detail = receipt_item.name
|
||||
item.purchase_receipt = receipt_item.parent
|
||||
|
||||
if item.pr_detail:
|
||||
if not self.asset_received_but_not_billed:
|
||||
self.asset_received_but_not_billed = self.get_company_default(
|
||||
|
||||
@@ -10,7 +10,10 @@ import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_invoice as make_pi_from_po
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
|
||||
@@ -307,7 +307,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.insert()
|
||||
|
||||
# with inclusive tax
|
||||
self.assertEqual(si.items[0].net_amount, 3947.368421052631)
|
||||
self.assertEqual(si.items[0].net_amount, 3947.37)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 3947.37)
|
||||
self.assertEqual(si.grand_total, 5000)
|
||||
|
||||
@@ -414,8 +415,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.account_head][i])
|
||||
|
||||
self.assertEqual(si.base_grand_total, 1500)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.base_grand_total, 1500.01)
|
||||
self.assertEqual(si.grand_total, 1500.01)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
def test_discount_amount_gl_entry(self):
|
||||
@@ -651,7 +652,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
62.5,
|
||||
625.0,
|
||||
50,
|
||||
499.97600115194473,
|
||||
499.98,
|
||||
],
|
||||
"_Test Item Home Desktop 200": [
|
||||
190.66,
|
||||
@@ -662,7 +663,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
190.66,
|
||||
953.3,
|
||||
150,
|
||||
749.9968530500239,
|
||||
750,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -675,20 +676,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||
|
||||
# check net total
|
||||
self.assertEqual(si.net_total, 1249.97)
|
||||
self.assertEqual(si.base_net_total, si.net_total)
|
||||
self.assertEqual(si.net_total, 1249.98)
|
||||
self.assertEqual(si.total, 1578.3)
|
||||
|
||||
# check tax calculation
|
||||
expected_values = {
|
||||
"keys": ["tax_amount", "total"],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.97],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.77],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.17],
|
||||
"_Test Account CST - _TC": [27.88, 1422.05],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.30],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.30],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.30],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.97],
|
||||
"_Test Account Excise Duty - _TC": [140, 1389.98],
|
||||
"_Test Account Education Cess - _TC": [2.8, 1392.78],
|
||||
"_Test Account S&H Education Cess - _TC": [1.4, 1394.18],
|
||||
"_Test Account CST - _TC": [27.88, 1422.06],
|
||||
"_Test Account VAT - _TC": [156.25, 1578.31],
|
||||
"_Test Account Customs Duty - _TC": [125, 1703.31],
|
||||
"_Test Account Shipping Charges - _TC": [100, 1803.31],
|
||||
"_Test Account Discount - _TC": [-180.33, 1622.98],
|
||||
}
|
||||
|
||||
for d in si.get("taxes"):
|
||||
@@ -724,7 +726,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 2500,
|
||||
"base_amount": 25000,
|
||||
"net_rate": 40,
|
||||
"net_amount": 399.9808009215558,
|
||||
"net_amount": 399.98,
|
||||
"base_net_rate": 2000,
|
||||
"base_net_amount": 19999,
|
||||
},
|
||||
@@ -738,7 +740,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
"base_rate": 7500,
|
||||
"base_amount": 37500,
|
||||
"net_rate": 118.01,
|
||||
"net_amount": 590.0531205155963,
|
||||
"net_amount": 590.05,
|
||||
"base_net_rate": 5900.5,
|
||||
"base_net_amount": 29502.5,
|
||||
},
|
||||
@@ -776,8 +778,13 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(si.base_grand_total, 60795)
|
||||
self.assertEqual(si.grand_total, 1215.90)
|
||||
self.assertEqual(si.rounding_adjustment, 0.01)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.50)
|
||||
# no rounding adjustment as the Smallest Currency Fraction Value of USD is 0.01
|
||||
if frappe.db.get_value("Currency", "USD", "smallest_currency_fraction_value") < 0.01:
|
||||
self.assertEqual(si.rounding_adjustment, 0.10)
|
||||
self.assertEqual(si.base_rounding_adjustment, 5.0)
|
||||
else:
|
||||
self.assertEqual(si.rounding_adjustment, 0.0)
|
||||
self.assertEqual(si.base_rounding_adjustment, 0.0)
|
||||
|
||||
def test_outstanding(self):
|
||||
w = self.make()
|
||||
@@ -2099,7 +2106,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.net_total, 19453.13)
|
||||
self.assertEqual(si.grand_total, 24900)
|
||||
self.assertEqual(si.total_taxes_and_charges, 5446.88)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.rounding_adjustment, 0.00)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
@@ -2126,7 +2133,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
def test_rounding_adjustment_2(self):
|
||||
si = create_sales_invoice(rate=400, do_not_save=True)
|
||||
for rate in [400, 600, 100]:
|
||||
for rate in [400.25, 600.30, 100.65]:
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
@@ -2152,17 +2159,18 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
)
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 1271.19)
|
||||
self.assertEqual(si.grand_total, 1500)
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 1272.20)
|
||||
self.assertEqual(si.grand_total, 1501.20)
|
||||
self.assertEqual(si.total_taxes_and_charges, 229)
|
||||
self.assertEqual(si.rounding_adjustment, -0.20)
|
||||
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.50],
|
||||
["_Test Account VAT - _TC", 0.0, 114.50],
|
||||
[si.debit_to, 1501, 0.0],
|
||||
["Round Off - _TC", 0.20, 0.0],
|
||||
["Sales - _TC", 0.0, 1272.20],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@@ -2220,7 +2228,8 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
self.assertEqual(si.net_total, si.base_net_total)
|
||||
self.assertEqual(si.net_total, 4007.15)
|
||||
self.assertEqual(si.grand_total, 4488.02)
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
@@ -2232,7 +2241,7 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
["Round Off - _TC", 0.01, 0.0],
|
||||
]
|
||||
)
|
||||
|
||||
@@ -3104,6 +3113,50 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_sales_invoice_cancel_with_common_party_advance_jv(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Supplier")
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Supplier").name
|
||||
|
||||
# create a party link between customer & supplier
|
||||
party_link = create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# enable common party accounting
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(customer=customer)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="parent",
|
||||
)
|
||||
|
||||
self.assertTrue(jv)
|
||||
|
||||
# cancel sales invoice
|
||||
si.cancel()
|
||||
|
||||
# check cancellation of journal entry
|
||||
jv_status = frappe.db.get_value("Journal Entry", jv, "docstatus")
|
||||
self.assertEqual(jv_status, 2)
|
||||
|
||||
party_link.delete()
|
||||
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -3625,6 +3678,88 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(len(res), 1)
|
||||
self.assertEqual(res[0][0], pos_return.return_against)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_foreign_currency_jv(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors USD",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party USD").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -694,10 +694,7 @@ class Subscription(Document):
|
||||
elif self.generate_invoice_at == "Days before the current subscription period":
|
||||
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
|
||||
|
||||
process_subscription = frappe.new_doc("Process Subscription")
|
||||
process_subscription.posting_date = processing_date
|
||||
process_subscription.subscription = self.name
|
||||
process_subscription.save().submit()
|
||||
self.process(posting_date=processing_date)
|
||||
|
||||
|
||||
def get_calendar_months(billing_interval):
|
||||
|
||||
@@ -179,50 +179,53 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
|
||||
|
||||
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
|
||||
if not cost_center_allocation:
|
||||
return gl_map
|
||||
|
||||
new_gl_map = []
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
# Validate budget against main cost center
|
||||
validate_expense_against_budget(d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision))
|
||||
|
||||
if cost_center and cost_center_allocation.get(cost_center):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
if not cost_center_allocation:
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
for sub_cost_center, percentage in cost_center_allocation:
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
|
||||
return new_gl_map
|
||||
|
||||
|
||||
def get_cost_center_allocation_data(company, posting_date):
|
||||
par = frappe.qb.DocType("Cost Center Allocation")
|
||||
child = frappe.qb.DocType("Cost Center Allocation Percentage")
|
||||
def get_cost_center_allocation_data(company, posting_date, cost_center):
|
||||
cost_center_allocation = frappe.db.get_value(
|
||||
"Cost Center Allocation",
|
||||
{
|
||||
"docstatus": 1,
|
||||
"company": company,
|
||||
"valid_from": ("<=", posting_date),
|
||||
"main_cost_center": cost_center,
|
||||
},
|
||||
pluck="name",
|
||||
order_by="valid_from desc",
|
||||
)
|
||||
|
||||
records = (
|
||||
frappe.qb.from_(par)
|
||||
.inner_join(child)
|
||||
.on(par.name == child.parent)
|
||||
.select(par.main_cost_center, child.cost_center, child.percentage)
|
||||
.where(par.docstatus == 1)
|
||||
.where(par.company == company)
|
||||
.where(par.valid_from <= posting_date)
|
||||
.orderby(par.valid_from, order=frappe.qb.desc)
|
||||
).run(as_dict=True)
|
||||
if not cost_center_allocation:
|
||||
return []
|
||||
|
||||
cc_allocation = frappe._dict()
|
||||
for d in records:
|
||||
cc_allocation.setdefault(d.main_cost_center, frappe._dict()).setdefault(d.cost_center, d.percentage)
|
||||
records = frappe.db.get_all(
|
||||
"Cost Center Allocation Percentage",
|
||||
{"parent": cost_center_allocation},
|
||||
["cost_center", "percentage"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
return cc_allocation
|
||||
return records
|
||||
|
||||
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
@@ -68,7 +68,7 @@ def get_party_details(
|
||||
pos_profile=None,
|
||||
):
|
||||
if not party:
|
||||
return {}
|
||||
return frappe._dict()
|
||||
if not frappe.db.exists(party_type, party):
|
||||
frappe.throw(_("{0}: {1} does not exists").format(party_type, party))
|
||||
return _get_party_details(
|
||||
|
||||
@@ -162,6 +162,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
label: __("Group by Voucher"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "handle_employee_advances",
|
||||
label: __("Handle Employee Advances"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -112,6 +112,26 @@ class ReceivablePayableReport:
|
||||
|
||||
self.build_data()
|
||||
|
||||
def build_voucher_dict(self, ple):
|
||||
return frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
|
||||
def init_voucher_balance(self):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
@@ -123,24 +143,8 @@ class ReceivablePayableReport:
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
voucher_no=ple.voucher_no,
|
||||
party=ple.party,
|
||||
party_account=ple.account,
|
||||
posting_date=ple.posting_date,
|
||||
account_currency=ple.account_currency,
|
||||
remarks=ple.remarks,
|
||||
invoiced=0.0,
|
||||
paid=0.0,
|
||||
credit_note=0.0,
|
||||
outstanding=0.0,
|
||||
invoiced_in_account_currency=0.0,
|
||||
paid_in_account_currency=0.0,
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
cost_center=ple.cost_center,
|
||||
)
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -208,6 +212,18 @@ class ReceivablePayableReport:
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
# Build and use a separate row for Employee Advances.
|
||||
# This allows Payments or Journals made against Emp Advance to be processed.
|
||||
if (
|
||||
not row
|
||||
and ple.against_voucher_type == "Employee Advance"
|
||||
and self.filters.handle_employee_advances
|
||||
):
|
||||
_d = self.build_voucher_dict(ple)
|
||||
_d.voucher_type = ple.against_voucher_type
|
||||
_d.voucher_no = ple.against_voucher_no
|
||||
row = self.voucher_balance[key] = _d
|
||||
|
||||
if not row:
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
if self.filters.get("ignore_accounts"):
|
||||
|
||||
@@ -46,4 +46,20 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter, filter) {
|
||||
if (column.fieldname == "payment_entry" && value == "Cheques and Deposits incorrectly cleared") {
|
||||
column.link_onclick =
|
||||
"frappe.query_reports['Bank Reconciliation Statement'].open_utility_report()";
|
||||
}
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
open_utility_report: function () {
|
||||
frappe.route_options = {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account: frappe.query_report.get_filter_value("account"),
|
||||
report_date: frappe.query_report.get_filter_value("report_date"),
|
||||
};
|
||||
frappe.open_in_new_tab = true;
|
||||
frappe.set_route("query-report", "Cheques and Deposits Incorrectly cleared");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -150,8 +150,8 @@ def get_payment_entries(filters):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
|
||||
@@ -43,7 +43,7 @@ function get_filters() {
|
||||
label: __("From Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
@@ -51,7 +51,7 @@ function get_filters() {
|
||||
label: __("To Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cheques and Deposits Incorrectly cleared"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Bank Account"),
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
default: frappe.defaults.get_user_default("Company")
|
||||
? locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]
|
||||
: "",
|
||||
reqd: 1,
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_account_list",
|
||||
filters: [
|
||||
["Account", "account_type", "in", "Bank, Cash"],
|
||||
["Account", "is_group", "=", 0],
|
||||
["Account", "disabled", "=", 0],
|
||||
["Account", "company", "=", company],
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "report_date",
|
||||
label: __("Date"),
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-07-30 17:20:07.570971",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-07-30 17:20:07.570971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Cheques and Deposits Incorrectly cleared",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Payment Entry",
|
||||
"report_name": "Cheques and Deposits Incorrectly cleared",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
data = build_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def build_payment_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
}
|
||||
)
|
||||
if row.get("payment_type") == "Receive" and row.get("party_type") in ["Customer", "Supplier"]:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": row.get("amount"),
|
||||
"credit": 0,
|
||||
}
|
||||
)
|
||||
else:
|
||||
row_dict.update(
|
||||
{
|
||||
"debit": 0,
|
||||
"credit": row.get("amount"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_journal_entry_dict(row: dict) -> dict:
|
||||
row_dict = frappe._dict()
|
||||
row_dict.update(
|
||||
{
|
||||
"payment_document": row.get("doctype"),
|
||||
"payment_entry": row.get("name"),
|
||||
"posting_date": row.get("posting_date"),
|
||||
"clearance_date": row.get("clearance_date"),
|
||||
"debit": row.get("debit_in_account_currency"),
|
||||
"credit": row.get("credit_in_account_currency"),
|
||||
}
|
||||
)
|
||||
return row_dict
|
||||
|
||||
|
||||
def build_data(filters):
|
||||
vouchers = get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters)
|
||||
data = []
|
||||
for x in vouchers:
|
||||
if x.doctype == "Payment Entry":
|
||||
data.append(build_payment_entry_dict(x))
|
||||
elif x.doctype == "Journal Entry":
|
||||
data.append(build_journal_entry_dict(x))
|
||||
return data
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
doctype_name = ConstantColumn("Journal Entry")
|
||||
|
||||
journals = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(je.name == jea.parent)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
je.name,
|
||||
jea.debit_in_account_currency,
|
||||
jea.credit_in_account_currency,
|
||||
je.posting_date,
|
||||
je.clearance_date,
|
||||
)
|
||||
.where(
|
||||
je.docstatus.eq(1)
|
||||
& jea.account.eq(filters.account)
|
||||
& je.posting_date.gt(filters.report_date)
|
||||
& je.clearance_date.lte(filters.report_date)
|
||||
& (je.is_opening.isnull() | je.is_opening.eq("No"))
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||
pe = qb.DocType("Payment Entry")
|
||||
doctype_name = ConstantColumn("Payment Entry")
|
||||
payments = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
doctype_name.as_("doctype"),
|
||||
pe.name,
|
||||
ifelse(pe.paid_from.eq(filters.account), pe.paid_amount, pe.received_amount).as_("amount"),
|
||||
pe.payment_type,
|
||||
pe.party_type,
|
||||
pe.posting_date,
|
||||
pe.clearance_date,
|
||||
)
|
||||
.where(
|
||||
pe.docstatus.eq(1)
|
||||
& (pe.paid_from.eq(filters.account) | pe.paid_to.eq(filters.account))
|
||||
& pe.posting_date.gt(filters.report_date)
|
||||
& pe.clearance_date.lte(filters.report_date)
|
||||
)
|
||||
.run(as_dict=1)
|
||||
)
|
||||
|
||||
return journals + payments
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "payment_document",
|
||||
"label": _("Payment Document Type"),
|
||||
"fieldtype": "Data",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry",
|
||||
"label": _("Payment Document"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "payment_document",
|
||||
"width": 220,
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": _("Debit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": _("Credit"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "account_currency",
|
||||
"width": 120,
|
||||
},
|
||||
{"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 110},
|
||||
{"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110},
|
||||
]
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "from_date",
|
||||
label: __("Start Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
},
|
||||
{
|
||||
fieldname: "to_date",
|
||||
label: __("End Date"),
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "account",
|
||||
label: __("Account"),
|
||||
fieldtype: "MultiSelectList",
|
||||
options: "Account",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Account", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
fieldtype: "Data",
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Invalid Ledger Entries"] = {
|
||||
filters: get_filters(),
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2024-09-09 12:31:25.295976",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2024-09-09 12:31:25.295976",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Invalid Ledger Entries",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Invalid Ledger Entries",
|
||||
"report_type": "Script Report",
|
||||
"roles": [],
|
||||
"timeout": 0
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
|
||||
|
||||
def execute(filters: dict | None = None):
|
||||
"""Return columns and data for the report.
|
||||
|
||||
This is the main entry point for the report. It accepts the filters as a
|
||||
dictionary and should return columns and data. It is called by the framework
|
||||
every time the report is refreshed or a filter is updated.
|
||||
"""
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns() -> list[dict]:
|
||||
"""Return columns for the report.
|
||||
|
||||
One field definition per column, just like a DocType field definition.
|
||||
"""
|
||||
return [
|
||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "fieldtype": "Link", "options": "DocType"},
|
||||
{
|
||||
"label": _("Voucher No"),
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "voucher_type",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(filters) -> list[list]:
|
||||
"""Return data for the report.
|
||||
|
||||
The report data is a list of rows, with each row being a list of cell values.
|
||||
"""
|
||||
active_vouchers = get_active_vouchers_for_period(filters)
|
||||
invalid_vouchers = identify_cancelled_vouchers(active_vouchers)
|
||||
|
||||
return invalid_vouchers
|
||||
|
||||
|
||||
def identify_cancelled_vouchers(active_vouchers: list[dict] | list | None = None) -> list[dict]:
|
||||
cancelled_vouchers = []
|
||||
if active_vouchers:
|
||||
# Group by voucher types and use single query to identify cancelled vouchers
|
||||
vtypes = set([x.voucher_type for x in active_vouchers])
|
||||
|
||||
for _t in vtypes:
|
||||
_names = [x.voucher_no for x in active_vouchers if x.voucher_type == _t]
|
||||
dt = qb.DocType(_t)
|
||||
non_active_vouchers = (
|
||||
qb.from_(dt)
|
||||
.select(ConstantColumn(_t).as_("voucher_type"), dt.name.as_("voucher_no"))
|
||||
.where(dt.docstatus.ne(1) & dt.name.isin(_names))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if non_active_vouchers:
|
||||
cancelled_vouchers.extend(non_active_vouchers)
|
||||
return cancelled_vouchers
|
||||
|
||||
|
||||
def validate_filters(filters: dict | None = None):
|
||||
if not filters:
|
||||
frappe.throw(_("Filters missing"))
|
||||
|
||||
if not filters.company:
|
||||
frappe.throw(_("Company is mandatory"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
frappe.throw(_("Start Date should be lower than End Date"))
|
||||
|
||||
|
||||
def build_query_filters(filters: dict | None = None) -> list:
|
||||
qb_filters = []
|
||||
if filters:
|
||||
if filters.account:
|
||||
qb_filters.append(qb.Field("account").isin(filters.account))
|
||||
|
||||
if filters.voucher_no:
|
||||
qb_filters.append(qb.Field("voucher_no").eq(filters.voucher_no))
|
||||
|
||||
return qb_filters
|
||||
|
||||
|
||||
def get_active_vouchers_for_period(filters: dict | None = None) -> list[dict]:
|
||||
uniq_vouchers = []
|
||||
|
||||
if filters:
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
qb_filters = build_query_filters(filters)
|
||||
|
||||
gl_vouchers = (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_type)
|
||||
.distinct()
|
||||
.select(gle.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
gle.is_cancelled.eq(0)
|
||||
& gle.company.eq(filters.company)
|
||||
& gle.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
pl_vouchers = (
|
||||
qb.from_(ple)
|
||||
.select(ple.voucher_type)
|
||||
.distinct()
|
||||
.select(ple.voucher_no)
|
||||
.distinct()
|
||||
.where(
|
||||
ple.delinked.eq(0)
|
||||
& ple.company.eq(filters.company)
|
||||
& ple.posting_date[filters.from_date : filters.to_date]
|
||||
)
|
||||
.where(Criterion.all(qb_filters))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
uniq_vouchers.extend(gl_vouchers)
|
||||
uniq_vouchers.extend(pl_vouchers)
|
||||
|
||||
return uniq_vouchers
|
||||
@@ -703,40 +703,114 @@ def cancel_exchange_gain_loss_journal(
|
||||
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": parent_doc.doctype,
|
||||
"reference_name": parent_doc.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
gain_loss_journals = get_linked_exchange_gain_loss_journal(
|
||||
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=1
|
||||
)
|
||||
|
||||
if journals:
|
||||
gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"name": ["in", [x[0] for x in journals]],
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"docstatus": 1,
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only cancel JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.cancel()
|
||||
else:
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc)
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only cancel JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.cancel()
|
||||
else:
|
||||
gain_loss_je.cancel()
|
||||
|
||||
|
||||
def delete_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Delete Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
if parent_doc.doctype in ["Sales Invoice", "Purchase Invoice", "Payment Entry", "Journal Entry"]:
|
||||
gain_loss_journals = get_linked_exchange_gain_loss_journal(
|
||||
referenced_dt=parent_doc.doctype, referenced_dn=parent_doc.name, je_docstatus=2
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc)
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only delete JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.delete()
|
||||
else:
|
||||
gain_loss_je.delete()
|
||||
|
||||
|
||||
def get_linked_exchange_gain_loss_journal(referenced_dt: str, referenced_dn: str, je_docstatus: int) -> list:
|
||||
"""
|
||||
Get all the linked exchange gain/loss journal entries for a given document.
|
||||
"""
|
||||
gain_loss_journals = []
|
||||
if journals := frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"reference_type": referenced_dt,
|
||||
"reference_name": referenced_dn,
|
||||
"docstatus": je_docstatus,
|
||||
},
|
||||
pluck="parent",
|
||||
):
|
||||
gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
{
|
||||
"name": ["in", journals],
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"is_system_generated": 1,
|
||||
"docstatus": je_docstatus,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
return gain_loss_journals
|
||||
|
||||
|
||||
def cancel_common_party_journal(self):
|
||||
if self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Accounts Settings", "enable_common_party_accounting"):
|
||||
return
|
||||
|
||||
party_link = self.get_common_party_link()
|
||||
if not party_link:
|
||||
return
|
||||
|
||||
journal_entry = frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
filters={
|
||||
"reference_type": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
fieldname="parent",
|
||||
)
|
||||
|
||||
if not journal_entry:
|
||||
return
|
||||
|
||||
common_party_journal = frappe.db.get_value(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"name": journal_entry,
|
||||
"is_system_generated": True,
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if not common_party_journal:
|
||||
return
|
||||
|
||||
common_party_je = frappe.get_doc("Journal Entry", common_party_journal)
|
||||
common_party_je.cancel()
|
||||
|
||||
|
||||
def update_accounting_ledgers_after_reference_removal(
|
||||
|
||||
@@ -11,6 +11,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
|
||||
@@ -175,7 +175,7 @@ def get_data(filters):
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"actual_cost": flt(pi_records.get(po.name)) or flt(po.amount),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
|
||||
@@ -84,7 +84,6 @@ force_item_fields = (
|
||||
"brand",
|
||||
"stock_uom",
|
||||
"is_fixed_asset",
|
||||
"item_tax_rate",
|
||||
"pricing_rules",
|
||||
"weight_per_unit",
|
||||
"weight_uom",
|
||||
@@ -327,11 +326,16 @@ class AccountsController(TransactionBase):
|
||||
repost_doc.save(ignore_permissions=True)
|
||||
|
||||
def on_trash(self):
|
||||
from erpnext.accounts.utils import delete_exchange_gain_loss_journal
|
||||
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
# delete linked exchange gain/loss journal
|
||||
delete_exchange_gain_loss_journal(self)
|
||||
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
frappe.qb.from_(ple).delete().where(
|
||||
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
|
||||
@@ -707,7 +711,6 @@ class AccountsController(TransactionBase):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
ret = get_item_details(args, self, for_validate=for_validate, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
if item.get(fieldname) is None or fieldname in force_item_fields:
|
||||
@@ -717,7 +720,10 @@ class AccountsController(TransactionBase):
|
||||
fieldname
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "item_tax_rate" and not (
|
||||
self.get("is_return") and self.get("return_against")
|
||||
):
|
||||
item.set(fieldname, value)
|
||||
elif fieldname == "serial_no":
|
||||
# Ensure that serial numbers are matched against Stock UOM
|
||||
item_conversion_factor = item.get("conversion_factor") or 1.0
|
||||
@@ -1192,6 +1198,12 @@ class AccountsController(TransactionBase):
|
||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||
if self.docstatus == 1:
|
||||
if dimensions_dict is None:
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
||||
|
||||
if self.get("doctype") == "Journal Entry":
|
||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
||||
@@ -1456,6 +1468,7 @@ class AccountsController(TransactionBase):
|
||||
remove_from_bank_transaction,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_common_party_journal,
|
||||
cancel_exchange_gain_loss_journal,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
)
|
||||
@@ -1467,6 +1480,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
# Cancel Exchange Gain/Loss Journal before unlinking
|
||||
cancel_exchange_gain_loss_journal(self)
|
||||
cancel_common_party_journal(self)
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "unlink_payment_on_cancellation_of_invoice"):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
@@ -2289,12 +2303,15 @@ class AccountsController(TransactionBase):
|
||||
|
||||
primary_account = get_party_account(primary_party_type, primary_party, self.company)
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
jv.posting_date = self.posting_date
|
||||
jv.company = self.company
|
||||
jv.remark = f"Adjustment for {self.doctype} {self.name}"
|
||||
jv.is_system_generated = True
|
||||
|
||||
reconcilation_entry = frappe._dict()
|
||||
advance_entry = frappe._dict()
|
||||
@@ -2328,6 +2345,10 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||
jv.multi_currency = 1
|
||||
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
|
||||
@@ -334,6 +334,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
doc = frappe.get_doc(target)
|
||||
doc.is_return = 1
|
||||
doc.ignore_pricing_rule = 1
|
||||
doc.pricing_rules = []
|
||||
doc.return_against = source.name
|
||||
doc.set_warehouse = ""
|
||||
if doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||
@@ -397,6 +399,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
|
||||
def update_item(source_doc, target_doc, source_parent):
|
||||
target_doc.qty = -1 * source_doc.qty
|
||||
target_doc.pricing_rules = None
|
||||
|
||||
if source_doc.serial_no:
|
||||
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
|
||||
|
||||
@@ -591,7 +591,7 @@ class SellingController(StockController):
|
||||
if self.doctype in ["Sales Order", "Quotation"]:
|
||||
for item in self.items:
|
||||
item.gross_profit = flt(
|
||||
((item.base_rate - flt(item.valuation_rate)) * item.stock_qty),
|
||||
((flt(item.stock_uom_rate) - flt(item.valuation_rate)) * item.stock_qty),
|
||||
self.precision("amount", item),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
|
||||
@@ -71,7 +72,7 @@ class calculate_taxes_and_totals:
|
||||
self.calculate_net_total()
|
||||
self.calculate_tax_withholding_net_total()
|
||||
self.calculate_taxes()
|
||||
self.manipulate_grand_total_for_inclusive_tax()
|
||||
self.adjust_grand_total_for_inclusive_tax()
|
||||
self.calculate_totals()
|
||||
self._cleanup()
|
||||
self.calculate_total_net_weight()
|
||||
@@ -89,6 +90,9 @@ class calculate_taxes_and_totals:
|
||||
self.doc.base_tax_withholding_net_total = sum_base_net_amount
|
||||
|
||||
def validate_item_tax_template(self):
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
return
|
||||
|
||||
for item in self._items:
|
||||
if item.item_code and item.get("item_tax_template"):
|
||||
item_doc = frappe.get_cached_doc("Item", item.item_code)
|
||||
@@ -237,7 +241,6 @@ class calculate_taxes_and_totals:
|
||||
"tax_fraction_for_current_item",
|
||||
"grand_total_fraction_for_current_item",
|
||||
]
|
||||
|
||||
if tax.charge_type != "Actual" and not (
|
||||
self.discount_amount_applied and self.doc.apply_discount_on == "Grand Total"
|
||||
):
|
||||
@@ -280,7 +283,7 @@ class calculate_taxes_and_totals:
|
||||
):
|
||||
amount = flt(item.amount) - total_inclusive_tax_amount_per_qty
|
||||
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction))
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), item.precision("net_amount"))
|
||||
item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate"))
|
||||
item.discount_percentage = flt(
|
||||
item.discount_percentage, item.precision("discount_percentage")
|
||||
@@ -505,7 +508,12 @@ class calculate_taxes_and_totals:
|
||||
tax.base_tax_amount = round(tax.base_tax_amount, 0)
|
||||
tax.base_tax_amount_after_discount_amount = round(tax.base_tax_amount_after_discount_amount, 0)
|
||||
|
||||
@deprecated
|
||||
def manipulate_grand_total_for_inclusive_tax(self):
|
||||
# for backward compatablility - if in case used by an external application
|
||||
return self.adjust_grand_total_for_inclusive_tax()
|
||||
|
||||
def adjust_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
@@ -527,17 +535,21 @@ class calculate_taxes_and_totals:
|
||||
diff = flt(diff, self.doc.precision("rounding_adjustment"))
|
||||
|
||||
if diff and abs(diff) <= (5.0 / 10 ** last_tax.precision("tax_amount")):
|
||||
self.doc.rounding_adjustment = diff
|
||||
self.doc.grand_total_diff = diff
|
||||
else:
|
||||
self.doc.grand_total_diff = 0
|
||||
|
||||
def calculate_totals(self):
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(
|
||||
self.doc.get("grand_total_diff")
|
||||
)
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.get("grand_total_diff")),
|
||||
self.doc.precision("total_taxes_and_charges"),
|
||||
)
|
||||
else:
|
||||
@@ -600,8 +612,8 @@ class calculate_taxes_and_totals:
|
||||
self.doc.grand_total, self.doc.currency, self.doc.precision("rounded_total")
|
||||
)
|
||||
|
||||
# if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(
|
||||
# rounding adjustment should always be the difference vetween grand and rounded total
|
||||
self.doc.rounding_adjustment = flt(
|
||||
self.doc.rounded_total - self.doc.grand_total, self.doc.precision("rounding_adjustment")
|
||||
)
|
||||
|
||||
|
||||
@@ -185,7 +185,8 @@
|
||||
{
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
"label": "Expected Closing Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
@@ -357,6 +358,7 @@
|
||||
"fieldname": "transaction_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Opportunity Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "transaction_date",
|
||||
"oldfieldtype": "Date",
|
||||
"reqd": 1,
|
||||
@@ -388,6 +390,7 @@
|
||||
"fieldname": "first_response_time",
|
||||
"fieldtype": "Duration",
|
||||
"label": "First Response Time",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -622,7 +625,7 @@
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-10-13 12:42:21.545636",
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
||||
@@ -8,7 +8,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import cint, cstr
|
||||
from frappe.utils import cint, cstr, date_diff, today
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
|
||||
get_leaf_boms,
|
||||
@@ -67,10 +67,12 @@ class BOMUpdateLog(Document):
|
||||
|
||||
wip_log = frappe.get_all(
|
||||
"BOM Update Log",
|
||||
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
fields=["name", "modified"],
|
||||
filters={"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||
limit_page_length=1,
|
||||
)
|
||||
if wip_log:
|
||||
|
||||
if wip_log and date_diff(today(), wip_log[0].modified) < 1:
|
||||
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
|
||||
frappe.throw(
|
||||
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
|
||||
|
||||
@@ -230,7 +230,8 @@ cur_frm.cscript.validate_taxes_and_charges = function(cdt, cdn) {
|
||||
|
||||
}
|
||||
|
||||
cur_frm.cscript.validate_inclusive_tax = function(tax) {
|
||||
cur_frm.cscript.validate_inclusive_tax = function(tax, frm) {
|
||||
this.frm = this.frm || frm;
|
||||
var actual_type_error = function() {
|
||||
var msg = __("Actual type tax cannot be included in Item rate in row {0}", [tax.idx])
|
||||
frappe.throw(msg);
|
||||
@@ -246,12 +247,12 @@ cur_frm.cscript.validate_inclusive_tax = function(tax) {
|
||||
if(tax.charge_type == "Actual") {
|
||||
// inclusive tax cannot be of type Actual
|
||||
actual_type_error();
|
||||
} else if(tax.charge_type == "On Previous Row Amount" &&
|
||||
} else if(tax.charge_type == "On Previous Row Amount" && this.frm &&
|
||||
!cint(this.frm.doc["taxes"][tax.row_id - 1].included_in_print_rate)
|
||||
) {
|
||||
// referred row should also be an inclusive tax
|
||||
on_previous_row_error(tax.row_id);
|
||||
} else if(tax.charge_type == "On Previous Row Total") {
|
||||
} else if(tax.charge_type == "On Previous Row Total" && this.frm) {
|
||||
var taxes_not_included = $.map(this.frm.doc["taxes"].slice(0, tax.row_id),
|
||||
function(t) { return cint(t.included_in_print_rate) ? null : t; });
|
||||
if(taxes_not_included.length > 0) {
|
||||
@@ -294,7 +295,7 @@ if(!erpnext.taxes.flags[cur_frm.cscript.tax_table]) {
|
||||
var tax = frappe.get_doc(cdt, cdn);
|
||||
try {
|
||||
cur_frm.cscript.validate_taxes_and_charges(cdt, cdn);
|
||||
cur_frm.cscript.validate_inclusive_tax(tax);
|
||||
cur_frm.cscript.validate_inclusive_tax(tax, frm);
|
||||
} catch(e) {
|
||||
tax.included_in_print_rate = 0;
|
||||
refresh_field("included_in_print_rate", tax.name, tax.parentfield);
|
||||
|
||||
@@ -103,7 +103,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.determine_exclusive_rate();
|
||||
this.calculate_net_total();
|
||||
this.calculate_taxes();
|
||||
this.manipulate_grand_total_for_inclusive_tax();
|
||||
this.adjust_grand_total_for_inclusive_tax();
|
||||
this.calculate_totals();
|
||||
this._cleanup();
|
||||
}
|
||||
@@ -243,7 +243,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) {
|
||||
var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty;
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction));
|
||||
item.net_amount = flt(amount / (1 + cumulated_tax_fraction), precision("net_amount", item));
|
||||
item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0;
|
||||
|
||||
me.set_in_company_currency(item, ["net_rate", "net_amount"]);
|
||||
@@ -298,6 +298,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
me.frm.doc.net_total += item.net_amount;
|
||||
me.frm.doc.base_net_total += item.base_net_amount;
|
||||
});
|
||||
|
||||
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
|
||||
}
|
||||
|
||||
calculate_shipping_charges() {
|
||||
@@ -506,8 +508,17 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use adjust_grand_total_for_inclusive_tax instead.
|
||||
*/
|
||||
manipulate_grand_total_for_inclusive_tax() {
|
||||
// for backward compatablility - if in case used by an external application
|
||||
this.adjust_grand_total_for_inclusive_tax()
|
||||
}
|
||||
|
||||
adjust_grand_total_for_inclusive_tax() {
|
||||
var me = this;
|
||||
|
||||
// if fully inclusive taxes and diff
|
||||
if (this.frm.doc["taxes"] && this.frm.doc["taxes"].length) {
|
||||
var any_inclusive_tax = false;
|
||||
@@ -533,7 +544,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
diff = flt(diff, precision("rounding_adjustment"));
|
||||
|
||||
if ( diff && Math.abs(diff) <= (5.0 / Math.pow(10, precision("tax_amount", last_tax))) ) {
|
||||
me.frm.doc.rounding_adjustment = diff;
|
||||
me.frm.doc.grand_total_diff = diff;
|
||||
} else {
|
||||
me.frm.doc.grand_total_diff = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,7 +557,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var me = this;
|
||||
var tax_count = this.frm.doc["taxes"] ? this.frm.doc["taxes"].length : 0;
|
||||
this.frm.doc.grand_total = flt(tax_count
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.grand_total_diff)
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
@@ -604,7 +617,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if(frappe.meta.get_docfield(this.frm.doc.doctype, "rounded_total", this.frm.doc.name)) {
|
||||
this.frm.doc.rounded_total = round_based_on_smallest_currency_fraction(this.frm.doc.grand_total,
|
||||
this.frm.doc.currency, precision("rounded_total"));
|
||||
this.frm.doc.rounding_adjustment += flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
|
||||
this.frm.doc.rounding_adjustment = flt(this.frm.doc.rounded_total - this.frm.doc.grand_total,
|
||||
precision("rounding_adjustment"));
|
||||
|
||||
this.set_in_company_currency(this.frm.doc, ["rounding_adjustment", "rounded_total"]);
|
||||
@@ -672,8 +685,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if (total_for_discount_amount) {
|
||||
$.each(this.frm._items || [], function(i, item) {
|
||||
distributed_amount = flt(me.frm.doc.discount_amount) * item.net_amount / total_for_discount_amount;
|
||||
item.net_amount = flt(item.net_amount - distributed_amount,
|
||||
precision("base_amount", item));
|
||||
item.net_amount = flt(item.net_amount - distributed_amount, precision("net_amount", item));
|
||||
net_total += item.net_amount;
|
||||
|
||||
// discount amount rounding loss adjustment if no taxes
|
||||
|
||||
@@ -1817,6 +1817,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let item_rates = {};
|
||||
let item_tax_templates = {};
|
||||
|
||||
if (me.frm.doc.is_return && me.frm.doc.return_against) return;
|
||||
|
||||
$.each(this.frm.doc.items || [], function(i, item) {
|
||||
if (item.item_code) {
|
||||
// Use combination of name and item code in case same item is added multiple times
|
||||
|
||||
@@ -115,14 +115,17 @@ erpnext.financial_statements = {
|
||||
erpnext.financial_statements.filters = get_filters();
|
||||
|
||||
let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
|
||||
var filters = report.get_values();
|
||||
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
if (!filters.period_start_date || !filters.period_end_date) {
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function (r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
period_start_date: fy.year_start_date,
|
||||
period_end_date: fy.year_end_date,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const views_menu = report.page.add_custom_button_group(__("Financial Statements"));
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
{
|
||||
label: __("Voucher Type"),
|
||||
fieldname: "voucher_type",
|
||||
fieldtype: "Dynamic Link",
|
||||
fieldtype: "Link",
|
||||
options: "DocType",
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
@@ -77,7 +77,7 @@ erpnext.accounts.unreconcile_payment = {
|
||||
{
|
||||
label: __("Voucher No"),
|
||||
fieldname: "voucher_no",
|
||||
fieldtype: "Link",
|
||||
fieldtype: "Dynamic Link",
|
||||
options: "voucher_type",
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
|
||||
@@ -385,28 +385,14 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
placeholder: discount ? discount + "%" : __("Enter discount percentage."),
|
||||
input_class: "input-xs",
|
||||
onchange: function () {
|
||||
if (flt(this.value) != 0) {
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
flt(this.value)
|
||||
);
|
||||
me.hide_discount_control(this.value);
|
||||
} else {
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
0
|
||||
);
|
||||
me.$add_discount_elem.css({
|
||||
border: "1px dashed var(--gray-500)",
|
||||
padding: "var(--padding-sm) var(--padding-md)",
|
||||
});
|
||||
me.$add_discount_elem.html(`${me.get_discount_icon()} ${__("Add Discount")}`);
|
||||
me.discount_field = undefined;
|
||||
}
|
||||
this.value = flt(this.value);
|
||||
frappe.model.set_value(
|
||||
frm.doc.doctype,
|
||||
frm.doc.name,
|
||||
"additional_discount_percentage",
|
||||
flt(this.value)
|
||||
);
|
||||
me.hide_discount_control(this.value);
|
||||
},
|
||||
},
|
||||
parent: this.$add_discount_elem.find(".add-discount-field"),
|
||||
@@ -417,9 +403,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
|
||||
hide_discount_control(discount) {
|
||||
if (!discount) {
|
||||
this.$add_discount_elem.css({ padding: "0px", border: "none" });
|
||||
this.$add_discount_elem.html(`<div class="add-discount-field"></div>`);
|
||||
if (!flt(discount)) {
|
||||
this.$add_discount_elem.css({
|
||||
border: "1px dashed var(--gray-500)",
|
||||
padding: "var(--padding-sm) var(--padding-md)",
|
||||
});
|
||||
this.$add_discount_elem.html(`${this.get_discount_icon()} ${__("Add Discount")}`);
|
||||
this.discount_field = undefined;
|
||||
} else {
|
||||
this.$add_discount_elem.css({
|
||||
border: "1px dashed var(--dark-green-500)",
|
||||
@@ -1044,6 +1034,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
this.highlight_checkout_btn(false);
|
||||
}
|
||||
|
||||
this.hide_discount_control(frm.doc.additional_discount_percentage);
|
||||
this.update_totals_section(frm);
|
||||
|
||||
if (frm.doc.docstatus === 1) {
|
||||
|
||||
@@ -16,7 +16,7 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = {
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
|
||||
@@ -16,7 +16,7 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = {
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
|
||||
@@ -16,7 +16,7 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = {
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.sys_defaults.fiscal_year,
|
||||
default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
|
||||
},
|
||||
{
|
||||
fieldname: "doctype",
|
||||
|
||||
@@ -211,52 +211,33 @@ class DeliveryNote(SellingController):
|
||||
self.validate_sales_invoice_references()
|
||||
|
||||
def validate_sales_order_references(self):
|
||||
err_msg = ""
|
||||
for item in self.items:
|
||||
if (item.against_sales_order and not item.so_detail) or (
|
||||
not item.against_sales_order and item.so_detail
|
||||
):
|
||||
if not item.against_sales_order:
|
||||
err_msg += (
|
||||
_("'Sales Order' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("against_sales_order")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
else:
|
||||
err_msg += (
|
||||
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("so_detail")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
|
||||
if err_msg:
|
||||
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
|
||||
self._validate_dependent_item_fields(
|
||||
"against_sales_order", "so_detail", _("References to Sales Orders are Incomplete")
|
||||
)
|
||||
|
||||
def validate_sales_invoice_references(self):
|
||||
err_msg = ""
|
||||
for item in self.items:
|
||||
if (item.against_sales_invoice and not item.si_detail) or (
|
||||
not item.against_sales_invoice and item.si_detail
|
||||
):
|
||||
if not item.against_sales_invoice:
|
||||
err_msg += (
|
||||
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
else:
|
||||
err_msg += (
|
||||
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("si_detail")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
self._validate_dependent_item_fields(
|
||||
"against_sales_invoice", "si_detail", _("References to Sales Invoices are Incomplete")
|
||||
)
|
||||
|
||||
if err_msg:
|
||||
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
|
||||
def _validate_dependent_item_fields(self, field_a: str, field_b: str, error_title: str):
|
||||
errors = []
|
||||
for item in self.items:
|
||||
missing_label = None
|
||||
if item.get(field_a) and not item.get(field_b):
|
||||
missing_label = item.meta.get_label(field_b)
|
||||
elif item.get(field_b) and not item.get(field_a):
|
||||
missing_label = item.meta.get_label(field_a)
|
||||
|
||||
if missing_label and missing_label != "No Label":
|
||||
errors.append(
|
||||
_("The field {0} in row {1} is not set").format(
|
||||
frappe.bold(_(missing_label)), frappe.bold(item.idx)
|
||||
)
|
||||
)
|
||||
|
||||
if errors:
|
||||
frappe.throw("<br>".join(errors), title=error_title)
|
||||
|
||||
def validate_proj_cust(self):
|
||||
"""check for does customer belong to same project as entered.."""
|
||||
|
||||
@@ -546,7 +546,7 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
if not (
|
||||
(erpnext.is_perpetual_inventory_enabled(self.company) and d.item_code in stock_items)
|
||||
or d.is_fixed_asset
|
||||
or (d.is_fixed_asset and not d.purchase_invoice)
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, cstr, flt, nowtime, today
|
||||
from frappe.utils import add_days, cint, cstr, flt, get_datetime, getdate, nowtime, today
|
||||
from pypika import functions as fn
|
||||
|
||||
import erpnext
|
||||
@@ -2603,6 +2603,71 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
company_doc.default_inventory_account = None
|
||||
company_doc.save()
|
||||
|
||||
def test_sles_with_same_posting_datetime_and_creation(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.report.stock_balance.stock_balance import execute
|
||||
|
||||
item_code = "Test Item for SLE with same posting datetime and creation"
|
||||
create_item(item_code)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_code,
|
||||
qty=10,
|
||||
rate=100,
|
||||
posting_date="2023-11-06",
|
||||
posting_time="00:00:00",
|
||||
)
|
||||
|
||||
sr = make_stock_entry(
|
||||
item_code=item_code,
|
||||
source=pr.items[0].warehouse,
|
||||
qty=10,
|
||||
posting_date="2023-11-07",
|
||||
posting_time="14:28:0.330404",
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
|
||||
"name",
|
||||
)
|
||||
|
||||
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
|
||||
sle_doc.db_set("creation", "2023-11-07 14:28:01.208930")
|
||||
|
||||
sle_doc.reload()
|
||||
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.208930"))
|
||||
|
||||
sr = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=pr.items[0].warehouse,
|
||||
qty=50,
|
||||
posting_date="2023-11-07",
|
||||
posting_time="14:28:0.920825",
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": sr.doctype, "voucher_no": sr.name, "item_code": sr.items[0].item_code},
|
||||
"name",
|
||||
)
|
||||
|
||||
sle_doc = frappe.get_doc("Stock Ledger Entry", sle)
|
||||
sle_doc.db_set("creation", "2023-11-07 14:28:01.044561")
|
||||
|
||||
sle_doc.reload()
|
||||
self.assertEqual(get_datetime(sle_doc.creation), get_datetime("2023-11-07 14:28:01.044561"))
|
||||
|
||||
pr.repost_future_sle_and_gle(force=True)
|
||||
|
||||
columns, data = execute(
|
||||
filters=frappe._dict(
|
||||
{"item_code": item_code, "warehouse": pr.items[0].warehouse, "company": pr.company}
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].get("bal_qty"), 50.0)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowtime, today
|
||||
from frappe.utils import add_days, cstr, flt, get_time, getdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
@@ -1727,6 +1727,74 @@ class TestStockEntry(FrappeTestCase):
|
||||
mr.cancel()
|
||||
mr.delete()
|
||||
|
||||
def test_stock_entry_for_same_posting_date_and_time(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item_code = "Test Stock Entry For Same Posting Datetime 1"
|
||||
make_item(item_code, {"is_stock_item": 1})
|
||||
posting_date = nowdate()
|
||||
posting_time = nowtime()
|
||||
|
||||
for index in range(25):
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
qty=1,
|
||||
to_warehouse=warehouse,
|
||||
posting_date=posting_date,
|
||||
posting_time=posting_time,
|
||||
do_not_submit=True,
|
||||
purpose="Material Receipt",
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
se.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code,
|
||||
"item_name": se.items[0].item_name,
|
||||
"description": se.items[0].description,
|
||||
"t_warehouse": se.items[0].t_warehouse,
|
||||
"basic_rate": 100,
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
"expense_account": se.items[0].expense_account,
|
||||
"cost_center": se.items[0].cost_center,
|
||||
"uom": se.items[0].uom,
|
||||
"stock_uom": se.items[0].stock_uom,
|
||||
},
|
||||
)
|
||||
|
||||
se.remarks = f"The current number is {cstr(index)}"
|
||||
|
||||
se.submit()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=[
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"actual_qty",
|
||||
"qty_after_transaction",
|
||||
"incoming_rate",
|
||||
"stock_value_difference",
|
||||
"stock_value",
|
||||
],
|
||||
filters={"item_code": item_code, "warehouse": warehouse},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
self.assertEqual(len(sles), 50)
|
||||
i = 0
|
||||
for sle in sles:
|
||||
i += 1
|
||||
self.assertEqual(getdate(sle.posting_date), getdate(posting_date))
|
||||
self.assertEqual(get_time(sle.posting_time), get_time(posting_time))
|
||||
self.assertEqual(sle.actual_qty, 1)
|
||||
self.assertEqual(sle.qty_after_transaction, i)
|
||||
self.assertEqual(sle.incoming_rate, 100)
|
||||
self.assertEqual(sle.stock_value_difference, 100)
|
||||
self.assertEqual(sle.stock_value, 100 * i)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -319,7 +319,8 @@
|
||||
{
|
||||
"fieldname": "posting_datetime",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Posting Datetime"
|
||||
"label": "Posting Datetime",
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
@@ -328,7 +329,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-07 09:18:13.999231",
|
||||
"modified": "2024-08-27 09:29:03.961443",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -1028,6 +1028,8 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(50, _get_stock_credit(final_consumption))
|
||||
|
||||
def test_tie_breaking(self):
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_entries
|
||||
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
|
||||
|
||||
@@ -1070,6 +1072,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual([10, 11], ordered_qty_after_transaction())
|
||||
|
||||
first.cancel()
|
||||
repost_entries()
|
||||
self.assertEqual([1], ordered_qty_after_transaction())
|
||||
|
||||
backdated = make_stock_entry(
|
||||
@@ -1169,7 +1172,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
qty=5,
|
||||
posting_date="2021-01-01",
|
||||
rate=10,
|
||||
posting_time="02:00:00.1234",
|
||||
posting_time="02:00:00",
|
||||
)
|
||||
|
||||
time.sleep(3)
|
||||
@@ -1181,7 +1184,7 @@ class TestStockLedgerEntry(FrappeTestCase, StockTestMixin):
|
||||
qty=100,
|
||||
rate=10,
|
||||
posting_date="2021-01-01",
|
||||
posting_time="02:00:00",
|
||||
posting_time="02:00:00.1234",
|
||||
)
|
||||
|
||||
sle = frappe.get_all(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, bold, msgprint
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import add_to_date, cint, cstr, flt
|
||||
from frappe.utils import add_to_date, cint, cstr, flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
@@ -357,7 +357,6 @@ class StockReconciliation(StockController):
|
||||
|
||||
sl_entries.append(args)
|
||||
|
||||
qty_after_transaction = 0
|
||||
for serial_no in serial_nos:
|
||||
args = self.get_sle_for_items(row, [serial_no])
|
||||
|
||||
@@ -373,27 +372,16 @@ class StockReconciliation(StockController):
|
||||
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
|
||||
# If serial no exists in different warehouse
|
||||
|
||||
warehouse = previous_sle.get("warehouse", "") or row.warehouse
|
||||
|
||||
if not qty_after_transaction:
|
||||
qty_after_transaction = get_stock_balance(
|
||||
row.item_code, warehouse, self.posting_date, self.posting_time
|
||||
frappe.throw(
|
||||
_(
|
||||
"The Serial No {0} already exists in the warehouse {1}. It cannot be transferred to the warehouse {2}"
|
||||
).format(
|
||||
get_link_to_form("Serial No", serial_no),
|
||||
bold(previous_sle.get("warehouse")),
|
||||
row.warehouse,
|
||||
)
|
||||
|
||||
qty_after_transaction -= 1
|
||||
|
||||
new_args = args.copy()
|
||||
new_args.update(
|
||||
{
|
||||
"actual_qty": -1,
|
||||
"qty_after_transaction": qty_after_transaction,
|
||||
"warehouse": warehouse,
|
||||
"valuation_rate": previous_sle.get("valuation_rate"),
|
||||
}
|
||||
)
|
||||
|
||||
sl_entries.append(new_args)
|
||||
|
||||
if row.qty:
|
||||
args = self.get_sle_for_items(row)
|
||||
|
||||
|
||||
@@ -1119,6 +1119,33 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
active_serial_no = frappe.get_all("Serial No", filters={"status": "Active", "item_code": item_code})
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
def test_stock_reco_for_serialized_item_with_different_warehouse(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
# Add new serial nos
|
||||
serial_item_code = "Stock-Reco-Serial-Item-11"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
serial_warehouse = "_Test Warehouse for Stock Reco1 - _TC"
|
||||
|
||||
self.make_item(
|
||||
serial_item_code, {"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SNT-SRS11.####"}
|
||||
)
|
||||
|
||||
make_stock_entry(item_code=serial_item_code, target=warehouse, qty=10, basic_rate=100)
|
||||
stock_entry = make_stock_entry(
|
||||
item_code=serial_item_code, target=serial_warehouse, qty=10, basic_rate=200
|
||||
)
|
||||
|
||||
sr = create_stock_reconciliation(
|
||||
item_code=serial_item_code, warehouse=warehouse, qty=11, rate=200, do_not_submit=True
|
||||
)
|
||||
|
||||
serial_nos = get_serial_nos(stock_entry.items[0].serial_no)
|
||||
|
||||
sr.items[0].serial_no += f"\n{serial_nos[0]}"
|
||||
sr.save()
|
||||
self.assertRaises(frappe.ValidationError, sr.submit)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-24 16:27:28.299520",
|
||||
"modified": "2024-08-14 16:08:15.733597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
@@ -320,6 +320,5 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "warehouse_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -855,14 +855,10 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
if price_list_rate is None or frappe.db.get_single_value(
|
||||
"Stock Settings", "update_existing_price_list_rate"
|
||||
):
|
||||
if args.get("is_internal_supplier") or args.get("is_internal_customer"):
|
||||
return out
|
||||
insert_item_price(args)
|
||||
|
||||
if args.price_list and args.rate:
|
||||
insert_item_price(args)
|
||||
|
||||
if not price_list_rate:
|
||||
return out
|
||||
if price_list_rate is None:
|
||||
return out
|
||||
|
||||
out.price_list_rate = flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
|
||||
|
||||
@@ -883,6 +879,14 @@ def get_price_list_rate(args, item_doc, out=None):
|
||||
|
||||
def insert_item_price(args):
|
||||
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
|
||||
if (
|
||||
not args.price_list
|
||||
or not args.rate
|
||||
or args.get("is_internal_supplier")
|
||||
or args.get("is_internal_customer")
|
||||
):
|
||||
return
|
||||
|
||||
if frappe.db.get_value("Price List", args.price_list, "currency", cache=True) == args.currency and cint(
|
||||
frappe.db.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
|
||||
):
|
||||
|
||||
@@ -17,7 +17,7 @@ frappe.query_reports["Available Batch Report"] = {
|
||||
fieldtype: "Date",
|
||||
width: "80",
|
||||
reqd: 1,
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
default: frappe.datetime.get_today(),
|
||||
},
|
||||
{
|
||||
fieldname: "item_code",
|
||||
|
||||
@@ -445,6 +445,7 @@ class update_entries_after:
|
||||
and (
|
||||
posting_datetime = %(posting_datetime)s
|
||||
)
|
||||
and creation = %(creation)s
|
||||
order by
|
||||
creation ASC
|
||||
for update
|
||||
@@ -1236,6 +1237,11 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
voucher_no = args.get("voucher_no")
|
||||
voucher_condition = f"and voucher_no != '{voucher_no}'"
|
||||
|
||||
elif args.get("creation") and args.get("sle_id"):
|
||||
creation = args.get("creation")
|
||||
operator = "<="
|
||||
voucher_condition = f"and creation < '{creation}'"
|
||||
|
||||
sle = frappe.db.sql(
|
||||
f"""
|
||||
select *, posting_datetime as "timestamp"
|
||||
@@ -1247,7 +1253,7 @@ def get_previous_sle_of_current_voucher(args, operator="<", exclude_current_vouc
|
||||
and (
|
||||
posting_datetime {operator} %(posting_datetime)s
|
||||
)
|
||||
order by posting_datetime desc, creation desc
|
||||
order by posting_date desc, posting_time desc, creation desc
|
||||
limit 1
|
||||
for update""",
|
||||
{
|
||||
@@ -1341,7 +1347,7 @@ def get_stock_ledger_entries(
|
||||
where item_code = %(item_code)s
|
||||
and is_cancelled = 0
|
||||
{conditions}
|
||||
order by posting_datetime {order}, creation {order}
|
||||
order by posting_date {order}, posting_time {order}, creation {order}
|
||||
{limit} {for_update}""".format(
|
||||
conditions=conditions,
|
||||
limit=limit or "",
|
||||
@@ -1446,7 +1452,7 @@ def get_valuation_rate(
|
||||
AND valuation_rate >= 0
|
||||
AND is_cancelled = 0
|
||||
AND NOT (voucher_no = %s AND voucher_type = %s)
|
||||
order by posting_datetime desc, name desc limit 1""",
|
||||
order by posting_date desc, posting_time desc, name desc limit 1""",
|
||||
(item_code, warehouse, voucher_no, voucher_type),
|
||||
)
|
||||
|
||||
@@ -1695,7 +1701,8 @@ def get_future_sle_with_negative_qty(sle):
|
||||
& (SLE.is_cancelled == 0)
|
||||
& (SLE.qty_after_transaction < 0)
|
||||
)
|
||||
.orderby(SLE.posting_datetime)
|
||||
.orderby(SLE.posting_date)
|
||||
.orderby(SLE.posting_time)
|
||||
.limit(1)
|
||||
)
|
||||
|
||||
@@ -1711,14 +1718,14 @@ def get_future_sle_with_negative_batch_qty(args):
|
||||
with batch_ledger as (
|
||||
select
|
||||
posting_date, posting_time, posting_datetime, voucher_type, voucher_no,
|
||||
sum(actual_qty) over (order by posting_datetime, creation) as cumulative_total
|
||||
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
item_code = %(item_code)s
|
||||
and warehouse = %(warehouse)s
|
||||
and batch_no=%(batch_no)s
|
||||
and is_cancelled = 0
|
||||
order by posting_datetime, creation
|
||||
order by posting_date, posting_time, creation
|
||||
)
|
||||
select * from batch_ledger
|
||||
where
|
||||
|
||||
@@ -9108,13 +9108,13 @@ Warehouse wise Stock Value,Warenwert nach Lager,
|
||||
Ex Works,Ab Werk,
|
||||
Free Carrier,Frei Frachtführer,
|
||||
Free Alongside Ship,Frei Längsseite Schiff,
|
||||
Free on Board,Frei an Bord,
|
||||
Free On Board,Frei an Bord,
|
||||
Carriage Paid To,Frachtfrei,
|
||||
Carriage and Insurance Paid to,Frachtfrei versichert,
|
||||
Cost and Freight,Kosten und Fracht,
|
||||
"Cost, Insurance and Freight","Kosten, Versicherung und Fracht",
|
||||
Delivered at Place,Geliefert benannter Ort,
|
||||
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
|
||||
Delivered At Place,Geliefert benannter Ort,
|
||||
Delivered At Place Unloaded,Geliefert benannter Ort entladen,
|
||||
Delivered Duty Paid,Geliefert verzollt,
|
||||
Discount Validity,Frist für den Rabatt,
|
||||
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user