mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-07 11:43:04 +00:00
Compare commits
179 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4089af5a7b | ||
|
|
06c73ef2da | ||
|
|
cccfbf193e | ||
|
|
37a7da3371 | ||
|
|
81c362dbe4 | ||
|
|
53034c332b | ||
|
|
0441984405 | ||
|
|
3ba6f40063 | ||
|
|
8e8d0c7bd0 | ||
|
|
f42f1bb35f | ||
|
|
0256c64634 | ||
|
|
e4f583d25a | ||
|
|
7259c0fe30 | ||
|
|
02547115e5 | ||
|
|
0bc9825238 | ||
|
|
bb66126dfa | ||
|
|
0e2abbd08e | ||
|
|
ff78fab176 | ||
|
|
04840762dd | ||
|
|
e7432fc60d | ||
|
|
97f2e88f4c | ||
|
|
37f24ae763 | ||
|
|
25b9127bae | ||
|
|
6e74e6f314 | ||
|
|
240118ee8b | ||
|
|
c1fd95ac66 | ||
|
|
8e340bb7fd | ||
|
|
96a6172999 | ||
|
|
d0d587432d | ||
|
|
07509b5e99 | ||
|
|
99d5b6dc71 | ||
|
|
56b1582027 | ||
|
|
149109649d | ||
|
|
0284328e2c | ||
|
|
a243873ab0 | ||
|
|
197e043fc9 | ||
|
|
925a164101 | ||
|
|
8f1a4b9717 | ||
|
|
67d4020241 | ||
|
|
fe4b2e36cc | ||
|
|
6759b90f85 | ||
|
|
99bc8e849c | ||
|
|
7a25d33547 | ||
|
|
2466e28bf5 | ||
|
|
1096528bb9 | ||
|
|
6ecb064264 | ||
|
|
92300b27c9 | ||
|
|
4fa9626de0 | ||
|
|
9b828b829a | ||
|
|
2936988cc6 | ||
|
|
9fca232578 | ||
|
|
fac22e93d0 | ||
|
|
3109efaf09 | ||
|
|
90ee21f868 | ||
|
|
36c46bb344 | ||
|
|
8c1f6196b8 | ||
|
|
12a31de25a | ||
|
|
3b9400755e | ||
|
|
2ce7300c3c | ||
|
|
b96b3b51b6 | ||
|
|
8f03769bf2 | ||
|
|
d20f3ab492 | ||
|
|
980ca1d8c5 | ||
|
|
4668a2d7d8 | ||
|
|
1a7efbb654 | ||
|
|
ccc2a47e73 | ||
|
|
f98716cc2a | ||
|
|
7903e8d669 | ||
|
|
9a50a0a129 | ||
|
|
1646517dc4 | ||
|
|
38811e792c | ||
|
|
edfb408464 | ||
|
|
ac48c3d4e7 | ||
|
|
1d66b7e5a3 | ||
|
|
ab9bde86f9 | ||
|
|
5c75bb8775 | ||
|
|
115a0123ed | ||
|
|
fdf1dfe46e | ||
|
|
106c154a16 | ||
|
|
00e8b862dd | ||
|
|
4195c50f02 | ||
|
|
fdb8e5b379 | ||
|
|
51cbbee4ca | ||
|
|
5000c09759 | ||
|
|
49e50662b6 | ||
|
|
f2f1f32826 | ||
|
|
fcf6500144 | ||
|
|
f2d5a69af4 | ||
|
|
d5c1c62622 | ||
|
|
e2f8e02c73 | ||
|
|
30cba7ee2c | ||
|
|
21a60c9927 | ||
|
|
0f275a9ff0 | ||
|
|
18402677da | ||
|
|
e9357c193d | ||
|
|
13895fa060 | ||
|
|
64f8498576 | ||
|
|
62ad466a3b | ||
|
|
45899b3017 | ||
|
|
d92a042bf7 | ||
|
|
4d6a71ab4b | ||
|
|
d5fa968078 | ||
|
|
71cbebd31b | ||
|
|
99317768f6 | ||
|
|
9fde7330e0 | ||
|
|
49fb6bec6a | ||
|
|
0f1f5b6f3d | ||
|
|
454e147592 | ||
|
|
701dd9e19b | ||
|
|
2cf82561f6 | ||
|
|
f2d113dcf2 | ||
|
|
97e4495f1f | ||
|
|
26c99351fd | ||
|
|
4d99449aa8 | ||
|
|
72b93805fd | ||
|
|
f31ed75578 | ||
|
|
003d7e9f3e | ||
|
|
650b25fc23 | ||
|
|
d5366c5873 | ||
|
|
bf36b4fa11 | ||
|
|
f4fc26b52e | ||
|
|
952a7b46d5 | ||
|
|
cc9857affd | ||
|
|
6de7a8de10 | ||
|
|
b593f57637 | ||
|
|
21bf7fd1f8 | ||
|
|
a2ee4631cc | ||
|
|
92ca6c6538 | ||
|
|
6028a19e6b | ||
|
|
87dc586543 | ||
|
|
22216b275c | ||
|
|
0e6edf763e | ||
|
|
e77534ba2b | ||
|
|
ae14b86a8c | ||
|
|
9665212607 | ||
|
|
5ae9ec2657 | ||
|
|
890289563a | ||
|
|
ee49e83020 | ||
|
|
598c581623 | ||
|
|
8f5278e3d4 | ||
|
|
aa38f69a67 | ||
|
|
57a81c4012 | ||
|
|
2fabcb0c50 | ||
|
|
f833923f2f | ||
|
|
077cb9e983 | ||
|
|
bcfc83d8d5 | ||
|
|
a5d1feef02 | ||
|
|
b050110544 | ||
|
|
e8f5c45751 | ||
|
|
eae5f27ec8 | ||
|
|
d48455393e | ||
|
|
7c699c8a38 | ||
|
|
20b8ee1e90 | ||
|
|
f9a7bb0544 | ||
|
|
6441bc7862 | ||
|
|
a5df0f2c94 | ||
|
|
e33416a4a7 | ||
|
|
245c6d8672 | ||
|
|
32fba94b2a | ||
|
|
a6ed10b712 | ||
|
|
7234625d65 | ||
|
|
625aa4ef6b | ||
|
|
e9b7d00afb | ||
|
|
e79e9e94d0 | ||
|
|
e94e15259b | ||
|
|
ab90b815e3 | ||
|
|
ee47440063 | ||
|
|
bc24e75c24 | ||
|
|
5d97c7cff9 | ||
|
|
e38383a757 | ||
|
|
a5bda0180e | ||
|
|
a53a80f01d | ||
|
|
b1a911aa9c | ||
|
|
18b6d50a31 | ||
|
|
0aa246c39e | ||
|
|
5468a3b0b7 | ||
|
|
d0a0a35d72 | ||
|
|
3b2044dcd7 | ||
|
|
7f2a54d95b |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.70.5"
|
||||
__version__ = "14.70.14"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-05-27 17:29:55.560840",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
@@ -126,6 +126,10 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Auditor"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -171,7 +171,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
!(frm.doc.accounts || []).length ||
|
||||
((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)
|
||||
) {
|
||||
if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) {
|
||||
if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) {
|
||||
return frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
|
||||
@@ -283,7 +283,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
filters: [[jvd.reference_type, "docstatus", "=", 1]],
|
||||
};
|
||||
|
||||
if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) {
|
||||
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
|
||||
// Filter by cost center
|
||||
if (jvd.cost_center) {
|
||||
@@ -295,7 +295,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
|
||||
}
|
||||
|
||||
if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
|
||||
if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) {
|
||||
// party_type and party mandatory
|
||||
frappe.model.validate_missing(jvd, "party_type");
|
||||
frappe.model.validate_missing(jvd, "party");
|
||||
|
||||
@@ -557,7 +557,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified": "2024-07-18 15:32:29.413598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
||||
@@ -21,7 +21,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_from", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
filters: {
|
||||
@@ -75,7 +75,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.set_query("paid_to", function() {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ?
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type) ?
|
||||
["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
filters: {
|
||||
@@ -121,7 +121,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
frm.set_query('payment_term', 'references', function(frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) {
|
||||
if (['Purchase Invoice', 'Sales Invoice'].includes(child.reference_doctype) && child.reference_name) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_payment_terms_for_references",
|
||||
filters: {
|
||||
@@ -485,7 +485,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
@@ -853,7 +853,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"))
|
||||
if(paid_amount > total_negative_outstanding) {
|
||||
if(total_negative_outstanding == 0) {
|
||||
@@ -988,7 +988,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Customer" &&
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||
!["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx]));
|
||||
@@ -996,7 +996,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
|
||||
if(frm.doc.party_type=="Supplier" &&
|
||||
!in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype)
|
||||
!["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
|
||||
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry", [row.idx]));
|
||||
@@ -1080,7 +1080,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
bank_account: function(frm) {
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from":"paid_to";
|
||||
if (frm.doc.bank_account && in_list(['Pay', 'Receive'], frm.doc.payment_type)) {
|
||||
if (frm.doc.bank_account && ['Pay', 'Receive'].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details",
|
||||
args: {
|
||||
|
||||
@@ -74,7 +74,6 @@ class PaymentEntry(AccountsController):
|
||||
self.set_exchange_rate()
|
||||
self.validate_mandatory()
|
||||
self.validate_reference_documents()
|
||||
self.set_tax_withholding()
|
||||
self.set_amounts()
|
||||
self.validate_amounts()
|
||||
self.apply_taxes()
|
||||
@@ -89,6 +88,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_allocated_amount()
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_tax_withholding()
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
@@ -674,9 +674,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
net_total = self.calculate_tax_withholding_net_total()
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -720,7 +718,26 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
def calculate_tax_withholding_net_total(self):
|
||||
net_total = 0
|
||||
order_details = self.get_order_wise_tax_withholding_net_total()
|
||||
|
||||
for d in self.references:
|
||||
tax_withholding_net_total = order_details.get(d.reference_name)
|
||||
if not tax_withholding_net_total:
|
||||
continue
|
||||
|
||||
net_taxable_outstanding = max(
|
||||
0, d.outstanding_amount - (d.total_amount - tax_withholding_net_total)
|
||||
)
|
||||
|
||||
net_total += min(net_taxable_outstanding, d.allocated_amount)
|
||||
|
||||
net_total += self.unallocated_amount
|
||||
|
||||
return net_total
|
||||
|
||||
def get_order_wise_tax_withholding_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
@@ -728,12 +745,15 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
doctype,
|
||||
filters={"name": ["in", docnames]},
|
||||
fields=["name", "base_tax_withholding_net_total"],
|
||||
as_list=True,
|
||||
)
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.ui.form.on("Payment Order", {
|
||||
|
||||
// payment Entry
|
||||
if (frm.doc.docstatus === 1 && frm.doc.payment_order_type === "Payment Request") {
|
||||
frm.add_custom_button(__("Create Payment Entries"), function () {
|
||||
frm.add_custom_button(__("Create Journal Entries"), function () {
|
||||
frm.trigger("make_payment_records");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,6 +200,7 @@ class PaymentReconciliation(Document):
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
conditions.append(doc.outstanding_amount != 0)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
@@ -1335,6 +1335,46 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_cr_note_payment_limit_filter(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
for _ in range(6):
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 6)
|
||||
self.assertEqual(len(pr.payments), 6)
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
# Limit should not affect in fetching the unallocated cr_note
|
||||
pr.invoice_limit = 5
|
||||
pr.payment_limit = 5
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
if (
|
||||
frm.doc.payment_request_type == "Inward" &&
|
||||
frm.doc.payment_channel !== "Phone" &&
|
||||
!in_list(["Initiated", "Paid"], frm.doc.status) &&
|
||||
!["Initiated", "Paid"].includes(frm.doc.status) &&
|
||||
!frm.doc.__islocal &&
|
||||
frm.doc.docstatus == 1
|
||||
) {
|
||||
|
||||
@@ -31,6 +31,7 @@ class PricingRule(Document):
|
||||
self.validate_price_list_with_currency()
|
||||
self.validate_dates()
|
||||
self.validate_condition()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
if not self.margin_type:
|
||||
self.margin_rate_or_amount = 0.0
|
||||
@@ -201,6 +202,10 @@ class PricingRule(Document):
|
||||
):
|
||||
frappe.throw(_("Invalid condition expression"))
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions and self.is_recursive:
|
||||
frappe.throw(_("Recursive Discounts with Mixed condition is not supported by the system"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -1087,6 +1087,18 @@ class TestPricingRule(unittest.TestCase):
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 1")
|
||||
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule 2")
|
||||
|
||||
def test_validation_on_mixed_condition_with_recursion(self):
|
||||
pricing_rule = make_pricing_rule(
|
||||
discount_percentage=10,
|
||||
selling=1,
|
||||
priority=2,
|
||||
min_qty=4,
|
||||
title="_Test Pricing Rule with Min Qty - 2",
|
||||
)
|
||||
pricing_rule.mixed_conditions = True
|
||||
pricing_rule.is_recursive = True
|
||||
self.assertRaises(frappe.ValidationError, pricing_rule.save)
|
||||
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class PromotionalScheme(Document):
|
||||
|
||||
self.validate_applicable_for()
|
||||
self.validate_pricing_rules()
|
||||
self.validate_mixed_with_recursion()
|
||||
|
||||
def validate_applicable_for(self):
|
||||
if self.applicable_for:
|
||||
@@ -94,7 +95,7 @@ class PromotionalScheme(Document):
|
||||
docnames = []
|
||||
|
||||
# If user has changed applicable for
|
||||
if self._doc_before_save.applicable_for == self.applicable_for:
|
||||
if self.get_doc_before_save() and self.get_doc_before_save().applicable_for == self.applicable_for:
|
||||
return
|
||||
|
||||
docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name})
|
||||
@@ -108,6 +109,7 @@ class PromotionalScheme(Document):
|
||||
frappe.delete_doc("Pricing Rule", docname.name)
|
||||
|
||||
def on_update(self):
|
||||
self.validate()
|
||||
pricing_rules = (
|
||||
frappe.get_all(
|
||||
"Pricing Rule",
|
||||
@@ -119,6 +121,15 @@ class PromotionalScheme(Document):
|
||||
)
|
||||
self.update_pricing_rules(pricing_rules)
|
||||
|
||||
def validate_mixed_with_recursion(self):
|
||||
if self.mixed_conditions:
|
||||
if self.product_discount_slabs:
|
||||
for slab in self.product_discount_slabs:
|
||||
if slab.is_recursive:
|
||||
frappe.throw(
|
||||
_("Recursive Discounts with Mixed condition is not supported by the system")
|
||||
)
|
||||
|
||||
def update_pricing_rules(self, pricing_rules):
|
||||
rules = {}
|
||||
count = 0
|
||||
|
||||
@@ -107,6 +107,25 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_validation_on_recurse_with_mixed_condition(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.set("price_discount_slabs", [])
|
||||
ps.set(
|
||||
"product_discount_slabs",
|
||||
[
|
||||
{
|
||||
"rule_description": "12+1",
|
||||
"min_qty": 12,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 12,
|
||||
}
|
||||
],
|
||||
)
|
||||
ps.mixed_conditions = True
|
||||
self.assertRaises(frappe.ValidationError, ps.save)
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -59,25 +59,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||
if(doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
|
||||
@@ -170,7 +170,6 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
@@ -361,7 +360,8 @@
|
||||
"description": "Once set, this invoice will be on hold till the set date",
|
||||
"fieldname": "release_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Release Date"
|
||||
"label": "Release Date",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cb_17",
|
||||
@@ -1590,15 +1590,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
@@ -1619,7 +1610,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 15:57:00.736868",
|
||||
"modified": "2024-07-25 19:42:36.931278",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -413,7 +413,7 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
validate_account_head(item.idx, item.expense_account, self.company, "Expense")
|
||||
|
||||
def set_against_expense_account(self):
|
||||
def set_against_expense_account(self, force=False):
|
||||
against_accounts = []
|
||||
for item in self.get("items"):
|
||||
if item.expense_account and (item.expense_account not in against_accounts):
|
||||
@@ -421,6 +421,10 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.against_expense_account = ",".join(against_accounts)
|
||||
|
||||
def force_set_against_expense_account(self):
|
||||
self.set_against_expense_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
@@ -586,17 +590,17 @@ class PurchaseInvoice(BuyingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||
@@ -1494,6 +1498,9 @@ class PurchaseInvoice(BuyingController):
|
||||
self.db_set("release_date", None)
|
||||
|
||||
def set_tax_withholding(self):
|
||||
self.set("advance_tax", [])
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
@@ -1535,8 +1542,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.remove(d)
|
||||
|
||||
## Add pending vouchers on which tax was withheld
|
||||
self.set("tax_withheld_vouchers", [])
|
||||
|
||||
for voucher_no, voucher_details in voucher_wise_amount.items():
|
||||
self.append(
|
||||
"tax_withheld_vouchers",
|
||||
@@ -1551,7 +1556,6 @@ class PurchaseInvoice(BuyingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
|
||||
self.set("advance_tax", [])
|
||||
for tax in advance_taxes:
|
||||
allocated_amount = 0
|
||||
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
|
||||
|
||||
@@ -1908,18 +1908,15 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
def test_default_cost_center_for_purchase(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
@@ -149,6 +149,10 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries_on_cancel()
|
||||
|
||||
doc.docstatus = 1
|
||||
if doc.doctype == "Sales Invoice":
|
||||
doc.force_set_against_income_account()
|
||||
else:
|
||||
doc.force_set_against_expense_account()
|
||||
doc.make_gl_entries()
|
||||
|
||||
elif doc.doctype in ["Payment Entry", "Journal Entry", "Expense Claim"]:
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-07 14:24:13.321522",
|
||||
"modified": "2024-06-06 13:56:37.908879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger Settings",
|
||||
@@ -30,13 +30,17 @@
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1
|
||||
"select": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -49,25 +49,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
this.frm.toggle_reqd("due_date", !this.frm.doc.is_return);
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice needs to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if (this.frm.doc.is_return) {
|
||||
this.frm.return_print_format = "Sales Invoice Return";
|
||||
}
|
||||
@@ -200,7 +181,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
if(cur_frm.meta._default_print_format) {
|
||||
cur_frm.meta.default_print_format = cur_frm.meta._default_print_format;
|
||||
cur_frm.meta._default_print_format = null;
|
||||
} else if(in_list([cur_frm.pos_print_format, cur_frm.return_print_format], cur_frm.meta.default_print_format)) {
|
||||
} else if([cur_frm.pos_print_format, cur_frm.return_print_format].includes(cur_frm.meta.default_print_format)) {
|
||||
cur_frm.meta.default_print_format = null;
|
||||
cur_frm.meta._default_print_format = null;
|
||||
}
|
||||
@@ -428,12 +409,16 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
frappe.msgprint(__("Please specify Company to proceed"));
|
||||
} else {
|
||||
var me = this;
|
||||
const for_validate = me.frm.doc.is_return ? true : false;
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
method: "set_missing_values",
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message && r.message.print_format) {
|
||||
args: {
|
||||
for_validate: for_validate,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
if (r.message && r.message.print_format) {
|
||||
me.frm.pos_print_format = r.message.print_format;
|
||||
}
|
||||
me.frm.trigger("update_stock");
|
||||
|
||||
@@ -213,7 +213,6 @@
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"remarks",
|
||||
"repost_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
@@ -2184,7 +2183,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-08 18:02:28.549041",
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -385,7 +385,6 @@ class SalesInvoice(SellingController):
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.db_set("status", "Cancelled")
|
||||
self.db_set("repost_required", 0)
|
||||
|
||||
if frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction":
|
||||
update_company_current_month_sales(self.company)
|
||||
@@ -532,23 +531,23 @@ class SalesInvoice(SellingController):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"is_opening",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.repost_accounting_entries()
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
@@ -768,6 +767,10 @@ class SalesInvoice(SellingController):
|
||||
against_acc.append(d.income_account)
|
||||
self.against_income_account = ",".join(against_acc)
|
||||
|
||||
def force_set_against_income_account(self):
|
||||
self.set_against_income_account()
|
||||
frappe.db.set_value(self.doctype, self.name, "against_income_account", self.against_income_account)
|
||||
|
||||
def add_remarks(self):
|
||||
if not self.remarks:
|
||||
if self.po_no and self.po_date:
|
||||
|
||||
@@ -2884,13 +2884,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
si.items[0].income_account = "Service - _TC"
|
||||
si.additional_discount_account = "_Test Account Sales - _TC"
|
||||
si.taxes[0].account_head = "TDS Payable - _TC"
|
||||
# Ledger reposted implicitly upon 'Update After Submit'
|
||||
si.save()
|
||||
|
||||
si.load_from_db()
|
||||
self.assertTrue(si.repost_required)
|
||||
|
||||
si.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Sales - _TC", 22.0, 0.0, nowdate()],
|
||||
["Debtors - _TC", 88, 0.0, nowdate()],
|
||||
@@ -2900,9 +2896,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
|
||||
si.load_from_db()
|
||||
self.assertFalse(si.repost_required)
|
||||
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
|
||||
@@ -236,6 +236,11 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
vouchers, voucher_wise_amount = get_invoice_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
payment_entry_vouchers = get_payment_entry_vouchers(
|
||||
parties, tax_details, inv.company, party_type=party_type
|
||||
)
|
||||
|
||||
advance_vouchers = get_advance_vouchers(
|
||||
parties,
|
||||
company=inv.company,
|
||||
@@ -243,7 +248,8 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
to_date=tax_details.to_date,
|
||||
party_type=party_type,
|
||||
)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
taxable_vouchers = vouchers + advance_vouchers + payment_entry_vouchers
|
||||
tax_deducted_on_advances = 0
|
||||
|
||||
if inv.doctype == "Purchase Invoice":
|
||||
@@ -355,6 +361,20 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
return vouchers, voucher_wise_amount
|
||||
|
||||
|
||||
def get_payment_entry_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
payment_entry_filters = {
|
||||
"party_type": party_type,
|
||||
"party": ("in", parties),
|
||||
"docstatus": 1,
|
||||
"apply_tax_withholding_amount": 1,
|
||||
"posting_date": ["between", (tax_details.from_date, tax_details.to_date)],
|
||||
"tax_withholding_category": tax_details.get("tax_withholding_category"),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
return frappe.db.get_all("Payment Entry", filters=payment_entry_filters, pluck="name")
|
||||
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type="Supplier"):
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
|
||||
@@ -139,6 +139,7 @@ class ReceivablePayableReport:
|
||||
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.get_invoices(ple)
|
||||
|
||||
@@ -253,7 +254,7 @@ class ReceivablePayableReport:
|
||||
row.paid -= amount
|
||||
row.paid_in_account_currency -= amount_in_account_currency
|
||||
|
||||
if ple.cost_center:
|
||||
if not row.cost_center and ple.cost_center:
|
||||
row.cost_center = str(ple.cost_center)
|
||||
|
||||
def update_sub_total_row(self, row, party):
|
||||
@@ -288,13 +289,13 @@ class ReceivablePayableReport:
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision
|
||||
if (abs(row.outstanding) >= 0.0 / 10**self.currency_precision) or (
|
||||
abs(row.outstanding_in_account_currency) >= 0.0 / 10**self.currency_precision
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) >= 1.0 / 10**self.currency_precision) and (
|
||||
(abs(row.outstanding_in_account_currency) >= 1.0 / 10**self.currency_precision)
|
||||
or (row.voucher_no in self.err_journals)
|
||||
):
|
||||
must_consider = True
|
||||
|
||||
@@ -53,11 +53,13 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def create_payment_entry(self, docname):
|
||||
def create_payment_entry(self, docname, do_not_submit=False):
|
||||
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
if not do_not_submit:
|
||||
pe.submit()
|
||||
return pe
|
||||
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
@@ -955,3 +957,69 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(
|
||||
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||
)
|
||||
|
||||
def test_accounts_receivable_output_for_minor_outstanding(self):
|
||||
"""
|
||||
AR/AP should report miniscule outstanding of 0.01. Or else there will be slight difference with General Ledger/Trial Balance
|
||||
"""
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account=self.cash, party_amount=99.99)
|
||||
pe.paid_from = self.debit_to
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 99.99, 0.01]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
def test_cost_center_on_report_output(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.cost_center = self.cost_center
|
||||
si.save().submit()
|
||||
|
||||
new_cc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "East Wing",
|
||||
"parent_cost_center": self.company + " - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
new_cc.save()
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
pe = self.create_payment_entry(si.name, do_not_submit=True)
|
||||
pe.cost_center = new_cc.name
|
||||
pe.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [si.name, si.cost_center, 60]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
@@ -694,7 +694,8 @@ class GrossProfitGenerator:
|
||||
|
||||
def get_average_buying_rate(self, row, item_code):
|
||||
args = row
|
||||
if item_code not in self.average_buying_rate:
|
||||
key = (item_code, row.warehouse)
|
||||
if key not in self.average_buying_rate:
|
||||
args.update(
|
||||
{
|
||||
"voucher_type": row.parenttype,
|
||||
@@ -705,9 +706,9 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
average_buying_rate = get_incoming_rate(args)
|
||||
self.average_buying_rate[item_code] = flt(average_buying_rate)
|
||||
self.average_buying_rate[key] = flt(average_buying_rate)
|
||||
|
||||
return self.average_buying_rate[item_code]
|
||||
return self.average_buying_rate[key]
|
||||
|
||||
def get_last_purchase_rate(self, item_code, row):
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
@@ -46,7 +46,7 @@ frappe.query_reports["Item-wise Purchase Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Supplier", "Item Group", "Item", "Invoice"],
|
||||
options: ["", "Supplier", "Item Group", "Item", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -306,14 +306,15 @@ def apply_conditions(query, pi, pii, filters):
|
||||
query = query.orderby(pi.posting_date, order=Order.desc)
|
||||
query = query.orderby(pii.item_group, order=Order.desc)
|
||||
else:
|
||||
query = apply_group_by_conditions(filters, "Purchase Invoice")
|
||||
query = apply_group_by_conditions(query, pi, pii, filters)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_items(filters, additional_table_columns):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pii = frappe.qb.DocType("Purchase Invoice Item")
|
||||
doctype = "Purchase Invoice"
|
||||
pi = frappe.qb.DocType(doctype)
|
||||
pii = frappe.qb.DocType(f"{doctype} Item")
|
||||
Item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(pi)
|
||||
@@ -322,7 +323,7 @@ def get_items(filters, additional_table_columns):
|
||||
.left_join(Item)
|
||||
.on(pii.item_code == Item.name)
|
||||
.select(
|
||||
pii.name.as_("pii_name"),
|
||||
pii.name,
|
||||
pii.parent,
|
||||
pi.posting_date,
|
||||
pi.credit_to,
|
||||
@@ -350,6 +351,7 @@ def get_items(filters, additional_table_columns):
|
||||
pi.mode_of_payment,
|
||||
)
|
||||
.where(pi.docstatus == 1)
|
||||
.where(pii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if filters.get("supplier"):
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.item_wise_purchase_register.item_wise_purchase_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWisePurchaseRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_supplier()
|
||||
self.create_item()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_purchase_invoice(self, do_not_submit=False):
|
||||
pi = make_purchase_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
do_not_save=1,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
qty=1,
|
||||
)
|
||||
|
||||
pi = pi.save()
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_basic_report_output(self):
|
||||
pi = self.create_purchase_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": pi.items[0].item_code,
|
||||
"invoice": pi.name,
|
||||
"posting_date": getdate(),
|
||||
"supplier": pi.supplier,
|
||||
"credit_to": pi.credit_to,
|
||||
"company": self.company,
|
||||
"expense_account": pi.items[0].expense_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": pi.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -64,7 +64,7 @@ frappe.query_reports["Item-wise Sales Register"] = {
|
||||
label: __("Group By"),
|
||||
fieldname: "group_by",
|
||||
fieldtype: "Select",
|
||||
options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
options: ["", "Customer Group", "Customer", "Item Group", "Item", "Territory", "Invoice"],
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -407,8 +407,9 @@ def apply_group_by_conditions(query, si, ii, filters):
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sii = frappe.qb.DocType("Sales Invoice Item")
|
||||
doctype = "Sales Invoice"
|
||||
si = frappe.qb.DocType(doctype)
|
||||
sii = frappe.qb.DocType(f"{doctype} Item")
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
query = (
|
||||
@@ -456,6 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.qty,
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sii.parenttype == doctype)
|
||||
)
|
||||
|
||||
if additional_query_columns:
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import execute
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
return si
|
||||
|
||||
def test_basic_report_output(self):
|
||||
si = self.create_sales_invoice()
|
||||
|
||||
filters = frappe._dict({"from_date": today(), "to_date": today(), "company": self.company})
|
||||
report = execute(filters)
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
|
||||
expected_result = {
|
||||
"item_code": si.items[0].item_code,
|
||||
"invoice": si.name,
|
||||
"posting_date": getdate(),
|
||||
"customer": si.customer,
|
||||
"debit_to": si.debit_to,
|
||||
"company": self.company,
|
||||
"income_account": si.items[0].income_account,
|
||||
"stock_qty": 1.0,
|
||||
"stock_uom": si.items[0].stock_uom,
|
||||
"rate": 100.0,
|
||||
"amount": 100.0,
|
||||
"total_tax": 0,
|
||||
"total_other_charges": 0,
|
||||
"total": 100.0,
|
||||
"currency": "INR",
|
||||
}
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
@@ -12,7 +12,7 @@ def execute(filters=None):
|
||||
else:
|
||||
party_naming_by = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
filters.update({"naming_series": party_naming_by})
|
||||
filters["naming_series"] = party_naming_by
|
||||
|
||||
validate_filters(filters)
|
||||
(
|
||||
@@ -63,21 +63,23 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
tax_withholding_category = tds_accounts.get(entry.account)
|
||||
# or else the consolidated value from the voucher document
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
tax_withholding_category = tax_category_map.get((voucher_type, name))
|
||||
# or else from the party default
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if net_total_map.get(name):
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
if rate:
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
)
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
@@ -97,7 +99,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
row["party_name"] = party_map.get(party, {}).get(party_name)
|
||||
|
||||
row.update(
|
||||
{
|
||||
@@ -279,7 +281,6 @@ def get_tds_docs(filters):
|
||||
journal_entries = []
|
||||
tax_category_map = frappe._dict()
|
||||
net_total_map = frappe._dict()
|
||||
frappe._dict()
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
@@ -412,7 +413,7 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
)
|
||||
|
||||
for entry in entries:
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
|
||||
if doctype == "Purchase Invoice":
|
||||
value = [
|
||||
entry.base_tax_withholding_net_total,
|
||||
@@ -427,7 +428,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
else:
|
||||
value = [entry.total_amount] * 3
|
||||
net_total_map.update({entry.name: value})
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
@@ -1571,6 +1571,18 @@ def auto_create_exchange_rate_revaluation_weekly() -> None:
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_monthly() -> None:
|
||||
"""
|
||||
Executed by background job
|
||||
"""
|
||||
companies = frappe.db.get_all(
|
||||
"Company",
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": "Montly"},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
|
||||
def get_payment_ledger_entries(gl_entries, cancel=0):
|
||||
ple_map = []
|
||||
if gl_entries:
|
||||
|
||||
@@ -78,7 +78,7 @@ frappe.ui.form.on("Asset", {
|
||||
frm.events.make_schedules_editable(frm);
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
|
||||
if (["Submitted", "Partially Depreciated", "Fully Depreciated"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(
|
||||
__("Transfer Asset"),
|
||||
function () {
|
||||
@@ -280,7 +280,7 @@ frappe.ui.form.on("Asset", {
|
||||
if (v.journal_entry) {
|
||||
asset_values.push(asset_value);
|
||||
} else {
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
|
||||
asset_values.push(null);
|
||||
} else {
|
||||
asset_values.push(asset_value);
|
||||
@@ -312,7 +312,7 @@ frappe.ui.form.on("Asset", {
|
||||
});
|
||||
}
|
||||
|
||||
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
|
||||
if (["Scrapped", "Sold"].includes(frm.doc.status)) {
|
||||
x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: "Date" }));
|
||||
asset_values.push(0);
|
||||
}
|
||||
|
||||
@@ -1689,12 +1689,12 @@ def create_asset(**args):
|
||||
return asset
|
||||
|
||||
|
||||
def create_asset_category():
|
||||
def create_asset_category(enable_cwip=1):
|
||||
asset_category = frappe.new_doc("Asset Category")
|
||||
asset_category.asset_category_name = "Computers"
|
||||
asset_category.total_number_of_depreciations = 3
|
||||
asset_category.frequency_of_depreciation = 3
|
||||
asset_category.enable_cwip_accounting = 1
|
||||
asset_category.enable_cwip_accounting = enable_cwip
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
|
||||
@@ -125,9 +125,10 @@ def get_data(filters):
|
||||
if assets_linked_to_fb and asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(
|
||||
asset.asset_id, finance_book
|
||||
) or get_asset_value_after_depreciation(asset.asset_id)
|
||||
depreciation_amount = depreciation_amount_map.get(asset.asset_id) or 0.0
|
||||
asset_value = (
|
||||
asset.gross_purchase_amount - asset.opening_accumulated_depreciation - depreciation_amount
|
||||
)
|
||||
|
||||
row = {
|
||||
"asset_id": asset.asset_id,
|
||||
@@ -139,7 +140,7 @@ def get_data(filters):
|
||||
or pi_supplier_map.get(asset.purchase_invoice),
|
||||
"gross_purchase_amount": asset.gross_purchase_amount,
|
||||
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
|
||||
"depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
|
||||
"depreciated_amount": depreciation_amount,
|
||||
"available_for_use_date": asset.available_for_use_date,
|
||||
"location": asset.location,
|
||||
"asset_category": asset.asset_category,
|
||||
|
||||
@@ -6,8 +6,8 @@ frappe.provide("erpnext.accounts.dimensions");
|
||||
{% include 'erpnext/public/js/controllers/buying.js' %};
|
||||
|
||||
frappe.ui.form.on("Purchase Order", {
|
||||
setup: function(frm) {
|
||||
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
if (frm.doc.is_old_subcontracting_flow) {
|
||||
frm.set_query("reserve_warehouse", "supplied_items", function() {
|
||||
return {
|
||||
@@ -180,7 +180,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
this.frm.fields_dict.items_section.wrapper.removeClass("hide-border");
|
||||
}
|
||||
|
||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
||||
if(!["Closed", "Delivered"].includes(doc.status)) {
|
||||
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
|
||||
// Don't add Update Items button if the PO is following the new subcontracting flow.
|
||||
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||
@@ -211,7 +211,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
this.frm.page.set_inner_btn_group_as_primary(__("Status"));
|
||||
}
|
||||
} else if(in_list(["Closed", "Delivered"], doc.status)) {
|
||||
} else if(["Closed", "Delivered"].includes(doc.status)) {
|
||||
if (this.frm.has_perm("submit")) {
|
||||
this.frm.add_custom_button(__('Re-open'), () => this.unclose_purchase_order(), __("Status"));
|
||||
}
|
||||
|
||||
@@ -345,7 +345,13 @@ class PurchaseOrder(BuyingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
|
||||
super().on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
"fieldname": "supplier_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Supplier Type",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ def get_data():
|
||||
),
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"dynamic_links": {"party": ["Supplier", "party_type"]},
|
||||
"transactions": [
|
||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
|
||||
@@ -43,9 +43,10 @@ def get_data(filters):
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(po)
|
||||
.from_(po_item)
|
||||
.inner_join(po_item)
|
||||
.on(po_item.parent == po.name)
|
||||
.left_join(pi_item)
|
||||
.on(pi_item.po_detail == po_item.name)
|
||||
.on((pi_item.po_detail == po_item.name) & (pi_item.docstatus == 1))
|
||||
.select(
|
||||
po.transaction_date.as_("date"),
|
||||
po_item.schedule_date.as_("required_date"),
|
||||
|
||||
@@ -742,6 +742,9 @@ class AccountsController(TransactionBase):
|
||||
# reset pricing rule fields if pricing_rule_removed
|
||||
item.set(fieldname, value)
|
||||
|
||||
elif fieldname == "expense_account" and not item.get("expense_account"):
|
||||
item.expense_account = value
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field(
|
||||
"is_fixed_asset"
|
||||
):
|
||||
@@ -2373,16 +2376,12 @@ class AccountsController(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.flags.ignore_permissions = True
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, cstr, flt, getdate
|
||||
from frappe.utils.data import nowtime
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
@@ -305,6 +306,8 @@ class BuyingController(SubcontractingController):
|
||||
else:
|
||||
item.valuation_rate = 0.0
|
||||
|
||||
update_regional_item_valuation_rate(self)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
|
||||
return
|
||||
@@ -894,3 +897,8 @@ def validate_item_type(doc, fieldname, message):
|
||||
).format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_regional_item_valuation_rate(doc):
|
||||
pass
|
||||
|
||||
@@ -31,7 +31,7 @@ class SellingController(StockController):
|
||||
def validate(self):
|
||||
super().validate()
|
||||
self.validate_items()
|
||||
if not self.get("is_debit_note"):
|
||||
if not (self.get("is_debit_note") or self.get("is_return")):
|
||||
self.validate_max_discount()
|
||||
self.validate_selling_price()
|
||||
self.set_qty_as_per_stock_uom()
|
||||
|
||||
@@ -51,7 +51,7 @@ class StockController(AccountsController):
|
||||
self.validate_internal_transfer()
|
||||
self.validate_putaway_capacity()
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False, via_landed_cost_voucher=False):
|
||||
if self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
@@ -72,7 +72,11 @@ class StockController(AccountsController):
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries(warehouse_account)
|
||||
gl_entries = (
|
||||
self.get_gl_entries(warehouse_account, via_landed_cost_voucher)
|
||||
if self.doctype == "Purchase Receipt"
|
||||
else self.get_gl_entries(warehouse_account)
|
||||
)
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
|
||||
def validate_serialized_batch(self):
|
||||
|
||||
@@ -463,6 +463,7 @@ scheduler_events = {
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -400,7 +400,7 @@ frappe.ui.form.on("BOM", {
|
||||
},
|
||||
|
||||
rm_cost_as_per(frm) {
|
||||
if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) {
|
||||
if (["Valuation Rate", "Last Purchase Rate"].includes(frm.doc.rm_cost_as_per)) {
|
||||
frm.set_value("plc_conversion_rate", 1.0);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -129,7 +129,7 @@ frappe.ui.form.on("Production Plan", {
|
||||
if (
|
||||
frm.doc.mr_items &&
|
||||
frm.doc.mr_items.length &&
|
||||
!in_list(["Material Requested", "Closed"], frm.doc.status)
|
||||
!["Material Requested", "Closed"].includes(frm.doc.status)
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
|
||||
@@ -194,7 +194,7 @@ frappe.ui.form.on("Work Order", {
|
||||
},
|
||||
|
||||
add_custom_button_to_return_components: function (frm) {
|
||||
if (frm.doc.docstatus === 1 && in_list(["Closed", "Completed"], frm.doc.status)) {
|
||||
if (frm.doc.docstatus === 1 && ["Closed", "Completed"].includes(frm.doc.status)) {
|
||||
let non_consumed_items = frm.doc.required_items.filter((d) => {
|
||||
return flt(d.consumed_qty) < flt(d.transferred_qty - d.returned_qty);
|
||||
});
|
||||
@@ -594,7 +594,7 @@ erpnext.work_order = {
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1 && !in_list(["Closed", "Completed"], doc.status)) {
|
||||
if (doc.docstatus === 1 && !["Closed", "Completed"].includes(doc.status)) {
|
||||
if (doc.status != "Stopped" && doc.status != "Completed") {
|
||||
frm.add_custom_button(
|
||||
__("Stop"),
|
||||
|
||||
@@ -271,6 +271,7 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
|
||||
erpnext.patches.v14_0.france_depreciation_warning
|
||||
erpnext.patches.v14_0.clear_reconciliation_values_from_singles
|
||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||
|
||||
[post_model_sync]
|
||||
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||
|
||||
@@ -9,15 +9,13 @@ def execute():
|
||||
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
records = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.notes, dt.modified_by, dt.modified)
|
||||
.where(dt.notes.isnotnull() & dt.notes != "")
|
||||
frappe.qb.from_(dt).select(dt.name, dt.notes).where(dt.notes.isnotnull() & dt.notes != "")
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in records:
|
||||
if strip_html(cstr(d.notes)).strip():
|
||||
doc = frappe.get_doc(doctype, d.name)
|
||||
doc.append("notes", {"note": d.notes, "added_by": d.modified_by, "added_on": d.modified})
|
||||
doc.append("notes", {"note": d.notes})
|
||||
doc.update_child_table("notes")
|
||||
|
||||
frappe.db.sql_ddl(f"alter table `tab{doctype}` drop column `notes`")
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in ["Customer", "Supplier"]:
|
||||
field = doctype.lower() + "_type"
|
||||
frappe.db.set_value(doctype, {field: "Proprietorship"}, field, "Individual")
|
||||
@@ -240,7 +240,7 @@ erpnext.AccountTreeGrid = class AccountTreeGrid extends frappe.views.TreeGridRep
|
||||
flt(account.closing_dr) -
|
||||
flt(account.closing_cr);
|
||||
me.set_debit_or_credit(parent_account, "closing", bal);
|
||||
} else if (in_list(["debit", "credit"], col.field)) {
|
||||
} else if (["debit", "credit"].includes(col.field)) {
|
||||
parent_account[col.field] =
|
||||
flt(parent_account[col.field]) + flt(account[col.field]);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ frappe.ui.form.on("Communication", {
|
||||
);
|
||||
}
|
||||
|
||||
if (!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
|
||||
if (!["Lead", "Opportunity"].includes(frm.doc.reference_doctype)) {
|
||||
frm.add_custom_button(
|
||||
__("Lead"),
|
||||
() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ frappe.ui.form.on(cur_frm.doctype, {
|
||||
setup: function(frm) {
|
||||
// set conditional display for rate column in taxes
|
||||
$(frm.wrapper).on('grid-row-render', function(e, grid_row) {
|
||||
if(in_list(['Sales Taxes and Charges', 'Purchase Taxes and Charges'], grid_row.doc.doctype)) {
|
||||
if(['Sales Taxes and Charges', 'Purchase Taxes and Charges'].includes(grid_row.doc.doctype)) {
|
||||
erpnext.taxes.set_conditional_mandatory_rate_or_amount(grid_row);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -136,7 +136,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
}
|
||||
|
||||
toggle_subcontracting_fields() {
|
||||
if (in_list(['Purchase Receipt', 'Purchase Invoice'], this.frm.doc.doctype)) {
|
||||
if (['Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
|
||||
this.frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty',
|
||||
'read_only', this.frm.doc.__onload && this.frm.doc.__onload.backflush_based_on === 'BOM');
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
apply_pricing_rule_on_item(item) {
|
||||
let effective_item_rate = item.price_list_rate;
|
||||
let item_rate = item.rate;
|
||||
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
|
||||
if (["Sales Order", "Quotation"].includes(item.parenttype) && item.blanket_order_rate) {
|
||||
effective_item_rate = item.blanket_order_rate;
|
||||
}
|
||||
if (item.margin_type == "Percentage") {
|
||||
@@ -52,7 +52,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
// Advance calculation applicable to Sales/Purchase Invoice
|
||||
if (
|
||||
in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)
|
||||
&& this.frm.doc.docstatus < 2
|
||||
&& !this.frm.doc.is_return
|
||||
) {
|
||||
@@ -60,7 +60,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
if (
|
||||
in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
|
||||
["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)
|
||||
&& this.frm.doc.is_pos
|
||||
&& this.frm.doc.is_return
|
||||
) {
|
||||
@@ -69,7 +69,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
|
||||
// Sales person's commission
|
||||
if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) {
|
||||
if (["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"].includes(this.frm.doc.doctype)) {
|
||||
this.calculate_commission();
|
||||
this.calculate_contribution();
|
||||
}
|
||||
@@ -547,7 +547,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
? this.frm.doc["taxes"][tax_count - 1].total + flt(this.frm.doc.rounding_adjustment)
|
||||
: this.frm.doc.net_total);
|
||||
|
||||
if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
|
||||
if(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
this.frm.doc.base_grand_total = (this.frm.doc.total_taxes_and_charges) ?
|
||||
flt(this.frm.doc.grand_total * this.frm.doc.conversion_rate) : this.frm.doc.base_net_total;
|
||||
} else {
|
||||
@@ -555,7 +555,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.frm.doc.taxes_and_charges_added = this.frm.doc.taxes_and_charges_deducted = 0.0;
|
||||
if(tax_count) {
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (in_list(["Valuation and Total", "Total"], tax.category)) {
|
||||
if (["Valuation and Total", "Total"].includes(tax.category)) {
|
||||
if(tax.add_deduct_tax == "Add") {
|
||||
me.frm.doc.taxes_and_charges_added += flt(tax.tax_amount_after_discount_amount);
|
||||
} else {
|
||||
@@ -702,7 +702,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
var actual_taxes_dict = {};
|
||||
|
||||
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
|
||||
if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) {
|
||||
if (["Actual", "On Item Quantity"].includes(tax.charge_type)) {
|
||||
var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount;
|
||||
tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
|
||||
actual_taxes_dict[tax.idx] = tax_amount;
|
||||
@@ -747,7 +747,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
// NOTE:
|
||||
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
|
||||
// total_advance is only for non POS Invoice
|
||||
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_return){
|
||||
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype) && this.frm.doc.is_return){
|
||||
this.calculate_paid_amount();
|
||||
}
|
||||
|
||||
@@ -755,7 +755,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
|
||||
|
||||
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) {
|
||||
if(["Sales Invoice", "POS Invoice", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
|
||||
let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||
let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total;
|
||||
|
||||
@@ -778,7 +778,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
this.frm.refresh_field("base_paid_amount");
|
||||
}
|
||||
|
||||
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)) {
|
||||
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)) {
|
||||
let total_amount_for_payment = (this.frm.doc.redeem_loyalty_points && this.frm.doc.loyalty_amount)
|
||||
? flt(total_amount_to_pay - this.frm.doc.loyalty_amount, precision("base_grand_total"))
|
||||
: total_amount_to_pay;
|
||||
@@ -882,7 +882,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
calculate_change_amount(){
|
||||
this.frm.doc.change_amount = 0.0;
|
||||
this.frm.doc.base_change_amount = 0.0;
|
||||
if(in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
|
||||
if(["Sales Invoice", "POS Invoice"].includes(this.frm.doc.doctype)
|
||||
&& this.frm.doc.paid_amount > this.frm.doc.grand_total && !this.frm.doc.is_return) {
|
||||
|
||||
var payment_types = $.map(this.frm.doc.payments, function(d) { return d.type; });
|
||||
|
||||
@@ -260,19 +260,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
setup_quality_inspection() {
|
||||
if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
|
||||
if(!["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const me = this;
|
||||
if (!this.frm.is_new() && this.frm.doc.docstatus === 0) {
|
||||
if (
|
||||
!this.frm.is_new()
|
||||
&& this.frm.doc.docstatus === 0
|
||||
&& frappe.model.can_create("Quality Inspection")
|
||||
) {
|
||||
this.frm.add_custom_button(__("Quality Inspection(s)"), () => {
|
||||
me.make_quality_inspection();
|
||||
}, __("Create"));
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
|
||||
const inspection_type = ["Purchase Receipt", "Purchase Invoice"].includes(this.frm.doc.doctype)
|
||||
? "Incoming" : "Outgoing";
|
||||
|
||||
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
|
||||
@@ -304,7 +308,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
make_payment_request() {
|
||||
let me = this;
|
||||
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
|
||||
const payment_request_type = (['Sales Order', 'Sales Invoice'].includes(this.frm.doc.doctype))
|
||||
? "Inward" : "Outward";
|
||||
|
||||
frappe.call({
|
||||
@@ -417,7 +421,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
setup_sms() {
|
||||
var me = this;
|
||||
let blacklist = ['Purchase Invoice', 'BOM'];
|
||||
if(this.frm.doc.docstatus===1 && !in_list(["Lost", "Stopped", "Closed"], this.frm.doc.status)
|
||||
if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
|
||||
&& !blacklist.includes(this.frm.doctype)) {
|
||||
this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); });
|
||||
}
|
||||
@@ -780,7 +784,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
var set_party_account = function(set_pricing) {
|
||||
if (in_list(["Sales Invoice", "Purchase Invoice"], me.frm.doc.doctype)) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(me.frm.doc.doctype)) {
|
||||
if(me.frm.doc.doctype=="Sales Invoice") {
|
||||
var party_type = "Customer";
|
||||
var party_account_field = 'debit_to';
|
||||
@@ -815,7 +819,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
|
||||
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
|
||||
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
|
||||
['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'].includes(this.frm.doctype)) {
|
||||
erpnext.utils.get_shipping_address(this.frm, function() {
|
||||
set_party_account(set_pricing);
|
||||
});
|
||||
@@ -1482,7 +1486,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"doctype": me.frm.doc.doctype,
|
||||
"name": me.frm.doc.name,
|
||||
"is_return": cint(me.frm.doc.is_return),
|
||||
"update_stock": in_list(['Sales Invoice', 'Purchase Invoice'], me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
|
||||
"update_stock": ['Sales Invoice', 'Purchase Invoice'].includes(me.frm.doc.doctype) ? cint(me.frm.doc.update_stock) : 0,
|
||||
"conversion_factor": me.frm.doc.conversion_factor,
|
||||
"pos_profile": me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
|
||||
"coupon_code": me.frm.doc.coupon_code
|
||||
@@ -1610,12 +1614,15 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
apply_product_discount(args) {
|
||||
const items = this.frm.doc.items.filter(d => (d.is_free_item)) || [];
|
||||
|
||||
const exist_items = items.map(row => (row.item_code, row.pricing_rules));
|
||||
const exist_items = items.map(row => { return {item_code: row.item_code, pricing_rules: row.pricing_rules};});
|
||||
|
||||
args.free_item_data.forEach(pr_row => {
|
||||
let row_to_modify = {};
|
||||
if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) {
|
||||
|
||||
// If there are no free items, or if the current free item doesn't exist in the table, add it
|
||||
if (!items || !exist_items.filter(e_row => {
|
||||
return e_row.item_code == pr_row.item_code && e_row.pricing_rules == pr_row.pricing_rules;
|
||||
}).length) {
|
||||
row_to_modify = frappe.model.add_child(this.frm.doc,
|
||||
this.frm.doc.doctype + ' Item', 'items');
|
||||
|
||||
@@ -1638,6 +1645,12 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
apply_price_list(item, reset_plc_conversion) {
|
||||
// We need to reset plc_conversion_rate sometimes because the call to
|
||||
// `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value
|
||||
|
||||
|
||||
if (this.frm.doc.doctype === "Material Request") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reset_plc_conversion) {
|
||||
this.frm.set_value("plc_conversion_rate", "");
|
||||
}
|
||||
@@ -1653,7 +1666,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
me.in_apply_price_list = true;
|
||||
return this.frm.call({
|
||||
method: "erpnext.stock.get_item_details.apply_price_list",
|
||||
args: { args: args },
|
||||
args: { args: args, doc: me.frm.doc },
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.run_serially([
|
||||
@@ -2126,7 +2139,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
get_method_for_payment() {
|
||||
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
|
||||
if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){
|
||||
if(in_list(['Sales Invoice', 'Purchase Invoice'], cur_frm.doc.doctype)){
|
||||
if(['Sales Invoice', 'Purchase Invoice'].includes( cur_frm.doc.doctype)){
|
||||
method = "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_invoice";
|
||||
}else {
|
||||
method= "erpnext.accounts.doctype.journal_entry.journal_entry.get_payment_entry_against_order";
|
||||
|
||||
@@ -218,7 +218,7 @@ erpnext.payments = class payments extends erpnext.stock.StockController {
|
||||
|
||||
update_paid_amount(update_write_off) {
|
||||
var me = this;
|
||||
if (in_list(["change_amount", "write_off_amount"], this.idx)) {
|
||||
if (["change_amount", "write_off_amount"].includes(this.idx)) {
|
||||
var value = me.selected_mode.val();
|
||||
if (me.idx == "change_amount") {
|
||||
me.change_amount(value);
|
||||
|
||||
@@ -28,11 +28,11 @@ erpnext.SMSManager = function SMSManager(doc) {
|
||||
"Purchase Receipt": "Items has been received against purchase receipt: " + doc.name,
|
||||
};
|
||||
|
||||
if (in_list(["Sales Order", "Delivery Note", "Sales Invoice"], doc.doctype))
|
||||
if (["Sales Order", "Delivery Note", "Sales Invoice"].includes(doc.doctype))
|
||||
this.show(doc.contact_person, "Customer", doc.customer, "", default_msg[doc.doctype]);
|
||||
else if (doc.doctype === "Quotation")
|
||||
this.show(doc.contact_person, "Customer", doc.party_name, "", default_msg[doc.doctype]);
|
||||
else if (in_list(["Purchase Order", "Purchase Receipt"], doc.doctype))
|
||||
else if (["Purchase Order", "Purchase Receipt"].includes(doc.doctype))
|
||||
this.show(doc.contact_person, "Supplier", doc.supplier, "", default_msg[doc.doctype]);
|
||||
else if (doc.doctype == "Lead") this.show("", "", "", doc.mobile_no, default_msg[doc.doctype]);
|
||||
else if (doc.doctype == "Opportunity")
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% for(var i=0, l=notes.length; i<l; i++) { %}
|
||||
<div class="comment-content p-3 row" name="{{ notes[i].name }}">
|
||||
<div class="mb-2 head col-xs-3">
|
||||
{% if (notes[i].added_by && notes[i].added_on) %}
|
||||
<div class="row">
|
||||
<div class="col-xs-2">
|
||||
{{ frappe.avatar(notes[i].added_by) }}
|
||||
@@ -25,6 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% } %}
|
||||
</div>
|
||||
<div class="content col-xs-8">
|
||||
{{ notes[i].note }}
|
||||
|
||||
@@ -14,10 +14,10 @@ erpnext.utils.get_party_details = function (frm, method, args, callback) {
|
||||
if (!args) {
|
||||
if (
|
||||
(frm.doctype != "Purchase Order" && frm.doc.customer) ||
|
||||
(frm.doc.party_name && in_list(["Quotation", "Opportunity"], frm.doc.doctype))
|
||||
(frm.doc.party_name && ["Quotation", "Opportunity"].includes(frm.doc.doctype))
|
||||
) {
|
||||
let party_type = "Customer";
|
||||
if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
|
||||
if (frm.doc.quotation_to && ["Lead", "Prospect"].includes(frm.doc.quotation_to)) {
|
||||
party_type = frm.doc.quotation_to;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,8 @@ frappe.ui.form.on("Import Supplier Invoice", {
|
||||
},
|
||||
|
||||
toggle_read_only_fields: function (frm) {
|
||||
if (in_list(["File Import Completed", "Processing File Data"], frm.doc.status)) {
|
||||
if (["File Import Completed", "Processing File Data"].includes(frm.doc.status)) {
|
||||
cur_frm.set_read_only();
|
||||
cur_frm.refresh_fields();
|
||||
frm.set_df_property("import_invoices", "hidden", 1);
|
||||
} else {
|
||||
frm.set_df_property("import_invoices", "hidden", 0);
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
"label": "Customer Type",
|
||||
"oldfieldname": "customer_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Company\nIndividual\nProprietorship\nPartnership",
|
||||
"options": "Company\nIndividual\nPartnership",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,7 +15,10 @@ def get_data():
|
||||
"Bank Account": "party",
|
||||
"Subscription": "party",
|
||||
},
|
||||
"dynamic_links": {"party_name": ["Customer", "quotation_to"]},
|
||||
"dynamic_links": {
|
||||
"party_name": ["Customer", "quotation_to"],
|
||||
"party": ["Customer", "party_type"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Pre Sales"), "items": ["Opportunity", "Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]},
|
||||
|
||||
@@ -48,8 +48,13 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.docstatus === 1 && frm.doc.status !== 'Closed'
|
||||
&& flt(frm.doc.per_delivered, 6) < 100 && flt(frm.doc.per_billed, 6) < 100) {
|
||||
if(
|
||||
frm.doc.docstatus === 1
|
||||
&& frm.doc.status !== "Closed"
|
||||
&& flt(frm.doc.per_delivered, 6) < 100
|
||||
&& flt(frm.doc.per_billed, 6) < 100
|
||||
&& frm.has_perm("write")
|
||||
) {
|
||||
frm.add_custom_button(__('Update Items'), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: frm,
|
||||
@@ -66,6 +71,10 @@ frappe.ui.form.on("Sales Order", {
|
||||
},
|
||||
|
||||
get_items_from_internal_purchase_order(frm) {
|
||||
if (!frappe.model.can_read("Purchase Order")) {
|
||||
return;
|
||||
}
|
||||
|
||||
frm.add_custom_button(__('Purchase Order'), () => {
|
||||
erpnext.utils.map_current_doc({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.make_inter_company_sales_order',
|
||||
@@ -150,7 +159,11 @@ frappe.ui.form.on("Sales Order", {
|
||||
frm.set_value("advance_paid", 0)
|
||||
}
|
||||
|
||||
frm.ignore_doctypes_on_cancel_all = ['Purchase Order'];
|
||||
frm.ignore_doctypes_on_cancel_all = [
|
||||
"Purchase Order",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
];
|
||||
},
|
||||
|
||||
delivery_date: function(frm) {
|
||||
@@ -223,7 +236,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
}
|
||||
|
||||
if (flt(doc.per_picked, 6) < 100 && flt(doc.per_delivered, 6) < 100) {
|
||||
if (
|
||||
flt(doc.per_picked, 6) < 100
|
||||
&& flt(doc.per_delivered, 6) < 100
|
||||
&& frappe.model.can_create("Pick List")
|
||||
) {
|
||||
this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create'));
|
||||
}
|
||||
|
||||
@@ -233,45 +250,105 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
const order_is_a_custom_sale = ["Sales", "Shopping Cart", "Maintenance"].indexOf(doc.order_type) === -1;
|
||||
|
||||
// delivery note
|
||||
if(flt(doc.per_delivered, 6) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) {
|
||||
this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create'));
|
||||
this.frm.add_custom_button(__('Work Order'), () => this.make_work_order(), __('Create'));
|
||||
if(
|
||||
flt(doc.per_delivered, 6) < 100
|
||||
&& (order_is_a_sale || order_is_a_custom_sale)
|
||||
&& allow_delivery
|
||||
) {
|
||||
if (frappe.model.can_create("Delivery Note")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => this.make_delivery_note_based_on_delivery_date(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
if (frappe.model.can_create("Work Order")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Work Order"),
|
||||
() => this.make_work_order(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// sales invoice
|
||||
if(flt(doc.per_billed, 6) < 100) {
|
||||
this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create'));
|
||||
if(flt(doc.per_billed, 6) < 100 && frappe.model.can_create("Sales Invoice")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Sales Invoice"),
|
||||
() => me.make_sales_invoice(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
// material request
|
||||
if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 6) < 100) {
|
||||
this.frm.add_custom_button(__('Material Request'), () => this.make_material_request(), __('Create'));
|
||||
this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create'));
|
||||
if (
|
||||
(
|
||||
!doc.order_type
|
||||
|| (order_is_a_sale || order_is_a_custom_sale)
|
||||
&& flt(doc.per_delivered, 6) < 100
|
||||
)
|
||||
&& frappe.model.can_create("Material Request")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__('Material Request'),
|
||||
() => this.make_material_request(),
|
||||
__('Create')
|
||||
);
|
||||
this.frm.add_custom_button(
|
||||
__('Request for Raw Materials'),
|
||||
() => this.make_raw_material_request(),
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
// Make Purchase Order
|
||||
if (!this.frm.doc.is_internal_customer) {
|
||||
this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create'));
|
||||
if (!this.frm.doc.is_internal_customer && frappe.model.can_create("Purchase Order")) {
|
||||
this.frm.add_custom_button(
|
||||
__('Purchase Order'),
|
||||
() => this.make_purchase_order(),
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
// maintenance
|
||||
if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
|
||||
this.frm.add_custom_button(__('Maintenance Visit'), () => this.make_maintenance_visit(), __('Create'));
|
||||
this.frm.add_custom_button(__('Maintenance Schedule'), () => this.make_maintenance_schedule(), __('Create'));
|
||||
if(frappe.model.can_create("Maintenance Visit")) {
|
||||
this.frm.add_custom_button(
|
||||
__('Maintenance Visit'),
|
||||
() => this.make_maintenance_visit(),
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
if(frappe.model.can_create("Maintenance Schedule")) {
|
||||
this.frm.add_custom_button(
|
||||
__('Maintenance Schedule'),
|
||||
() => this.make_maintenance_schedule(),
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// project
|
||||
if(flt(doc.per_delivered, 2) < 100) {
|
||||
if(flt(doc.per_delivered, 2) < 100 && frappe.model.can_create("Project")) {
|
||||
this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create'));
|
||||
}
|
||||
|
||||
if(!doc.auto_repeat) {
|
||||
this.frm.add_custom_button(__('Subscription'), function() {
|
||||
erpnext.utils.make_subscription(doc.doctype, doc.name)
|
||||
}, __('Create'))
|
||||
if(!doc.auto_repeat && frappe.model.can_create("Auto Repeat")) {
|
||||
this.frm.add_custom_button(
|
||||
__('Subscription'),
|
||||
function() {
|
||||
erpnext.utils.make_subscription(doc.doctype, doc.name)
|
||||
},
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
|
||||
if (
|
||||
doc.docstatus === 1 &&
|
||||
!doc.inter_company_order_reference &&
|
||||
frappe.model.can_create("Purchase Order")
|
||||
) {
|
||||
let me = this;
|
||||
let internal = me.frm.doc.is_internal_customer;
|
||||
if (internal) {
|
||||
@@ -286,14 +363,25 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
}
|
||||
// payment request
|
||||
if(flt(doc.per_billed, precision('per_billed', doc)) < 100 + frappe.boot.sysdefaults.over_billing_allowance) {
|
||||
this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create'));
|
||||
this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create'));
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
() => this.make_payment_request(),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (frappe.model.can_create("Payment Entry")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.frm.doc.docstatus===0) {
|
||||
if (this.frm.doc.docstatus===0 && frappe.model.can_read("Quotation")) {
|
||||
this.frm.add_custom_button(__('Quotation'),
|
||||
function() {
|
||||
let d = erpnext.utils.map_current_doc({
|
||||
|
||||
@@ -269,7 +269,13 @@ class SalesOrder(SellingController):
|
||||
update_coupon_code_count(self.coupon_code, "used")
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super().on_cancel()
|
||||
|
||||
# Cannot cancel closed SO
|
||||
@@ -337,6 +343,10 @@ class SalesOrder(SellingController):
|
||||
def update_status(self, status):
|
||||
self.check_modified_date()
|
||||
self.set_status(update=True, status=status)
|
||||
# Upon Sales Order Re-open, check for credit limit.
|
||||
# Limit should be checked after the 'Hold/Closed' status is reset.
|
||||
if status == "Draft" and self.docstatus == 1:
|
||||
self.check_credit_limit()
|
||||
self.update_reserved_qty()
|
||||
self.notify_update()
|
||||
clear_doctype_notifications(self)
|
||||
@@ -576,6 +586,11 @@ def get_requested_item_qty(sales_order):
|
||||
def make_material_request(source_name, target_doc=None):
|
||||
requested_item_qty = get_requested_item_qty(source_name)
|
||||
|
||||
def postprocess(source, target):
|
||||
if source.tc_name and frappe.db.get_value("Terms and Conditions", source.tc_name, "buying") != 1:
|
||||
target.tc_name = None
|
||||
target.terms = None
|
||||
|
||||
def get_remaining_qty(so_item):
|
||||
return flt(
|
||||
flt(so_item.qty)
|
||||
@@ -631,6 +646,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
postprocess,
|
||||
)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -9,6 +9,7 @@ from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
make_maintenance_schedule,
|
||||
@@ -31,7 +32,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
|
||||
class TestSalesOrder(FrappeTestCase):
|
||||
class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
@@ -52,6 +53,9 @@ class TestSalesOrder(FrappeTestCase):
|
||||
)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.create_customer("_Test Customer Credit")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -2159,6 +2163,28 @@ class TestSalesOrder(FrappeTestCase):
|
||||
frappe.db.set_single_value("Stock Settings", "update_existing_price_list_rate", 0)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
def test_credit_limit_on_so_reopning(self):
|
||||
# set credit limit
|
||||
company = "_Test Company"
|
||||
customer = frappe.get_doc("Customer", self.customer)
|
||||
customer.credit_limits = []
|
||||
customer.append(
|
||||
"credit_limits", {"company": company, "credit_limit": 1000, "bypass_credit_limit_check": False}
|
||||
)
|
||||
customer.save()
|
||||
|
||||
so1 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||
so1.customer = self.customer
|
||||
so1.save().submit()
|
||||
|
||||
so1.update_status("Closed")
|
||||
|
||||
so2 = make_sales_order(qty=9, rate=100, do_not_submit=True)
|
||||
so2.customer = self.customer
|
||||
so2.save().submit()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
||||
@@ -73,7 +73,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
const { status } = doc;
|
||||
let indicator_color = "";
|
||||
|
||||
in_list(["Paid", "Consolidated"], status) && (indicator_color = "green");
|
||||
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
|
||||
status === "Draft" && (indicator_color = "red");
|
||||
status === "Return" && (indicator_color = "grey");
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
serial_no: item.serial_no || "",
|
||||
},
|
||||
callback:function(r){
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
if (['Delivery Note', 'Sales Invoice'].includes(doc.doctype)) {
|
||||
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
|
||||
if (has_batch_no) {
|
||||
me.set_batch_number(cdt, cdn);
|
||||
@@ -332,7 +332,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
if ((doc.packed_items || []).length) {
|
||||
$(cur_frm.fields_dict.packing_list.row.wrapper).toggle(true);
|
||||
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
if (['Delivery Note', 'Sales Invoice'].includes(doc.doctype)) {
|
||||
var help_msg = "<div class='alert alert-warning'>" +
|
||||
__("For 'Product Bundle' items, Warehouse, Serial No and Batch No will be considered from the 'Packing List' table. If Warehouse and Batch No are same for all packing items for any 'Product Bundle' item, those values can be entered in the main Item table, values will be copied to 'Packing List' table.")+
|
||||
"</div>";
|
||||
@@ -340,7 +340,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
}
|
||||
} else {
|
||||
$(cur_frm.fields_dict.packing_list.row.wrapper).toggle(false);
|
||||
if (in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
if (['Delivery Note', 'Sales Invoice'].includes(doc.doctype)) {
|
||||
frappe.meta.get_docfield(doc.doctype, 'product_bundle_help', doc.name).options = '';
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate) {
|
||||
super.conversion_factor(doc, cdt, cdn, dont_fetch_price_list_rate);
|
||||
if(frappe.meta.get_docfield(cdt, "stock_qty", cdn) &&
|
||||
in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
['Delivery Note', 'Sales Invoice'].includes(doc.doctype)) {
|
||||
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
|
||||
this.set_batch_number(cdt, cdn);
|
||||
}
|
||||
@@ -376,7 +376,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
qty(doc, cdt, cdn) {
|
||||
super.qty(doc, cdt, cdn);
|
||||
|
||||
if(in_list(['Delivery Note', 'Sales Invoice'], doc.doctype)) {
|
||||
if(['Delivery Note', 'Sales Invoice'].includes(doc.doctype)) {
|
||||
if (doc.doctype === 'Sales Invoice' && (!doc.update_stock)) return;
|
||||
this.set_batch_number(cdt, cdn);
|
||||
}
|
||||
@@ -440,7 +440,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
||||
};
|
||||
|
||||
frappe.ui.form.on(cur_frm.doctype,"project", function(frm) {
|
||||
if(in_list(["Delivery Note", "Sales Invoice"], frm.doc.doctype)) {
|
||||
if(["Delivery Note", "Sales Invoice"].includes(frm.doc.doctype)) {
|
||||
if(frm.doc.project) {
|
||||
frappe.call({
|
||||
method:'erpnext.projects.doctype.project.project.get_cost_center_name' ,
|
||||
|
||||
@@ -698,7 +698,7 @@
|
||||
"fieldname": "auto_err_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "Daily\nWeekly"
|
||||
"options": "Daily\nWeekly\nMonthly"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -712,7 +712,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-23 10:19:24.322898",
|
||||
"modified": "2024-07-24 18:17:56.413971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
@@ -768,6 +768,10 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Auditor",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:26:46.826501",
|
||||
"modified": "2024-06-12 16:10:31.451257",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Department",
|
||||
@@ -132,6 +132,10 @@
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"role": "Employee",
|
||||
"select": 1
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -19,6 +19,7 @@ class HolidayList(Document):
|
||||
def validate(self):
|
||||
self.validate_days()
|
||||
self.total_holidays = len(self.holidays)
|
||||
self.sort_holidays()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_weekly_off_dates(self):
|
||||
@@ -33,8 +34,6 @@ class HolidayList(Document):
|
||||
|
||||
self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
|
||||
|
||||
self.sort_holidays()
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supported_countries(self):
|
||||
from holidays.utils import list_supported_countries
|
||||
@@ -76,8 +75,6 @@ class HolidayList(Document):
|
||||
"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
|
||||
)
|
||||
|
||||
self.sort_holidays()
|
||||
|
||||
def sort_holidays(self):
|
||||
self.holidays.sort(key=lambda x: getdate(x.holiday_date))
|
||||
for i in range(len(self.holidays)):
|
||||
|
||||
@@ -162,7 +162,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ def make_item_tax_template(company_name, template):
|
||||
# Ingone validations to make doctypes faster
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.insert(ignore_permissions=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ def get_or_create_account(company_name, account):
|
||||
doc = frappe.get_doc(account)
|
||||
doc.flags.ignore_links = True
|
||||
doc.flags.ignore_validate = True
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
doc.insert(ignore_permissions=True, ignore_mandatory=True, ignore_if_duplicate=True)
|
||||
return doc
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.ui.form.on("Closing Stock Balance", {
|
||||
},
|
||||
|
||||
generate_closing_balance(frm) {
|
||||
if (in_list(["Queued", "Failed"], frm.doc.status)) {
|
||||
if (["Queued", "Failed"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(__("Generate Closing Stock Balance"), () => {
|
||||
frm.call({
|
||||
method: "enqueue_job",
|
||||
|
||||
@@ -86,7 +86,7 @@ frappe.ui.form.on("Delivery Note", {
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus === 1 && frm.doc.is_return === 1 && frm.doc.per_billed !== 100) {
|
||||
if (frm.doc.docstatus === 1 && frm.doc.is_return === 1 && frm.doc.per_billed !== 100 && frappe.model.can_create("Sales Invoice")) {
|
||||
frm.add_custom_button(__('Credit Note'), function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
@@ -96,7 +96,11 @@ frappe.ui.form.on("Delivery Note", {
|
||||
frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
|
||||
if (
|
||||
frm.doc.docstatus == 1 &&
|
||||
!frm.doc.inter_company_reference &&
|
||||
frappe.model.can_create("Purchase Receipt")
|
||||
) {
|
||||
let internal = frm.doc.is_internal_customer;
|
||||
if (internal) {
|
||||
let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" :
|
||||
@@ -135,64 +139,88 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn
|
||||
refresh(doc, dt, dn) {
|
||||
var me = this;
|
||||
super.refresh();
|
||||
if ((!doc.is_return) && (doc.status!="Closed" || this.frm.is_new())) {
|
||||
if (this.frm.doc.docstatus===0) {
|
||||
this.frm.add_custom_button(__('Sales Order'),
|
||||
function() {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer")
|
||||
});
|
||||
if (
|
||||
!doc.is_return
|
||||
&& (doc.status!="Closed" || this.frm.is_new())
|
||||
&& this.frm.has_perm("write")
|
||||
&& frappe.model.can_read("Sales Order")
|
||||
&& this.frm.doc.docstatus===0
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__('Sales Order'),
|
||||
function() {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer")
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
customer: me.frm.doc.customer,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
status: ["not in", ["Closed", "On Hold"]],
|
||||
per_delivered: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
project: me.frm.doc.project || undefined,
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
customer: me.frm.doc.customer,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
status: ["not in", ["Closed", "On Hold"]],
|
||||
per_delivered: ["<", 99.99],
|
||||
company: me.frm.doc.company,
|
||||
project: me.frm.doc.project || undefined,
|
||||
}
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
}
|
||||
})
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc.is_return && doc.status!="Closed") {
|
||||
if(doc.docstatus == 1) {
|
||||
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
|
||||
this.frm.add_custom_button(__('Shipment'), function() {
|
||||
me.make_shipment() }, __('Create'));
|
||||
}
|
||||
|
||||
if(flt(doc.per_installed, 2) < 100 && doc.docstatus==1)
|
||||
this.frm.add_custom_button(__('Installation Note'), function() {
|
||||
me.make_installation_note() }, __('Create'));
|
||||
if (
|
||||
flt(doc.per_installed, 2) < 100
|
||||
&& doc.docstatus==1
|
||||
&& frappe.model.can_create("Installation Note")
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__('Installation Note'),
|
||||
function() {
|
||||
me.make_installation_note()
|
||||
},
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
if (doc.docstatus==1) {
|
||||
if (doc.docstatus==1 && this.frm.has_perm("create")) {
|
||||
this.frm.add_custom_button(__('Sales Return'), function() {
|
||||
me.make_sales_return() }, __('Create'));
|
||||
}
|
||||
|
||||
if (doc.docstatus==1) {
|
||||
if (doc.docstatus==1 && frappe.model.can_create("Delivery Trip")) {
|
||||
this.frm.add_custom_button(__('Delivery Trip'), function() {
|
||||
me.make_delivery_trip() }, __('Create'));
|
||||
}
|
||||
|
||||
if(doc.docstatus==0 && !doc.__islocal) {
|
||||
if (doc.__onload && doc.__onload.has_unpacked_items) {
|
||||
this.frm.add_custom_button(__('Packing Slip'), function() {
|
||||
if (
|
||||
doc.docstatus==0
|
||||
&& !doc.__islocal
|
||||
&& frappe.model.can_create("Packing Slip")
|
||||
&& (doc.__onload && doc.__onload.has_unpacked_items)
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__('Packing Slip'),
|
||||
function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.stock.doctype.delivery_note.delivery_note.make_packing_slip",
|
||||
frm: me.frm
|
||||
}) }, __('Create')
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
__('Create')
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc.__islocal && doc.docstatus==1) {
|
||||
@@ -211,7 +239,13 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn
|
||||
}
|
||||
}
|
||||
|
||||
if(doc.docstatus==1 && !doc.is_return && doc.status!="Closed" && flt(doc.per_billed) < 100) {
|
||||
if(
|
||||
doc.docstatus==1
|
||||
&& !doc.is_return
|
||||
&& doc.status!="Closed"
|
||||
&& flt(doc.per_billed) < 100
|
||||
&& frappe.model.can_create("Sales Invoice")
|
||||
) {
|
||||
// show Make Invoice button only if Delivery Note is not created from Sales Invoice
|
||||
var from_sales_invoice = false;
|
||||
from_sales_invoice = me.frm.doc.items.some(function(item) {
|
||||
@@ -230,7 +264,12 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends erpn
|
||||
}
|
||||
erpnext.stock.delivery_note.set_print_hide(doc, dt, dn);
|
||||
|
||||
if(doc.docstatus==1 && !doc.is_return && !doc.auto_repeat) {
|
||||
if(
|
||||
doc.docstatus==1
|
||||
&& !doc.is_return
|
||||
&& !doc.auto_repeat
|
||||
&& frappe.model.can_create("Auto Repeat")
|
||||
) {
|
||||
cur_frm.add_custom_button(__('Subscription'), function() {
|
||||
erpnext.utils.make_subscription(doc.doctype, doc.name)
|
||||
}, __('Create'))
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
frappe.listview_settings["Delivery Trip"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (in_list(["Cancelled", "Draft"], doc.status)) {
|
||||
if (["Cancelled", "Draft"].includes(doc.status)) {
|
||||
return [__(doc.status), "red", "status,=," + doc.status];
|
||||
} else if (in_list(["In Transit", "Scheduled"], doc.status)) {
|
||||
} else if (["In Transit", "Scheduled"].includes(doc.status)) {
|
||||
return [__(doc.status), "orange", "status,=," + doc.status];
|
||||
} else if (doc.status === "Completed") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
||||
@@ -156,6 +156,33 @@ class TestItem(FrappeTestCase):
|
||||
for key, value in to_check.items():
|
||||
self.assertEqual(value, details.get(key), key)
|
||||
|
||||
def test_get_asset_item_details(self):
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||
|
||||
create_asset_category(0)
|
||||
create_fixed_asset_item()
|
||||
|
||||
details = get_item_details(
|
||||
{
|
||||
"item_code": "Macbook Pro",
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Receipt",
|
||||
}
|
||||
)
|
||||
self.assertEqual(details.get("expense_account"), "_Test Fixed Asset - _TC")
|
||||
|
||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", "1")
|
||||
details = get_item_details(
|
||||
{
|
||||
"item_code": "Macbook Pro",
|
||||
"company": "_Test Company",
|
||||
"currency": "INR",
|
||||
"doctype": "Purchase Receipt",
|
||||
}
|
||||
)
|
||||
self.assertEqual(details.get("expense_account"), "CWIP Account - _TC")
|
||||
|
||||
def test_item_tax_template(self):
|
||||
expected_item_tax_template = [
|
||||
{
|
||||
|
||||
@@ -224,7 +224,10 @@ class LandedCostVoucher(Document):
|
||||
# update stock & gl entries for submit state of PR
|
||||
doc.docstatus = 1
|
||||
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
|
||||
doc.make_gl_entries()
|
||||
if d.receipt_document_type == "Purchase Receipt":
|
||||
doc.make_gl_entries(via_landed_cost_voucher=True)
|
||||
else:
|
||||
doc.make_gl_entries()
|
||||
doc.repost_future_sle_and_gle()
|
||||
|
||||
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections import OrderedDict, defaultdict
|
||||
from itertools import groupby
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
@@ -26,6 +26,9 @@ from erpnext.stock.get_item_details import get_conversion_factor
|
||||
class PickList(Document):
|
||||
def validate(self):
|
||||
self.validate_for_qty()
|
||||
if self.pick_manually and self.get("locations"):
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
@@ -35,6 +38,60 @@ class PickList(Document):
|
||||
if self.get("locations"):
|
||||
self.validate_sales_order_percentage()
|
||||
|
||||
def validate_stock_qty(self):
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
for row in self.get("locations"):
|
||||
if row.batch_no and not row.qty:
|
||||
batch_qty = get_batch_qty(row.batch_no, row.warehouse, row.item_code)
|
||||
|
||||
if row.qty > batch_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} for the batch {4} in the warehouse {5}."
|
||||
).format(row.idx, row.item_code, batch_qty, row.batch_no, bold(row.warehouse)),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
bin_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": row.item_code, "warehouse": row.warehouse},
|
||||
"actual_qty",
|
||||
)
|
||||
|
||||
if row.qty > bin_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"At Row #{0}: The picked quantity {1} for the item {2} is greater than available stock {3} in the warehouse {4}."
|
||||
).format(row.idx, row.qty, bold(row.item_code), bin_qty, bold(row.warehouse)),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def check_serial_no_status(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for row in self.get("locations"):
|
||||
if not row.serial_no:
|
||||
continue
|
||||
|
||||
picked_serial_nos = get_serial_nos(row.serial_no)
|
||||
validated_serial_nos = frappe.get_all(
|
||||
"Serial No",
|
||||
filters={"name": ("in", picked_serial_nos), "warehouse": row.warehouse},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
incorrect_serial_nos = set(picked_serial_nos) - set(validated_serial_nos)
|
||||
if incorrect_serial_nos:
|
||||
frappe.throw(
|
||||
_("The Serial No at Row #{0}: {1} is not available in warehouse {2}.").format(
|
||||
row.idx, ", ".join(incorrect_serial_nos), row.warehouse
|
||||
),
|
||||
title=_("Incorrect Warehouse"),
|
||||
)
|
||||
|
||||
def validate_sales_order_percentage(self):
|
||||
# set percentage picked in SO
|
||||
for location in self.get("locations"):
|
||||
|
||||
@@ -802,3 +802,45 @@ class TestPickList(FrappeTestCase):
|
||||
a = set(picked_serial_no)
|
||||
b = set([x for x in location.serial_no.split("\n") if x])
|
||||
self.assertSetEqual(b, b.difference(a))
|
||||
|
||||
def test_validate_picked_qty_with_manual_option(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
non_serialized_item = make_item(
|
||||
"Test Non Serialized Pick List Item For Manual Option", properties={"is_stock_item": 1}
|
||||
).name
|
||||
|
||||
serialized_item = make_item(
|
||||
"Test Serialized Pick List Item For Manual Option",
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "SN-HSNMSPLI-.####"},
|
||||
).name
|
||||
|
||||
batched_item = make_item(
|
||||
"Test Batched Pick List Item For Manual Option",
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "SN-HBNMSPLI-.####",
|
||||
"create_new_batch": 1,
|
||||
},
|
||||
).name
|
||||
|
||||
make_stock_entry(item=non_serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item=serialized_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
make_stock_entry(item=batched_item, to_warehouse=warehouse, qty=10, basic_rate=100)
|
||||
|
||||
so = make_sales_order(
|
||||
item_code=non_serialized_item, qty=10, rate=100, do_not_save=True, warehouse=warehouse
|
||||
)
|
||||
so.append("items", {"item_code": serialized_item, "qty": 10, "rate": 100, "warehouse": warehouse})
|
||||
so.append("items", {"item_code": batched_item, "qty": 10, "rate": 100, "warehouse": warehouse})
|
||||
so.set_missing_values()
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
pl = create_pick_list(so.name)
|
||||
pl.pick_manually = 1
|
||||
|
||||
for row in pl.locations:
|
||||
row.qty = row.qty + 10
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pl.save)
|
||||
|
||||
@@ -307,13 +307,13 @@ class PurchaseReceipt(BuyingController):
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None):
|
||||
def get_gl_entries(self, warehouse_account=None, via_landed_cost_voucher=False):
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
|
||||
gl_entries = []
|
||||
|
||||
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
|
||||
self.make_tax_gl_entries(gl_entries)
|
||||
self.make_tax_gl_entries(gl_entries, via_landed_cost_voucher)
|
||||
update_regional_gl_entries(gl_entries, self)
|
||||
|
||||
return process_gl_map(gl_entries)
|
||||
@@ -558,15 +558,7 @@ class PurchaseReceipt(BuyingController):
|
||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||
|
||||
if d.is_fixed_asset:
|
||||
account_type = (
|
||||
"capital_work_in_progress_account"
|
||||
if is_cwip_accounting_enabled(d.asset_category)
|
||||
else "fixed_asset_account"
|
||||
)
|
||||
|
||||
stock_asset_account_name = get_asset_account(
|
||||
account_type, asset_category=d.asset_category, company=self.company
|
||||
)
|
||||
stock_asset_account_name = d.expense_account
|
||||
|
||||
stock_value_diff = (
|
||||
flt(d.base_net_amount) + flt(d.item_tax_amount) + flt(d.landed_cost_voucher_amount)
|
||||
@@ -661,9 +653,15 @@ class PurchaseReceipt(BuyingController):
|
||||
posting_date=posting_date,
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
def is_landed_cost_booked_for_any_item(self) -> bool:
|
||||
for x in self.items:
|
||||
if x.landed_cost_voucher_amount != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries, via_landed_cost_voucher=False):
|
||||
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
|
||||
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
|
||||
# Cost center-wise amount breakup for other charges included for valuation
|
||||
valuation_tax = {}
|
||||
for tax in self.get("taxes"):
|
||||
@@ -688,27 +686,10 @@ class PurchaseReceipt(BuyingController):
|
||||
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
|
||||
total_valuation_amount = sum(valuation_tax.values())
|
||||
amount_including_divisional_loss = negative_expense_to_be_booked
|
||||
stock_rbnb = (
|
||||
self.get("asset_received_but_not_billed")
|
||||
if is_asset_pr
|
||||
else self.get_company_default("stock_received_but_not_billed")
|
||||
)
|
||||
i = 1
|
||||
for tax in self.get("taxes"):
|
||||
if valuation_tax.get(tax.name):
|
||||
negative_expense_booked_in_pi = frappe.db.sql(
|
||||
"""select name from `tabPurchase Invoice Item` pi
|
||||
where docstatus = 1 and purchase_receipt=%s
|
||||
and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
|
||||
and voucher_no=pi.parent and account=%s)""",
|
||||
(self.name, tax.account_head),
|
||||
)
|
||||
|
||||
if negative_expense_booked_in_pi:
|
||||
account = stock_rbnb
|
||||
else:
|
||||
account = tax.account_head
|
||||
|
||||
account = tax.account_head
|
||||
if i == len(valuation_tax):
|
||||
applicable_amount = amount_including_divisional_loss
|
||||
else:
|
||||
|
||||
@@ -9,6 +9,7 @@ from pypika import functions as fn
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.controllers.buying_controller import QtyMismatchError
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -1640,7 +1641,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||
|
||||
def test_internal_pr_gl_entries(self):
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
@@ -2301,6 +2301,308 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
for index, d in enumerate(data):
|
||||
self.assertEqual(d.qty_after_transaction, 11 + index)
|
||||
|
||||
def test_valuation_taxes_lcv_repost_after_billing(self):
|
||||
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||
make_landed_cost_voucher,
|
||||
)
|
||||
|
||||
old_perpetual_inventory = erpnext.is_perpetual_inventory_enabled("_Test Company")
|
||||
frappe.local.enable_perpetual_inventory["_Test Company"] = 1
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
"_Test Company",
|
||||
"stock_received_but_not_billed",
|
||||
"Stock Received But Not Billed - _TC",
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(qty=10, rate=1000, do_not_submit=1)
|
||||
pr.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Valuation and Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "Freight and Forwarding Charges - _TC",
|
||||
"tax_amount": 2000,
|
||||
"description": "Test",
|
||||
},
|
||||
)
|
||||
pr.submit()
|
||||
pi = make_purchase_invoice(pr.name)
|
||||
pi.submit()
|
||||
make_landed_cost_voucher(
|
||||
company=pr.company,
|
||||
receipt_document_type="Purchase Receipt",
|
||||
receipt_document=pr.name,
|
||||
charges=2000,
|
||||
distribute_charges_based_on="Qty",
|
||||
expense_account="Expenses Included In Valuation - _TC",
|
||||
)
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name, skip_cancelled=True, as_dict=False)
|
||||
warehouse_account = get_warehouse_account_map("_Test Company")
|
||||
expected_gle = (
|
||||
("Stock Received But Not Billed - _TC", 0, 10000, "Main - _TC"),
|
||||
("Freight and Forwarding Charges - _TC", 0, 2000, "Main - _TC"),
|
||||
("Expenses Included In Valuation - _TC", 0, 2000, "Main - _TC"),
|
||||
(warehouse_account[pr.items[0].warehouse]["account"], 14000, 0, "Main - _TC"),
|
||||
)
|
||||
self.assertSequenceEqual(expected_gle, gl_entries)
|
||||
frappe.local.enable_perpetual_inventory["_Test Company"] = old_perpetual_inventory
|
||||
|
||||
def test_tax_account_heads_on_lcv_and_item_repost(self):
|
||||
"""
|
||||
PO -> PR -> PI
|
||||
PR -> LCV
|
||||
Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb
|
||||
"""
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
make_pr_against_po,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
|
||||
stock_rbnb = "Stock Received But Not Billed - _TC"
|
||||
stock_in_hand = "Stock In Hand - _TC"
|
||||
test_cc = "_Test Cost Center - _TC"
|
||||
test_company = "_Test Company"
|
||||
creditors = "Creditors - _TC"
|
||||
lcv_expense_account = "Expenses Included In Valuation - _TC"
|
||||
|
||||
company_doc = frappe.get_doc("Company", test_company)
|
||||
company_doc.enable_perpetual_inventory = True
|
||||
company_doc.stock_received_but_not_billed = stock_rbnb
|
||||
company_doc.default_inventory_account = stock_in_hand
|
||||
company_doc.save()
|
||||
|
||||
packaging_charges_account = create_account(
|
||||
account_name="Packaging Charges",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company=test_company,
|
||||
account_type="Tax",
|
||||
)
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
po.taxes = []
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Valuation and Total",
|
||||
"account_head": packaging_charges_account,
|
||||
"cost_center": test_cc,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 250,
|
||||
},
|
||||
)
|
||||
po.save().submit()
|
||||
|
||||
pr = make_pr_against_po(po.name, received_qty=10)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# Make PI against Purchase Receipt
|
||||
pi = make_purchase_invoice(pr.name).save().submit()
|
||||
pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True)
|
||||
expected_pi_gles = [
|
||||
{"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None},
|
||||
]
|
||||
self.assertEqual(expected_pi_gles, pi_gl_entries)
|
||||
|
||||
lcv = self.create_lcv(pr.doctype, pr.name, test_company, lcv_expense_account)
|
||||
pr_gles_after_lcv = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_lcv = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles_after_lcv, pr_gles_after_lcv)
|
||||
|
||||
# Trigger Repost Item Valudation on a older date
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": pr.items[0].item_code,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"posting_date": add_days(pr.posting_date, -1),
|
||||
"posting_time": "00:00:00",
|
||||
"company": pr.company,
|
||||
"allow_negative_stock": 1,
|
||||
"via_landed_cost_voucher": 0,
|
||||
"allow_zero_rate": 0,
|
||||
}
|
||||
)
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1300.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
{"account": lcv_expense_account, "debit": 0.0, "credit": 50.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
lcv.reload()
|
||||
lcv.cancel()
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company_doc.enable_perpetual_inventory = False
|
||||
company_doc.stock_received_but_not_billed = None
|
||||
company_doc.default_inventory_account = None
|
||||
company_doc.save()
|
||||
|
||||
def create_lcv(self, receipt_document_type, receipt_document, company, expense_account, charges=50):
|
||||
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
|
||||
|
||||
lcv = frappe.new_doc("Landed Cost Voucher")
|
||||
lcv.company = company
|
||||
lcv.distribute_charges_based_on = "Qty"
|
||||
lcv.set(
|
||||
"purchase_receipts",
|
||||
[
|
||||
{
|
||||
"receipt_document_type": receipt_document_type,
|
||||
"receipt_document": receipt_document,
|
||||
"supplier": ref_doc.supplier,
|
||||
"posting_date": ref_doc.posting_date,
|
||||
"grand_total": ref_doc.base_grand_total,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
lcv.set(
|
||||
"taxes",
|
||||
[
|
||||
{
|
||||
"description": "Testing",
|
||||
"expense_account": expense_account,
|
||||
"amount": charges,
|
||||
}
|
||||
],
|
||||
)
|
||||
lcv.save().submit()
|
||||
return lcv
|
||||
|
||||
def test_tax_account_heads_on_item_repost_without_lcv(self):
|
||||
"""
|
||||
PO -> PR -> PI
|
||||
Backdated `Repost Item valuation` should not merge tax account heads into stock_rbnb if Purchase Receipt was created first
|
||||
This scenario is without LCV
|
||||
"""
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
make_pr_against_po,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
|
||||
|
||||
stock_rbnb = "Stock Received But Not Billed - _TC"
|
||||
stock_in_hand = "Stock In Hand - _TC"
|
||||
test_cc = "_Test Cost Center - _TC"
|
||||
test_company = "_Test Company"
|
||||
creditors = "Creditors - _TC"
|
||||
|
||||
company_doc = frappe.get_doc("Company", test_company)
|
||||
company_doc.enable_perpetual_inventory = True
|
||||
company_doc.stock_received_but_not_billed = stock_rbnb
|
||||
company_doc.default_inventory_account = stock_in_hand
|
||||
company_doc.save()
|
||||
|
||||
packaging_charges_account = create_account(
|
||||
account_name="Packaging Charges",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company=test_company,
|
||||
account_type="Tax",
|
||||
)
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
po.taxes = []
|
||||
po.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Valuation and Total",
|
||||
"account_head": packaging_charges_account,
|
||||
"cost_center": test_cc,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
"charge_type": "Actual",
|
||||
"tax_amount": 250,
|
||||
},
|
||||
)
|
||||
po.save().submit()
|
||||
|
||||
pr = make_pr_against_po(po.name, received_qty=10)
|
||||
pr_gl_entries = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(expected_pr_gles, pr_gl_entries)
|
||||
|
||||
# Make PI against Purchase Receipt
|
||||
pi = make_purchase_invoice(pr.name).save().submit()
|
||||
pi_gl_entries = get_gl_entries(pi.doctype, pi.name, skip_cancelled=True)
|
||||
expected_pi_gles = [
|
||||
{"account": stock_rbnb, "debit": 1000.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": creditors, "debit": 0.0, "credit": 1250.0, "cost_center": None},
|
||||
]
|
||||
self.assertEqual(expected_pi_gles, pi_gl_entries)
|
||||
|
||||
# Trigger Repost Item Valudation on a older date
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": pr.items[0].item_code,
|
||||
"warehouse": pr.items[0].warehouse,
|
||||
"posting_date": add_days(pr.posting_date, -1),
|
||||
"posting_time": "00:00:00",
|
||||
"company": pr.company,
|
||||
"allow_negative_stock": 1,
|
||||
"via_landed_cost_voucher": 0,
|
||||
"allow_zero_rate": 0,
|
||||
}
|
||||
)
|
||||
repost_doc.save().submit()
|
||||
|
||||
pr_gles_after_repost = get_gl_entries(pr.doctype, pr.name, skip_cancelled=True)
|
||||
expected_pr_gles_after_repost = [
|
||||
{"account": stock_rbnb, "debit": 0.0, "credit": 1000.0, "cost_center": test_cc},
|
||||
{"account": stock_in_hand, "debit": 1250.0, "credit": 0.0, "cost_center": test_cc},
|
||||
{"account": packaging_charges_account, "debit": 0.0, "credit": 250.0, "cost_center": test_cc},
|
||||
]
|
||||
self.assertEqual(len(pr_gles_after_repost), len(expected_pr_gles_after_repost))
|
||||
self.assertEqual(expected_pr_gles_after_repost, pr_gles_after_repost)
|
||||
|
||||
# teardown
|
||||
pi.reload()
|
||||
pi.cancel()
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
|
||||
company_doc.enable_perpetual_inventory = False
|
||||
company_doc.stock_received_but_not_billed = None
|
||||
company_doc.default_inventory_account = None
|
||||
company_doc.save()
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
@@ -2347,14 +2649,24 @@ def get_sl_entries(voucher_type, voucher_no):
|
||||
)
|
||||
|
||||
|
||||
def get_gl_entries(voucher_type, voucher_no):
|
||||
return frappe.db.sql(
|
||||
"""select account, debit, credit, cost_center, is_cancelled
|
||||
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
|
||||
order by account desc""",
|
||||
(voucher_type, voucher_no),
|
||||
as_dict=1,
|
||||
def get_gl_entries(voucher_type, voucher_no, skip_cancelled=False, as_dict=True):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
gl_query = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
gl.account,
|
||||
gl.debit,
|
||||
gl.credit,
|
||||
gl.cost_center,
|
||||
)
|
||||
.where((gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no))
|
||||
.orderby(gl.account, order=frappe.qb.desc)
|
||||
)
|
||||
if skip_cancelled:
|
||||
gl_query = gl_query.where(gl.is_cancelled == 0)
|
||||
else:
|
||||
gl_query = gl_query.select(gl.is_cancelled)
|
||||
return gl_query.run(as_dict=as_dict)
|
||||
|
||||
|
||||
def get_taxes(**args):
|
||||
|
||||
@@ -45,7 +45,7 @@ class RepostItemValuation(Document):
|
||||
|
||||
def validate_period_closing_voucher(self):
|
||||
# Period Closing Voucher
|
||||
year_end_date = self.get_max_year_end_date(self.company)
|
||||
year_end_date = self.get_max_period_closing_date(self.company)
|
||||
if year_end_date and getdate(self.posting_date) <= getdate(year_end_date):
|
||||
date = frappe.format(year_end_date, "Date")
|
||||
msg = f"Due to period closing, you cannot repost item valuation before {date}"
|
||||
@@ -88,24 +88,16 @@ class RepostItemValuation(Document):
|
||||
return frappe.get_all("Closing Stock Balance", fields=["name", "to_date"], filters=filters)
|
||||
|
||||
@staticmethod
|
||||
def get_max_year_end_date(company):
|
||||
data = frappe.get_all(
|
||||
"Period Closing Voucher", fields=["fiscal_year"], filters={"docstatus": 1, "company": company}
|
||||
)
|
||||
|
||||
if not data:
|
||||
return
|
||||
|
||||
fiscal_years = [d.fiscal_year for d in data]
|
||||
table = frappe.qb.DocType("Fiscal Year")
|
||||
def get_max_period_closing_date(company):
|
||||
table = frappe.qb.DocType("Period Closing Voucher")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Max(table.year_end_date))
|
||||
.where((table.name.isin(fiscal_years)) & (table.disabled == 0))
|
||||
.select(Max(table.posting_date))
|
||||
.where((table.company == company) & (table.docstatus == 1))
|
||||
).run()
|
||||
|
||||
return query[0][0] if query else None
|
||||
return query[0][0] if query and query[0][0] else None
|
||||
|
||||
def validate_accounts_freeze(self):
|
||||
acc_settings = frappe.db.get_value(
|
||||
@@ -245,9 +237,23 @@ def repost(doc):
|
||||
doc.log_error("Unable to repost item valuation")
|
||||
|
||||
message = frappe.message_log.pop() if frappe.message_log else ""
|
||||
|
||||
status = "Failed"
|
||||
# If failed because of timeout, set status to In Progress
|
||||
if traceback and "timeout" in traceback.lower():
|
||||
status = "In Progress"
|
||||
|
||||
if traceback:
|
||||
message += "<br>" + "Traceback: <br>" + traceback
|
||||
frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
|
||||
|
||||
frappe.db.set_value(
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
{
|
||||
"error_log": message,
|
||||
"status": status,
|
||||
},
|
||||
)
|
||||
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
@@ -255,7 +261,6 @@ def repost(doc):
|
||||
|
||||
if outgoing_email_account and not isinstance(e, RecoverableErrors):
|
||||
notify_error_to_stock_managers(doc, message)
|
||||
doc.set_status("Failed")
|
||||
finally:
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -77,7 +77,7 @@ frappe.ui.form.on('Stock Entry', {
|
||||
if(!item.item_code) {
|
||||
frappe.throw(__("Please enter Item Code to get Batch Number"));
|
||||
} else {
|
||||
if (in_list(["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"], doc.purpose)) {
|
||||
if (["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"].includes(doc.purpose)) {
|
||||
var filters = {
|
||||
'item_code': item.item_code,
|
||||
'posting_date': frm.doc.posting_date || frappe.datetime.nowdate()
|
||||
|
||||
@@ -327,12 +327,26 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
|
||||
expense_account = None
|
||||
|
||||
if args.get("doctype") == "Purchase Invoice" and item.is_fixed_asset:
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
if item.is_fixed_asset:
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
||||
|
||||
expense_account = get_asset_category_account(
|
||||
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
||||
)
|
||||
if is_cwip_accounting_enabled(item.asset_category):
|
||||
expense_account = get_asset_account(
|
||||
"capital_work_in_progress_account",
|
||||
asset_category=item.asset_category,
|
||||
company=args.company,
|
||||
)
|
||||
elif args.get("doctype") in (
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Purchase Order",
|
||||
"Material Request",
|
||||
):
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
|
||||
expense_account = get_asset_category_account(
|
||||
fieldname="fixed_asset_account", item=args.item_code, company=args.company
|
||||
)
|
||||
|
||||
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
|
||||
if not args.get("uom"):
|
||||
@@ -1283,7 +1297,7 @@ def get_batch_qty(batch_no, warehouse, item_code):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_price_list(args, as_doc=False):
|
||||
def apply_price_list(args, as_doc=False, doc=None):
|
||||
"""Apply pricelist on a document-like dict object and return as
|
||||
{'parent': dict, 'children': list}
|
||||
|
||||
@@ -1322,7 +1336,7 @@ def apply_price_list(args, as_doc=False):
|
||||
for item in item_list:
|
||||
args_copy = frappe._dict(args.copy())
|
||||
args_copy.update(item)
|
||||
item_details = apply_price_list_on_item(args_copy)
|
||||
item_details = apply_price_list_on_item(args_copy, doc=doc)
|
||||
children.append(item_details)
|
||||
|
||||
if as_doc:
|
||||
@@ -1340,10 +1354,10 @@ def apply_price_list(args, as_doc=False):
|
||||
return {"parent": parent, "children": children}
|
||||
|
||||
|
||||
def apply_price_list_on_item(args):
|
||||
def apply_price_list_on_item(args, doc=None):
|
||||
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
|
||||
item_details = get_price_list_rate(args, item_doc)
|
||||
item_details.update(get_pricing_rule_for_item(args))
|
||||
item_details.update(get_pricing_rule_for_item(args, doc=doc))
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
|
||||
frappe.query_reports["Product Bundle Balance"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "date",
|
||||
label: __("Date"),
|
||||
|
||||
@@ -224,6 +224,9 @@ def get_stock_ledger_entries(filters, items):
|
||||
.where((sle2.name.isnull()) & (sle.docstatus < 2) & (sle.item_code.isin(items)))
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(sle.company == filters.get("company"))
|
||||
|
||||
if date := filters.get("date"):
|
||||
query = query.where(sle.posting_date <= date)
|
||||
else:
|
||||
@@ -237,7 +240,7 @@ def get_stock_ledger_entries(filters, items):
|
||||
if warehouse_details:
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
sle.warehouse.isin(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where((wh.lft >= warehouse_details.lft) & (wh.rgt <= warehouse_details.rgt))
|
||||
|
||||
@@ -203,13 +203,17 @@ def repost_future_sle(
|
||||
"posting_time": args[i].get("posting_time"),
|
||||
"creation": args[i].get("creation"),
|
||||
"distinct_item_warehouses": distinct_item_warehouses,
|
||||
"items_to_be_repost": args,
|
||||
"current_index": i,
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
affected_transactions.update(obj.affected_transactions)
|
||||
|
||||
distinct_item_warehouses[(args[i].get("item_code"), args[i].get("warehouse"))].reposting_status = True
|
||||
key = (args[i].get("item_code"), args[i].get("warehouse"))
|
||||
if distinct_item_warehouses.get(key):
|
||||
distinct_item_warehouses[key].reposting_status = True
|
||||
|
||||
if obj.new_items_found:
|
||||
for _item_wh, data in distinct_item_warehouses.items():
|
||||
@@ -484,11 +488,20 @@ class update_entries_after:
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
else:
|
||||
# Check if the dependent voucher is reposted
|
||||
# If not, then do not add it to the list
|
||||
if not self.is_dependent_voucher_reposted(dependant_sle):
|
||||
return
|
||||
|
||||
existing_sle_posting_date = self.distinct_item_warehouses[key].get("sle", {}).get("posting_date")
|
||||
|
||||
dependent_voucher_detail_nos = self.get_dependent_voucher_detail_nos(key)
|
||||
|
||||
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
|
||||
if dependent_voucher_detail_nos and dependant_sle.voucher_detail_no in set(
|
||||
dependent_voucher_detail_nos
|
||||
):
|
||||
return
|
||||
|
||||
val.sle_changed = True
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
@@ -502,6 +515,27 @@ class update_entries_after:
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
self.distinct_item_warehouses[key] = val
|
||||
|
||||
def is_dependent_voucher_reposted(self, dependant_sle) -> bool:
|
||||
# Return False if the dependent voucher is not reposted
|
||||
|
||||
if self.args.items_to_be_repost and self.args.current_index:
|
||||
index = self.args.current_index
|
||||
while index < len(self.args.items_to_be_repost):
|
||||
if (
|
||||
self.args.items_to_be_repost[index].get("item_code") == dependant_sle.item_code
|
||||
and self.args.items_to_be_repost[index].get("warehouse") == dependant_sle.warehouse
|
||||
):
|
||||
if getdate(self.args.items_to_be_repost[index].get("posting_date")) > getdate(
|
||||
dependant_sle.posting_date
|
||||
):
|
||||
self.args.items_to_be_repost[index]["posting_date"] = dependant_sle.posting_date
|
||||
|
||||
return False
|
||||
|
||||
index += 1
|
||||
|
||||
return True
|
||||
|
||||
def get_dependent_voucher_detail_nos(self, key):
|
||||
if "dependent_voucher_detail_nos" not in self.distinct_item_warehouses[key]:
|
||||
self.distinct_item_warehouses[key].dependent_voucher_detail_nos = []
|
||||
@@ -1529,6 +1563,7 @@ def get_next_stock_reco(kwargs):
|
||||
sle.batch_no,
|
||||
sle.actual_qty,
|
||||
)
|
||||
.force_index("item_warehouse")
|
||||
.where(
|
||||
(sle.item_code == kwargs.get("item_code"))
|
||||
& (sle.warehouse == kwargs.get("warehouse"))
|
||||
|
||||
@@ -240,6 +240,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
|
||||
def validate_available_qty_for_consumption(self):
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "BOM"
|
||||
):
|
||||
return
|
||||
|
||||
for item in self.get("supplied_items"):
|
||||
precision = item.precision("consumed_qty")
|
||||
if (
|
||||
|
||||
@@ -75,6 +75,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(scr.get("items")[0].rm_supp_cost, flt(rm_supp_cost))
|
||||
|
||||
def test_available_qty_for_consumption(self):
|
||||
set_backflush_based_on("BOM")
|
||||
make_stock_entry(item_code="_Test Item", qty=100, target="_Test Warehouse 1 - _TC", basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
@@ -119,7 +120,7 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
self.assertRaises(frappe.ValidationError, scr.submit)
|
||||
scr.submit()
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
|
||||
@@ -556,6 +557,21 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
# consumed_qty should be (accepted_qty * qty_consumed_per_unit) = (6 * 1) = 6
|
||||
self.assertEqual(scr.supplied_items[0].consumed_qty, 6)
|
||||
|
||||
# Do not transfer materials to the supplier warehouse and check whether system allows to consumed directly from the supplier's warehouse
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
|
||||
# Transfer RM's
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items, warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
# Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.submit()
|
||||
self.assertEqual(scr.docstatus, 1)
|
||||
|
||||
for item in scr.supplied_items:
|
||||
self.assertFalse(item.available_qty_for_consumption)
|
||||
|
||||
def test_supplied_items_cost_after_reposting(self):
|
||||
# Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
actual_qty = (frm.doc.doctype==="Sales Order"
|
||||
? doc.projected_qty : doc.actual_qty);
|
||||
if(flt(frm.doc.per_delivered) < 100
|
||||
&& in_list(["Sales Order Item", "Delivery Note Item"], doc.doctype)) {
|
||||
&& ["Sales Order Item", "Delivery Note Item"].includes(doc.doctype)) {
|
||||
if(actual_qty != undefined) {
|
||||
if(actual_qty >= doc.qty) {
|
||||
var color = "green";
|
||||
|
||||
@@ -1248,6 +1248,8 @@ Is Default,Ist Standard,
|
||||
Is Existing Asset,Vermögenswert existiert bereits.,
|
||||
Is Frozen,Ist gesperrt,
|
||||
Is Group,Ist Gruppe,
|
||||
Is Group Warehouse,Ist Lagergruppe,
|
||||
Is Rejected Warehouse,Ist Lager für abgelehnte Ware,
|
||||
Issue,Anfrage,
|
||||
Issue Material,Material ausgeben,
|
||||
Issued,Ausgestellt,
|
||||
@@ -1311,6 +1313,7 @@ Items for Raw Material Request,Artikel für Rohstoffanforderung,
|
||||
Job Card,Jobkarte,
|
||||
Job card {0} created,Jobkarte {0} erstellt,
|
||||
Join,Beitreten,
|
||||
Joining,Beitritt,
|
||||
Journal Entries {0} are un-linked,Buchungssätze {0} sind nicht verknüpft,
|
||||
Journal Entry,Buchungssatz,
|
||||
Journal Entry {0} does not have account {1} or already matched against other voucher,Buchungssatz {0} gehört nicht zu Konto {1} oder ist bereits mit einem anderen Beleg abgeglichen,
|
||||
@@ -2033,6 +2036,7 @@ Product Search,Produkt Suche,
|
||||
Production,Produktion,
|
||||
Production Item,Produktions-Artikel,
|
||||
Products,Produkte,
|
||||
Profile,Profil,
|
||||
Profit and Loss,Gewinn und Verlust,
|
||||
Profit for the year,Jahresüberschuss,
|
||||
Program,Programm,
|
||||
@@ -2094,6 +2098,9 @@ Qty To Manufacture,Herzustellende Menge,
|
||||
Qty Total,Gesamtmenge,
|
||||
Qty for {0},Menge für {0},
|
||||
Qualification,Qualifikation,
|
||||
Qualification Status,Qualifikationsstatus,
|
||||
Qualified By,Qualifiziert von,
|
||||
Qualified on,Qualifiziert am,
|
||||
Quality,Qualität,
|
||||
Quality Action,Qualitätsmaßnahme,
|
||||
Quality Goal.,Qualitätsziel.,
|
||||
@@ -3295,6 +3302,7 @@ Warehouse Type,Lagertyp,
|
||||
'Date' is required,'Datum' ist erforderlich,
|
||||
Budgets,Budgets,
|
||||
Bundle Qty,Bundle Menge,
|
||||
Company Details,Unternehmensdetails,
|
||||
Company GSTIN,Unternehmen GSTIN,
|
||||
Company field is required,Firmenfeld ist erforderlich,
|
||||
Creating Dimensions...,Dimensionen erstellen ...,
|
||||
@@ -3659,6 +3667,7 @@ Performance,Performance,
|
||||
Period based On,Zeitraum basierend auf,
|
||||
Perpetual inventory required for the company {0} to view this report.,"Permanente Bestandsaufnahme erforderlich, damit das Unternehmen {0} diesen Bericht anzeigen kann.",
|
||||
Phone,Telefon,
|
||||
Phone Ext.,Telefon Ext.,
|
||||
Pick List,Auswahlliste,
|
||||
Plaid authentication error,Plaid-Authentifizierungsfehler,
|
||||
Plaid public token error,Plaid public token error,
|
||||
@@ -5096,7 +5105,7 @@ Number of Depreciations Booked,Anzahl der gebuchten Abschreibungen,
|
||||
Finance Books,Finanzbücher,
|
||||
Straight Line,Gerade Linie,
|
||||
Double Declining Balance,Doppelte degressive,
|
||||
Manual,Handbuch,
|
||||
Manual,Manuell,
|
||||
Value After Depreciation,Wert nach Abschreibung,
|
||||
Total Number of Depreciations,Gesamtzahl der Abschreibungen,
|
||||
Frequency of Depreciation (Months),Die Häufigkeit der Abschreibungen (Monate),
|
||||
@@ -5156,6 +5165,7 @@ Maintenance Team Name,Name des Wartungsteams,
|
||||
Maintenance Team Members,Mitglieder des Wartungsteams,
|
||||
Purpose,Zweck,
|
||||
Stock Manager,Lagerleiter,
|
||||
Stock Movement,Lagerbewegung,
|
||||
Asset Movement Item,Vermögensbewegungsgegenstand,
|
||||
Source Location,Quellspeicherort,
|
||||
From Employee,Von Mitarbeiter,
|
||||
@@ -5252,6 +5262,7 @@ Default Bank Account,Standardbankkonto,
|
||||
Is Transporter,Ist Transporter,
|
||||
Represents Company,Repräsentiert das Unternehmen,
|
||||
Supplier Type,Lieferantentyp,
|
||||
Allow Purchase,Einkauf zulassen,
|
||||
Allow Purchase Invoice Creation Without Purchase Order,Erstellen von Eingangsrechnung ohne Bestellung zulassen,
|
||||
Allow Purchase Invoice Creation Without Purchase Receipt,Erstellen von Eingangsrechnung ohne Kaufbeleg ohne Kaufbeleg zulassen,
|
||||
Warn RFQs,Warnung Ausschreibungen,
|
||||
@@ -6028,6 +6039,7 @@ Occupational Hazards and Environmental Factors,Berufsrisiken und Umweltfaktoren,
|
||||
Other Risk Factors,Andere Risikofaktoren,
|
||||
Patient Details,Patientendetails,
|
||||
Additional information regarding the patient,Zusätzliche Informationen zum Patienten,
|
||||
Additional Info,Zusätzliche Informationen,
|
||||
HLC-APP-.YYYY.-,HLC-APP-.YYYY.-,
|
||||
Patient Age,Patient Alter,
|
||||
Get Prescribed Clinical Procedures,Holen Sie sich vorgeschriebene klinische Verfahren,
|
||||
@@ -6194,6 +6206,7 @@ Date Of Retirement,Zeitpunkt der Pensionierung,
|
||||
Department and Grade,Abteilung und Klasse,
|
||||
Reports to,Vorgesetzter,
|
||||
Attendance and Leave Details,Anwesenheits- und Urlaubsdetails,
|
||||
Attendance & Leaves,Anwesenheit & Urlaub,
|
||||
Attendance Device ID (Biometric/RF tag ID),Anwesenheitsgeräte-ID (biometrische / RF-Tag-ID),
|
||||
Applicable Holiday List,Geltende Feiertagsliste,
|
||||
Default Shift,Standardverschiebung,
|
||||
@@ -6746,7 +6759,7 @@ Default Costing Rate,Standardkosten,
|
||||
Default Billing Rate,Standard-Rechnungspreis,
|
||||
Dependent Task,Abhängiger Vorgang,
|
||||
Project Type,Projekttyp,
|
||||
% Complete Method,% abgeschlossene Methode,
|
||||
% Complete Method,Fertigstellung bemessen nach,
|
||||
Task Completion,Aufgabenerledigung,
|
||||
Task Progress,Vorgangsentwicklung,
|
||||
% Completed,% abgeschlossen,
|
||||
@@ -6909,6 +6922,7 @@ Restaurant Reservation,Restaurant Reservierung,
|
||||
Waitlisted,Auf der Warteliste,
|
||||
No Show,Nicht angetreten,
|
||||
No of People,Anzahl von Personen,
|
||||
No of Employees,Anzahl der Mitarbeiter,
|
||||
Reservation Time,Reservierungszeit,
|
||||
Reservation End Time,Reservierungsendzeit,
|
||||
No of Seats,Anzahl der Sitze,
|
||||
@@ -6920,6 +6934,7 @@ Default Company Bank Account,Standard-Bankkonto des Unternehmens,
|
||||
From Lead,Aus Lead,
|
||||
Account Manager,Kundenberater,
|
||||
Accounts Manager,Buchhalter,
|
||||
Allow Sales,Verkauf zulassen,
|
||||
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Auftrag,
|
||||
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung von Ausgangsrechnungen ohne Lieferschein,
|
||||
Default Price List,Standardpreisliste,
|
||||
@@ -6979,7 +6994,8 @@ Not Delivered,Nicht geliefert,
|
||||
Fully Delivered,Komplett geliefert,
|
||||
Partly Delivered,Teilweise geliefert,
|
||||
Not Applicable,Nicht andwendbar,
|
||||
% Delivered,% geliefert,
|
||||
% Delivered,% Geliefert,
|
||||
% Picked,% Kommissioniert,
|
||||
% of materials delivered against this Sales Order,% der für diesen Auftrag gelieferten Materialien,
|
||||
% of materials billed against this Sales Order,% der Materialien welche zu diesem Auftrag gebucht wurden,
|
||||
Not Billed,Nicht abgerechnet,
|
||||
@@ -7113,6 +7129,7 @@ New Income,Neuer Verdienst,
|
||||
New Expenses,Neue Ausgaben,
|
||||
Annual Income,Jährliches Einkommen,
|
||||
Annual Expenses,Jährliche Kosten,
|
||||
Annual Revenue,Jährlicher Umsatz,
|
||||
Bank Balance,Kontostand,
|
||||
Bank Credit Balance,Bankguthaben,
|
||||
Receivables,Forderungen,
|
||||
@@ -7595,6 +7612,7 @@ Actual Qty After Transaction,Tatsächliche Anzahl nach Transaktionen,
|
||||
Stock Value Difference,Lagerwert-Differenz,
|
||||
Stock Queue (FIFO),Lagerverfahren (FIFO),
|
||||
Is Cancelled,Ist storniert,
|
||||
Is Cash or Non Trade Discount,Ist Bar- oder Nicht-Handelsrabatt,
|
||||
Stock Reconciliation,Bestandsabgleich,
|
||||
This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.,"Dieses Werkzeug hilft Ihnen dabei, die Menge und die Bewertung von Bestand im System zu aktualisieren oder zu ändern. Es wird in der Regel verwendet, um die Systemwerte und den aktuellen Bestand Ihrer Lager zu synchronisieren.",
|
||||
Reconciliation JSON,Abgleich JSON (JavaScript Object Notation),
|
||||
@@ -7696,6 +7714,7 @@ Warranty / AMC Status,Status der Garantie / des jährlichen Wartungsvertrags,
|
||||
Resolved By,Entschieden von,
|
||||
Service Address,Serviceadresse,
|
||||
If different than customer address,Falls abweichend von Kundenadresse,
|
||||
"If yes, then this warehouse will be used to store rejected materials","Falls aktiviert, wird dieses Lager verwendet, um abgelehnte Ware zu lagern",
|
||||
Raised By,Gemeldet durch,
|
||||
From Company,Von Unternehmen,
|
||||
Rename Tool,Werkzeug zum Umbenennen,
|
||||
@@ -8940,6 +8959,7 @@ Print Receipt,Druckeingang,
|
||||
Edit Receipt,Beleg bearbeiten,
|
||||
Focus on search input,Konzentrieren Sie sich auf die Sucheingabe,
|
||||
Focus on Item Group filter,Fokus auf Artikelgruppenfilter,
|
||||
Footer will display correctly only in PDF,Die Fußzeile wird nur im PDF korrekt angezeigt,
|
||||
Checkout Order / Submit Order / New Order,Kaufabwicklung / Bestellung abschicken / Neue Bestellung,
|
||||
Add Order Discount,Bestellrabatt hinzufügen,
|
||||
Item Code: {0} is not available under warehouse {1}.,Artikelcode: {0} ist unter Lager {1} nicht verfügbar.,
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user