mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-24 19:39:50 +00:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc0a895760 | ||
|
|
6ec370ad86 | ||
|
|
994f992fcf | ||
|
|
3e816f6ea1 | ||
|
|
789cea9a85 | ||
|
|
2ea583ee16 | ||
|
|
00db37e306 | ||
|
|
c84952169f | ||
|
|
d103409077 | ||
|
|
d1dd3ca097 | ||
|
|
4e2a42d092 | ||
|
|
9c3f4b69af | ||
|
|
e2e1ac0dba | ||
|
|
b557a03ba7 | ||
|
|
11eab0c852 | ||
|
|
8683f49e8b | ||
|
|
6ea07ba56d | ||
|
|
e02a55b188 | ||
|
|
baa6d2bcdc | ||
|
|
631ffd55ef | ||
|
|
7a457dafe0 | ||
|
|
94a5867303 | ||
|
|
1663b875c0 | ||
|
|
d47f3cc101 | ||
|
|
07c3755f31 | ||
|
|
c42dcbe739 | ||
|
|
233fe9b7e6 | ||
|
|
3f3fd20b31 | ||
|
|
f84a6c0d3a | ||
|
|
5c9c17c649 | ||
|
|
a4b5a74644 | ||
|
|
ed1c270398 | ||
|
|
56d04761f8 | ||
|
|
d49b64dc7c | ||
|
|
42b43b372a | ||
|
|
4dae4b987e | ||
|
|
54e672e078 | ||
|
|
62a09a3cb0 | ||
|
|
b017f07343 | ||
|
|
92afae7185 | ||
|
|
eb9d656b8f | ||
|
|
7ddf63faa8 | ||
|
|
8a310efc97 | ||
|
|
bdf150bdf8 | ||
|
|
75323fda01 | ||
|
|
f94f628884 | ||
|
|
e132c457f2 | ||
|
|
048d6f6942 | ||
|
|
6c47353205 | ||
|
|
4f067085e7 | ||
|
|
3fcbb10155 | ||
|
|
f27b754570 | ||
|
|
95387b4bf0 | ||
|
|
7f2d26ec20 | ||
|
|
d950de2d09 | ||
|
|
5d8a368ad2 | ||
|
|
615a5fa5ba | ||
|
|
823cdf211d | ||
|
|
20c21a4dc0 | ||
|
|
040873a442 | ||
|
|
a39bc626c7 | ||
|
|
bd3503a3d8 | ||
|
|
161c1fb7a4 | ||
|
|
4d506d2d9c | ||
|
|
04abc6b5e5 | ||
|
|
2c0501b05f | ||
|
|
7b484de0a9 | ||
|
|
a83331bd2f | ||
|
|
4ccdedeb12 | ||
|
|
65061cc459 | ||
|
|
fe0739aadf | ||
|
|
0dad1957c8 | ||
|
|
bf39ae6db0 | ||
|
|
09bcff13ad | ||
|
|
b91fb6b280 | ||
|
|
0fec34e886 | ||
|
|
b6d57ff8a5 | ||
|
|
7ce97ce0c2 | ||
|
|
a5ed9fdc67 | ||
|
|
5e5850d89a | ||
|
|
56aa86f77e | ||
|
|
f321725b49 | ||
|
|
4e3697284e | ||
|
|
4c1cada222 | ||
|
|
8bb19eece4 | ||
|
|
e10ed89be5 | ||
|
|
c455e7390e | ||
|
|
3f9fbb7b75 | ||
|
|
e21baec246 | ||
|
|
a2bf53ff0a | ||
|
|
06cd48feaa | ||
|
|
825ccd3422 | ||
|
|
c56da286f7 | ||
|
|
a7577d428f | ||
|
|
e81b85b241 | ||
|
|
0eb76f4d2c | ||
|
|
24013ddf27 | ||
|
|
e8ee5f8c9b | ||
|
|
0fb5f75e93 | ||
|
|
4847e24553 | ||
|
|
f548f0b231 | ||
|
|
82386b18aa | ||
|
|
bbbcb456ce | ||
|
|
4c78f9b9cb | ||
|
|
37a03f10ab | ||
|
|
c2dd37a89e | ||
|
|
f458faa819 | ||
|
|
30f6b53240 | ||
|
|
7ee82813fd | ||
|
|
c88a277ad9 | ||
|
|
9e73d083b5 | ||
|
|
03c8d92796 | ||
|
|
31ad9a6346 | ||
|
|
fc40a3c376 | ||
|
|
b8e3db0179 | ||
|
|
366bb8269c | ||
|
|
3975627b33 | ||
|
|
f706811757 | ||
|
|
17e415d03a | ||
|
|
8911bad792 | ||
|
|
a30c481ca1 | ||
|
|
5a0bf7a5e1 | ||
|
|
f135954944 | ||
|
|
c32ad9711f | ||
|
|
f6842041c9 | ||
|
|
4dcd502b76 | ||
|
|
19f81c9204 | ||
|
|
67b6b3779d | ||
|
|
c836016358 | ||
|
|
84a2596ce5 | ||
|
|
98bc7b41fc | ||
|
|
abffbcad99 | ||
|
|
bc77704462 | ||
|
|
5abb2d510d | ||
|
|
ae040024af | ||
|
|
fe1247dd4b | ||
|
|
84789daf59 | ||
|
|
cecd177286 | ||
|
|
c26ed80d67 | ||
|
|
15f0c28cda | ||
|
|
ba9b63af49 | ||
|
|
9c81f448c8 | ||
|
|
50f9521feb | ||
|
|
0ff32ac554 | ||
|
|
fe72ea6d6b | ||
|
|
62959e5a70 | ||
|
|
6c8e8384d5 | ||
|
|
0c8004530a | ||
|
|
b18afa8bdd | ||
|
|
f54933e657 | ||
|
|
a1862020e8 | ||
|
|
249d6b3619 | ||
|
|
a80e0d9d39 | ||
|
|
6b3bb45900 | ||
|
|
f43fac50ed | ||
|
|
d0e511fd2f | ||
|
|
d50538dbf7 | ||
|
|
cd5a836685 | ||
|
|
4bc76be130 | ||
|
|
f4cdb49126 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.80.0"
|
||||
__version__ = "15.82.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -116,7 +116,7 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
@@ -238,7 +238,7 @@
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-18 17:24:57.044666",
|
||||
"modified": "2025-09-26 17:06:29.207673",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -36,7 +36,7 @@ class BankTransaction(Document):
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
payment_entries: DF.Table[BankTransactionPayments]
|
||||
reference_number: DF.Data | None
|
||||
reference_number: DF.SmallText | None
|
||||
status: DF.Literal["", "Pending", "Settled", "Unreconciled", "Reconciled", "Cancelled"]
|
||||
transaction_id: DF.Data | None
|
||||
transaction_type: DF.Data | None
|
||||
|
||||
@@ -253,7 +253,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
if not self.cost_center or self.is_cancelled:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
|
||||
@@ -189,8 +189,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
self.make_gl_entries()
|
||||
self.check_credit_limit()
|
||||
self.update_asset_value()
|
||||
self.update_inter_company_jv()
|
||||
self.update_invoice_discounting()
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.utils import flt, nowdate
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||
from erpnext.exceptions import InvalidAccountCurrency
|
||||
from erpnext.selling.doctype.customer.test_customer import make_customer, set_credit_limit
|
||||
|
||||
|
||||
class TestJournalEntry(unittest.TestCase):
|
||||
@@ -592,6 +593,15 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.assertEqual(jv.pay_to_recd_from, "_Test Receiver 2")
|
||||
|
||||
def test_credit_limit_for_customer(self):
|
||||
customer = make_customer("_Test New Customer")
|
||||
set_credit_limit("_Test New Customer", "_Test Company", 50)
|
||||
jv = make_journal_entry(account1="Debtors - _TC", account2="_Test Cash - _TC", amount=100, save=False)
|
||||
jv.accounts[0].party_type = "Customer"
|
||||
jv.accounts[0].party = customer
|
||||
jv.save()
|
||||
self.assertRaises(frappe.ValidationError, jv.submit)
|
||||
|
||||
|
||||
def make_journal_entry(
|
||||
account1,
|
||||
|
||||
@@ -601,6 +601,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.events.paid_amount(frm);
|
||||
}
|
||||
frm.events.paid_from_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
@@ -624,6 +625,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.events.received_amount(frm);
|
||||
}
|
||||
}
|
||||
frm.events.paid_to_account_currency(frm);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -127,7 +127,13 @@ class PaymentRequest(Document):
|
||||
|
||||
existing_payment_request_amount = flt(get_existing_payment_request_amount(ref_doc))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
if (
|
||||
flt(
|
||||
existing_payment_request_amount + flt(self.grand_total, self.precision("grand_total")),
|
||||
get_currency_precision(),
|
||||
)
|
||||
> ref_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("Total Payment Request amount cannot be greater than {0} amount").format(
|
||||
self.reference_doctype
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
"description",
|
||||
"section_break_4",
|
||||
"due_date",
|
||||
"invoice_portion",
|
||||
"mode_of_payment",
|
||||
"column_break_5",
|
||||
"invoice_portion",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"section_break_6",
|
||||
"discount_type",
|
||||
"discount_date",
|
||||
"column_break_9",
|
||||
"discount",
|
||||
"discount_type",
|
||||
"column_break_9",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
"section_break_9",
|
||||
"payment_amount",
|
||||
"outstanding",
|
||||
@@ -172,12 +177,50 @@
|
||||
"label": "Paid Amount (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "due_date_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Due Date Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
|
||||
"fieldname": "credit_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Days",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
|
||||
"fieldname": "credit_months",
|
||||
"fieldtype": "Int",
|
||||
"label": "Credit Months",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount",
|
||||
"fieldname": "discount_validity_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Discount Validity Based On",
|
||||
"options": "\nDay(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "discount_validity_based_on",
|
||||
"fieldname": "discount_validity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Discount Validity",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-11 11:06:51.792982",
|
||||
"modified": "2025-07-31 08:38:25.820701",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Schedule",
|
||||
@@ -189,4 +232,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,27 @@ class PaymentSchedule(Document):
|
||||
base_outstanding: DF.Currency
|
||||
base_paid_amount: DF.Currency
|
||||
base_payment_amount: DF.Currency
|
||||
credit_days: DF.Int
|
||||
credit_months: DF.Int
|
||||
description: DF.SmallText | None
|
||||
discount: DF.Float
|
||||
discount_date: DF.Date | None
|
||||
discount_type: DF.Literal["Percentage", "Amount"]
|
||||
discount_validity: DF.Int
|
||||
discount_validity_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
discounted_amount: DF.Currency
|
||||
due_date: DF.Date
|
||||
due_date_based_on: DF.Literal[
|
||||
"",
|
||||
"Day(s) after invoice date",
|
||||
"Day(s) after the end of the invoice month",
|
||||
"Month(s) after the end of the invoice month",
|
||||
]
|
||||
invoice_portion: DF.Percent
|
||||
mode_of_payment: DF.Link | None
|
||||
outstanding: DF.Currency
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2150,19 +2150,16 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(rate, 500)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
@@ -2188,7 +2185,6 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
|
||||
@@ -483,18 +483,23 @@ class Subscription(Document):
|
||||
|
||||
return invoice
|
||||
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: bool | None = None) -> list[dict]:
|
||||
def get_items_from_plans(self, plans: list[dict[str, str]], prorate: int = 0) -> list[dict]:
|
||||
"""
|
||||
Returns the `Item`s linked to `Subscription Plan`
|
||||
"""
|
||||
if prorate is None:
|
||||
prorate = False
|
||||
|
||||
prorate_factor = 1
|
||||
if prorate:
|
||||
prorate_factor = get_prorata_factor(
|
||||
self.current_invoice_end,
|
||||
self.current_invoice_start,
|
||||
cint(self.generate_invoice_at == "Beginning of the current subscription period"),
|
||||
cint(
|
||||
self.generate_invoice_at
|
||||
in [
|
||||
"Beginning of the current subscription period",
|
||||
"Days before the current subscription period",
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
items = []
|
||||
@@ -511,33 +516,19 @@ class Subscription(Document):
|
||||
|
||||
deferred = frappe.db.get_value("Item", item_code, deferred_field)
|
||||
|
||||
if not prorate:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
else:
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
item = {
|
||||
"item_code": item_code,
|
||||
"qty": plan.qty,
|
||||
"rate": get_plan_rate(
|
||||
plan.plan,
|
||||
plan.qty,
|
||||
party,
|
||||
self.current_invoice_start,
|
||||
self.current_invoice_end,
|
||||
prorate_factor,
|
||||
),
|
||||
"cost_center": plan_doc.cost_center,
|
||||
}
|
||||
|
||||
if deferred:
|
||||
item.update(
|
||||
|
||||
@@ -8,6 +8,7 @@ from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_to_date,
|
||||
add_years,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
@@ -555,6 +556,33 @@ class TestSubscription(FrappeTestCase):
|
||||
subscription.reload()
|
||||
self.assertEqual(len(subscription.invoices), 0)
|
||||
|
||||
def test_invoice_generation_days_before_subscription_period_with_prorate(self):
|
||||
settings = frappe.get_single("Subscription Settings")
|
||||
settings.prorate = 1
|
||||
settings.save()
|
||||
|
||||
create_plan(
|
||||
plan_name="_Test Plan Name 5",
|
||||
cost=1000,
|
||||
billing_interval="Year",
|
||||
billing_interval_count=1,
|
||||
currency="INR",
|
||||
)
|
||||
|
||||
start_date = add_days(nowdate(), 2)
|
||||
|
||||
subscription = create_subscription(
|
||||
start_date=start_date,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier",
|
||||
generate_invoice_at="Days before the current subscription period",
|
||||
generate_new_invoices_past_due_date=1,
|
||||
number_of_days=2,
|
||||
plans=[{"plan": "_Test Plan Name 5", "qty": 1}],
|
||||
)
|
||||
subscription.process(nowdate())
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
|
||||
def make_plans():
|
||||
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")
|
||||
|
||||
@@ -1270,7 +1270,7 @@ class ReceivablePayableReport:
|
||||
def setup_ageing_columns(self):
|
||||
# for charts
|
||||
self.ageing_column_labels = []
|
||||
ranges = [*self.ranges, "Above"]
|
||||
ranges = [*self.ranges, _("Above")]
|
||||
|
||||
prev_range_value = 0
|
||||
for idx, curr_range_value in enumerate(ranges):
|
||||
|
||||
@@ -171,7 +171,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(_("Difference"), fieldname="diff")
|
||||
|
||||
self.setup_ageing_columns()
|
||||
self.add_column(label="Total Amount Due", fieldname="total_due")
|
||||
self.add_column(label=_("Total Amount Due"), fieldname="total_due")
|
||||
|
||||
if self.filters.show_future_payments:
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
|
||||
@@ -5,28 +5,33 @@ frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statement
|
||||
|
||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
},
|
||||
],
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
let heading_ratios = ["Liquidity Ratios", "Solvency Ratios", "Turnover Ratios"];
|
||||
let heading_ratios = [__("Liquidity Ratios"), __("Solvency Ratios"), __("Turnover Ratios")];
|
||||
|
||||
if (heading_ratios.includes(value)) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
@@ -60,7 +60,7 @@ frappe.query_reports["Financial Ratios"] = {
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
}
|
||||
|
||||
if (heading_ratios.includes(row[1].content) && column.fieldtype == "Float") {
|
||||
if (heading_ratios.includes(row[1]?.content) && column.fieldtype == "Float") {
|
||||
column.fieldtype = "Data";
|
||||
}
|
||||
|
||||
|
||||
@@ -147,9 +147,9 @@ def get_gl_data(filters, period_list, years):
|
||||
|
||||
def add_liquidity_ratios(data, years, current_asset, current_liability, quick_asset):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Liquidity Ratios"})
|
||||
data.append({"ratio": _("Liquidity Ratios")})
|
||||
|
||||
ratio_data = [["Current Ratio", current_asset], ["Quick Ratio", quick_asset]]
|
||||
ratio_data = [[_("Current Ratio"), current_asset], [_("Quick Ratio"), quick_asset]]
|
||||
|
||||
for d in ratio_data:
|
||||
row = {
|
||||
@@ -165,13 +165,13 @@ def add_solvency_ratios(
|
||||
data, years, total_asset, total_liability, net_sales, cogs, total_income, total_expense
|
||||
):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Solvency Ratios"})
|
||||
data.append({"ratio": _("Solvency Ratios")})
|
||||
|
||||
debt_equity_ratio = {"ratio": "Debt Equity Ratio"}
|
||||
gross_profit_ratio = {"ratio": "Gross Profit Ratio"}
|
||||
net_profit_ratio = {"ratio": "Net Profit Ratio"}
|
||||
return_on_asset_ratio = {"ratio": "Return on Asset Ratio"}
|
||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||
debt_equity_ratio = {"ratio": _("Debt Equity Ratio")}
|
||||
gross_profit_ratio = {"ratio": _("Gross Profit Ratio")}
|
||||
net_profit_ratio = {"ratio": _("Net Profit Ratio")}
|
||||
return_on_asset_ratio = {"ratio": _("Return on Asset Ratio")}
|
||||
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
@@ -195,7 +195,7 @@ def add_solvency_ratios(
|
||||
|
||||
def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sales, cogs, direct_expense):
|
||||
precision = frappe.db.get_single_value("System Settings", "float_precision")
|
||||
data.append({"ratio": "Turnover Ratios"})
|
||||
data.append({"ratio": _("Turnover Ratios")})
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
@@ -208,10 +208,10 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
)
|
||||
|
||||
ratio_data = [
|
||||
["Fixed Asset Turnover Ratio", net_sales, total_asset],
|
||||
["Debtor Turnover Ratio", net_sales, avg_debtors],
|
||||
["Creditor Turnover Ratio", direct_expense, avg_creditors],
|
||||
["Inventory Turnover Ratio", cogs, avg_stock],
|
||||
[_("Fixed Asset Turnover Ratio"), net_sales, total_asset],
|
||||
[_("Debtor Turnover Ratio"), net_sales, avg_debtors],
|
||||
[_("Creditor Turnover Ratio"), direct_expense, avg_creditors],
|
||||
[_("Inventory Turnover Ratio"), cogs, avg_stock],
|
||||
]
|
||||
for ratio in ratio_data:
|
||||
row = {
|
||||
|
||||
@@ -212,7 +212,7 @@ def get_data(
|
||||
company_currency,
|
||||
accumulated_values=filters.accumulated_values,
|
||||
)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map, filters.show_zero_values)
|
||||
|
||||
if out and total:
|
||||
add_total_row(out, root_type, balance_must_be, period_list, company_currency)
|
||||
@@ -323,18 +323,24 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency, accum
|
||||
|
||||
|
||||
def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False):
|
||||
def get_all_parents(account, parent_children_map):
|
||||
for parent, children in parent_children_map.items():
|
||||
for child in children:
|
||||
if child["name"] == account and parent:
|
||||
accounts_to_show.add(parent)
|
||||
get_all_parents(parent, parent_children_map)
|
||||
|
||||
data_with_value = []
|
||||
accounts_to_show = set()
|
||||
|
||||
for d in data:
|
||||
if show_zero_values or d.get("has_value"):
|
||||
accounts_to_show.add(d.get("account"))
|
||||
get_all_parents(d.get("account"), parent_children_map)
|
||||
|
||||
for d in data:
|
||||
if d.get("account") in accounts_to_show:
|
||||
data_with_value.append(d)
|
||||
else:
|
||||
# show group with zero balance, if there are balances against child
|
||||
children = [child.name for child in parent_children_map.get(d.get("account")) or []]
|
||||
if children:
|
||||
for row in data:
|
||||
if row.get("account") in children and row.get("has_value"):
|
||||
data_with_value.append(d)
|
||||
break
|
||||
|
||||
return data_with_value
|
||||
|
||||
|
||||
@@ -178,7 +178,12 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
|
||||
# to display item as Item Code: Item Name
|
||||
columns[0] = "Sales Invoice:Link/Item:300"
|
||||
# removing Item Code and Item Name columns
|
||||
del columns[4:6]
|
||||
supplier_master_name = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
customer_master_name = frappe.db.get_single_value("Selling Settings", "cust_master_name")
|
||||
if supplier_master_name == "Supplier Name" and customer_master_name == "Customer Name":
|
||||
del columns[4:6]
|
||||
else:
|
||||
del columns[5:7]
|
||||
|
||||
total_base_amount = 0
|
||||
total_buying_amount = 0
|
||||
@@ -275,7 +280,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"width": 100,
|
||||
"width": 120,
|
||||
},
|
||||
"posting_time": {
|
||||
"label": _("Posting Time"),
|
||||
@@ -677,7 +682,9 @@ class GrossProfitGenerator:
|
||||
si.name = si_item.parent
|
||||
and si.docstatus = 1
|
||||
and si.is_return = 1
|
||||
and si.posting_date between %(from_date)s and %(to_date)s
|
||||
""",
|
||||
{"from_date": self.filters.from_date, "to_date": self.filters.to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import add_days, flt, get_first_day, get_last_day, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note, make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
@@ -392,7 +392,6 @@ class TestGrossProfit(FrappeTestCase):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
@@ -635,3 +634,42 @@ class TestGrossProfit(FrappeTestCase):
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
def test_profit_for_later_period_return(self):
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
# create sales invoice on month start date
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, do_not_save=True, do_not_submit=True)
|
||||
sinv.set_posting_time = 1
|
||||
sinv.posting_date = month_start_date
|
||||
sinv.save().submit()
|
||||
|
||||
# create credit note on next month start date
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note.set_posting_time = 1
|
||||
cr_note.posting_date = add_days(month_end_date, 1)
|
||||
cr_note.save().submit()
|
||||
|
||||
# apply filters for invoiced period
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=month_start_date, to_date=month_end_date, group_by="Invoice"
|
||||
)
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 100.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 100.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 100.0)
|
||||
|
||||
# extend filters upto returned period
|
||||
filters.update(to_date=add_days(month_end_date, 1))
|
||||
|
||||
_, data = execute(filters=filters)
|
||||
total = data[-1]
|
||||
|
||||
self.assertEqual(total.selling_amount, 0.0)
|
||||
self.assertEqual(total.buying_amount, 0.0)
|
||||
self.assertEqual(total.gross_profit, 0.0)
|
||||
self.assertEqual(total.get("gross_profit_%"), 0.0)
|
||||
|
||||
@@ -5,29 +5,34 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financi
|
||||
|
||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
});
|
||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
{
|
||||
fieldname: "selected_view",
|
||||
label: __("Select View"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "Report", label: __("Report View") },
|
||||
{ value: "Growth", label: __("Growth View") },
|
||||
{ value: "Margin", label: __("Margin View") },
|
||||
],
|
||||
default: "Report",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
label: __("Include Default FB Entries"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_zero_values",
|
||||
label: __("Show zero values"),
|
||||
fieldtype: "Check",
|
||||
}
|
||||
);
|
||||
|
||||
@@ -216,7 +216,7 @@ def get_opening_balance(
|
||||
ignore_is_opening=0,
|
||||
):
|
||||
closing_balance = frappe.qb.DocType(doctype)
|
||||
account = frappe.qb.DocType("Account")
|
||||
accounts = frappe.db.get_all("Account", filters={"report_type": report_type}, pluck="name")
|
||||
|
||||
opening_balance = (
|
||||
frappe.qb.from_(closing_balance)
|
||||
@@ -228,14 +228,7 @@ def get_opening_balance(
|
||||
Sum(closing_balance.debit_in_account_currency).as_("debit_in_account_currency"),
|
||||
Sum(closing_balance.credit_in_account_currency).as_("credit_in_account_currency"),
|
||||
)
|
||||
.where(
|
||||
(closing_balance.company == filters.company)
|
||||
& (
|
||||
closing_balance.account.isin(
|
||||
frappe.qb.from_(account).select("name").where(account.report_type == report_type)
|
||||
)
|
||||
)
|
||||
)
|
||||
.where((closing_balance.company == filters.company) & (closing_balance.account.isin(accounts)))
|
||||
.groupby(closing_balance.account)
|
||||
)
|
||||
|
||||
@@ -290,21 +283,24 @@ def get_opening_balance(
|
||||
if filters.project:
|
||||
opening_balance = opening_balance.where(closing_balance.project == filters.project)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
if frappe.db.count("Finance Book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
company_fb = frappe.get_cached_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(_("To use a different finance book, please uncheck 'Include Default FB Entries'"))
|
||||
if filters.finance_book and company_fb and cstr(filters.finance_book) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default FB Entries'")
|
||||
)
|
||||
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), cstr(company_fb), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
else:
|
||||
opening_balance = opening_balance.where(
|
||||
(closing_balance.finance_book.isin([cstr(filters.finance_book), ""]))
|
||||
| (closing_balance.finance_book.isnull())
|
||||
)
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
|
||||
@@ -947,19 +947,28 @@ def update_accounting_ledgers_after_reference_removal(
|
||||
adv_ple.run()
|
||||
|
||||
|
||||
def remove_ref_from_advance_section(ref_doc: object = None):
|
||||
def remove_ref_from_advance_section(ref_doc: object = None, payment_name: str | None = None):
|
||||
# TODO: this might need some testing
|
||||
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
ref_doc.set("advances", [])
|
||||
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
|
||||
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
|
||||
row_names = []
|
||||
for adv in ref_doc.get("advances") or []:
|
||||
if adv.get("reference_name", None) == payment_name:
|
||||
row_names.append(adv.name)
|
||||
|
||||
if not row_names:
|
||||
return
|
||||
|
||||
child_table = (
|
||||
"Sales Invoice Advance" if ref_doc.doctype == "Sales Invoice" else "Purchase Invoice Advance"
|
||||
)
|
||||
frappe.db.delete(child_table, {"name": ("in", row_names)})
|
||||
|
||||
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str | None = None):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_from_advance_section(ref_doc)
|
||||
remove_ref_from_advance_section(ref_doc, payment_name)
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_jv(
|
||||
@@ -1026,7 +1035,6 @@ def remove_ref_doc_link_from_pe(
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
|
||||
@@ -790,17 +790,33 @@ frappe.ui.form.on("Asset Finance Book", {
|
||||
});
|
||||
|
||||
erpnext.asset.scrap_asset = function (frm) {
|
||||
frappe.confirm(__("Do you really want to scrap this asset?"), function () {
|
||||
frappe.call({
|
||||
args: {
|
||||
asset_name: frm.doc.name,
|
||||
var scrap_dialog = new frappe.ui.Dialog({
|
||||
title: __("Enter date to scrap asset"),
|
||||
fields: [
|
||||
{
|
||||
label: __("Select the date"),
|
||||
fieldname: "scrap_date",
|
||||
fieldtype: "Date",
|
||||
reqd: 1,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.depreciation.scrap_asset",
|
||||
callback: function (r) {
|
||||
cur_frm.reload_doc();
|
||||
},
|
||||
});
|
||||
],
|
||||
size: "medium",
|
||||
primary_action_label: "Submit",
|
||||
primary_action(values) {
|
||||
frappe.call({
|
||||
args: {
|
||||
asset_name: frm.doc.name,
|
||||
scrap_date: values.scrap_date,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.depreciation.scrap_asset",
|
||||
callback: function (r) {
|
||||
frm.reload_doc();
|
||||
scrap_dialog.hide();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
scrap_dialog.show();
|
||||
};
|
||||
|
||||
erpnext.asset.restore_asset = function (frm) {
|
||||
|
||||
@@ -394,7 +394,7 @@ def get_comma_separated_links(names, doctype):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def scrap_asset(asset_name):
|
||||
def scrap_asset(asset_name, scrap_date=None):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
if asset.docstatus != 1:
|
||||
@@ -402,7 +402,11 @@ def scrap_asset(asset_name):
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized"):
|
||||
frappe.throw(_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status))
|
||||
|
||||
date = today()
|
||||
today_date = getdate(today())
|
||||
date = getdate(scrap_date) or today_date
|
||||
purchase_date = getdate(asset.purchase_date)
|
||||
|
||||
validate_scrap_date(date, today_date, purchase_date, asset.calculate_depreciation, asset_name)
|
||||
|
||||
notes = _("This schedule was created when Asset {0} was scrapped.").format(
|
||||
get_link_to_form(asset.doctype, asset.name)
|
||||
@@ -436,6 +440,36 @@ def scrap_asset(asset_name):
|
||||
frappe.msgprint(_("Asset scrapped via Journal Entry {0}").format(je.name))
|
||||
|
||||
|
||||
def validate_scrap_date(scrap_date, today_date, purchase_date, calculate_depreciation, asset_name):
|
||||
if scrap_date > today_date:
|
||||
frappe.throw(_("Future date is not allowed"))
|
||||
elif scrap_date < purchase_date:
|
||||
frappe.throw(_("Scrap date cannot be before purchase date"))
|
||||
|
||||
if calculate_depreciation:
|
||||
asset_depreciation_schedules = frappe.db.get_all(
|
||||
"Asset Depreciation Schedule", filters={"asset": asset_name, "docstatus": 1}, fields=["name"]
|
||||
)
|
||||
|
||||
for depreciation_schedule in asset_depreciation_schedules:
|
||||
last_booked_depreciation_date = frappe.db.get_value(
|
||||
"Depreciation Schedule",
|
||||
{
|
||||
"parent": depreciation_schedule["name"],
|
||||
"docstatus": 1,
|
||||
"journal_entry": ["!=", ""],
|
||||
},
|
||||
"schedule_date",
|
||||
order_by="schedule_date desc",
|
||||
)
|
||||
if (
|
||||
last_booked_depreciation_date
|
||||
and scrap_date < last_booked_depreciation_date
|
||||
and scrap_date > purchase_date
|
||||
):
|
||||
frappe.throw(_("Asset cannot be scrapped before the last depreciation entry."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def restore_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@@ -15,6 +15,7 @@ from frappe.utils import (
|
||||
is_last_day_of_the_month,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils.data import add_to_date
|
||||
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -219,6 +220,31 @@ class TestAsset(AssetSetup):
|
||||
)
|
||||
self.assertEqual(accumulated_depr_amount, 18000.0)
|
||||
|
||||
asset_depreciation = frappe.db.get_value(
|
||||
"Asset Depreciation Schedule", {"asset": asset.name, "docstatus": 1}, "name"
|
||||
)
|
||||
last_booked_depreciation_date = frappe.db.get_value(
|
||||
"Depreciation Schedule",
|
||||
{
|
||||
"parent": asset_depreciation,
|
||||
"docstatus": 1,
|
||||
"journal_entry": ["!=", ""],
|
||||
},
|
||||
"schedule_date",
|
||||
order_by="schedule_date desc",
|
||||
)
|
||||
|
||||
before_purchase_date = add_to_date(asset.purchase_date, days=-1)
|
||||
future_date = add_to_date(nowdate(), days=1)
|
||||
if last_booked_depreciation_date:
|
||||
before_last_booked_depreciation_date = add_to_date(last_booked_depreciation_date, days=-1)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_purchase_date)
|
||||
self.assertRaises(frappe.ValidationError, scrap_asset, asset.name, scrap_date=future_date)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, scrap_asset, asset.name, scrap_date=before_last_booked_depreciation_date
|
||||
)
|
||||
|
||||
scrap_asset(asset.name)
|
||||
asset.load_from_db()
|
||||
first_asset_depr_schedule.load_from_db()
|
||||
|
||||
@@ -142,10 +142,18 @@ class AssetMovement(Document):
|
||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||
asset = frappe.get_doc("Asset", asset_id)
|
||||
|
||||
updates = {}
|
||||
if employee and employee != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", employee)
|
||||
updates["custodian"] = employee
|
||||
|
||||
elif not employee and asset.custodian:
|
||||
updates["custodian"] = ""
|
||||
|
||||
if location and location != asset.location:
|
||||
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||
updates["location"] = location
|
||||
|
||||
if updates:
|
||||
frappe.db.set_value("Asset", asset_id, updates)
|
||||
|
||||
def log_asset_activity(self, asset_id, location, employee):
|
||||
if location and employee:
|
||||
|
||||
@@ -268,6 +268,7 @@ def get_asset_depreciation_amount_map(filters, finance_book):
|
||||
.where(gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account))
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.is_opening == "No")
|
||||
.where(company.name == filters.company)
|
||||
.where(asset.docstatus == 1)
|
||||
)
|
||||
|
||||
@@ -541,12 +541,8 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||
self.assertRaises(frappe.ValidationError, pi.submit)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_make_purchase_invoice_with_terms(self):
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
|
||||
@@ -570,7 +566,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
|
||||
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
|
||||
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
from erpnext.stock.utils import InvalidWarehouseCompany
|
||||
@@ -718,6 +713,7 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(due_date, "2023-03-31")
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 0})
|
||||
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
@@ -905,18 +901,16 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
bo.load_from_db()
|
||||
self.assertEqual(bo.items[0].ordered_qty, 5)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -930,8 +924,6 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_internal_transfer_flow(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
|
||||
@@ -109,21 +109,9 @@ frappe.ui.form.on("Supplier", {
|
||||
__("View")
|
||||
);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Bank Account"),
|
||||
function () {
|
||||
erpnext.utils.make_bank_account(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
frm.add_custom_button(__("Bank Account"), () => frm.make_methods["Bank Account"](), __("Create"));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Pricing Rule"),
|
||||
function () {
|
||||
erpnext.utils.make_pricing_rule(frm.doc.doctype, frm.doc.name);
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
frm.add_custom_button(__("Pricing Rule"), () => frm.make_methods["Pricing Rule"](), __("Create"));
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Get Supplier Group Details"),
|
||||
|
||||
@@ -231,6 +231,11 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_date_with_fiscal_year()
|
||||
self.validate_party_accounts()
|
||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_inter_company_reference()
|
||||
# validate inter company transaction rate
|
||||
@@ -282,11 +287,6 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.set_advance_gain_or_loss()
|
||||
|
||||
if self.is_return:
|
||||
self.validate_qty()
|
||||
else:
|
||||
self.validate_deferred_start_and_end_date()
|
||||
|
||||
self.validate_deferred_income_expense_account()
|
||||
self.set_inter_company_account()
|
||||
|
||||
@@ -2558,6 +2558,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.payment_schedule = []
|
||||
self.payment_terms_template = po_or_so.payment_terms_template
|
||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
||||
|
||||
for schedule in po_or_so.payment_schedule:
|
||||
payment_schedule = {
|
||||
@@ -2570,6 +2571,17 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
|
||||
if automatically_fetch_payment_terms:
|
||||
if schedule.due_date_based_on:
|
||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||
|
||||
if schedule.discount_validity_based_on:
|
||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||
|
||||
payment_schedule["payment_amount"] = flt(
|
||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||
schedule.precision("payment_amount"),
|
||||
@@ -3369,14 +3381,27 @@ def get_payment_term_details(
|
||||
term = frappe.get_doc("Payment Term", term)
|
||||
else:
|
||||
term_details.payment_term = term.payment_term
|
||||
term_details.description = term.description
|
||||
term_details.invoice_portion = term.invoice_portion
|
||||
|
||||
fields_to_copy = [
|
||||
"description",
|
||||
"invoice_portion",
|
||||
"discount_type",
|
||||
"discount",
|
||||
"mode_of_payment",
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity_based_on",
|
||||
"discount_validity",
|
||||
]
|
||||
|
||||
for field in fields_to_copy:
|
||||
term_details[field] = term.get(field)
|
||||
|
||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||
term_details.discount_type = term.discount_type
|
||||
term_details.discount = term.discount
|
||||
term_details.outstanding = term_details.payment_amount
|
||||
term_details.mode_of_payment = term.mode_of_payment
|
||||
term_details.base_outstanding = term_details.base_payment_amount
|
||||
|
||||
if bill_date:
|
||||
term_details.due_date = get_due_date(term, bill_date)
|
||||
@@ -3395,11 +3420,11 @@ def get_due_date(term, posting_date=None, bill_date=None):
|
||||
due_date = None
|
||||
date = bill_date or posting_date
|
||||
if term.due_date_based_on == "Day(s) after invoice date":
|
||||
due_date = add_days(date, term.credit_days)
|
||||
due_date = add_days(date, cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||
due_date = add_days(get_last_day(date), term.credit_days)
|
||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||
due_date = get_last_day(add_months(date, term.credit_months))
|
||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||
return due_date
|
||||
|
||||
|
||||
@@ -3407,11 +3432,11 @@ def get_discount_date(term, posting_date=None, bill_date=None):
|
||||
discount_validity = None
|
||||
date = bill_date or posting_date
|
||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||
discount_validity = add_days(date, term.discount_validity)
|
||||
discount_validity = add_days(date, cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||
discount_validity = add_days(get_last_day(date), term.discount_validity)
|
||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||
discount_validity = get_last_day(add_months(date, term.discount_validity))
|
||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||
return discount_validity
|
||||
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters:
|
||||
if filters.get("customer"):
|
||||
qb_filter_and_conditions.append(
|
||||
(proj.customer == filters.get("customer")) | proj.customer.isnull() | proj.customer == ""
|
||||
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
|
||||
@@ -200,7 +200,11 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
current_stock_qty = args.get(column)
|
||||
elif args.get("return_qty_from_rejected_warehouse"):
|
||||
reference_qty = ref.get("rejected_qty") * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
current_stock_qty = (
|
||||
args.get(column) * args.get("conversion_factor", 1.0)
|
||||
if column != "stock_qty"
|
||||
else args.get(column)
|
||||
)
|
||||
else:
|
||||
reference_qty = ref.get(column) * ref.get("conversion_factor", 1.0)
|
||||
current_stock_qty = args.get(column) * args.get("conversion_factor", 1.0)
|
||||
@@ -845,13 +849,14 @@ def available_serial_batch_for_return(field, doctype, reference_ids, is_rejected
|
||||
|
||||
def get_available_serial_batches(field, doctype, reference_ids, is_rejected=False):
|
||||
_bundle_ids = get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=is_rejected)
|
||||
|
||||
if not _bundle_ids:
|
||||
return frappe._dict({})
|
||||
|
||||
return get_serial_batches_based_on_bundle(field, _bundle_ids)
|
||||
return get_serial_batches_based_on_bundle(doctype, field, _bundle_ids)
|
||||
|
||||
|
||||
def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
||||
available_dict = frappe._dict({})
|
||||
batch_serial_nos = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
@@ -863,6 +868,7 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
"`tabSerial and Batch Bundle`.`voucher_detail_no`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_type`",
|
||||
"`tabSerial and Batch Bundle`.`voucher_no`",
|
||||
"`tabSerial and Batch Bundle`.`item_code`",
|
||||
],
|
||||
filters=[
|
||||
["Serial and Batch Bundle", "name", "in", _bundle_ids],
|
||||
@@ -876,6 +882,16 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
if frappe.get_cached_value(row.voucher_type, row.voucher_no, "is_return"):
|
||||
key = frappe.get_cached_value(row.voucher_type + " Item", row.voucher_detail_no, field)
|
||||
|
||||
if doctype == "Packed Item":
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
if row.voucher_type == "Delivery Note":
|
||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||
elif row.voucher_type == "Sales Invoice":
|
||||
key = frappe.get_cached_value("Sales Invoice Item", key, "sales_invoice_item")
|
||||
|
||||
key = (row.item_code, key)
|
||||
|
||||
if row.voucher_type in ["Sales Invoice", "Delivery Note"]:
|
||||
row.qty = -1 * row.qty
|
||||
|
||||
@@ -904,6 +920,8 @@ def get_serial_batches_based_on_bundle(field, _bundle_ids):
|
||||
|
||||
def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False):
|
||||
filters = {"docstatus": 1, "name": ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
if doctype == "Packed Item":
|
||||
filters = get_filters_for_packed_item(field, reference_ids)
|
||||
|
||||
pluck_field = "serial_and_batch_bundle"
|
||||
if is_rejected:
|
||||
@@ -917,10 +935,14 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
pluck=pluck_field,
|
||||
)
|
||||
|
||||
if _bundle_ids and doctype == "Packed Item":
|
||||
return _bundle_ids
|
||||
|
||||
if not _bundle_ids:
|
||||
return {}
|
||||
|
||||
del filters["name"]
|
||||
if "name" in filters:
|
||||
del filters["name"]
|
||||
|
||||
filters[field] = ("in", reference_ids)
|
||||
|
||||
@@ -963,10 +985,29 @@ def get_serial_and_batch_bundle(field, doctype, reference_ids, is_rejected=False
|
||||
return _bundle_ids
|
||||
|
||||
|
||||
def get_filters_for_packed_item(field, reference_ids):
|
||||
names = []
|
||||
filters = {"docstatus": 1, "dn_detail": ("in", reference_ids)}
|
||||
if dns := frappe.get_all("Delivery Note Item", filters=filters, pluck="name"):
|
||||
names.extend(dns)
|
||||
|
||||
filters = {"docstatus": 1, "sales_invoice_item": ("in", reference_ids)}
|
||||
if sis := frappe.get_all("Sales Invoice Item", filters=filters, pluck="name"):
|
||||
names.extend(sis)
|
||||
|
||||
if names:
|
||||
reference_ids.extend(names)
|
||||
|
||||
return {"docstatus": 1, field: ("in", reference_ids), "serial_and_batch_bundle": ("is", "set")}
|
||||
|
||||
|
||||
def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field=None):
|
||||
if not qty_field:
|
||||
qty_field = "stock_qty"
|
||||
|
||||
if not hasattr(row, qty_field):
|
||||
qty_field = "qty"
|
||||
|
||||
if not warehouse_field:
|
||||
warehouse_field = "warehouse"
|
||||
|
||||
@@ -1056,6 +1097,9 @@ def make_serial_batch_bundle_for_return(data, child_doc, parent_doc, warehouse_f
|
||||
if not qty_field:
|
||||
qty_field = "stock_qty"
|
||||
|
||||
if not hasattr(child_doc, qty_field):
|
||||
qty_field = "qty"
|
||||
|
||||
warehouse = child_doc.get(warehouse_field)
|
||||
if parent_doc.get("is_internal_customer"):
|
||||
warehouse = child_doc.get("target_warehouse")
|
||||
|
||||
@@ -517,8 +517,15 @@ class SellingController(StockController):
|
||||
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
|
||||
continue
|
||||
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", d.item_code, ["has_serial_no", "has_batch_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not self.get("return_against") or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
get_valuation_method(d.item_code) == "Moving Average"
|
||||
and self.get("is_return")
|
||||
and not item_details.has_serial_no
|
||||
and not item_details.has_batch_no
|
||||
):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
@@ -997,6 +1004,9 @@ def set_default_income_account_for_item(obj):
|
||||
def get_serial_and_batch_bundle(child, parent, delivery_note_child=None):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if parent.get("is_return") and parent.get("packed_items"):
|
||||
return
|
||||
|
||||
if child.get("use_serial_batch_fields"):
|
||||
return
|
||||
|
||||
|
||||
@@ -335,10 +335,20 @@ class StockController(AccountsController):
|
||||
return
|
||||
|
||||
child_doctype = self.doctype + " Item"
|
||||
if table_name == "packed_items":
|
||||
field = "parent_detail_docname"
|
||||
child_doctype = "Packed Item"
|
||||
|
||||
available_dict = available_serial_batch_for_return(field, child_doctype, reference_ids)
|
||||
|
||||
for row in self.get(table_name):
|
||||
if data := available_dict.get(row.get(field)):
|
||||
value = row.get(field)
|
||||
if table_name == "packed_items" and row.get("parent_detail_docname"):
|
||||
value = self.get_value_for_packed_item(row)
|
||||
if not value:
|
||||
continue
|
||||
|
||||
if data := available_dict.get(value):
|
||||
data = filter_serial_batches(self, data, row)
|
||||
bundle = make_serial_batch_bundle_for_return(data, row, self)
|
||||
row.db_set(
|
||||
@@ -354,6 +364,14 @@ class StockController(AccountsController):
|
||||
"incoming_rate", frappe.db.get_value("Serial and Batch Bundle", bundle, "avg_rate")
|
||||
)
|
||||
|
||||
def get_value_for_packed_item(self, row):
|
||||
parent_items = self.get("items", {"name": row.parent_detail_docname})
|
||||
if parent_items:
|
||||
ref = parent_items[0].get("dn_detail")
|
||||
return (row.item_code, ref)
|
||||
|
||||
return None
|
||||
|
||||
def get_reference_ids(self, table_name, qty_field=None, bundle_field=None) -> tuple[str, list[str]]:
|
||||
field = {
|
||||
"Sales Invoice": "sales_invoice_item",
|
||||
@@ -388,6 +406,12 @@ class StockController(AccountsController):
|
||||
):
|
||||
reference_ids.append(row.get(field))
|
||||
|
||||
if table_name == "packed_items" and row.get("parent_detail_docname"):
|
||||
parent_rows = self.get("items", {"name": row.parent_detail_docname}) or []
|
||||
for d in parent_rows:
|
||||
if d.get(field) and not d.get(bundle_field):
|
||||
reference_ids.append(d.get(field))
|
||||
|
||||
return field, reference_ids
|
||||
|
||||
@frappe.request_cache
|
||||
|
||||
@@ -12,6 +12,8 @@ from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
get_available_serial_nos,
|
||||
get_voucher_wise_serial_batch_from_bundle,
|
||||
)
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -52,9 +54,42 @@ class SubcontractingController(StockController):
|
||||
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
|
||||
self.validate_items()
|
||||
self.create_raw_materials_supplied()
|
||||
self.set_valuation_rate_for_rm()
|
||||
else:
|
||||
super().validate()
|
||||
|
||||
def set_valuation_rate_for_rm(self):
|
||||
rate_changed = False
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
for row in self.supplied_items:
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": row.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": flt(row.consumed_qty) * (-1 if not self.is_return else 1),
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"serial_and_batch_bundle": row.serial_and_batch_bundle,
|
||||
"voucher_detail_no": row.name,
|
||||
"batch_no": row.batch_no,
|
||||
"serial_no": row.serial_no,
|
||||
"use_serial_batch_fields": row.use_serial_batch_fields,
|
||||
}
|
||||
)
|
||||
|
||||
rate = get_incoming_rate(kwargs)
|
||||
precision = frappe.get_precision("Subcontracting Receipt Supplied Item", "rate")
|
||||
if flt(rate, precision) != flt(row.rate, precision):
|
||||
row.rate = rate
|
||||
row.amount = flt(row.consumed_qty) * flt(rate)
|
||||
rate_changed = True
|
||||
|
||||
if rate_changed:
|
||||
self.calculate_items_qty_and_amount()
|
||||
|
||||
def validate_rejected_warehouse(self):
|
||||
for item in self.get("items"):
|
||||
if flt(item.rejected_qty) and not item.rejected_warehouse:
|
||||
@@ -166,6 +201,9 @@ class SubcontractingController(StockController):
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
if not self.get(self.raw_material_table):
|
||||
return
|
||||
|
||||
item_dict = self.__get_data_before_save()
|
||||
if not item_dict:
|
||||
return True
|
||||
@@ -610,6 +648,67 @@ class SubcontractingController(StockController):
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
self.set_batch_for_supplied_items()
|
||||
|
||||
def set_batch_for_supplied_items(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
|
||||
from erpnext.stock.get_item_details import get_filtered_serial_nos
|
||||
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
for row in self.supplied_items:
|
||||
item_details = frappe.get_cached_value(
|
||||
"Item", row.rm_item_code, ["has_batch_no", "has_serial_no"], as_dict=1
|
||||
)
|
||||
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
continue
|
||||
|
||||
if not row.use_serial_batch_fields:
|
||||
continue
|
||||
|
||||
kwargs = frappe._dict(
|
||||
{
|
||||
"item_code": row.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": flt(row.consumed_qty),
|
||||
}
|
||||
)
|
||||
|
||||
if item_details.has_serial_no and not row.serial_and_batch_bundle and not row.serial_no:
|
||||
serial_nos = get_available_serial_nos(kwargs)
|
||||
if serial_nos:
|
||||
serial_nos = [sn.get("serial_no") for sn in serial_nos]
|
||||
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
|
||||
row.serial_no = "\n".join(serial_nos)
|
||||
|
||||
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
if batches:
|
||||
consumed_qty = row.consumed_qty
|
||||
for index, d in enumerate(batches):
|
||||
if consumed_qty <= 0:
|
||||
break
|
||||
|
||||
if index == 0:
|
||||
row.batch_no = d.get("batch_no")
|
||||
row.consumed_qty = d.get("qty")
|
||||
consumed_qty -= d.get("qty")
|
||||
else:
|
||||
new_row = self.append("supplied_items", {})
|
||||
new_row.update(frappe.copy_doc(row).as_dict())
|
||||
new_row.update(
|
||||
{
|
||||
"consumed_qty": d.get("qty"),
|
||||
"batch_no": d.get("batch_no"),
|
||||
"rate": row.rate,
|
||||
"amount": flt(d.get("qty")) * flt(row.rate),
|
||||
}
|
||||
)
|
||||
consumed_qty -= d.get("qty")
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
|
||||
@@ -1308,6 +1308,7 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
"Subcontracted Item SA9": {"stock_uom": "Litre"},
|
||||
"Subcontracted Item SA10": {},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
@@ -1329,6 +1330,7 @@ def make_raw_materials():
|
||||
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
|
||||
"Subcontracted SRM Item 8": {},
|
||||
"Subcontracted SRM Item 9": {"stock_uom": "Litre"},
|
||||
"Subcontracted SRM Item 10": {},
|
||||
}
|
||||
|
||||
for item, properties in raw_materials.items():
|
||||
@@ -1357,6 +1359,7 @@ def make_service_items():
|
||||
"Subcontracted Service Item 7": {},
|
||||
"Subcontracted Service Item 8": {},
|
||||
"Subcontracted Service Item 9": {},
|
||||
"Subcontracted Service Item 10": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
@@ -1381,6 +1384,7 @@ def make_bom_for_subcontracted_items():
|
||||
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
|
||||
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
|
||||
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
|
||||
"Subcontracted Item SA10": ["Subcontracted SRM Item 10"],
|
||||
}
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"code_list",
|
||||
"canonical_uri",
|
||||
"title",
|
||||
"common_code",
|
||||
"description",
|
||||
@@ -71,10 +72,17 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"max_height": "60px"
|
||||
},
|
||||
{
|
||||
"fetch_from": "code_list.canonical_uri",
|
||||
"fieldname": "canonical_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Canonical URI"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"links": [],
|
||||
"modified": "2024-11-06 07:46:17.175687",
|
||||
"modified": "2025-10-04 17:22:28.176155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "EDI",
|
||||
"name": "Common Code",
|
||||
@@ -94,10 +102,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "common_code,description",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class CommonCode(Document):
|
||||
|
||||
additional_data: DF.Code | None
|
||||
applies_to: DF.Table[DynamicLink]
|
||||
canonical_uri: DF.Data | None
|
||||
code_list: DF.Link
|
||||
common_code: DF.Data
|
||||
description: DF.SmallText | None
|
||||
|
||||
@@ -253,6 +253,13 @@ class BOMCreator(Document):
|
||||
if not row.fg_reference_id and production_item_wise_rm.get((row.fg_item, row.fg_reference_id)):
|
||||
frappe.throw(_("Please set Parent Row No for item {0}").format(row.fg_item))
|
||||
|
||||
key = (row.fg_item, row.fg_reference_id)
|
||||
if key not in production_item_wise_rm:
|
||||
production_item_wise_rm.setdefault(
|
||||
key,
|
||||
frappe._dict({"items": [], "bom_no": "", "fg_item_data": row}),
|
||||
)
|
||||
|
||||
production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
|
||||
|
||||
reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
|
||||
|
||||
@@ -748,6 +748,7 @@ class ProductionPlan(Document):
|
||||
work_order_data = {
|
||||
"wip_warehouse": default_warehouses.get("wip_warehouse"),
|
||||
"fg_warehouse": default_warehouses.get("fg_warehouse"),
|
||||
"scrap_warehouse": default_warehouses.get("scrap_warehouse"),
|
||||
"company": self.get("company"),
|
||||
}
|
||||
|
||||
@@ -1631,7 +1632,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
}
|
||||
)
|
||||
|
||||
sales_order = doc.get("sales_order")
|
||||
sales_order = data.get("sales_order")
|
||||
|
||||
for item_code, details in item_details.items():
|
||||
so_item_details.setdefault(sales_order, frappe._dict())
|
||||
@@ -1821,7 +1822,7 @@ def get_sub_assembly_items(
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
for field in ["wip_warehouse", "fg_warehouse"]:
|
||||
for field in ["wip_warehouse", "fg_warehouse", "scrap_warehouse"]:
|
||||
if not row.get(field):
|
||||
row[field] = default_warehouses.get(field)
|
||||
|
||||
|
||||
@@ -2828,6 +2828,111 @@ class TestWorkOrder(FrappeTestCase):
|
||||
wo.operations[3].planned_start_time, add_to_date(wo.operations[1].planned_end_time, minutes=10)
|
||||
)
|
||||
|
||||
def test_req_qty_clamping_in_manufacture_entry(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
fg_item = "Test Unconsumed RM FG Item"
|
||||
rm_item_1 = "Test Unconsumed RM Item 1"
|
||||
rm_item_2 = "Test Unconsumed RM Item 2"
|
||||
|
||||
source_warehouse = "_Test Warehouse - _TC"
|
||||
wip_warehouse = "Stores - _TC"
|
||||
fg_warehouse = create_warehouse("_Test Finished Goods Warehouse", company="_Test Company")
|
||||
|
||||
make_item(fg_item, {"is_stock_item": 1})
|
||||
make_item(rm_item_1, {"is_stock_item": 1})
|
||||
make_item(rm_item_2, {"is_stock_item": 1})
|
||||
|
||||
# create a BOM: 1 FG = 1 RM1 + 1 RM2
|
||||
bom = make_bom(
|
||||
item=fg_item,
|
||||
source_warehouse=source_warehouse,
|
||||
raw_materials=[rm_item_1, rm_item_2],
|
||||
operating_cost_per_bom_quantity=1,
|
||||
do_not_submit=True,
|
||||
)
|
||||
|
||||
for row in bom.exploded_items:
|
||||
make_stock_entry_test_record(
|
||||
item_code=row.item_code,
|
||||
target=source_warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo = make_wo_order_test_record(
|
||||
item=fg_item,
|
||||
qty=50,
|
||||
source_warehouse=source_warehouse,
|
||||
wip_warehouse=wip_warehouse,
|
||||
)
|
||||
wo.submit()
|
||||
|
||||
# first partial transfer & manufacture (6 units)
|
||||
se_transfer_1 = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", 6, wip_warehouse)
|
||||
)
|
||||
se_transfer_1.insert()
|
||||
se_transfer_1.submit()
|
||||
|
||||
stock_entry_1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 6, fg_warehouse))
|
||||
|
||||
# remove rm_2 from the items to simulate unconsumed RM scenario
|
||||
stock_entry_1.items = [row for row in stock_entry_1.items if row.item_code != rm_item_2]
|
||||
stock_entry_1.save()
|
||||
stock_entry_1.submit()
|
||||
|
||||
wo.reload()
|
||||
|
||||
se_transfer_2 = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", 20, wip_warehouse)
|
||||
)
|
||||
se_transfer_2.insert()
|
||||
se_transfer_2.submit()
|
||||
|
||||
stock_entry_2 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 20, fg_warehouse))
|
||||
|
||||
# validate rm_item_2 quantity is clamped correctly (per-unit BOM = 1 → max 20)
|
||||
for row in stock_entry_2.items:
|
||||
if row.item_code == rm_item_2:
|
||||
self.assertLessEqual(row.qty, 20)
|
||||
self.assertGreaterEqual(row.qty, 0)
|
||||
|
||||
def test_overproduction_allowed_qty(self):
|
||||
"""Test overproduction allowed qty in work order"""
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 50)
|
||||
|
||||
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=10)
|
||||
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item", target="Stores - _TC", qty=100, basic_rate=100
|
||||
)
|
||||
test_stock_entry.make_stock_entry(
|
||||
item_code="_Test Item Home Desktop 100",
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=100,
|
||||
basic_rate=1000.0,
|
||||
)
|
||||
|
||||
mt_stock_entry = frappe.get_doc(
|
||||
make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 10)
|
||||
)
|
||||
mt_stock_entry.submit()
|
||||
|
||||
fg_stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||
fg_stock_entry.items[2].qty = 15
|
||||
fg_stock_entry.fg_completed_qty = 15
|
||||
fg_stock_entry.submit()
|
||||
|
||||
wo_order.reload()
|
||||
|
||||
self.assertEqual(wo_order.produced_qty, 15)
|
||||
self.assertEqual(wo_order.status, "Completed")
|
||||
|
||||
allow_overproduction("overproduction_percentage_for_work_order", 0)
|
||||
|
||||
|
||||
def make_stock_in_entries_and_get_batches(rm_item, source_warehouse, wip_warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
|
||||
@@ -263,6 +263,7 @@ execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Deta
|
||||
erpnext.patches.v14_0.update_proprietorship_to_individual
|
||||
erpnext.patches.v15_0.rename_subcontracting_fields
|
||||
erpnext.patches.v15_0.unset_incorrect_additional_discount_percentage
|
||||
erpnext.patches.v16_0.create_company_custom_fields
|
||||
|
||||
[post_model_sync]
|
||||
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
|
||||
|
||||
@@ -9,5 +9,9 @@ def execute():
|
||||
docs = frappe.get_all(
|
||||
"Material Request", filters={"buying_price_list": ["is", "not set"], "docstatus": 1}, pluck="name"
|
||||
)
|
||||
for doc in docs:
|
||||
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
try:
|
||||
for doc in docs:
|
||||
frappe.db.set_value("Material Request", doc, "buying_price_list", default_buying_price_list)
|
||||
finally:
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
6
erpnext/patches/v16_0/create_company_custom_fields.py
Normal file
6
erpnext/patches/v16_0/create_company_custom_fields.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from erpnext.setup.install import create_custom_company_links
|
||||
|
||||
|
||||
def execute():
|
||||
"""Add link fields to Company in Email Account and Communication."""
|
||||
create_custom_company_links()
|
||||
@@ -173,8 +173,11 @@ erpnext.buying = {
|
||||
callback: (r) => {
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
|
||||
if(!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
if (!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) return;
|
||||
this.frm.set_value(
|
||||
"shipping_address",
|
||||
r.message.shipping_address || this.frm.doc.shipping_address || ""
|
||||
);
|
||||
},
|
||||
});
|
||||
erpnext.utils.set_letter_head(this.frm)
|
||||
|
||||
@@ -76,9 +76,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
|
||||
// Update paid amount on return/debit note creation
|
||||
if (
|
||||
this.frm.doc.doctype === "Purchase Invoice"
|
||||
&& this.frm.doc.is_return
|
||||
&& (this.frm.doc.grand_total > this.frm.doc.paid_amount)
|
||||
this.frm.doc.doctype === "Purchase Invoice" &&
|
||||
this.frm.doc.is_return &&
|
||||
this.frm.doc.grand_total < 0 &&
|
||||
this.frm.doc.grand_total > this.frm.doc.paid_amount
|
||||
) {
|
||||
this.frm.doc.paid_amount = flt(this.frm.doc.grand_total, precision("grand_total"));
|
||||
}
|
||||
|
||||
@@ -1082,12 +1082,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
due_date(doc, cdt) {
|
||||
discount_date(doc, cdt, cdn) {
|
||||
// Remove fields as discount_date is auto-managed by payment terms
|
||||
const row = locals[cdt][cdn];
|
||||
["discount_validity", "discount_validity_based_on"].forEach((field) => {
|
||||
row[field] = "";
|
||||
});
|
||||
this.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
|
||||
due_date(doc, cdt, cdn) {
|
||||
// due_date is to be changed, payment terms template and/or payment schedule must
|
||||
// be removed as due_date is automatically changed based on payment terms
|
||||
if (doc.doctype !== cdt) {
|
||||
// triggered by change to the due_date field in payment schedule child table
|
||||
// do nothing to avoid infinite clearing loop
|
||||
// Remove fields as due_date is auto-managed by payment terms
|
||||
const row = locals[cdt][cdn];
|
||||
["due_date_based_on", "credit_days", "credit_months"].forEach((field) => {
|
||||
row[field] = "";
|
||||
});
|
||||
this.frm.refresh_field("payment_schedule");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2137,16 +2150,22 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
this.frm.doc.name).options,
|
||||
"master_name": this.frm.doc.taxes_and_charges
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(me.frm.doc.shipping_rule && me.frm.doc.taxes) {
|
||||
for (let tax of r.message) {
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (me.frm.doc?.cost_center && !tax.cost_center) {
|
||||
tax.cost_center = me.frm.doc.cost_center;
|
||||
}
|
||||
});
|
||||
if (me.frm.doc.shipping_rule && me.frm.doc.taxes) {
|
||||
for (let tax of taxes) {
|
||||
me.frm.add_child("taxes", tax);
|
||||
}
|
||||
|
||||
refresh_field("taxes");
|
||||
} else {
|
||||
me.frm.set_value("taxes", r.message);
|
||||
me.frm.set_value("taxes", taxes);
|
||||
me.calculate_taxes_and_totals();
|
||||
}
|
||||
}
|
||||
@@ -2573,6 +2592,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
|
||||
'item_group': item.item_group,
|
||||
"base_net_rate": item.base_net_rate,
|
||||
"disabled": 0,
|
||||
}
|
||||
|
||||
if (doc.tax_category)
|
||||
@@ -2614,6 +2634,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
payment_term(doc, cdt, cdn) {
|
||||
const me = this;
|
||||
var row = locals[cdt][cdn];
|
||||
// empty date condition fields
|
||||
[
|
||||
"due_date_based_on",
|
||||
"credit_days",
|
||||
"credit_months",
|
||||
"discount_validity",
|
||||
"discount_validity_based_on",
|
||||
].forEach(function (field) {
|
||||
row[field] = "";
|
||||
});
|
||||
|
||||
if(row.payment_term) {
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.accounts_controller.get_payment_term_details",
|
||||
@@ -2626,14 +2657,17 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message && !r.exc) {
|
||||
for (var d in r.message) {
|
||||
frappe.model.set_value(cdt, cdn, d, r.message[d]);
|
||||
const company_currency = me.get_company_currency();
|
||||
me.update_payment_schedule_grid_labels(company_currency);
|
||||
const company_currency = me.get_company_currency();
|
||||
for (let d in r.message) {
|
||||
row[d] = r.message[d];
|
||||
}
|
||||
me.update_payment_schedule_grid_labels(company_currency)
|
||||
me.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
} else {
|
||||
me.frm.refresh_field("payment_schedule");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -443,3 +443,14 @@ def create_internal_customer(customer_name=None, represents_company=None, allowe
|
||||
customer_name = frappe.db.get_value("Customer", customer_name)
|
||||
|
||||
return customer_name
|
||||
|
||||
|
||||
def make_customer(customer_name):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
customer.customer_type = "Individual"
|
||||
customer.insert()
|
||||
return customer.name
|
||||
else:
|
||||
return customer_name
|
||||
|
||||
@@ -10,7 +10,7 @@ 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 InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, get_due_date, update_child_qty_rate
|
||||
from erpnext.maintenance.doctype.maintenance_schedule.test_maintenance_schedule import (
|
||||
make_maintenance_schedule,
|
||||
)
|
||||
@@ -1680,14 +1680,13 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
so.load_from_db()
|
||||
self.assertRaises(frappe.LinkExistsError, so.cancel)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
so.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1701,8 +1700,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
|
||||
compare_payment_schedules(self, so, si)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_zero_amount_sales_order_billing_status(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
@@ -2421,16 +2418,14 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(si2.items[0].qty, 20)
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
accounts_settings.automatically_fetch_payment_terms = enable
|
||||
accounts_settings.save()
|
||||
|
||||
|
||||
def compare_payment_schedules(doc, doc1, doc2):
|
||||
for index, schedule in enumerate(doc1.get("payment_schedule")):
|
||||
posting_date = doc1.get("bill_date") or doc1.get("posting_date") or doc1.get("transaction_date")
|
||||
due_date = schedule.due_date
|
||||
if schedule.due_date_based_on:
|
||||
due_date = get_due_date(schedule, posting_date=posting_date)
|
||||
doc.assertEqual(schedule.payment_term, doc2.payment_schedule[index].payment_term)
|
||||
doc.assertEqual(getdate(schedule.due_date), doc2.payment_schedule[index].due_date)
|
||||
doc.assertEqual(due_date, doc2.payment_schedule[index].due_date)
|
||||
doc.assertEqual(schedule.invoice_portion, doc2.payment_schedule[index].invoice_portion)
|
||||
doc.assertEqual(schedule.payment_amount, doc2.payment_schedule[index].payment_amount)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"role_to_override_stop_action",
|
||||
"column_break_15",
|
||||
"maintain_same_sales_rate",
|
||||
"fallback_to_default_price_list",
|
||||
"editable_price_list_rate",
|
||||
"validate_selling_price",
|
||||
"editable_bundle_item_rates",
|
||||
@@ -216,6 +217,12 @@
|
||||
"fieldname": "allow_zero_qty_in_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Quotation with Zero Quantity"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "fallback_to_default_price_list",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Prices from Default Price List as Fallback"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -224,7 +231,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-06 15:23:14.332971",
|
||||
"modified": "2025-09-23 21:10:14.826653",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
@@ -33,6 +34,7 @@ class SellingSettings(Document):
|
||||
editable_bundle_item_rates: DF.Check
|
||||
editable_price_list_rate: DF.Check
|
||||
enable_discount_accounting: DF.Check
|
||||
fallback_to_default_price_list: DF.Check
|
||||
hide_tax_id: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
maintain_same_sales_rate: DF.Check
|
||||
@@ -69,16 +71,35 @@ class SellingSettings(Document):
|
||||
hide_name_field=False,
|
||||
)
|
||||
|
||||
self.validate_fallback_to_default_price_list()
|
||||
|
||||
def validate_fallback_to_default_price_list(self):
|
||||
if (
|
||||
self.fallback_to_default_price_list
|
||||
and self.has_value_changed("fallback_to_default_price_list")
|
||||
and frappe.get_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing")
|
||||
):
|
||||
stock_meta = frappe.get_meta("Stock Settings")
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted into the transaction price list."
|
||||
).format(
|
||||
"<i>{}</i>".format(_(self.meta.get_label("fallback_to_default_price_list"))),
|
||||
"<i>{}</i>".format(_(stock_meta.get_label("auto_insert_price_list_rate_if_missing"))),
|
||||
frappe.bold(_("Stock Settings")),
|
||||
)
|
||||
)
|
||||
|
||||
def toggle_hide_tax_id(self):
|
||||
self.hide_tax_id = cint(self.hide_tax_id)
|
||||
_hide_tax_id = cint(self.hide_tax_id)
|
||||
|
||||
# Make property setters to hide tax_id fields
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Delivery Note"):
|
||||
make_property_setter(
|
||||
doctype, "tax_id", "hidden", self.hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
doctype, "tax_id", "hidden", _hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "tax_id", "print_hide", self.hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
doctype, "tax_id", "print_hide", _hide_tax_id, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
def toggle_editable_rate_for_bundle_items(self):
|
||||
|
||||
@@ -25,6 +25,7 @@ def after_install():
|
||||
|
||||
set_single_defaults()
|
||||
create_print_setting_custom_fields()
|
||||
create_custom_company_links()
|
||||
add_all_roles_to("Administrator")
|
||||
create_default_success_action()
|
||||
create_default_energy_point_rules()
|
||||
@@ -132,6 +133,39 @@ def create_print_setting_custom_fields():
|
||||
)
|
||||
|
||||
|
||||
def create_custom_company_links():
|
||||
"""Add link fields to Company in Email Account and Communication.
|
||||
|
||||
These DocTypes are provided by the Frappe Framework but need to be associated
|
||||
with a company in ERPNext to allow for multitenancy. I.e. one company should
|
||||
not be able to access emails and communications from another company.
|
||||
"""
|
||||
create_custom_fields(
|
||||
{
|
||||
"Email Account": [
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"insert_after": "email_id",
|
||||
},
|
||||
],
|
||||
"Communication": [
|
||||
{
|
||||
"label": _("Company"),
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"insert_after": "email_account",
|
||||
"fetch_from": "email_account.company",
|
||||
"read_only": 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def create_default_success_action():
|
||||
for success_action in get_default_success_action():
|
||||
if not frappe.db.exists("Success Action", success_action.get("ref_doctype")):
|
||||
|
||||
@@ -7,15 +7,16 @@ from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import flt, nowtime
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from pypika import Order
|
||||
from pypika.functions import Coalesce
|
||||
|
||||
|
||||
class DeprecatedSerialNoValuation:
|
||||
@deprecated
|
||||
def calculate_stock_value_from_deprecarated_ledgers(self):
|
||||
if not has_sle_for_serial_nos(self.sle.item_code):
|
||||
return
|
||||
serial_nos = []
|
||||
if hasattr(self, "old_serial_nos"):
|
||||
serial_nos = self.old_serial_nos
|
||||
|
||||
serial_nos = self.get_filterd_serial_nos()
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
@@ -25,17 +26,6 @@ class DeprecatedSerialNoValuation:
|
||||
|
||||
self.stock_value_change += flt(stock_value_change)
|
||||
|
||||
def get_filterd_serial_nos(self):
|
||||
serial_nos = []
|
||||
non_filtered_serial_nos = self.get_serial_nos()
|
||||
|
||||
# If the serial no inwarded using the Serial and Batch Bundle, then the serial no should not be considered
|
||||
for serial_no in non_filtered_serial_nos:
|
||||
if serial_no and serial_no not in self.serial_no_incoming_rate:
|
||||
serial_nos.append(serial_no)
|
||||
|
||||
return serial_nos
|
||||
|
||||
@deprecated
|
||||
def get_incoming_value_for_serial_nos(self, serial_nos):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
@@ -81,20 +71,6 @@ class DeprecatedSerialNoValuation:
|
||||
return incoming_values
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def has_sle_for_serial_nos(item_code):
|
||||
serial_nos = frappe.db.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["name"],
|
||||
filters={"serial_no": ("is", "set"), "is_cancelled": 0, "item_code": item_code},
|
||||
limit=1,
|
||||
)
|
||||
if serial_nos:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class DeprecatedBatchNoValuation:
|
||||
@deprecated
|
||||
def calculate_avg_rate_from_deprecarated_ledgers(self):
|
||||
@@ -197,9 +173,15 @@ class DeprecatedBatchNoValuation:
|
||||
|
||||
@deprecated
|
||||
def set_balance_value_for_non_batchwise_valuation_batches(self):
|
||||
self.last_sle = self.get_last_sle_for_non_batch()
|
||||
if hasattr(self, "prev_sle"):
|
||||
self.last_sle = self.prev_sle
|
||||
else:
|
||||
self.last_sle = self.get_last_sle_for_non_batch()
|
||||
|
||||
if self.last_sle and self.last_sle.stock_queue:
|
||||
self.stock_queue = json.loads(self.last_sle.stock_queue or "[]") or []
|
||||
self.stock_queue = self.last_sle.stock_queue
|
||||
if isinstance(self.stock_queue, str):
|
||||
self.stock_queue = json.loads(self.stock_queue) or []
|
||||
|
||||
self.set_balance_value_from_sl_entries()
|
||||
self.set_balance_value_from_bundle()
|
||||
@@ -293,10 +275,7 @@ class DeprecatedBatchNoValuation:
|
||||
query = query.where(sle.name != self.sle.name)
|
||||
|
||||
if self.sle.serial_and_batch_bundle:
|
||||
query = query.where(
|
||||
(sle.serial_and_batch_bundle != self.sle.serial_and_batch_bundle)
|
||||
| (sle.serial_and_batch_bundle.isnull())
|
||||
)
|
||||
query = query.where(Coalesce(sle.serial_and_batch_bundle, "") != self.sle.serial_and_batch_bundle)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -22,6 +22,17 @@ frappe.ui.form.on("Batch", {
|
||||
frappe.set_route("query-report", "Stock Ledger");
|
||||
});
|
||||
frm.trigger("make_dashboard");
|
||||
|
||||
frm.add_custom_button(__("Recalculate Batch Qty"), () => {
|
||||
frm.call({
|
||||
method: "recalculate_batch_qty",
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frm.reload_doc();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
item: (frm) => {
|
||||
|
||||
@@ -156,6 +156,17 @@ class Batch(Document):
|
||||
if frappe.db.get_value("Item", self.item, "has_batch_no") == 0:
|
||||
frappe.throw(_("The selected item cannot have Batch"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def recalculate_batch_qty(self):
|
||||
batches = get_batch_qty(batch_no=self.name, item_code=self.item, for_stock_levels=True)
|
||||
batch_qty = 0.0
|
||||
if batches:
|
||||
for row in batches:
|
||||
batch_qty += row.get("qty")
|
||||
|
||||
self.db_set("batch_qty", batch_qty)
|
||||
frappe.msgprint(_("Batch Qty updated to {0}").format(batch_qty), alert=True)
|
||||
|
||||
def set_batchwise_valuation(self):
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from erpnext.accounts.utils import get_balance_on
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
create_dn_against_so,
|
||||
make_sales_order,
|
||||
@@ -1300,14 +1299,13 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
frappe.db.set_single_value("Accounts Settings", "delete_linked_ledger_entries", 0)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_sales_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
so.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1327,8 +1325,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(so.payment_terms_template, si.payment_terms_template)
|
||||
compare_payment_schedules(self, so, si)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_returned_qty_in_return_dn(self):
|
||||
# SO ---> SI ---> DN
|
||||
# |
|
||||
@@ -2600,6 +2596,129 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn.per_billed, 100)
|
||||
self.assertEqual(dn.per_returned, 100)
|
||||
|
||||
def test_sales_return_for_product_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_return
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
rm_items = []
|
||||
for item_code, properties in {
|
||||
"_Packed Service Item": {"is_stock_item": 0},
|
||||
"_Packed FG Item New 1": {
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-PACKED-1-.#####",
|
||||
},
|
||||
"_Packed FG Item New 2": {
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-PACKED-2-.#####",
|
||||
},
|
||||
"_Packed FG Item New 3": {
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-PACKED-3-.#####",
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-PACKED-3-.#####",
|
||||
},
|
||||
}.items():
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
make_item(item_code, properties)
|
||||
|
||||
if item_code != "_Packed Service Item":
|
||||
rm_items.append(item_code)
|
||||
|
||||
for rate in [100, 200]:
|
||||
make_stock_entry(item=item_code, target="_Test Warehouse - _TC", qty=5, rate=rate)
|
||||
|
||||
make_product_bundle("_Packed Service Item", rm_items)
|
||||
dn = create_delivery_note(
|
||||
item_code="_Packed Service Item",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=5,
|
||||
)
|
||||
|
||||
dn.reload()
|
||||
|
||||
serial_batch_map = {}
|
||||
for row in dn.packed_items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
if row.item_code not in serial_batch_map:
|
||||
serial_batch_map[row.item_code] = frappe._dict(
|
||||
{
|
||||
"serial_nos": [],
|
||||
"batches": defaultdict(int),
|
||||
"serial_no_valuation": defaultdict(float),
|
||||
"batch_no_valuation": defaultdict(float),
|
||||
}
|
||||
)
|
||||
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
for entry in doc.entries:
|
||||
if entry.serial_no:
|
||||
serial_batch_map[row.item_code].serial_nos.append(entry.serial_no)
|
||||
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no] = entry.incoming_rate
|
||||
if entry.batch_no:
|
||||
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
|
||||
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no] = entry.incoming_rate
|
||||
|
||||
dn1 = make_sales_return(dn.name)
|
||||
dn1.items[0].qty = -2
|
||||
dn1.submit()
|
||||
dn1.reload()
|
||||
|
||||
for row in dn1.packed_items:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
for entry in doc.entries:
|
||||
if entry.serial_no:
|
||||
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
|
||||
self.assertEqual(
|
||||
entry.incoming_rate,
|
||||
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
|
||||
)
|
||||
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
|
||||
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
|
||||
|
||||
elif entry.batch_no:
|
||||
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
|
||||
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
|
||||
self.assertEqual(entry.qty, 2.0)
|
||||
self.assertEqual(
|
||||
entry.incoming_rate,
|
||||
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
|
||||
)
|
||||
|
||||
dn2 = make_sales_return(dn.name)
|
||||
dn2.items[0].qty = -3
|
||||
dn2.submit()
|
||||
dn2.reload()
|
||||
|
||||
for row in dn2.packed_items:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
for entry in doc.entries:
|
||||
if entry.serial_no:
|
||||
self.assertTrue(entry.serial_no in serial_batch_map[row.item_code].serial_nos)
|
||||
self.assertEqual(
|
||||
entry.incoming_rate,
|
||||
serial_batch_map[row.item_code].serial_no_valuation[entry.serial_no],
|
||||
)
|
||||
serial_batch_map[row.item_code].serial_nos.remove(entry.serial_no)
|
||||
serial_batch_map[row.item_code].serial_no_valuation.pop(entry.serial_no)
|
||||
|
||||
elif entry.batch_no:
|
||||
serial_batch_map[row.item_code].batches[entry.batch_no] += entry.qty
|
||||
self.assertEqual(serial_batch_map[row.item_code].batches[entry.batch_no], 0.0)
|
||||
|
||||
self.assertTrue(entry.batch_no in serial_batch_map[row.item_code].batches)
|
||||
|
||||
self.assertEqual(entry.qty, 3.0)
|
||||
self.assertEqual(
|
||||
entry.incoming_rate,
|
||||
serial_batch_map[row.item_code].batch_no_valuation[entry.batch_no],
|
||||
)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -140,7 +140,6 @@ frappe.ui.form.on("Pick List", {
|
||||
frm.trigger("add_get_items_button");
|
||||
if (frm.doc.docstatus === 1) {
|
||||
const status_completed = frm.doc.status === "Completed";
|
||||
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
|
||||
|
||||
if (!status_completed) {
|
||||
frm.add_custom_button(__("Update Current Stock"), () =>
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
"options": "Work Order"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "locations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Item Locations",
|
||||
@@ -247,7 +246,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-23 08:34:32.099673",
|
||||
"modified": "2025-10-03 18:36:52.282355",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
||||
@@ -74,8 +74,11 @@ class PickList(TransactionBase):
|
||||
if self.has_reserved_stock():
|
||||
self.set_onload("has_reserved_stock", True)
|
||||
|
||||
for item in self.get("locations"):
|
||||
item.update(get_item_details(item.item_code, item.uom, item.warehouse, self.company))
|
||||
if self.docstatus.is_draft() and not hasattr(self, "_action"):
|
||||
company = self.company
|
||||
|
||||
for item in self.get("locations"):
|
||||
item.update(get_item_details(item.item_code, item.uom, item.warehouse, company))
|
||||
|
||||
def validate(self):
|
||||
self.validate_expired_batches()
|
||||
@@ -1446,7 +1449,7 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, uom=None, warehouse=None, company=None):
|
||||
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
|
||||
details = frappe.db.get_value("Item", item_code, "stock_uom", as_dict=1)
|
||||
details.uom = uom or details.stock_uom
|
||||
if uom:
|
||||
details.update(get_conversion_factor(item_code, uom))
|
||||
|
||||
@@ -1180,6 +1180,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
|
||||
|
||||
@change_settings("Accounts Settings", {"automatically_fetch_payment_terms": 1})
|
||||
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import (
|
||||
create_payment_terms_template,
|
||||
@@ -1190,12 +1191,9 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
make_pr_against_po,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
compare_payment_schedules,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
|
||||
create_payment_terms_template()
|
||||
po.payment_terms_template = "Test Receivable Template"
|
||||
@@ -1213,8 +1211,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
|
||||
compare_payment_schedules(self, po, pi)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
@change_settings("Stock Settings", {"allow_negative_stock": 1})
|
||||
def test_neg_to_positive(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@@ -311,7 +311,7 @@ class SerialandBatchBundle(Document):
|
||||
def throw_error_message(self, message, exception=frappe.ValidationError):
|
||||
frappe.throw(_(message), exception, title=_("Error"))
|
||||
|
||||
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False):
|
||||
def set_incoming_rate(self, parent=None, row=None, save=False, allow_negative_stock=False, prev_sle=None):
|
||||
if self.type_of_transaction not in ["Inward", "Outward"] or self.voucher_type in [
|
||||
"Installation Note",
|
||||
"Job Card",
|
||||
@@ -321,15 +321,15 @@ class SerialandBatchBundle(Document):
|
||||
return
|
||||
|
||||
if return_against := self.get_return_against(parent=parent):
|
||||
self.set_valuation_rate_for_return_entry(return_against, row, save)
|
||||
self.set_valuation_rate_for_return_entry(return_against, row, save, prev_sle=prev_sle)
|
||||
elif self.type_of_transaction == "Outward":
|
||||
self.set_incoming_rate_for_outward_transaction(
|
||||
row, save, allow_negative_stock=allow_negative_stock
|
||||
)
|
||||
else:
|
||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
|
||||
|
||||
def set_valuation_rate_for_return_entry(self, return_against, row, save=False):
|
||||
def set_valuation_rate_for_return_entry(self, return_against, row, save=False, prev_sle=None):
|
||||
if valuation_details := self.get_valuation_rate_for_return_entry(return_against):
|
||||
for row in self.entries:
|
||||
if valuation_details:
|
||||
@@ -361,7 +361,7 @@ class SerialandBatchBundle(Document):
|
||||
)
|
||||
|
||||
elif self.type_of_transaction == "Inward":
|
||||
self.set_incoming_rate_for_inward_transaction(row, save)
|
||||
self.set_incoming_rate_for_inward_transaction(row, save, prev_sle=prev_sle)
|
||||
|
||||
def validate_returned_serial_batch_no(self, return_against, row, original_inv_details):
|
||||
if frappe.flags.through_repost_item_valuation:
|
||||
@@ -529,7 +529,11 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
if save:
|
||||
d.db_set(
|
||||
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
|
||||
{
|
||||
"incoming_rate": d.incoming_rate,
|
||||
"stock_value_difference": d.stock_value_difference,
|
||||
"stock_queue": d.get("stock_queue"),
|
||||
}
|
||||
)
|
||||
|
||||
def validate_negative_batch(self, batch_no, available_qty):
|
||||
@@ -606,7 +610,11 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
return return_against
|
||||
|
||||
def set_incoming_rate_for_inward_transaction(self, row=None, save=False):
|
||||
def set_incoming_rate_for_inward_transaction(self, row=None, save=False, prev_sle=None):
|
||||
from erpnext.stock.utils import get_valuation_method
|
||||
|
||||
valuation_method = get_valuation_method(self.item_code)
|
||||
|
||||
valuation_field = "valuation_rate"
|
||||
if self.voucher_type in ["Sales Invoice", "Delivery Note", "Quotation"]:
|
||||
valuation_field = "incoming_rate"
|
||||
@@ -630,19 +638,56 @@ class SerialandBatchBundle(Document):
|
||||
if not rate and self.voucher_detail_no and self.voucher_no:
|
||||
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
||||
|
||||
is_packed_item = False
|
||||
if rate is None and child_table in ["Delivery Note Item", "Sales Invoice Item"]:
|
||||
rate = frappe.db.get_value(
|
||||
"Packed Item",
|
||||
self.voucher_detail_no,
|
||||
"incoming_rate",
|
||||
)
|
||||
|
||||
if rate is not None:
|
||||
is_packed_item = True
|
||||
|
||||
stock_queue = []
|
||||
batches = []
|
||||
if prev_sle and prev_sle.stock_queue:
|
||||
batches = frappe.get_all(
|
||||
"Batch",
|
||||
filters={
|
||||
"name": ("in", [d.batch_no for d in self.entries if d.batch_no]),
|
||||
"use_batchwise_valuation": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if batches and valuation_method == "FIFO":
|
||||
stock_queue = parse_json(prev_sle.stock_queue)
|
||||
|
||||
for d in self.entries:
|
||||
if self.is_rejected:
|
||||
rate = 0.0
|
||||
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||
elif (d.incoming_rate == rate) and not stock_queue and d.qty and d.stock_value_difference:
|
||||
continue
|
||||
|
||||
if is_packed_item and d.incoming_rate:
|
||||
rate = d.incoming_rate
|
||||
|
||||
d.incoming_rate = flt(rate)
|
||||
if d.qty:
|
||||
d.stock_value_difference = flt(d.qty) * d.incoming_rate
|
||||
|
||||
if stock_queue and valuation_method == "FIFO" and d.batch_no in batches:
|
||||
stock_queue.append([d.qty, d.incoming_rate])
|
||||
d.stock_queue = json.dumps(stock_queue)
|
||||
|
||||
if save:
|
||||
d.db_set(
|
||||
{"incoming_rate": d.incoming_rate, "stock_value_difference": d.stock_value_difference}
|
||||
{
|
||||
"incoming_rate": d.incoming_rate,
|
||||
"stock_value_difference": d.stock_value_difference,
|
||||
"stock_queue": d.get("stock_queue"),
|
||||
}
|
||||
)
|
||||
|
||||
def set_serial_and_batch_values(self, parent, row, qty_field=None):
|
||||
|
||||
@@ -285,6 +285,25 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
|
||||
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=10,
|
||||
rate=100,
|
||||
batch_no=batch_id,
|
||||
use_serial_batch_fields=True,
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Serial and Batch Entry",
|
||||
{"parent": se.items[0].serial_and_batch_bundle, "docstatus": 1},
|
||||
["stock_value_difference", "stock_queue"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
|
||||
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
@@ -301,7 +320,7 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference), 1000.00)
|
||||
self.assertEqual(json.loads(sle.stock_queue), [[20, 200]])
|
||||
self.assertEqual(json.loads(sle.stock_queue), [[20, 200], [10, 100]])
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
@@ -319,6 +338,24 @@ class TestSerialandBatchBundle(FrappeTestCase):
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference), 5000.00 * -1)
|
||||
self.assertFalse(json.loads(sle.stock_queue or "[]"))
|
||||
self.assertEqual(flt(sle.stock_value), 1000.0)
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=batch_item_code,
|
||||
source="_Test Warehouse - _TC",
|
||||
qty=10,
|
||||
use_serial_batch_fields=False,
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"item_code": batch_item_code, "is_cancelled": 0, "voucher_no": se.name},
|
||||
["stock_value_difference", "stock_queue", "stock_value"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
self.assertEqual(flt(sle.stock_value_difference), 1000.00 * -1)
|
||||
self.assertFalse(json.loads(sle.stock_queue or "[]"))
|
||||
self.assertEqual(flt(sle.stock_value), 0.0)
|
||||
|
||||
def test_old_serial_no_valuation(self):
|
||||
|
||||
@@ -823,9 +823,28 @@ frappe.ui.form.on("Stock Entry", {
|
||||
refresh_field("process_loss_qty");
|
||||
}
|
||||
},
|
||||
|
||||
set_fg_completed_qty(frm) {
|
||||
let fg_completed_qty = 0;
|
||||
|
||||
frm.doc.items.forEach((item) => {
|
||||
if (item.is_finished_item) {
|
||||
fg_completed_qty += flt(item.qty);
|
||||
}
|
||||
});
|
||||
|
||||
frm.doc.fg_completed_qty = fg_completed_qty;
|
||||
frm.refresh_field("fg_completed_qty");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Stock Entry Detail", {
|
||||
items_add(frm, cdt, cdn) {
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.is_finished_item) {
|
||||
frm.events.set_fg_completed_qty(frm);
|
||||
}
|
||||
},
|
||||
set_basic_rate_manually(frm, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
frm.fields_dict.items.grid.update_docfield_property(
|
||||
@@ -837,6 +856,10 @@ frappe.ui.form.on("Stock Entry Detail", {
|
||||
|
||||
qty(frm, cdt, cdn) {
|
||||
frm.events.set_basic_rate(frm, cdt, cdn);
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.is_finished_item) {
|
||||
frm.events.set_fg_completed_qty(frm);
|
||||
}
|
||||
},
|
||||
|
||||
conversion_factor(frm, cdt, cdn) {
|
||||
|
||||
@@ -1356,12 +1356,6 @@ class StockEntry(StockController):
|
||||
d.item_code, self.work_order
|
||||
)
|
||||
)
|
||||
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
|
||||
frappe.throw(
|
||||
_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}").format(
|
||||
d.idx, d.transfer_qty, self.fg_completed_qty
|
||||
)
|
||||
)
|
||||
|
||||
finished_items.append(d.item_code)
|
||||
|
||||
@@ -2041,6 +2035,9 @@ class StockEntry(StockController):
|
||||
# in case of BOM
|
||||
to_warehouse = item.get("default_warehouse")
|
||||
|
||||
expense_account = item.get("expense_account")
|
||||
if not expense_account:
|
||||
expense_account = frappe.get_cached_value("Company", self.company, "stock_adjustment_account")
|
||||
args = {
|
||||
"to_warehouse": to_warehouse,
|
||||
"from_warehouse": "",
|
||||
@@ -2048,7 +2045,7 @@ class StockEntry(StockController):
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"stock_uom": item.stock_uom,
|
||||
"expense_account": item.get("expense_account"),
|
||||
"expense_account": expense_account,
|
||||
"cost_center": item.get("buying_cost_center"),
|
||||
"is_finished_item": 1,
|
||||
}
|
||||
@@ -2276,10 +2273,12 @@ class StockEntry(StockController):
|
||||
|
||||
wo_item_qty = item.transferred_qty or item.required_qty
|
||||
|
||||
wo_qty_consumed = flt(wo_item_qty) - flt(item.consumed_qty)
|
||||
wo_qty_unconsumed = flt(wo_item_qty) - flt(item.consumed_qty)
|
||||
wo_qty_to_produce = flt(work_order_qty) - flt(wo.produced_qty)
|
||||
bom_qty_per_unit = item.required_qty / wo.qty # per-unit BOM qty
|
||||
|
||||
req_qty_each = (wo_qty_consumed) / (wo_qty_to_produce or 1)
|
||||
req_qty_each = (wo_qty_unconsumed) / (wo_qty_to_produce or 1)
|
||||
req_qty_each = min(req_qty_each, bom_qty_per_unit)
|
||||
|
||||
qty = req_qty_each * flt(self.fg_completed_qty)
|
||||
|
||||
|
||||
@@ -1323,9 +1323,18 @@ class TestStockEntry(FrappeTestCase):
|
||||
posting_date="2021-07-02", # Illegal SE
|
||||
purpose="Material Transfer",
|
||||
),
|
||||
dict(
|
||||
item_code=item_code,
|
||||
qty=2,
|
||||
from_warehouse=warehouse_names[0],
|
||||
to_warehouse=warehouse_names[1],
|
||||
batch_no=batch_no,
|
||||
posting_date="2021-07-02", # Illegal SE
|
||||
purpose="Material Transfer",
|
||||
),
|
||||
]
|
||||
|
||||
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
|
||||
self.assertRaises(frappe.ValidationError, create_stock_entries, sequence_of_entries)
|
||||
|
||||
@change_settings("Stock Settings", {"allow_negative_stock": 0})
|
||||
def test_future_negative_sle_batch(self):
|
||||
|
||||
@@ -73,7 +73,8 @@
|
||||
"label": "Batch No",
|
||||
"oldfieldname": "batch_no",
|
||||
"oldfieldtype": "Data",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
@@ -361,7 +362,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-22 12:37:41.304109",
|
||||
"modified": "2025-10-04 09:59:15.546556",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -84,7 +84,7 @@ def get_reposting_entries():
|
||||
def get_stock_ledgers(vouchers):
|
||||
return frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["item_code", "warehouse", "posting_date"],
|
||||
fields=["item_code", "warehouse", "posting_date", "posting_time", "posting_datetime"],
|
||||
filters={"voucher_no": ("in", vouchers)},
|
||||
)
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ class StockSettings(Document):
|
||||
self.validate_clean_description_html()
|
||||
self.validate_pending_reposts()
|
||||
self.validate_stock_reservation()
|
||||
self.validate_auto_insert_price_list_rate_if_missing()
|
||||
self.change_precision_for_for_sales()
|
||||
self.change_precision_for_purchase()
|
||||
|
||||
@@ -219,6 +220,23 @@ class StockSettings(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_auto_insert_price_list_rate_if_missing(self):
|
||||
if (
|
||||
self.auto_insert_price_list_rate_if_missing
|
||||
and self.has_value_changed("auto_insert_price_list_rate_if_missing")
|
||||
and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list")
|
||||
):
|
||||
selling_meta = frappe.get_meta("Selling Settings")
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"You have enabled {0} and {1} in {2}. This can lead to prices from the default price list being inserted in the transaction price list."
|
||||
).format(
|
||||
"<i>{}</i>".format(_(self.meta.get_label("auto_insert_price_list_rate_if_missing"))),
|
||||
"<i>{}</i>".format(_(selling_meta.get_label("fallback_to_default_price_list"))),
|
||||
frappe.bold(_("Selling Settings")),
|
||||
)
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
self.toggle_warehouse_field_for_inter_warehouse_transfer()
|
||||
|
||||
|
||||
@@ -98,6 +98,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
|
||||
out.update(get_price_list_rate(args, item))
|
||||
|
||||
if (
|
||||
not out.price_list_rate
|
||||
and args.transaction_type == "selling"
|
||||
and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list")
|
||||
):
|
||||
fallback_args = args.copy()
|
||||
fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list")
|
||||
out.update(get_price_list_rate(fallback_args, item))
|
||||
|
||||
args.customer = current_customer
|
||||
|
||||
if args.customer and cint(args.is_pos):
|
||||
@@ -260,10 +269,13 @@ def filter_batches(batches, doc):
|
||||
del batches[row.get("batch_no")]
|
||||
|
||||
|
||||
def get_filtered_serial_nos(serial_nos, doc):
|
||||
def get_filtered_serial_nos(serial_nos, doc, table=None):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
for row in doc.get("items"):
|
||||
if not table:
|
||||
table = "items"
|
||||
|
||||
for row in doc.get(table):
|
||||
if row.get("serial_no"):
|
||||
for serial_no in get_serial_nos(row.get("serial_no")):
|
||||
if serial_no in serial_nos:
|
||||
@@ -692,8 +704,10 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
taxes_with_no_validity = []
|
||||
|
||||
for tax in taxes:
|
||||
tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, "company")
|
||||
if tax_company == args["company"]:
|
||||
disabled, tax_company = frappe.get_cached_value(
|
||||
"Item Tax Template", tax.item_tax_template, ["disabled", "company"]
|
||||
)
|
||||
if not disabled and tax_company == args["company"]:
|
||||
if tax.valid_from or tax.maximum_net_rate:
|
||||
# In purchase Invoice first preference will be given to supplier invoice date
|
||||
# if supplier date is not present then posting date
|
||||
@@ -1504,7 +1518,7 @@ def get_valuation_rate(item_code, company, warehouse=None):
|
||||
|
||||
return frappe.db.get_value(
|
||||
"Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True
|
||||
) or {"valuation_rate": 0}
|
||||
) or {"valuation_rate": item.get("valuation_rate") or 0}
|
||||
|
||||
elif not item.get("is_stock_item"):
|
||||
pi_item = frappe.qb.DocType("Purchase Invoice Item")
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form, parse_json
|
||||
from frappe.utils import get_datetime, get_link_to_form, parse_json
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
|
||||
from erpnext.stock.doctype.stock_reposting_settings.stock_reposting_settings import get_stock_ledgers
|
||||
from erpnext.stock.doctype.warehouse.warehouse import get_warehouses_based_on_account
|
||||
|
||||
|
||||
@@ -141,18 +142,29 @@ def create_reposting_entries(rows, company):
|
||||
rows = parse_json(rows)
|
||||
|
||||
entries = []
|
||||
for row in rows:
|
||||
row = frappe._dict(row)
|
||||
|
||||
item_wh = frappe._dict()
|
||||
vouchers = [row.get("voucher_no") for row in rows]
|
||||
sles = get_stock_ledgers(vouchers)
|
||||
for sle in sles:
|
||||
key = (sle.item_code, sle.warehouse)
|
||||
if key not in item_wh:
|
||||
item_wh[key] = sle
|
||||
elif get_datetime(item_wh.get(key).posting_datetime) > get_datetime(sle.posting_datetime):
|
||||
item_wh[key] = sle
|
||||
|
||||
for key, sle in item_wh.items():
|
||||
item_code, warehouse = key
|
||||
try:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Transaction",
|
||||
"based_on": "Item and Warehouse",
|
||||
"status": "Queued",
|
||||
"voucher_type": row.voucher_type,
|
||||
"voucher_no": row.voucher_no,
|
||||
"posting_date": row.posting_date,
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": sle.posting_date,
|
||||
"posting_time": sle.posting_time,
|
||||
"company": company,
|
||||
"allow_nagative_stock": 1,
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ frappe.query_reports["Stock Qty vs Serial No Count"] = {
|
||||
},
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_disables_items",
|
||||
label: __("Show Disabled Items"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
formatter: function (value, row, column, data, default_formatter) {
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
def execute(filters=None):
|
||||
validate_warehouse(filters)
|
||||
columns = get_columns()
|
||||
data = get_data(filters.warehouse)
|
||||
data = get_data(filters.warehouse, filters.show_disables_items)
|
||||
return columns, data
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ def get_columns():
|
||||
return columns
|
||||
|
||||
|
||||
def get_data(warehouse):
|
||||
def get_data(warehouse, show_disables_items):
|
||||
filters = {"has_serial_no": True}
|
||||
if not show_disables_items:
|
||||
filters["disabled"] = False
|
||||
serial_item_list = frappe.get_all(
|
||||
"Item",
|
||||
filters={
|
||||
"has_serial_no": True,
|
||||
},
|
||||
filters=filters,
|
||||
fields=["item_code", "item_name"],
|
||||
)
|
||||
|
||||
|
||||
@@ -209,7 +209,20 @@ class SerialBatchBundle:
|
||||
elif sn_doc.has_batch_no and len(sn_doc.entries) == 1:
|
||||
values_to_update["batch_no"] = sn_doc.entries[0].batch_no
|
||||
|
||||
frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
|
||||
if self.child_doctype == "Packed Item":
|
||||
name = frappe.db.get_value(
|
||||
"Packed Item",
|
||||
{
|
||||
"parent_detail_docname": sn_doc.voucher_detail_no,
|
||||
"item_code": self.sle.item_code,
|
||||
"serial_and_batch_bundle": ("is", "not set"),
|
||||
},
|
||||
"name",
|
||||
)
|
||||
|
||||
frappe.db.set_value(self.child_doctype, name, values_to_update)
|
||||
else:
|
||||
frappe.db.set_value(self.child_doctype, self.sle.voucher_detail_no, values_to_update)
|
||||
|
||||
@property
|
||||
def child_doctype(self):
|
||||
@@ -227,6 +240,13 @@ class SerialBatchBundle:
|
||||
if self.sle.voucher_type == "Asset Repair":
|
||||
child_doctype = "Asset Repair Consumed Item"
|
||||
|
||||
if self.sle.voucher_type in ["Delivery Note", "Sales Invoice"] and self.sle.voucher_detail_no:
|
||||
if (
|
||||
frappe.db.get_value(self.sle.voucher_type + " Item", self.sle.voucher_detail_no, "item_code")
|
||||
!= self.sle.item_code
|
||||
):
|
||||
child_doctype = "Packed Item"
|
||||
|
||||
return child_doctype
|
||||
|
||||
def is_rejected_entry(self):
|
||||
@@ -580,11 +600,13 @@ class SerialNoValuation(DeprecatedSerialNoValuation):
|
||||
else:
|
||||
self.serial_no_incoming_rate = defaultdict(float)
|
||||
self.stock_value_change = 0.0
|
||||
self.old_serial_nos = []
|
||||
|
||||
serial_nos = self.get_serial_nos()
|
||||
for serial_no in serial_nos:
|
||||
incoming_rate = self.get_incoming_rate_from_bundle(serial_no)
|
||||
if incoming_rate is None:
|
||||
self.old_serial_nos.append(serial_no)
|
||||
continue
|
||||
|
||||
self.stock_value_change += incoming_rate
|
||||
@@ -1227,7 +1249,7 @@ class SerialBatchCreation:
|
||||
def create_batch(self):
|
||||
from erpnext.stock.doctype.batch.batch import make_batch
|
||||
|
||||
if self.is_rejected:
|
||||
if hasattr(self, "is_rejected") and self.is_rejected:
|
||||
bundle = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
@@ -1363,18 +1385,19 @@ def get_batch_current_qty(batch):
|
||||
|
||||
|
||||
def throw_negative_batch_validation(batch_no, qty):
|
||||
frappe.throw(
|
||||
_("The Batch {0} has negative quantity {1}. Please correct the quantity.").format(
|
||||
bold(batch_no), bold(qty)
|
||||
),
|
||||
title=_("Negative Batch Quantity"),
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"The Batch {0} has negative batch quantity {1}. To fix this, go to the batch and click on Recalculate Batch Qty. If the issue still persists, create an inward entry."
|
||||
).format(bold(get_link_to_form("Batch", batch_no)), bold(qty)),
|
||||
title=_("Warning!"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
|
||||
def get_batchwise_qty(voucher_type, voucher_no):
|
||||
bundles = frappe.get_all(
|
||||
"Serial and Batch Bundle",
|
||||
filters={"voucher_no": voucher_no, "voucher_type": voucher_type},
|
||||
filters={"voucher_no": voucher_no, "voucher_type": voucher_type, "docstatus": (">", 0)},
|
||||
pluck="name",
|
||||
)
|
||||
if not bundles:
|
||||
|
||||
@@ -725,32 +725,15 @@ 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
|
||||
existing_sle = self.distinct_item_warehouses[key].get("sle", {})
|
||||
if getdate(existing_sle.get("posting_date")) > getdate(dependant_sle.posting_date):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
|
||||
# Future dependent voucher needs to be repost to get the correct stock value
|
||||
# If dependent voucher has not reposted, then add it to the list
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
self.new_items_found = True
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
elif dependant_sle.voucher_type == "Stock Entry" and is_transfer_stock_entry(
|
||||
dependant_sle.voucher_no
|
||||
):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
|
||||
def is_dependent_voucher_reposted(self, dependant_sle) -> bool:
|
||||
# Return False if the dependent voucher is not reposted
|
||||
@@ -1027,16 +1010,17 @@ class update_entries_after:
|
||||
if not frappe.db.exists("Serial and Batch Bundle", sle.serial_and_batch_bundle):
|
||||
return
|
||||
|
||||
if self.args.get("sle_id") and sle.actual_qty < 0:
|
||||
doc = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
sle.serial_and_batch_bundle,
|
||||
["total_amount", "total_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
if sle.actual_qty < 0 and (
|
||||
sle.voucher_type in ["Stock Reconciliation", "Asset Capitalization"]
|
||||
or not frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_return")
|
||||
):
|
||||
doc = frappe._dict({})
|
||||
self.update_serial_batch_no_valuation(sle, doc, prev_sle=self.wh_data)
|
||||
else:
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
doc.set_incoming_rate(save=True, allow_negative_stock=self.allow_negative_stock)
|
||||
doc.set_incoming_rate(
|
||||
save=True, allow_negative_stock=self.allow_negative_stock, prev_sle=self.wh_data
|
||||
)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
|
||||
if stock_queue := frappe.get_all(
|
||||
@@ -1055,6 +1039,87 @@ class update_entries_after:
|
||||
self.wh_data.qty_after_transaction, self.flt_precision
|
||||
)
|
||||
|
||||
def update_serial_batch_no_valuation(self, sle, doc, prev_sle=None):
|
||||
from erpnext.stock.serial_batch_bundle import BatchNoValuation, SerialNoValuation
|
||||
|
||||
sabb_data = get_serial_from_sabb(sle.serial_and_batch_bundle)
|
||||
if not sabb_data:
|
||||
doc.update({"total_amount": 0.0, "total_qty": 0.0, "avg_rate": 0.0})
|
||||
return
|
||||
|
||||
serial_nos = [d.serial_no for d in sabb_data if d.serial_no]
|
||||
if serial_nos:
|
||||
sle["serial_nos"] = get_serial_nos_data(",".join(serial_nos))
|
||||
sn_obj = SerialNoValuation(
|
||||
sle=sle,
|
||||
item_code=self.item_code,
|
||||
warehouse=sle.warehouse,
|
||||
)
|
||||
else:
|
||||
sle["batch_nos"] = {row.batch_no: row for row in sabb_data if row.batch_no}
|
||||
sn_obj = BatchNoValuation(
|
||||
sle=sle,
|
||||
item_code=self.item_code,
|
||||
warehouse=sle.warehouse,
|
||||
prev_sle=prev_sle,
|
||||
)
|
||||
|
||||
tot_amt = 0.0
|
||||
total_qty = 0.0
|
||||
avg_rate = 0.0
|
||||
|
||||
for d in sabb_data:
|
||||
incoming_rate = get_incoming_rate_for_serial_and_batch(self.item_code, d, sn_obj)
|
||||
amount = incoming_rate * flt(d.qty)
|
||||
tot_amt += flt(amount)
|
||||
total_qty += flt(d.qty)
|
||||
|
||||
if flt(incoming_rate, self.currency_precision) == flt(
|
||||
d.incoming_rate, self.currency_precision
|
||||
) and not getattr(d, "stock_queue", None):
|
||||
continue
|
||||
|
||||
values_to_update = {
|
||||
"incoming_rate": incoming_rate,
|
||||
"stock_value_difference": amount,
|
||||
}
|
||||
|
||||
if d.stock_queue:
|
||||
values_to_update["stock_queue"] = d.stock_queue
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Entry",
|
||||
d.name,
|
||||
values_to_update,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
if total_qty:
|
||||
avg_rate = tot_amt / total_qty
|
||||
|
||||
doc.update(
|
||||
{
|
||||
"total_amount": tot_amt,
|
||||
"total_qty": total_qty,
|
||||
"avg_rate": avg_rate,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Serial and Batch Bundle",
|
||||
sle.serial_and_batch_bundle,
|
||||
{
|
||||
"total_qty": total_qty,
|
||||
"avg_rate": avg_rate,
|
||||
"total_amount": tot_amt,
|
||||
},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
for key in ("serial_nos", "batch_nos"):
|
||||
if key in sle:
|
||||
del sle[key]
|
||||
|
||||
def get_outgoing_rate_for_batched_item(self, sle):
|
||||
if self.wh_data.qty_after_transaction == 0:
|
||||
return 0
|
||||
@@ -1772,6 +1837,8 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
"posting_time",
|
||||
"voucher_detail_no",
|
||||
"posting_datetime as timestamp",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -2303,3 +2370,52 @@ def get_stock_value_difference(item_code, warehouse, posting_date, posting_time,
|
||||
|
||||
difference_amount = query.run()
|
||||
return flt(difference_amount[0][0]) if difference_amount else 0
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def is_transfer_stock_entry(voucher_no):
|
||||
purpose = frappe.get_cached_value("Stock Entry", voucher_no, "purpose")
|
||||
|
||||
return purpose in ["Material Transfer", "Material Transfer for Manufacture", "Send to Subcontractor"]
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_serial_from_sabb(serial_and_batch_bundle):
|
||||
return frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": serial_and_batch_bundle},
|
||||
fields=["serial_no", "batch_no", "name", "qty", "incoming_rate"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
|
||||
def get_incoming_rate_for_serial_and_batch(item_code, row, sn_obj):
|
||||
if row.serial_no:
|
||||
return abs(sn_obj.serial_no_incoming_rate.get(row.serial_no, 0.0))
|
||||
else:
|
||||
stock_queue = []
|
||||
if hasattr(sn_obj, "stock_queue") and sn_obj.stock_queue:
|
||||
stock_queue = parse_json(sn_obj.stock_queue)
|
||||
|
||||
val_method = get_valuation_method(item_code)
|
||||
|
||||
actual_qty = row.qty
|
||||
if stock_queue and val_method == "FIFO" and row.batch_no in sn_obj.non_batchwise_valuation_batches:
|
||||
if actual_qty < 0:
|
||||
stock_queue = FIFOValuation(stock_queue)
|
||||
_prev_qty, prev_stock_value = stock_queue.get_total_stock_and_value()
|
||||
|
||||
stock_queue.remove_stock(qty=abs(actual_qty))
|
||||
_qty, stock_value = stock_queue.get_total_stock_and_value()
|
||||
|
||||
stock_value_difference = stock_value - prev_stock_value
|
||||
incoming_rate = abs(flt(stock_value_difference) / abs(flt(actual_qty)))
|
||||
stock_queue = stock_queue.state
|
||||
else:
|
||||
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
|
||||
stock_queue.append([row.qty, incoming_rate])
|
||||
row.stock_queue = json.dumps(stock_queue)
|
||||
else:
|
||||
incoming_rate = abs(flt(sn_obj.batch_avg_rate.get(row.batch_no)))
|
||||
|
||||
return incoming_rate
|
||||
|
||||
@@ -252,14 +252,18 @@ class SubcontractingOrder(SubcontractingController):
|
||||
if si.fg_item:
|
||||
item = frappe.get_doc("Item", si.fg_item)
|
||||
|
||||
po_item = frappe.get_doc("Purchase Order Item", si.purchase_order_item)
|
||||
available_qty = po_item.qty - po_item.subcontracted_quantity
|
||||
qty, subcontracted_quantity, fg_item_qty = frappe.db.get_value(
|
||||
"Purchase Order Item",
|
||||
si.purchase_order_item,
|
||||
["qty", "subcontracted_quantity", "fg_item_qty"],
|
||||
)
|
||||
available_qty = flt(qty) - flt(subcontracted_quantity)
|
||||
|
||||
if available_qty == 0:
|
||||
continue
|
||||
|
||||
si.qty = available_qty
|
||||
conversion_factor = po_item.qty / po_item.fg_item_qty
|
||||
conversion_factor = flt(qty) / flt(fg_item_qty)
|
||||
si.fg_item_qty = flt(
|
||||
available_qty / conversion_factor, frappe.get_precision("Purchase Order Item", "qty")
|
||||
)
|
||||
@@ -338,13 +342,24 @@ class SubcontractingOrder(SubcontractingController):
|
||||
|
||||
def update_subcontracted_quantity_in_po(self, cancel=False):
|
||||
for service_item in self.service_items:
|
||||
doc = frappe.get_doc("Purchase Order Item", service_item.purchase_order_item)
|
||||
doc.subcontracted_quantity = (
|
||||
(doc.subcontracted_quantity + service_item.qty)
|
||||
if not cancel
|
||||
else (doc.subcontracted_quantity - service_item.qty)
|
||||
subcontracted_quantity = flt(
|
||||
frappe.db.get_value(
|
||||
"Purchase Order Item", service_item.purchase_order_item, "subcontracted_quantity"
|
||||
)
|
||||
)
|
||||
|
||||
subcontracted_quantity = (
|
||||
(subcontracted_quantity + service_item.qty)
|
||||
if not cancel
|
||||
else (subcontracted_quantity - service_item.qty)
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Purchase Order Item",
|
||||
service_item.purchase_order_item,
|
||||
"subcontracted_quantity",
|
||||
subcontracted_quantity,
|
||||
)
|
||||
doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -336,6 +336,10 @@ frappe.ui.form.on("Subcontracting Receipt", {
|
||||
|
||||
reset_raw_materials_table: (frm) => {
|
||||
frm.clear_table("supplied_items");
|
||||
frm.doc.__unsaved = true;
|
||||
if (!frm.doc.set_posting_time) {
|
||||
frm.set_value("posting_time", frappe.datetime.now_time());
|
||||
}
|
||||
|
||||
frm.call({
|
||||
method: "reset_raw_materials",
|
||||
|
||||
@@ -649,6 +649,7 @@
|
||||
"label": "Raw Materials Actions"
|
||||
},
|
||||
{
|
||||
"description": "Click this button if you encounter a negative stock error for a serial or batch item. The system will fetch the available serials or batches automatically.",
|
||||
"fieldname": "reset_raw_materials_table",
|
||||
"fieldtype": "Button",
|
||||
"label": "Reset Raw Materials Table"
|
||||
@@ -678,7 +679,7 @@
|
||||
"in_create": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-06 15:24:38.384232",
|
||||
"modified": "2025-10-08 21:43:27.065640",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
@@ -739,6 +740,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status, posting_date, supplier",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -116,7 +116,13 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.validate_items_qty()
|
||||
self.set_items_bom()
|
||||
self.set_items_cost_center()
|
||||
self.set_items_expense_account()
|
||||
|
||||
if self.company:
|
||||
default_expense_account = self.get_company_default(
|
||||
"default_expense_account", ignore_validation=True
|
||||
)
|
||||
self.set_service_expense_account(default_expense_account)
|
||||
self.set_expense_account_for_subcontracted_items(default_expense_account)
|
||||
|
||||
def validate(self):
|
||||
self.reset_supplied_items()
|
||||
@@ -196,6 +202,39 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
if item.subcontracting_order:
|
||||
check_on_hold_or_closed_status("Subcontracting Order", item.subcontracting_order)
|
||||
|
||||
def set_service_expense_account(self, default_expense_account):
|
||||
for row in self.get("items"):
|
||||
if not row.service_expense_account and row.purchase_order_item:
|
||||
service_item = frappe.db.get_value(
|
||||
"Purchase Order Item", row.purchase_order_item, "item_code"
|
||||
)
|
||||
|
||||
if service_item:
|
||||
if default := (
|
||||
get_item_defaults(service_item, self.company)
|
||||
or get_item_group_defaults(service_item, self.company)
|
||||
or get_brand_defaults(service_item, self.company)
|
||||
):
|
||||
if service_expense_account := default.get("expense_account"):
|
||||
row.service_expense_account = service_expense_account
|
||||
|
||||
if not row.service_expense_account:
|
||||
row.service_expense_account = default_expense_account
|
||||
|
||||
def set_expense_account_for_subcontracted_items(self, default_expense_account):
|
||||
for row in self.get("items"):
|
||||
if not row.expense_account:
|
||||
if default := (
|
||||
get_item_defaults(row.item_code, self.company)
|
||||
or get_item_group_defaults(row.item_code, self.company)
|
||||
or get_brand_defaults(row.item_code, self.company)
|
||||
):
|
||||
if expense_account := default.get("expense_account"):
|
||||
row.expense_account = expense_account
|
||||
|
||||
if not row.expense_account:
|
||||
row.expense_account = default_expense_account
|
||||
|
||||
def validate_items_qty(self):
|
||||
for item in self.items:
|
||||
if not (item.qty or item.rejected_qty):
|
||||
@@ -242,14 +281,6 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
self.company,
|
||||
)
|
||||
|
||||
def set_items_expense_account(self):
|
||||
if self.company:
|
||||
expense_account = self.get_company_default("default_expense_account", ignore_validation=True)
|
||||
|
||||
for item in self.items:
|
||||
if not item.expense_account:
|
||||
item.expense_account = expense_account
|
||||
|
||||
def set_supplied_items_expense_account(self):
|
||||
for item in self.supplied_items:
|
||||
if not item.expense_account:
|
||||
@@ -599,13 +630,17 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
service_cost = flt(
|
||||
item.service_cost_per_qty, item.precision("service_cost_per_qty")
|
||||
) * flt(item.qty, item.precision("qty"))
|
||||
# Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=item.expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=stock_value_diff,
|
||||
credit=flt(stock_value_diff) - service_cost,
|
||||
remarks=remarks,
|
||||
against_account=accepted_warehouse_account,
|
||||
account_currency=get_account_currency(item.expense_account),
|
||||
@@ -613,6 +648,21 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
item=item,
|
||||
)
|
||||
|
||||
service_account = item.service_expense_account or item.expense_account
|
||||
# Expense Account (Credit)
|
||||
self.add_gl_entry(
|
||||
gl_entries=gl_entries,
|
||||
account=service_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=service_cost,
|
||||
remarks=remarks,
|
||||
against_account=accepted_warehouse_account,
|
||||
account_currency=get_account_currency(service_account),
|
||||
project=item.project,
|
||||
item=item,
|
||||
)
|
||||
|
||||
if flt(item.rm_supp_cost) and supplier_warehouse_account:
|
||||
for rm_item in supplied_items_details.get(item.name):
|
||||
# Supplier Warehouse Account (Credit)
|
||||
|
||||
@@ -422,6 +422,79 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||
|
||||
def test_subcontracting_receipt_for_service_expense_account(self):
|
||||
service_expense_account = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Service Expense",
|
||||
"account_type": "Expense Account",
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"is_group": 0,
|
||||
"parent_account": "Indirect Expenses - TCP1",
|
||||
}
|
||||
)
|
||||
.insert(ignore_if_duplicate=True)
|
||||
.name
|
||||
)
|
||||
|
||||
service_item_doc = frappe.get_doc("Item", "Subcontracted Service Item 10")
|
||||
service_item_doc.append(
|
||||
"item_defaults",
|
||||
{
|
||||
"company": "_Test Company with perpetual inventory",
|
||||
"expense_account": service_expense_account,
|
||||
"default_warehouse": "Stores - TCP1",
|
||||
},
|
||||
)
|
||||
|
||||
service_item_doc.save()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "Stores - TCP1",
|
||||
"item_code": "Subcontracted Service Item 10",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA10",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(
|
||||
company="_Test Company with perpetual inventory",
|
||||
warehouse="Stores - TCP1",
|
||||
supplier_warehouse="Work In Progress - TCP1",
|
||||
service_items=service_items,
|
||||
)
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.submit()
|
||||
|
||||
for item in scr.items:
|
||||
self.assertEqual(item.service_expense_account, service_expense_account)
|
||||
|
||||
gl_entries = get_gl_entries("Subcontracting Receipt", scr.name)
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
fg_warehouse_ac = get_inventory_account(scr.company, scr.items[0].warehouse)
|
||||
expense_account = scr.items[0].expense_account
|
||||
expected_values = {
|
||||
fg_warehouse_ac: [2000, 1000],
|
||||
expense_account: [1000, 1000],
|
||||
service_expense_account: [0, 1000],
|
||||
}
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||
|
||||
@change_settings("Stock Settings", {"use_serial_batch_fields": 0})
|
||||
def test_subcontracting_receipt_with_zero_service_cost(self):
|
||||
warehouse = "Stores - TCP1"
|
||||
@@ -740,13 +813,13 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
for row in scr.supplied_items:
|
||||
self.assertEqual(row.rate, 300.00)
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
auto_created_serial_batch = frappe.db.get_value(
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": scr.name, "voucher_detail_no": row.name},
|
||||
"auto_created_serial_and_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
self.assertTrue(auto_created_serial_batch)
|
||||
self.assertTrue(serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(scr.items[0].rm_cost_per_qty, 900)
|
||||
self.assertEqual(scr.items[0].service_cost_per_qty, 100)
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"manufacturer_part_no",
|
||||
"accounting_details_section",
|
||||
"expense_account",
|
||||
"column_break_exht",
|
||||
"service_expense_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
@@ -580,12 +582,22 @@
|
||||
"fieldname": "add_serial_batch_for_rejected_qty",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No (Rejected Qty)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_exht",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_expense_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Service Expense Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-12-06 15:23:58.680169",
|
||||
"modified": "2025-09-26 12:00:38.877638",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt Item",
|
||||
|
||||
@@ -55,6 +55,7 @@ class SubcontractingReceiptItem(Document):
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.SmallText | None
|
||||
service_cost_per_qty: DF.Currency
|
||||
service_expense_account: DF.Link | None
|
||||
stock_uom: DF.Link
|
||||
subcontracting_order: DF.Link | None
|
||||
subcontracting_order_item: DF.Data | None
|
||||
|
||||
@@ -233,6 +233,8 @@
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fetch_from": "email_account.company",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
@@ -391,7 +393,7 @@
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 7,
|
||||
"links": [],
|
||||
"modified": "2025-02-18 21:18:52.797745",
|
||||
"modified": "2025-09-25 11:10:53.556731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Issue",
|
||||
|
||||
Reference in New Issue
Block a user