mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-02 01:04:36 +00:00
Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0854333e42 | ||
|
|
48e0ce1132 | ||
|
|
ba7e0b9506 | ||
|
|
a94a13a7c1 | ||
|
|
e573c6094a | ||
|
|
524619e0b4 | ||
|
|
5f5fa2f8e1 | ||
|
|
65c277fd27 | ||
|
|
8371dafb1a | ||
|
|
f8bc0e47ff | ||
|
|
d3a95f04cc | ||
|
|
5f63794739 | ||
|
|
41bca34f6a | ||
|
|
f4b7093838 | ||
|
|
536264896e | ||
|
|
177b23c624 | ||
|
|
f81dba6380 | ||
|
|
b7b5f6acf3 | ||
|
|
d60d8a2625 | ||
|
|
e0efe922a6 | ||
|
|
500972e8aa | ||
|
|
b833dfab9d | ||
|
|
818ddc0b8c | ||
|
|
d0d1d63d31 | ||
|
|
f2d644ba29 | ||
|
|
9e831bbfc6 | ||
|
|
7690efa4f7 | ||
|
|
00669661e5 | ||
|
|
2ab1b42033 | ||
|
|
2c156b58f0 | ||
|
|
c617b343b3 | ||
|
|
b2de9cdef2 | ||
|
|
ae59908f0a | ||
|
|
a10e3948b2 | ||
|
|
fbe1f449d8 | ||
|
|
b3fa0ac596 | ||
|
|
82d03e2617 | ||
|
|
3f004db14f | ||
|
|
4545213adc | ||
|
|
a336e19bb8 | ||
|
|
9a99ccc166 | ||
|
|
9935f04bde | ||
|
|
cc5c4a3f9a | ||
|
|
0fefccc128 | ||
|
|
a8448f9e60 | ||
|
|
450061c7db | ||
|
|
5df8ad6ef1 | ||
|
|
6b41dc2fed | ||
|
|
ed77c15ebc | ||
|
|
d4700e5560 | ||
|
|
f4c9a6e59c | ||
|
|
f1621d15ff | ||
|
|
8638654c13 | ||
|
|
5cd36c318b | ||
|
|
8eede1d266 | ||
|
|
26db582499 | ||
|
|
89458f07c9 | ||
|
|
a4633d6e75 | ||
|
|
dbe107345c | ||
|
|
a2436e4b6e | ||
|
|
9a538c6843 | ||
|
|
d8f5924eb2 | ||
|
|
9312a5b762 | ||
|
|
edef03ac22 | ||
|
|
74948aabda | ||
|
|
cfedaf5dc1 | ||
|
|
f25097da1d | ||
|
|
3952f5d913 | ||
|
|
9d23199c25 | ||
|
|
b1abcd5577 | ||
|
|
09f866022b | ||
|
|
f087da927a | ||
|
|
171d86a3ab | ||
|
|
58c6031c1f | ||
|
|
ff362843cb | ||
|
|
86b14dd4be | ||
|
|
037ef10fcb | ||
|
|
ab20b965ca | ||
|
|
e1a726bdd4 | ||
|
|
221e5a2190 | ||
|
|
f0724f2f04 | ||
|
|
c404faaa6d | ||
|
|
abfe3c8365 | ||
|
|
61f4547860 | ||
|
|
c69bb746ce | ||
|
|
bf78f6173c | ||
|
|
e628a37b99 | ||
|
|
f1062c61f6 | ||
|
|
f479675ce6 | ||
|
|
ce7dbf3090 | ||
|
|
ac587b9c94 | ||
|
|
1ea3daeb17 | ||
|
|
b2f6fed42c | ||
|
|
352642096e | ||
|
|
ab61b46a01 | ||
|
|
9b8fffd1d4 | ||
|
|
46243fdb5f | ||
|
|
cfc8c610fa | ||
|
|
d73ff810d9 | ||
|
|
2b37287b19 | ||
|
|
f31b008502 | ||
|
|
6f24c02121 | ||
|
|
a1eab1db74 | ||
|
|
fcf9f82092 | ||
|
|
e817561dce | ||
|
|
a49026e9d2 | ||
|
|
7574069af2 | ||
|
|
0e2bca4b34 | ||
|
|
7de15b74d4 | ||
|
|
a0db227a7a | ||
|
|
af55ce0f6c | ||
|
|
894bb703f6 | ||
|
|
b6e09531d7 | ||
|
|
8f47505604 | ||
|
|
5463a8b6cf | ||
|
|
b8a773e3e1 | ||
|
|
dc642fbc41 | ||
|
|
ddbdcbb026 | ||
|
|
df938f24d4 | ||
|
|
bde63ed0e5 | ||
|
|
1c3ac9c1fd | ||
|
|
0c07dfadfe | ||
|
|
1f7eccdac5 | ||
|
|
ef202d7cd0 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.66.0"
|
||||
__version__ = "15.69.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -147,8 +147,8 @@ class AccountsSettings(Document):
|
||||
if self.add_taxes_from_item_tax_template and self.add_taxes_from_taxes_and_charges_template:
|
||||
frappe.throw(
|
||||
_("You cannot enable both the settings '{0}' and '{1}'.").format(
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_item_tax_template")),
|
||||
frappe.bold(self.meta.get_label("add_taxes_from_taxes_and_charges_template")),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_item_tax_template"))),
|
||||
frappe.bold(_(self.meta.get_label("add_taxes_from_taxes_and_charges_template"))),
|
||||
),
|
||||
title=_("Auto Tax Settings Error"),
|
||||
)
|
||||
|
||||
@@ -118,8 +118,9 @@ class BankClearance(Document):
|
||||
|
||||
else:
|
||||
# using db_set to trigger notification
|
||||
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
|
||||
payment_entry.db_set("clearance_date", d.clearance_date)
|
||||
frappe.db.set_value(
|
||||
d.payment_document, d.payment_entry, "clearance_date", d.clearance_date
|
||||
)
|
||||
|
||||
clearance_date_updated = True
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import now_datetime, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import BudgetError, get_actual_expense
|
||||
from erpnext.accounts.doctype.budget.budget import (
|
||||
BudgetError,
|
||||
get_accumulated_monthly_budget,
|
||||
get_actual_expense,
|
||||
)
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -96,6 +100,10 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
|
||||
|
||||
accumulated_limit = get_accumulated_monthly_budget(
|
||||
budget.monthly_distribution, nowdate(), budget.fiscal_year, budget.accounts[0].budget_amount
|
||||
)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Material Request",
|
||||
@@ -109,7 +117,7 @@ class TestBudget(unittest.TestCase):
|
||||
"uom": "_Test UOM",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"schedule_date": nowdate(),
|
||||
"rate": 100000,
|
||||
"rate": accumulated_limit + 1,
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_stock_accounts,
|
||||
get_stock_and_account_balance,
|
||||
@@ -146,8 +147,8 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
self.title = self.get_title()
|
||||
if self.is_new() or not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
def validate_advance_accounts(self):
|
||||
journal_accounts = set([x.account for x in self.accounts])
|
||||
@@ -238,9 +239,10 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
@@ -1042,7 +1044,9 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def set_print_format_fields(self):
|
||||
bank_amount = party_amount = total_amount = 0.0
|
||||
currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
|
||||
currency = (
|
||||
bank_account_currency
|
||||
) = party_account_currency = pay_to_recd_from = self.pay_to_recd_from = None
|
||||
party_type = None
|
||||
for d in self.get("accounts"):
|
||||
if d.party_type in ["Customer", "Supplier"] and d.party:
|
||||
|
||||
@@ -46,8 +46,10 @@ from erpnext.accounts.party import (
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
get_advance_payment_doctypes,
|
||||
get_balance_on,
|
||||
get_outstanding_invoices,
|
||||
get_reconciliation_effect_date,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import (
|
||||
AccountsController,
|
||||
@@ -568,7 +570,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate_mandatory(self):
|
||||
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
|
||||
if not self.get(field):
|
||||
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
|
||||
frappe.throw(_("{0} is mandatory").format(_(self.meta.get_label(field))))
|
||||
|
||||
def validate_reference_documents(self):
|
||||
valid_reference_doctypes = self.get_valid_reference_doctypes()
|
||||
@@ -1028,7 +1030,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_doctype in get_advance_payment_doctypes():
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
@@ -1308,8 +1310,7 @@ class PaymentEntry(AccountsController):
|
||||
if not self.party_account:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
if self.payment_type == "Receive":
|
||||
against_account = self.paid_to
|
||||
else:
|
||||
@@ -1492,23 +1493,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
# For backwards compatibility
|
||||
# Supporting reposting on payment entries reconciled before select field introduction
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", self.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if invoice.reference_doctype in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
posting_date = frappe.db.get_value(
|
||||
invoice.reference_doctype, invoice.reference_name, date_field
|
||||
)
|
||||
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
posting_date = nowdate()
|
||||
posting_date = get_reconciliation_effect_date(invoice, self.company, self.posting_date)
|
||||
frappe.db.set_value("Payment Entry Reference", invoice.name, "reconcile_effect_on", posting_date)
|
||||
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
@@ -1699,12 +1684,15 @@ class PaymentEntry(AccountsController):
|
||||
return flt(gl_dict.get(field, 0) / (conversion_rate or 1))
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
if self.payment_type not in ("Receive", "Pay") or not self.party:
|
||||
return
|
||||
|
||||
advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
self.reference_no = reference_doc.name
|
||||
|
||||
@@ -589,7 +589,7 @@ class PaymentReconciliation(Document):
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname)))
|
||||
frappe.throw(_("Please select {0} first").format(_(self.meta.get_label(fieldname))))
|
||||
|
||||
def validate_entries(self):
|
||||
if not self.get("invoices"):
|
||||
@@ -826,7 +826,7 @@ def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
|
||||
create_gain_loss_journal(
|
||||
company,
|
||||
today(),
|
||||
inv.difference_posting_date,
|
||||
inv.party_type,
|
||||
inv.party,
|
||||
inv.account,
|
||||
|
||||
@@ -829,8 +829,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
if not references:
|
||||
return
|
||||
|
||||
precision = references[0].precision("allocated_amount")
|
||||
|
||||
precision = frappe.get_precision("Payment Entry Reference", "allocated_amount")
|
||||
referenced_payment_requests = frappe.get_all(
|
||||
"Payment Request",
|
||||
filters={"name": ["in", {row.payment_request for row in references if row.payment_request}]},
|
||||
|
||||
@@ -630,29 +630,58 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
def test_payment_request_on_unreconcile(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=500)
|
||||
pi.submit()
|
||||
|
||||
pr = make_payment_request(
|
||||
dt=pi.doctype,
|
||||
dn=pi.name,
|
||||
mute_email=1,
|
||||
submit_doc=True,
|
||||
return_doc=True,
|
||||
)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": pe.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
unreconcile.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
@@ -113,12 +114,22 @@
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"print_hide": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-01 11:36:42.456429",
|
||||
"modified": "2025-07-02 17:08:04.747202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -179,8 +190,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,10 @@ class POSInvoiceMergeLog(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import (
|
||||
POSInvoiceReference,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_invoice_reference.pos_invoice_reference import POSInvoiceReference
|
||||
|
||||
amended_from: DF.Link | None
|
||||
company: DF.Link
|
||||
consolidated_credit_note: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
customer: DF.Link
|
||||
@@ -584,6 +583,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.company = closing_entry.get("company") if closing_entry else None
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
merge_log.set("pos_invoices", _invoices)
|
||||
|
||||
@@ -169,7 +169,7 @@ class PricingRule(Document):
|
||||
|
||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||
if tocheck and not self.get(tocheck):
|
||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||
throw(_("{0} is required").format(_(self.meta.get_label(tocheck))), frappe.MandatoryError)
|
||||
|
||||
if self.apply_rule_on_other:
|
||||
o_field = "other_" + frappe.scrub(self.apply_rule_on_other)
|
||||
|
||||
@@ -205,6 +205,56 @@ class TestPricingRule(FrappeTestCase):
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 10)
|
||||
|
||||
def test_unset_group_condition(self):
|
||||
"""
|
||||
If args are not set for group condition, then pricing rule should not be applied.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
test_record = {
|
||||
"doctype": "Pricing Rule",
|
||||
"title": "_Test Pricing Rule",
|
||||
"apply_on": "Item Code",
|
||||
"items": [{"item_code": "_Test Item"}],
|
||||
"currency": "USD",
|
||||
"selling": 1,
|
||||
"rate_or_discount": "Discount Percentage",
|
||||
"rate": 0,
|
||||
"discount_percentage": 10,
|
||||
"applicable_for": "Territory",
|
||||
"territory": "All Territories",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
frappe.get_doc(test_record.copy()).insert()
|
||||
args = frappe._dict(
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"company": "_Test Company",
|
||||
"price_list": "_Test Price List",
|
||||
"currency": "_Test Currency",
|
||||
"doctype": "Sales Order",
|
||||
"conversion_rate": 1,
|
||||
"price_list_currency": "_Test Currency",
|
||||
"plc_conversion_rate": 1,
|
||||
"order_type": "Sales",
|
||||
"customer": "_Test Customer",
|
||||
"name": None,
|
||||
}
|
||||
)
|
||||
|
||||
# without territory in customer
|
||||
customer = frappe.get_doc("Customer", "_Test Customer")
|
||||
territory = customer.territory
|
||||
|
||||
customer.territory = None
|
||||
customer.save()
|
||||
|
||||
details = get_item_details(args)
|
||||
self.assertEqual(details.get("discount_percentage"), 0)
|
||||
|
||||
customer.territory = territory
|
||||
customer.save()
|
||||
|
||||
def test_pricing_rule_for_variants(self):
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
|
||||
|
||||
@@ -223,6 +223,10 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
|
||||
elif allow_blank:
|
||||
condition = f"ifnull({table}.{field}, '') = ''"
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
original_gle = [
|
||||
["Debtors - _TC", 3000.0, 0, "2023-07-01"],
|
||||
[deferred_account, 0.0, 3000, "2023-07-01"],
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
|
||||
process_deferred_accounting = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
@@ -63,6 +70,12 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2023-07-01")
|
||||
|
||||
# cancel the process deferred accounting document
|
||||
process_deferred_accounting.cancel()
|
||||
|
||||
# check if gl entries are cancelled
|
||||
check_gl_entries(self, si.name, original_gle, "2023-07-01")
|
||||
change_acc_settings()
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
|
||||
@@ -54,6 +54,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("account", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -61,6 +64,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("cost_center", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -68,6 +74,9 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
};
|
||||
});
|
||||
frm.set_query("project", function () {
|
||||
if (!frm.doc.company) {
|
||||
frappe.throw(__("Please set Company"));
|
||||
}
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
@@ -79,6 +88,11 @@ frappe.ui.form.on("Process Statement Of Accounts", {
|
||||
frm.set_value("to_date", frappe.datetime.get_today());
|
||||
}
|
||||
},
|
||||
company: function (frm) {
|
||||
frm.set_value("account", "");
|
||||
frm.set_value("cost_center", "");
|
||||
frm.set_value("project", "");
|
||||
},
|
||||
report: function (frm) {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
|
||||
@@ -376,7 +376,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "ignore_exchange_rate_revaluation_journals",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Exchange Rate Revaluation Journals"
|
||||
"label": "Ignore Exchange Rate Revaluation and Gain / Loss Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -400,7 +400,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-04-30 14:43:23.643006",
|
||||
"modified": "2025-07-08 16:52:12.602384",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -82,6 +82,10 @@ class ProcessStatementOfAccounts(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
self.validate_company_for_table("Cost Center")
|
||||
self.validate_company_for_table("Project")
|
||||
|
||||
if not self.subject:
|
||||
self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
|
||||
if not self.body:
|
||||
@@ -104,6 +108,43 @@ class ProcessStatementOfAccounts(Document):
|
||||
self.to_date = self.start_date
|
||||
self.from_date = add_months(self.to_date, -1 * self.filter_duration)
|
||||
|
||||
def validate_account(self):
|
||||
if not self.account:
|
||||
return
|
||||
|
||||
if self.company != frappe.get_cached_value("Account", self.account, "company"):
|
||||
frappe.throw(
|
||||
_("Account {0} doesn't belong to Company {1}").format(
|
||||
frappe.bold(self.account),
|
||||
frappe.bold(self.company),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_company_for_table(self, doctype):
|
||||
field = frappe.scrub(doctype)
|
||||
if not self.get(field):
|
||||
return
|
||||
|
||||
fieldname = field + "_name"
|
||||
|
||||
values = set(d.get(fieldname) for d in self.get(field))
|
||||
invalid_values = frappe.db.get_all(
|
||||
doctype, filters={"name": ["in", values], "company": ["!=", self.company]}, pluck="name"
|
||||
)
|
||||
|
||||
if invalid_values:
|
||||
msg = _("<p>Following {0}s doesn't belong to Company {1} :</p>").format(
|
||||
doctype, frappe.bold(self.company)
|
||||
)
|
||||
|
||||
msg += (
|
||||
"<ul>"
|
||||
+ "".join(_("<li>{}</li>").format(frappe.bold(row)) for row in invalid_values)
|
||||
+ "</ul>"
|
||||
)
|
||||
|
||||
frappe.throw(_(msg))
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
|
||||
@@ -1336,17 +1336,12 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
warehouse_debit_amount = stock_amount
|
||||
|
||||
elif self.is_return and self.update_stock and self.is_internal_supplier and warehouse_debit_amount:
|
||||
elif self.is_return and self.update_stock and (self.is_internal_supplier or not self.return_against):
|
||||
net_rate = item.base_net_amount
|
||||
if item.sales_incoming_rate: # for internal transfer
|
||||
net_rate = item.qty * item.sales_incoming_rate
|
||||
|
||||
stock_amount = (
|
||||
net_rate
|
||||
+ item.item_tax_amount
|
||||
+ flt(item.landed_cost_voucher_amount)
|
||||
+ flt(item.get("amount_difference_with_purchase_invoice"))
|
||||
)
|
||||
stock_amount = net_rate + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
|
||||
|
||||
if flt(stock_amount, net_amt_precision) != flt(warehouse_debit_amount, net_amt_precision):
|
||||
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
|
||||
|
||||
@@ -2024,6 +2024,7 @@
|
||||
"fieldname": "amount_eligible_for_commission",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -2188,7 +2189,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2025-03-17 19:32:31.809658",
|
||||
"modified": "2025-06-26 14:06:56.773552",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -2497,6 +2497,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Days", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2551,6 +2555,10 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"book_deferred_entries_based_on": "Months", "book_deferred_entries_via_journal_entry": 0},
|
||||
)
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -2558,10 +2566,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
@@ -2601,10 +2605,6 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Days"
|
||||
acc_settings.save()
|
||||
|
||||
def test_validate_inter_company_transaction_address_links(self):
|
||||
def _validate_address_link(address, link_doctype, link_name):
|
||||
return frappe.db.get_value(
|
||||
@@ -2853,7 +2853,9 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(si.items[0].rate, rate)
|
||||
self.assertEqual(target_doc.items[0].rate, rate)
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
check_gl_entries(
|
||||
self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1), voucher_type="Purchase Invoice"
|
||||
)
|
||||
|
||||
def test_internal_transfer_gl_precision_issues(self):
|
||||
# Make a stock queue of an item with two valuations
|
||||
@@ -4587,6 +4589,8 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date, voucher_type="
|
||||
)
|
||||
gl_entries = q.run(as_dict=True)
|
||||
|
||||
doc.assertGreater(len(gl_entries), 0)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
doc.assertEqual(expected_gle[i][0], gle.account)
|
||||
doc.assertEqual(expected_gle[i][1], gle.debit)
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_advance_payment_doctypes,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
@@ -84,7 +85,7 @@ class UnreconcilePayment(Document):
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
if doc.doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if doc.doctype in get_advance_payment_doctypes():
|
||||
doc.set_total_advance_paid()
|
||||
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
@@ -692,7 +692,18 @@ def make_reverse_gl_entries(
|
||||
query.run()
|
||||
else:
|
||||
if not immutable_ledger_enabled:
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
gle_names = [x.get("name") for x in gl_entries]
|
||||
|
||||
# if names are available, cancel only that set of entries
|
||||
if not all(gle_names):
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
else:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabGL Entry` SET is_cancelled = 1,
|
||||
modified=%s, modified_by=%s
|
||||
where name in %s and is_cancelled = 0""",
|
||||
(now(), frappe.session.user, tuple(gle_names)),
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
new_gle = copy.deepcopy(entry)
|
||||
|
||||
@@ -15,7 +15,11 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes,
|
||||
get_currency_precision,
|
||||
get_party_types_from_account_type,
|
||||
)
|
||||
|
||||
# This report gives a summary of all Outstanding Invoices considering the following
|
||||
|
||||
@@ -62,6 +66,7 @@ class ReceivablePayableReport:
|
||||
frappe.db.get_single_value("Accounts Settings", "receivable_payable_fetch_method")
|
||||
or "Buffered Cursor"
|
||||
) # Fail Safe
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
def run(self, args):
|
||||
self.filters.update(args)
|
||||
@@ -85,6 +90,7 @@ class ReceivablePayableReport:
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
self.advance_payment_doctypes = get_advance_payment_doctypes()
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.previous_party = ""
|
||||
@@ -181,7 +187,10 @@ class ReceivablePayableReport:
|
||||
if key not in self.voucher_balance:
|
||||
self.voucher_balance[key] = self.build_voucher_dict(ple)
|
||||
|
||||
if ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no:
|
||||
if (ple.voucher_type == ple.against_voucher_type and ple.voucher_no == ple.against_voucher_no) or (
|
||||
ple.voucher_type in ("Payment Entry", "Journal Entry")
|
||||
and ple.against_voucher_type in self.advance_payment_doctypes
|
||||
):
|
||||
self.voucher_balance[key].cost_center = ple.cost_center
|
||||
|
||||
self.get_invoices(ple)
|
||||
|
||||
@@ -79,6 +79,14 @@ class Deferred_Item:
|
||||
return - estimated amount to post for given period
|
||||
Calculated based on already booked amount and item service period
|
||||
"""
|
||||
if self.filters.book_deferred_entries_based_on == "Months":
|
||||
# if the deferred entries are based on service period, use service start and end date
|
||||
return self.calculate_monthly_amount(start_date, end_date)
|
||||
|
||||
else:
|
||||
return self.calculate_days_amount(start_date, end_date)
|
||||
|
||||
def calculate_monthly_amount(self, start_date, end_date):
|
||||
total_months = (
|
||||
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||
+ (self.service_end_date.month - self.service_start_date.month)
|
||||
@@ -105,6 +113,19 @@ class Deferred_Item:
|
||||
|
||||
return base_amount
|
||||
|
||||
def calculate_days_amount(self, start_date, end_date):
|
||||
base_amount = 0
|
||||
total_days = date_diff(self.service_end_date, self.service_start_date) + 1
|
||||
total_booking_days = date_diff(end_date, start_date) + 1
|
||||
already_booked_amount = self.get_item_total()
|
||||
|
||||
base_amount = flt(self.base_net_amount * total_booking_days / flt(total_days))
|
||||
|
||||
if base_amount + already_booked_amount > self.base_net_amount:
|
||||
base_amount = self.base_net_amount - already_booked_amount
|
||||
|
||||
return base_amount
|
||||
|
||||
def make_dummy_gle(self, name, date, amount):
|
||||
"""
|
||||
return - frappe._dict() of a dummy gle entry
|
||||
@@ -245,6 +266,10 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
else:
|
||||
self.filters = frappe._dict(filters)
|
||||
|
||||
self.filters.book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
|
||||
self.period_list = None
|
||||
self.deferred_invoices = []
|
||||
# holds period wise total for report
|
||||
@@ -289,7 +314,11 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
.join(inv)
|
||||
.on(inv.name == inv_item.parent)
|
||||
.left_join(gle)
|
||||
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||
.on(
|
||||
(inv_item.name == gle.voucher_detail_no)
|
||||
& (deferred_account_field == gle.account)
|
||||
& (gle.is_cancelled == 0)
|
||||
)
|
||||
.select(
|
||||
inv.name.as_("doc"),
|
||||
inv.posting_date,
|
||||
|
||||
@@ -9,7 +9,7 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Max, Min, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@@ -109,14 +109,19 @@ def get_period_list(
|
||||
|
||||
|
||||
def get_fiscal_year_data(from_fiscal_year, to_fiscal_year):
|
||||
fiscal_year = frappe.db.sql(
|
||||
"""select min(year_start_date) as year_start_date,
|
||||
max(year_end_date) as year_end_date from `tabFiscal Year` where
|
||||
name between %(from_fiscal_year)s and %(to_fiscal_year)s""",
|
||||
{"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year},
|
||||
as_dict=1,
|
||||
from_year_start_date = frappe.get_cached_value("Fiscal Year", from_fiscal_year, "year_start_date")
|
||||
to_year_end_date = frappe.get_cached_value("Fiscal Year", to_fiscal_year, "year_end_date")
|
||||
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(Min(fy.year_start_date).as_("year_start_date"), Max(fy.year_end_date).as_("year_end_date"))
|
||||
.where(fy.year_start_date >= from_year_start_date)
|
||||
.where(fy.year_end_date <= to_year_end_date)
|
||||
)
|
||||
|
||||
fiscal_year = query.run(as_dict=True)
|
||||
return fiscal_year[0] if fiscal_year else {}
|
||||
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
},
|
||||
{
|
||||
fieldname: "ignore_err",
|
||||
label: __("Ignore Exchange Rate Revaluation Journals"),
|
||||
label: __("Ignore Exchange Rate Revaluation and Gain / Loss Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,12 +6,9 @@ from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import flt, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import execute
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
|
||||
class TestGeneralLedger(FrappeTestCase):
|
||||
@@ -171,90 +168,6 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
self.assertEqual(data[3]["debit"], 100)
|
||||
self.assertEqual(data[3]["credit"], 100)
|
||||
|
||||
@change_settings("Accounts Settings", {"delete_linked_ledger_entries": True})
|
||||
def test_debit_in_exchange_gain_loss_account(self):
|
||||
company = "_Test Company"
|
||||
|
||||
exchange_gain_loss_account = frappe.db.get_value("Company", "exchange_gain_loss_account")
|
||||
if not exchange_gain_loss_account:
|
||||
frappe.db.set_value(
|
||||
"Company", company, "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
)
|
||||
|
||||
account_name = "_Test Receivable USD - _TC"
|
||||
customer_name = "_Test Customer USD"
|
||||
|
||||
sales_invoice = create_sales_invoice(
|
||||
company=company,
|
||||
customer=customer_name,
|
||||
currency="USD",
|
||||
debit_to=account_name,
|
||||
conversion_rate=85,
|
||||
posting_date=today(),
|
||||
)
|
||||
|
||||
payment_entry = create_payment_entry(
|
||||
company=company,
|
||||
party_type="Customer",
|
||||
party=customer_name,
|
||||
payment_type="Receive",
|
||||
paid_from=account_name,
|
||||
paid_from_account_currency="USD",
|
||||
paid_to="Cash - _TC",
|
||||
paid_to_account_currency="INR",
|
||||
paid_amount=10,
|
||||
do_not_submit=True,
|
||||
)
|
||||
payment_entry.base_paid_amount = 800
|
||||
payment_entry.received_amount = 800
|
||||
payment_entry.currency = "USD"
|
||||
payment_entry.source_exchange_rate = 80
|
||||
payment_entry.append(
|
||||
"references",
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"reference_name": sales_invoice.name,
|
||||
"total_amount": 10,
|
||||
"outstanding_amount": 10,
|
||||
"exchange_rate": 85,
|
||||
"allocated_amount": 10,
|
||||
"exchange_gain_loss": -50,
|
||||
}
|
||||
),
|
||||
)
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
journal_entry = frappe.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": sales_invoice.name}, fields=["parent"]
|
||||
)
|
||||
|
||||
columns, data = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"company": company,
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"include_dimensions": 1,
|
||||
"include_default_book_entries": 1,
|
||||
"account": ["_Test Exchange Gain/Loss - _TC"],
|
||||
"categorize_by": "Categorize by Voucher (Consolidated)",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
entry = data[1]
|
||||
self.assertEqual(entry["debit"], 50)
|
||||
self.assertEqual(entry["voucher_type"], "Journal Entry")
|
||||
self.assertEqual(entry["voucher_no"], journal_entry[0]["parent"])
|
||||
|
||||
payment_entry.cancel()
|
||||
payment_entry.delete()
|
||||
sales_invoice.reload()
|
||||
sales_invoice.cancel()
|
||||
sales_invoice.delete()
|
||||
|
||||
def test_ignore_exchange_rate_journals_filter(self):
|
||||
# create a new account with USD currency
|
||||
account_name = "Test Debtors USD"
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import (
|
||||
get_customer_details,
|
||||
)
|
||||
@@ -433,7 +434,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
si.is_internal_customer,
|
||||
si.customer,
|
||||
si.remarks,
|
||||
si.territory,
|
||||
fn.IfNull(si.territory, "Not Specified").as_("territory"),
|
||||
si.company,
|
||||
si.base_net_total,
|
||||
sii.project,
|
||||
@@ -456,7 +457,7 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
sii.base_net_rate,
|
||||
sii.base_net_amount,
|
||||
si.customer_name,
|
||||
si.customer_group,
|
||||
fn.IfNull(si.customer_group, "Not Specified").as_("customer_group"),
|
||||
sii.so_detail,
|
||||
si.update_stock,
|
||||
sii.uom,
|
||||
|
||||
@@ -121,7 +121,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@@ -67,11 +67,12 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
mid_year = add_to_date(fiscal_year[1], months=6)
|
||||
tds_doc = frappe.get_doc("Tax Withholding Category", "TDS - 3")
|
||||
tds_doc.rates[0].to_date = mid_year
|
||||
from_date = add_to_date(mid_year, days=1)
|
||||
tds_doc.append(
|
||||
"rates",
|
||||
{
|
||||
"tax_withholding_rate": 20,
|
||||
"from_date": add_to_date(mid_year, days=1),
|
||||
"from_date": from_date,
|
||||
"to_date": fiscal_year[2],
|
||||
"single_threshold": 1,
|
||||
"cumulative_threshold": 1,
|
||||
@@ -80,18 +81,19 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
tds_doc.save()
|
||||
|
||||
inv_1 = make_purchase_invoice(rate=1000, do_not_submit=True)
|
||||
inv_1 = make_purchase_invoice(
|
||||
rate=1000, posting_date=add_to_date(fiscal_year[1], days=1), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
inv_1.set_posting_time = 1
|
||||
inv_1.apply_tds = 1
|
||||
inv_1.tax_withholding_category = "TDS - 3"
|
||||
inv_1.tax_withholding_category = tds_doc.name
|
||||
inv_1.save()
|
||||
inv_1.submit()
|
||||
|
||||
inv_2 = make_purchase_invoice(
|
||||
rate=1000, do_not_submit=True, posting_date=add_to_date(mid_year, days=1), do_not_save=True
|
||||
)
|
||||
inv_2 = make_purchase_invoice(rate=1000, posting_date=from_date, do_not_save=True, do_not_submit=True)
|
||||
inv_2.set_posting_time = 1
|
||||
|
||||
inv_1.apply_tds = 1
|
||||
inv_2.tax_withholding_category = "TDS - 3"
|
||||
inv_2.apply_tds = 1
|
||||
inv_2.tax_withholding_category = tds_doc.name
|
||||
inv_2.save()
|
||||
inv_2.submit()
|
||||
|
||||
|
||||
@@ -107,11 +107,7 @@ def convert_to_presentation_currency(gl_entries, currency_info):
|
||||
credit_in_account_currency = flt(entry["credit_in_account_currency"])
|
||||
account_currency = entry["account_currency"]
|
||||
|
||||
if (
|
||||
len(account_currencies) == 1
|
||||
and account_currency == presentation_currency
|
||||
and (debit_in_account_currency or credit_in_account_currency)
|
||||
):
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
entry["debit"] = debit_in_account_currency
|
||||
entry["credit"] = credit_in_account_currency
|
||||
else:
|
||||
|
||||
@@ -169,7 +169,7 @@ def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
|
||||
if doc:
|
||||
doc.fiscal_year = years[0]
|
||||
else:
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(label, formatdate(date), fiscal_year))
|
||||
throw(_("{0} '{1}' not in Fiscal Year {2}").format(_(label), formatdate(date), fiscal_year))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -629,6 +629,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
update_advance_paid = []
|
||||
|
||||
if jv_detail.get("reference_type") in ["Sales Order", "Purchase Order"]:
|
||||
update_advance_paid.append((jv_detail.reference_type, jv_detail.reference_name))
|
||||
|
||||
@@ -713,23 +714,8 @@ def update_reference_in_payment_entry(
|
||||
update_advance_paid = []
|
||||
|
||||
# Update Reconciliation effect date in reference
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", payment_entry.company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
if payment_entry.book_advance_payments_in_separate_party_account:
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if d.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(d.against_voucher_type, d.against_voucher, date_field)
|
||||
|
||||
if getdate(reconcile_on) < getdate(payment_entry.posting_date):
|
||||
reconcile_on = payment_entry.posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
reconcile_on = get_reconciliation_effect_date(d, payment_entry.company, payment_entry.posting_date)
|
||||
reference_details.update({"reconcile_effect_on": reconcile_on})
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@@ -783,6 +769,28 @@ def update_reference_in_payment_entry(
|
||||
return row, update_advance_paid
|
||||
|
||||
|
||||
def get_reconciliation_effect_date(reference, company, posting_date):
|
||||
reconciliation_takes_effect_on = frappe.get_cached_value(
|
||||
"Company", company, "reconciliation_takes_effect_on"
|
||||
)
|
||||
|
||||
if reconciliation_takes_effect_on == "Advance Payment Date":
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Oldest Of Invoice Or Advance":
|
||||
date_field = "posting_date"
|
||||
if reference.against_voucher_type in ["Sales Order", "Purchase Order"]:
|
||||
date_field = "transaction_date"
|
||||
reconcile_on = frappe.db.get_value(
|
||||
reference.against_voucher_type, reference.against_voucher, date_field
|
||||
)
|
||||
if getdate(reconcile_on) < getdate(posting_date):
|
||||
reconcile_on = posting_date
|
||||
elif reconciliation_takes_effect_on == "Reconciliation Date":
|
||||
reconcile_on = nowdate()
|
||||
|
||||
return reconcile_on
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str | None = None, referenced_dn: str | None = None
|
||||
) -> None:
|
||||
@@ -997,58 +1005,79 @@ def remove_ref_doc_link_from_pe(
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
pay = qb.DocType("Payment Entry")
|
||||
|
||||
linked_pe = (
|
||||
query = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.where((per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2)))
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_pe = convert_to_list(linked_pe)
|
||||
# remove reference only from specified payment
|
||||
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
|
||||
|
||||
if linked_pe:
|
||||
update_query = (
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
|
||||
.select("*")
|
||||
.where(
|
||||
(per.reference_doctype == ref_type)
|
||||
& (per.reference_name == ref_no)
|
||||
& (per.docstatus.lt(2))
|
||||
& (per.parenttype == "Payment Entry")
|
||||
)
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(per.parent == payment_name)
|
||||
# update reference only from specified payment
|
||||
if payment_name:
|
||||
query = query.where(per.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
reference_rows = query.run(as_dict=True)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
if not reference_rows:
|
||||
return
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [
|
||||
x
|
||||
for x in pe_doc.references
|
||||
if x.reference_doctype == ref_type and x.reference_name == ref_no
|
||||
]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
linked_pe = set()
|
||||
row_names = set()
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
for row in reference_rows:
|
||||
linked_pe.add(row.parent)
|
||||
row_names.add(row.name)
|
||||
|
||||
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
|
||||
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
|
||||
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
|
||||
pay.modified_by, frappe.session.user
|
||||
).where(pay.name == pe).run()
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import (
|
||||
update_payment_requests_as_per_pe_references,
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
# Update payment request amount
|
||||
update_payment_requests_as_per_pe_references(reference_rows, cancel=True)
|
||||
|
||||
# Update allocated amounts and modified fields in one go
|
||||
(
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(per.name.isin(row_names))
|
||||
.where(per.parenttype == "Payment Entry")
|
||||
.run()
|
||||
)
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
pe_doc.set_amounts()
|
||||
|
||||
# Call cancel on only removed reference
|
||||
references = [x for x in pe_doc.references if x.name in row_names]
|
||||
[pe_doc.make_advance_gl_entries(x, cancel=1) for x in references]
|
||||
|
||||
pe_doc.clear_unallocated_reference_document_rows()
|
||||
pe_doc.validate_payment_type_with_outstanding()
|
||||
except Exception:
|
||||
msg = _("There were issues unlinking payment entry {0}.").format(pe_doc.name)
|
||||
msg += "<br>"
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
|
||||
(
|
||||
qb.update(pay)
|
||||
.set(pay.total_allocated_amount, pe_doc.total_allocated_amount)
|
||||
.set(pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount)
|
||||
.set(pay.unallocated_amount, pe_doc.unallocated_amount)
|
||||
.set(pay.modified, now())
|
||||
.set(pay.modified_by, frappe.session.user)
|
||||
.where(pay.name == pe)
|
||||
.run()
|
||||
)
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -2235,6 +2264,15 @@ def get_party_types_from_account_type(account_type):
|
||||
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")
|
||||
|
||||
|
||||
def get_advance_payment_doctypes():
|
||||
"""
|
||||
Get list of advance payment doctypes based on type.
|
||||
:param type: Optional, can be "receivable" or "payable". If not provided, returns both.
|
||||
"""
|
||||
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
|
||||
def run_ledger_health_checks():
|
||||
health_monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
if health_monitor_settings.enable_health_monitor:
|
||||
|
||||
@@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 0, reqd: 0 },
|
||||
to_employee: { read_only: 1, reqd: 0 },
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 0 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
@@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", {
|
||||
from_employee: { read_only: 1, reqd: 0 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
} else if (frm.doc.purpose === "Transfer and Issue") {
|
||||
fieldnames_to_be_altered = {
|
||||
target_location: { read_only: 0, reqd: 1 },
|
||||
source_location: { read_only: 0, reqd: 1 },
|
||||
from_employee: { read_only: 0, reqd: 1 },
|
||||
to_employee: { read_only: 0, reqd: 1 },
|
||||
};
|
||||
}
|
||||
if (fieldnames_to_be_altered) {
|
||||
Object.keys(fieldnames_to_be_altered).forEach((fieldname) => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"fieldname": "purpose",
|
||||
"fieldtype": "Select",
|
||||
"label": "Purpose",
|
||||
"options": "\nIssue\nReceipt\nTransfer",
|
||||
"options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -93,10 +93,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-28 16:54:26.571083",
|
||||
"modified": "2025-05-30 17:01:55.864353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Movement",
|
||||
@@ -149,7 +150,8 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,105 +24,81 @@ class AssetMovement(Document):
|
||||
amended_from: DF.Link | None
|
||||
assets: DF.Table[AssetMovementItem]
|
||||
company: DF.Link
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer"]
|
||||
purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"]
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
transaction_date: DF.Datetime
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_asset()
|
||||
self.validate_location()
|
||||
self.validate_employee()
|
||||
|
||||
def validate_asset(self):
|
||||
for d in self.assets:
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
self.validate_asset(d)
|
||||
self.validate_movement(d)
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
def validate_asset(self, d):
|
||||
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
|
||||
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
|
||||
frappe.throw(_("{0} asset cannot be transferred").format(status))
|
||||
|
||||
if not (d.source_location or d.target_location or d.from_employee or d.to_employee):
|
||||
frappe.throw(_("Either location or employee must be required"))
|
||||
if company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
|
||||
|
||||
def validate_location(self):
|
||||
for d in self.assets:
|
||||
if self.purpose in ["Transfer", "Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the location {1}").format(
|
||||
d.asset, d.source_location
|
||||
)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
def validate_movement(self, d):
|
||||
if self.purpose == "Transfer and Issue":
|
||||
self.validate_location_and_employee(d)
|
||||
elif self.purpose in ["Receipt", "Transfer"]:
|
||||
self.validate_location(d)
|
||||
else:
|
||||
self.validate_employee(d)
|
||||
|
||||
if self.purpose == "Issue":
|
||||
if d.target_location:
|
||||
def validate_location_and_employee(self, d):
|
||||
self.validate_location(d)
|
||||
self.validate_employee(d)
|
||||
|
||||
def validate_location(self, d):
|
||||
if self.purpose in ["Transfer", "Transfer and Issue"]:
|
||||
current_location = frappe.db.get_value("Asset", d.asset, "location")
|
||||
if d.source_location:
|
||||
if current_location != d.source_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if self.purpose == "Transfer":
|
||||
if d.to_employee:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
|
||||
).format(d.asset),
|
||||
title=_("Incorrect Movement Purpose"),
|
||||
)
|
||||
if not d.target_location:
|
||||
frappe.throw(
|
||||
_("Target Location is required while transferring Asset {0}").format(d.asset)
|
||||
)
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not (d.source_location) and not (d.target_location or d.to_employee):
|
||||
frappe.throw(
|
||||
_("Target Location or To Employee is required while receiving Asset {0}").format(
|
||||
d.asset
|
||||
)
|
||||
)
|
||||
elif d.source_location:
|
||||
if d.from_employee and not d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Target Location is required while receiving Asset {0} from an employee"
|
||||
).format(d.asset)
|
||||
)
|
||||
elif d.to_employee and d.target_location:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset {0} cannot be received at a location and given to an employee in a single movement"
|
||||
).format(d.asset)
|
||||
)
|
||||
|
||||
def validate_employee(self):
|
||||
for d in self.assets:
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
_("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
|
||||
)
|
||||
else:
|
||||
d.source_location = current_location
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset))
|
||||
if d.source_location == d.target_location:
|
||||
frappe.throw(_("Source and Target Location cannot be same"))
|
||||
|
||||
if self.purpose == "Receipt":
|
||||
if not d.target_location:
|
||||
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def validate_employee(self, d):
|
||||
if self.purpose == "Tranfer and Issue":
|
||||
if not d.from_employee:
|
||||
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.from_employee:
|
||||
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
|
||||
|
||||
if current_custodian != d.from_employee:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
|
||||
)
|
||||
|
||||
if not d.to_employee:
|
||||
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
|
||||
|
||||
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
|
||||
frappe.throw(
|
||||
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
|
||||
)
|
||||
|
||||
def on_submit(self):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
@@ -130,53 +106,63 @@ class AssetMovement(Document):
|
||||
self.set_latest_location_and_custodian_in_asset()
|
||||
|
||||
def set_latest_location_and_custodian_in_asset(self):
|
||||
for d in self.assets:
|
||||
current_location, current_employee = self.get_latest_location_and_custodian(d.asset)
|
||||
self.update_asset_location_and_custodian(d.asset, current_location, current_employee)
|
||||
self.log_asset_activity(d.asset, current_location, current_employee)
|
||||
|
||||
def get_latest_location_and_custodian(self, asset):
|
||||
current_location, current_employee = "", ""
|
||||
cond = "1=1"
|
||||
|
||||
for d in self.assets:
|
||||
args = {"asset": d.asset, "company": self.company}
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
args = {"asset": asset, "company": self.company}
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item
|
||||
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
|
||||
WHERE
|
||||
asm_item.asset = %(asset)s AND
|
||||
asm.company = %(company)s AND
|
||||
asm.docstatus = 1 AND {cond}
|
||||
ORDER BY asm.transaction_date DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
args,
|
||||
)
|
||||
|
||||
# latest entry corresponds to current document's location, employee when transaction date > previous dates
|
||||
# In case of cancellation it corresponds to previous latest document's location, employee
|
||||
latest_movement_entry = frappe.db.sql(
|
||||
f"""
|
||||
SELECT asm_item.target_location, asm_item.to_employee
|
||||
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm
|
||||
WHERE
|
||||
asm_item.parent=asm.name and
|
||||
asm_item.asset=%(asset)s and
|
||||
asm.company=%(company)s and
|
||||
asm.docstatus=1 and {cond}
|
||||
ORDER BY
|
||||
asm.transaction_date desc limit 1
|
||||
""",
|
||||
args,
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
return current_location, current_employee
|
||||
|
||||
def update_asset_location_and_custodian(self, asset_id, location, employee):
|
||||
asset = frappe.get_doc("Asset", asset_id)
|
||||
|
||||
if employee and employee != asset.custodian:
|
||||
frappe.db.set_value("Asset", asset_id, "custodian", employee)
|
||||
if location and location != asset.location:
|
||||
frappe.db.set_value("Asset", asset_id, "location", location)
|
||||
|
||||
def log_asset_activity(self, asset_id, location, employee):
|
||||
if location and employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", location),
|
||||
get_link_to_form("Employee", employee),
|
||||
),
|
||||
)
|
||||
elif location:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset transferred to Location {0}").format(get_link_to_form("Location", location)),
|
||||
)
|
||||
elif employee:
|
||||
add_asset_activity(
|
||||
asset_id,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)),
|
||||
)
|
||||
|
||||
if latest_movement_entry:
|
||||
current_location = latest_movement_entry[0][0]
|
||||
current_employee = latest_movement_entry[0][1]
|
||||
|
||||
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False)
|
||||
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
|
||||
|
||||
if current_location and current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset received at Location {0} and issued to Employee {1}").format(
|
||||
get_link_to_form("Location", current_location),
|
||||
get_link_to_form("Employee", current_employee),
|
||||
),
|
||||
)
|
||||
elif current_location:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset transferred to Location {0}").format(
|
||||
get_link_to_form("Location", current_location)
|
||||
),
|
||||
)
|
||||
elif current_employee:
|
||||
add_asset_activity(
|
||||
d.asset,
|
||||
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)),
|
||||
)
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestAssetMovement(unittest.TestCase):
|
||||
)
|
||||
|
||||
# after issuing, asset should belong to an employee not at a location
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None)
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
|
||||
|
||||
create_asset_movement(
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.ui.form.on("Asset Repair", {
|
||||
setup: function (frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frm.fields_dict.cost_center.get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
@@ -167,4 +169,37 @@ frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, "total_value", row.consumed_quantity * row.valuation_rate);
|
||||
},
|
||||
|
||||
pick_serial_and_batch(frm, cdt, cdn) {
|
||||
let item = locals[cdt][cdn];
|
||||
let doc = frm.doc;
|
||||
|
||||
frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]).then((r) => {
|
||||
if (r.message && (r.message.has_batch_no || r.message.has_serial_no)) {
|
||||
item.has_serial_no = r.message.has_serial_no;
|
||||
item.has_batch_no = r.message.has_batch_no;
|
||||
item.qty = item.consumed_quantity;
|
||||
item.type_of_transaction = item.consumed_quantity > 0 ? "Outward" : "Inward";
|
||||
|
||||
item.title = item.has_serial_no ? __("Select Serial No") : __("Select Batch No");
|
||||
|
||||
if (item.has_serial_no && item.has_batch_no) {
|
||||
item.title = __("Select Serial and Batch");
|
||||
}
|
||||
frm.doc.posting_date = frappe.datetime.get_today();
|
||||
frm.doc.posting_time = frappe.datetime.now_time();
|
||||
|
||||
new erpnext.SerialBatchPackageSelector(frm, item, (r) => {
|
||||
if (r) {
|
||||
frappe.model.set_value(item.doctype, item.name, {
|
||||
serial_and_batch_bundle: r.name,
|
||||
use_serial_batch_fields: 0,
|
||||
valuation_rate: r.avg_rate,
|
||||
consumed_quantity: Math.abs(r.total_qty),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -138,6 +138,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -248,7 +249,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 16:14:14.398356",
|
||||
"modified": "2025-06-29 22:30:00.589597",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
@@ -292,4 +293,4 @@
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class AssetRepair(AccountsController):
|
||||
|
||||
def validate(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
self.validate_asset()
|
||||
self.validate_dates()
|
||||
self.update_status()
|
||||
|
||||
@@ -60,6 +61,14 @@ class AssetRepair(AccountsController):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
frappe.throw(
|
||||
_("Asset {0} is in {1} status and cannot be repaired.").format(
|
||||
get_link_to_form("Asset", self.asset), self.asset_doc.status
|
||||
)
|
||||
)
|
||||
|
||||
def validate_dates(self):
|
||||
if self.completion_date and (self.failure_date > self.completion_date):
|
||||
frappe.throw(
|
||||
@@ -131,6 +140,13 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
def cancel_sabb(self):
|
||||
for row in self.stock_items:
|
||||
if sabb := row.serial_and_batch_bundle:
|
||||
row.db_set("serial_and_batch_bundle", None)
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", sabb)
|
||||
doc.cancel()
|
||||
|
||||
def before_cancel(self):
|
||||
self.asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
|
||||
@@ -172,6 +188,8 @@ class AssetRepair(AccountsController):
|
||||
),
|
||||
)
|
||||
|
||||
self.cancel_sabb()
|
||||
|
||||
def after_delete(self):
|
||||
frappe.get_doc("Asset", self.asset).set_status()
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, nowdate, nowtime, today
|
||||
from frappe.utils import add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
@@ -33,6 +34,33 @@ class TestAssetRepair(unittest.TestCase):
|
||||
create_item("_Test Stock Item")
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_asset_status(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.due_date = date
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({"company": "_Test Company", "asset": asset.name, "asset_name": asset.asset_name})
|
||||
self.assertRaises(frappe.ValidationError, asset_repair.save)
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
"consumed_quantity",
|
||||
"total_value",
|
||||
"serial_no",
|
||||
"column_break_xzfr",
|
||||
"pick_serial_and_batch",
|
||||
"serial_and_batch_bundle"
|
||||
],
|
||||
"fields": [
|
||||
@@ -61,12 +63,21 @@
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_serial_and_batch",
|
||||
"fieldtype": "Button",
|
||||
"label": "Pick Serial / Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xzfr",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-13 12:01:47.147333",
|
||||
"modified": "2025-06-27 14:52:56.311166",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
@@ -76,4 +87,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,6 +192,10 @@ class AssetValueAdjustment(Document):
|
||||
if asset.calculate_depreciation:
|
||||
for row in asset.finance_books:
|
||||
if cstr(row.finance_book) == cstr(self.finance_book):
|
||||
salvage_value_adjustment = (
|
||||
self.get_adjusted_salvage_value_amount(row, difference_amount) or 0
|
||||
)
|
||||
row.expected_value_after_useful_life += salvage_value_adjustment
|
||||
row.value_after_depreciation += flt(difference_amount)
|
||||
row.db_update()
|
||||
|
||||
@@ -208,6 +212,11 @@ class AssetValueAdjustment(Document):
|
||||
asset.save()
|
||||
asset.set_status()
|
||||
|
||||
def get_adjusted_salvage_value_amount(self, row, difference_amount):
|
||||
if row.expected_value_after_useful_life:
|
||||
salvage_value_adjustment = (difference_amount * row.salvage_value_percentage) / 100
|
||||
return flt(salvage_value_adjustment if self.docstatus == 1 else -1 * salvage_value_adjustment)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value_of_accounting_dimensions(asset_name):
|
||||
|
||||
@@ -292,6 +292,43 @@ class TestAssetValueAdjustment(unittest.TestCase):
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.value_after_depreciation, 50000.0)
|
||||
|
||||
def test_expected_value_after_useful_life(self):
|
||||
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location")
|
||||
|
||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
asset_doc.calculate_depreciation = 1
|
||||
asset_doc.available_for_use_date = "2023-01-15"
|
||||
asset_doc.purchase_date = "2023-01-15"
|
||||
|
||||
asset_doc.append(
|
||||
"finance_books",
|
||||
{
|
||||
"expected_value_after_useful_life": 5000,
|
||||
"salvage_value_percentage": 5,
|
||||
"depreciation_method": "Straight Line",
|
||||
"total_number_of_depreciations": 12,
|
||||
"frequency_of_depreciation": 1,
|
||||
"depreciation_start_date": "2023-01-31",
|
||||
},
|
||||
)
|
||||
asset_doc.submit()
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 5000.0)
|
||||
|
||||
current_asset_value = get_asset_value_after_depreciation(asset_doc.name)
|
||||
adj_doc = make_asset_value_adjustment(
|
||||
asset=asset_doc.name,
|
||||
current_asset_value=current_asset_value,
|
||||
new_asset_value=40000,
|
||||
date="2023-08-21",
|
||||
)
|
||||
adj_doc.submit()
|
||||
difference_amount = adj_doc.new_asset_value - adj_doc.current_asset_value
|
||||
self.assertEqual(difference_amount, -60000)
|
||||
asset_doc.load_from_db()
|
||||
self.assertEqual(asset_doc.finance_books[0].value_after_depreciation, 40000.0)
|
||||
self.assertEqual(asset_doc.finance_books[0].expected_value_after_useful_life, 2000.0)
|
||||
|
||||
|
||||
def make_asset_value_adjustment(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -807,11 +807,19 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.amount = flt(obj.amount) - flt(obj.billed_amt)
|
||||
target.base_amount = target.amount * flt(source_parent.conversion_rate)
|
||||
target.qty = (
|
||||
target.amount / flt(obj.rate) if (flt(obj.rate) and flt(obj.billed_amt)) else flt(obj.qty)
|
||||
)
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
|
||||
@@ -1286,6 +1286,25 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertFalse(po.per_billed)
|
||||
self.assertEqual(po.status, "To Receive and Bill")
|
||||
|
||||
@change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||
def test_purchase_invoice_creation_with_partial_qty(self):
|
||||
po = create_purchase_order(qty=100, rate=10)
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
pi.items[0].qty = 42
|
||||
pi.items[0].rate = 7.5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 58)
|
||||
self.assertEqual(pi.items[0].rate, 10)
|
||||
pi.items[0].qty = 8
|
||||
pi.items[0].rate = 5
|
||||
pi.submit()
|
||||
|
||||
pi = make_pi_from_po(po.name)
|
||||
self.assertEqual(pi.items[0].qty, 50)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -51,6 +51,9 @@ from erpnext.accounts.utils import (
|
||||
get_fiscal_years,
|
||||
validate_fiscal_year,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
get_advance_payment_doctypes as _get_advance_payment_doctypes,
|
||||
)
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@@ -386,9 +389,7 @@ class AccountsController(TransactionBase):
|
||||
adv = qb.DocType("Advance Payment Ledger Entry")
|
||||
qb.from_(adv).delete().where(adv.voucher_type.eq(self.doctype) & adv.voucher_no.eq(self.name)).run()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks("advance_payment_doctypes")
|
||||
|
||||
if self.doctype in advance_payment_doctypes:
|
||||
if self.doctype in self.get_advance_payment_doctypes():
|
||||
qb.from_(adv).delete().where(
|
||||
adv.against_voucher_type.eq(self.doctype) & adv.against_voucher_no.eq(self.name)
|
||||
).run()
|
||||
@@ -1134,6 +1135,10 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
|
||||
def set_taxes_and_charges(self):
|
||||
if self.doctype == "Material Request":
|
||||
# Material Request does not have taxes
|
||||
return
|
||||
|
||||
if self.get("taxes") or self.get("is_pos"):
|
||||
return
|
||||
|
||||
@@ -2908,7 +2913,7 @@ class AccountsController(TransactionBase):
|
||||
repost_ledger.submit()
|
||||
|
||||
def get_advance_payment_doctypes(self) -> list:
|
||||
return frappe.get_hooks("advance_payment_doctypes")
|
||||
return _get_advance_payment_doctypes()
|
||||
|
||||
def make_advance_payment_ledger_for_journal(self):
|
||||
advance_payment_doctypes = self.get_advance_payment_doctypes()
|
||||
@@ -3961,6 +3966,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
||||
@@ -156,6 +156,17 @@ status_map = {
|
||||
["Draft", None],
|
||||
["Completed", "eval:self.docstatus == 1"],
|
||||
],
|
||||
"Pick List": [
|
||||
["Draft", None],
|
||||
["Open", "eval:self.docstatus == 1"],
|
||||
["Completed", "stock_entry_exists"],
|
||||
[
|
||||
"Partly Delivered",
|
||||
"eval:self.purpose == 'Delivery' and self.delivery_status == 'Partly Delivered'",
|
||||
],
|
||||
["Completed", "eval:self.purpose == 'Delivery' and self.delivery_status == 'Fully Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1648,8 +1648,9 @@ def make_quality_inspections(doctype, docname, items):
|
||||
"sample_size": flt(item.get("sample_size")),
|
||||
"item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
|
||||
"batch_no": item.get("batch_no"),
|
||||
"child_row_reference": item.get("child_row_reference"),
|
||||
}
|
||||
).insert()
|
||||
)
|
||||
quality_inspection.save()
|
||||
inspections.append(quality_inspection.name)
|
||||
|
||||
@@ -1662,14 +1663,9 @@ def is_reposting_pending():
|
||||
)
|
||||
|
||||
|
||||
def future_sle_exists(args, sl_entries=None, allow_force_reposting=True):
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
if allow_force_reposting and frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_reposting_for_each_stock_transaction"
|
||||
):
|
||||
return True
|
||||
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
@@ -601,12 +601,15 @@ class SubcontractingController(StockController):
|
||||
rm_obj.use_serial_batch_fields = 1
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt" and not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
if not use_serial_batch_fields:
|
||||
rm_obj.serial_and_batch_bundle = self.__set_serial_and_batch_bundle(
|
||||
item_row, rm_obj, rm_obj.consumed_qty
|
||||
)
|
||||
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
self.set_rate_for_supplied_items(rm_obj, item_row)
|
||||
elif self.backflush_based_on == "BOM":
|
||||
self.update_rate_for_supplied_items()
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
|
||||
@@ -47,7 +47,7 @@ def get_columns(filters, trans):
|
||||
def validate_filters(filters):
|
||||
for f in ["Fiscal Year", "Based On", "Period", "Company"]:
|
||||
if not filters.get(f.lower().replace(" ", "_")):
|
||||
frappe.throw(_("{0} is mandatory").format(f))
|
||||
frappe.throw(_("{0} is mandatory").format(_(f)))
|
||||
|
||||
if not frappe.db.exists("Fiscal Year", filters.get("fiscal_year")):
|
||||
frappe.throw(_("Fiscal Year {0} Does Not Exist").format(filters.get("fiscal_year")))
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -512,11 +512,12 @@
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2025-01-31 13:40:08.094759",
|
||||
"modified": "2025-06-26 11:02:01.158901",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
@@ -575,6 +576,7 @@
|
||||
"role": "Sales User"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "lead_name,lead_owner,status",
|
||||
"sender_field": "email_id",
|
||||
"sender_name_field": "lead_name",
|
||||
|
||||
@@ -323,7 +323,8 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False):
|
||||
target.customer_type = "Individual"
|
||||
target.customer_name = source.lead_name
|
||||
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
if not target.customer_group:
|
||||
target.customer_group = frappe.db.get_default("Customer Group")
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Lead",
|
||||
|
||||
@@ -613,7 +613,7 @@
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
"label": "State/Province"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
@@ -622,10 +622,11 @@
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2024-08-20 04:12:29.095761",
|
||||
"modified": "2025-06-26 11:16:13.665866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
@@ -657,6 +658,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date,party_name,opportunity_type,territory,company",
|
||||
"sender_field": "contact_email",
|
||||
"show_name_in_global_search": 1,
|
||||
|
||||
@@ -58,7 +58,7 @@ def get_columns():
|
||||
{"label": _("Address"), "fieldname": "address", "fieldtype": "Data", "width": 130},
|
||||
{"label": _("Postal Code"), "fieldname": "pincode", "fieldtype": "Data", "width": 90},
|
||||
{"label": _("City"), "fieldname": "city", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("State/Province"), "fieldname": "state", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Country"),
|
||||
"fieldname": "country",
|
||||
|
||||
@@ -987,7 +987,7 @@ class BOM(WebsiteGenerator):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against and not self.is_new():
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
|
||||
@@ -51,9 +51,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let excess_transfer_allowed = frm.doc.__onload.job_card_excess_transfer;
|
||||
|
||||
if (to_request || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Request"), () => {
|
||||
frm.trigger("make_material_request");
|
||||
});
|
||||
frm.add_custom_button(
|
||||
__("Material Request"),
|
||||
() => {
|
||||
frm.trigger("make_material_request");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
|
||||
// check if any row has untransferred materials
|
||||
@@ -61,9 +65,13 @@ frappe.ui.form.on("Job Card", {
|
||||
let to_transfer = frm.doc.items.some((row) => row.transferred_qty < row.required_qty);
|
||||
|
||||
if (to_transfer || excess_transfer_allowed) {
|
||||
frm.add_custom_button(__("Material Transfer"), () => {
|
||||
frm.trigger("make_stock_entry");
|
||||
}).addClass("btn-primary");
|
||||
frm.add_custom_button(
|
||||
__("Material Transfer"),
|
||||
() => {
|
||||
frm.trigger("make_stock_entry");
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2368,6 +2368,105 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
stock_entry.submit()
|
||||
|
||||
def test_disassembly_order_with_qty_behavior(self):
|
||||
# Create raw material and FG item
|
||||
raw_item = make_item("Test Raw for Disassembly", {"is_stock_item": 1}).name
|
||||
fg_item = make_item("Test FG for Disassembly", {"is_stock_item": 1}).name
|
||||
bom = make_bom(item=fg_item, quantity=10, raw_materials=[raw_item], rm_qty=5)
|
||||
|
||||
# Create and submit a Work Order for 10 qty
|
||||
wo = make_wo_order_test_record(production_item=fg_item, qty=10, bom_no=bom.name, status="Not Started")
|
||||
|
||||
# create material receipt stock entry for raw material
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import (
|
||||
make_stock_entry as make_stock_entry_test_record,
|
||||
)
|
||||
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.wip_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
make_stock_entry_test_record(
|
||||
item_code=raw_item,
|
||||
purpose="Material Receipt",
|
||||
target=wo.fg_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
# create material transfer for manufacture stock entry
|
||||
se_for_material_tranfer_mfr = frappe.get_doc(
|
||||
make_stock_entry(wo.name, "Material Transfer for Manufacture", wo.qty)
|
||||
)
|
||||
se_for_material_tranfer_mfr.items[0].s_warehouse = wo.wip_warehouse
|
||||
se_for_material_tranfer_mfr.save()
|
||||
se_for_material_tranfer_mfr.submit()
|
||||
|
||||
se_for_manufacture = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", wo.qty))
|
||||
se_for_manufacture.submit()
|
||||
|
||||
# Simulate a disassembly stock entry
|
||||
disassemble_qty = 4
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo.name, "Disassemble", disassemble_qty))
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": fg_item,
|
||||
"qty": disassemble_qty,
|
||||
"s_warehouse": wo.fg_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
for bom_item in bom.items:
|
||||
stock_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": (bom_item.qty / bom.quantity) * disassemble_qty,
|
||||
"t_warehouse": wo.source_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
wo.reload()
|
||||
stock_entry.save()
|
||||
stock_entry.submit()
|
||||
|
||||
# Assert FG item is present with correct qty
|
||||
finished_good_entry = next((item for item in stock_entry.items if item.item_code == fg_item), None)
|
||||
self.assertIsNotNone(finished_good_entry, "Finished good item missing from stock entry")
|
||||
self.assertEqual(
|
||||
finished_good_entry.qty,
|
||||
disassemble_qty,
|
||||
f"Expected FG qty {disassemble_qty}, found {finished_good_entry.qty}",
|
||||
)
|
||||
|
||||
# Assert raw materials
|
||||
for item in stock_entry.items:
|
||||
if item.item_code == fg_item:
|
||||
continue
|
||||
bom_item = next((i for i in bom.items if i.item_code == item.item_code), None)
|
||||
if bom_item:
|
||||
expected_qty = (bom_item.qty / bom.quantity) * disassemble_qty
|
||||
self.assertAlmostEqual(
|
||||
item.qty,
|
||||
expected_qty,
|
||||
places=3,
|
||||
msg=f"Raw item {item.item_code} qty mismatch: expected {expected_qty}, got {item.qty}",
|
||||
)
|
||||
else:
|
||||
self.fail(f"Unexpected item {item.item_code} found in stock entry")
|
||||
|
||||
wo.reload()
|
||||
# Assert disassembled_qty field updated in Work Order
|
||||
self.assertEqual(
|
||||
wo.disassembled_qty,
|
||||
disassemble_qty,
|
||||
f"Work Order disassembled_qty mismatch: expected {disassemble_qty}, got {wo.disassembled_qty}",
|
||||
)
|
||||
|
||||
def test_components_alternate_item_for_bom_based_manufacture_entry(self):
|
||||
frappe.db.set_single_value("Manufacturing Settings", "backflush_raw_materials_based_on", "BOM")
|
||||
frappe.db.set_single_value("Manufacturing Settings", "validate_components_quantities_per_bom", 1)
|
||||
@@ -3118,6 +3217,7 @@ def make_wo_order_test_record(**args):
|
||||
wo_order.transfer_material_against = args.transfer_material_against or "Work Order"
|
||||
wo_order.from_wip_warehouse = args.from_wip_warehouse or 0
|
||||
wo_order.batch_size = args.batch_size or 0
|
||||
wo_order.status = args.status or "Draft"
|
||||
|
||||
if args.source_warehouse:
|
||||
wo_order.source_warehouse = args.source_warehouse
|
||||
|
||||
@@ -803,7 +803,7 @@ erpnext.work_order = {
|
||||
get_max_transferable_qty: (frm, purpose) => {
|
||||
let max = 0;
|
||||
if (purpose === "Disassemble") {
|
||||
return flt(frm.doc.produced_qty);
|
||||
return flt(frm.doc.produced_qty - frm.doc.disassembled_qty);
|
||||
}
|
||||
|
||||
if (frm.doc.skip_transfer) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"qty",
|
||||
"material_transferred_for_manufacturing",
|
||||
"produced_qty",
|
||||
"disassembled_qty",
|
||||
"process_loss_qty",
|
||||
"project",
|
||||
"section_break_ndpq",
|
||||
@@ -586,6 +587,14 @@
|
||||
{
|
||||
"fieldname": "section_break_ndpq",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus==1",
|
||||
"fieldname": "disassembled_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Disassembled Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
@@ -593,7 +602,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-11 15:47:13.454422",
|
||||
"modified": "2025-06-21 00:55:45.916224",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
@@ -629,4 +638,4 @@
|
||||
"title_field": "production_item",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ class WorkOrder(Document):
|
||||
company: DF.Link
|
||||
corrective_operation_cost: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
disassembled_qty: DF.Float
|
||||
expected_delivery_date: DF.Date | None
|
||||
fg_warehouse: DF.Link
|
||||
from_wip_warehouse: DF.Check
|
||||
@@ -389,7 +390,7 @@ class WorkOrder(Document):
|
||||
if qty > completed_qty:
|
||||
frappe.throw(
|
||||
_("{0} ({1}) cannot be greater than planned quantity ({2}) in Work Order {3}").format(
|
||||
self.meta.get_label(fieldname), qty, completed_qty, self.name
|
||||
_(self.meta.get_label(fieldname)), qty, completed_qty, self.name
|
||||
),
|
||||
StockOverProductionError,
|
||||
)
|
||||
@@ -406,6 +407,18 @@ class WorkOrder(Document):
|
||||
self.set_produced_qty_for_sub_assembly_item()
|
||||
self.update_production_plan_status()
|
||||
|
||||
def update_disassembled_qty(self, qty, is_cancel=False):
|
||||
if is_cancel:
|
||||
self.disassembled_qty = max(0, self.disassembled_qty - qty)
|
||||
else:
|
||||
if self.docstatus == 1:
|
||||
self.disassembled_qty += qty
|
||||
|
||||
if not is_cancel and self.disassembled_qty > self.produced_qty:
|
||||
frappe.throw(_("Cannot disassemble more than produced quantity."))
|
||||
|
||||
self.db_set("disassembled_qty", self.disassembled_qty)
|
||||
|
||||
def get_transferred_or_manufactured_qty(self, purpose):
|
||||
table = frappe.qb.DocType("Stock Entry")
|
||||
query = frappe.qb.from_(table).where(
|
||||
@@ -1064,7 +1077,7 @@ class WorkOrder(Document):
|
||||
self.transfer_material_against = "Work Order"
|
||||
if not self.transfer_material_against:
|
||||
frappe.throw(
|
||||
_("Setting {} is required").format(self.meta.get_label("transfer_material_against")),
|
||||
_("Setting {0} is required").format(_(self.meta.get_label("transfer_material_against"))),
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
@@ -1475,7 +1488,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items()
|
||||
stock_entry.get_items(qty, work_order.production_item)
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Floor, Sum
|
||||
from frappe.utils import cint
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -20,8 +18,7 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
return [
|
||||
_("Item") + ":Link/Item:150",
|
||||
_("Item Name") + "::240",
|
||||
_("Description") + "::300",
|
||||
@@ -32,55 +29,54 @@ def get_columns():
|
||||
_("Enough Parts to Build") + ":Float:200",
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
qty_to_produce = filters.get("qty_to_produce")
|
||||
if cint(qty_to_produce) <= 0:
|
||||
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||
|
||||
if filters.get("show_exploded_view"):
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
bom_item_table = "BOM Explosion Item" if filters.get("show_exploded_view") else "BOM Item"
|
||||
|
||||
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
|
||||
warehouse = filters.get("warehouse")
|
||||
warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1)
|
||||
|
||||
BOM = frappe.qb.DocType("BOM")
|
||||
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||
BIN = frappe.qb.DocType("Bin")
|
||||
WH = frappe.qb.DocType("Warehouse")
|
||||
CONDITIONS = ()
|
||||
|
||||
if warehouse_details:
|
||||
CONDITIONS = ExistsCriterion(
|
||||
frappe.qb.from_(WH)
|
||||
.select(WH.name)
|
||||
.where(
|
||||
(WH.lft >= warehouse_details.lft)
|
||||
& (WH.rgt <= warehouse_details.rgt)
|
||||
& (BIN.warehouse == WH.name)
|
||||
)
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.join(WH)
|
||||
.on(BIN.warehouse == WH.name)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where((WH.lft >= warehouse_details.lft) & (WH.rgt <= warehouse_details.rgt))
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
else:
|
||||
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||
bin_subquery = (
|
||||
frappe.qb.from_(BIN)
|
||||
.select(BIN.item_code, Sum(BIN.actual_qty).as_("actual_qty"))
|
||||
.where(BIN.warehouse == warehouse)
|
||||
.groupby(BIN.item_code)
|
||||
)
|
||||
|
||||
QUERY = (
|
||||
frappe.qb.from_(BOM)
|
||||
.inner_join(BOM_ITEM)
|
||||
.join(BOM_ITEM)
|
||||
.on(BOM.name == BOM_ITEM.parent)
|
||||
.left_join(BIN)
|
||||
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||
.left_join(bin_subquery)
|
||||
.on(BOM_ITEM.item_code == bin_subquery.item_code)
|
||||
.select(
|
||||
BOM_ITEM.item_code,
|
||||
BOM_ITEM.item_name,
|
||||
BOM_ITEM.description,
|
||||
BOM_ITEM.stock_qty,
|
||||
Sum(BOM_ITEM.stock_qty),
|
||||
BOM_ITEM.stock_uom,
|
||||
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||
BIN.actual_qty.as_("actual_qty"),
|
||||
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||
(Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity,
|
||||
bin_subquery.actual_qty,
|
||||
Floor(bin_subquery.actual_qty / ((Sum(BOM_ITEM.stock_qty) * qty_to_produce) / BOM.quantity)),
|
||||
)
|
||||
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||
.groupby(BOM_ITEM.item_code)
|
||||
|
||||
@@ -409,4 +409,6 @@ erpnext.patches.v15_0.set_cancelled_status_to_cancelled_pos_invoice
|
||||
erpnext.patches.v15_0.rename_group_by_to_categorize_by_in_custom_reports
|
||||
erpnext.patches.v14_0.update_full_name_in_contract
|
||||
erpnext.patches.v15_0.drop_sle_indexes
|
||||
erpnext.patches.v15_0.update_pick_list_fields
|
||||
erpnext.patches.v15_0.update_pegged_currencies
|
||||
erpnext.patches.v15_0.set_company_on_pos_inv_merge_log
|
||||
|
||||
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal file
12
erpnext/patches/v15_0/set_company_on_pos_inv_merge_log.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
pos_invoice_merge_logs = frappe.db.get_all(
|
||||
"POS Invoice Merge Log", {"docstatus": 1}, ["name", "pos_closing_entry"]
|
||||
)
|
||||
|
||||
for log in pos_invoice_merge_logs:
|
||||
if log.pos_closing_entry and frappe.db.exists("POS Closing Entry", log.pos_closing_entry):
|
||||
company = frappe.db.get_value("POS Closing Entry", log.pos_closing_entry, "company")
|
||||
frappe.db.set_value("POS Invoice Merge Log", log.name, "company", company)
|
||||
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
28
erpnext/patches/v15_0/update_pick_list_fields.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
from frappe.query_builder.functions import IfNull
|
||||
|
||||
|
||||
def execute():
|
||||
update_delivery_note()
|
||||
update_pick_list_items()
|
||||
|
||||
|
||||
def update_delivery_note():
|
||||
DN = frappe.qb.DocType("Delivery Note")
|
||||
DNI = frappe.qb.DocType("Delivery Note Item")
|
||||
|
||||
frappe.qb.update(DNI).join(DN).on(DN.name == DNI.parent).set(DNI.against_pick_list, DN.pick_list).where(
|
||||
IfNull(DN.pick_list, "") != ""
|
||||
).run()
|
||||
|
||||
|
||||
def update_pick_list_items():
|
||||
PL = frappe.qb.DocType("Pick List")
|
||||
PLI = frappe.qb.DocType("Pick List Item")
|
||||
|
||||
pick_lists = frappe.qb.from_(PL).select(PL.name).where(PL.status == "Completed").run(pluck="name")
|
||||
|
||||
if not pick_lists:
|
||||
return
|
||||
|
||||
frappe.qb.update(PLI).set(PLI.delivered_qty, PLI.picked_qty).where(PLI.parent.isin(pick_lists)).run()
|
||||
@@ -202,6 +202,12 @@ frappe.ui.form.on("Project", {
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
collect_progress: function (frm) {
|
||||
if (frm.doc.collect_progress && !frm.doc.subject) {
|
||||
frm.set_value("subject", __("For project {0}, update your status", [frm.doc.name]));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function open_form(frm, doctype, child_doctype, parentfield) {
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"day_to_send",
|
||||
"weekly_time_to_send",
|
||||
"column_break_45",
|
||||
"subject",
|
||||
"message"
|
||||
],
|
||||
"fields": [
|
||||
@@ -447,6 +448,13 @@
|
||||
"print_hide": 1,
|
||||
"reqd": 1,
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "collect_progress",
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"mandatory_depends_on": "collect_progress"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-puzzle-piece",
|
||||
@@ -454,7 +462,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2024-04-24 10:56:16.001032",
|
||||
"modified": "2025-07-03 10:54:30.444139",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
@@ -501,6 +509,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "project_name,customer, status, priority, is_active",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -509,4 +518,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "project_name",
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class Project(Document):
|
||||
sales_order: DF.Link | None
|
||||
second_email: DF.Time | None
|
||||
status: DF.Literal["Open", "Completed", "Cancelled"]
|
||||
subject: DF.Data | None
|
||||
to_time: DF.Time | None
|
||||
total_billable_amount: DF.Currency
|
||||
total_billed_amount: DF.Currency
|
||||
@@ -606,8 +607,6 @@ def send_project_update_email_to_users(project):
|
||||
}
|
||||
).insert()
|
||||
|
||||
subject = "For project %s, update your status" % (project)
|
||||
|
||||
incoming_email_account = frappe.db.get_value(
|
||||
"Email Account", dict(enable_incoming=1, default_incoming=1), "email_id"
|
||||
)
|
||||
@@ -615,7 +614,7 @@ def send_project_update_email_to_users(project):
|
||||
frappe.sendmail(
|
||||
recipients=get_users_email(doc),
|
||||
message=doc.message,
|
||||
subject=_(subject),
|
||||
subject=doc.subject,
|
||||
reference_doctype=project_update.doctype,
|
||||
reference_name=project_update.name,
|
||||
reply_to=incoming_email_account,
|
||||
|
||||
@@ -581,7 +581,8 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
||||
transaction_date: frm.doc.transaction_date || frm.doc.posting_date,
|
||||
ignore_pricing_rule: frm.doc.ignore_pricing_rule,
|
||||
doctype: frm.doc.doctype
|
||||
}
|
||||
},
|
||||
price_list: frm.doc.price_list,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
|
||||
@@ -8,7 +8,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let me = this;
|
||||
|
||||
this.set_fields_onload_for_line_item();
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
frappe.ui.form.on(this.frm.doctype + " Item", "rate", function(frm, cdt, cdn) {
|
||||
@@ -371,6 +371,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
"inspection_type": inspection_type,
|
||||
"reference_type": me.frm.doc.doctype,
|
||||
"reference_name": me.frm.doc.name,
|
||||
"child_row_reference": row.doc.name,
|
||||
"item_code": row.doc.item_code,
|
||||
"description": row.doc.description,
|
||||
"item_serial_no": row.doc.serial_no ? row.doc.serial_no.split("\n")[0] : null,
|
||||
@@ -385,7 +386,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
docstatus: ["<", 2],
|
||||
inspection_type: inspection_type,
|
||||
reference_name: doc.name,
|
||||
item_code: d.item_code
|
||||
item_code: d.item_code,
|
||||
child_row_reference : d.name
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2427,12 +2429,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
fields: fields,
|
||||
primary_action: function () {
|
||||
const data = dialog.get_values();
|
||||
const selected_data = data.items.filter(item => item?.__checked == 1 );
|
||||
frappe.call({
|
||||
method: "erpnext.controllers.stock_controller.make_quality_inspections",
|
||||
args: {
|
||||
doctype: me.frm.doc.doctype,
|
||||
docname: me.frm.doc.name,
|
||||
items: data.items
|
||||
items: selected_data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
|
||||
@@ -1004,7 +1004,7 @@ erpnext.utils.map_current_doc = function (opts) {
|
||||
|
||||
if (
|
||||
opts.allow_child_item_selection ||
|
||||
["Purchase Receipt", "Delivery Note"].includes(opts.source_doctype)
|
||||
["Purchase Receipt", "Delivery Note", "Pick List"].includes(opts.source_doctype)
|
||||
) {
|
||||
// args contains filtered child docnames
|
||||
opts.args = args;
|
||||
|
||||
@@ -81,7 +81,7 @@ frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm
|
||||
fieldtype: "Data",
|
||||
},
|
||||
{
|
||||
label: __("State"),
|
||||
label: __("State/Province"),
|
||||
fieldname: "state",
|
||||
fieldtype: "Data",
|
||||
},
|
||||
|
||||
@@ -284,13 +284,24 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: var(--margin-md);
|
||||
column-gap: var(--padding-sm);
|
||||
row-gap: var(--padding-xs);
|
||||
row-gap: var(--padding-sm);
|
||||
}
|
||||
|
||||
> .transactions-label {
|
||||
@extend .label;
|
||||
> .transactions-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--margin-md);
|
||||
margin-bottom: var(--margin-sm);
|
||||
|
||||
> .recent-transactions {
|
||||
@extend .label;
|
||||
}
|
||||
|
||||
> .last-transaction {
|
||||
font-weight: 400;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,8 +310,8 @@
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
margin-right: -12px;
|
||||
padding-right: 12px;
|
||||
margin-left: -10px;
|
||||
scrollbar-width: thin;
|
||||
|
||||
> .no-transactions-placeholder {
|
||||
height: 100%;
|
||||
@@ -611,6 +622,11 @@
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
&.invoice-selected {
|
||||
background-color: var(--control-bg);
|
||||
}
|
||||
|
||||
> .invoice-name-customer,
|
||||
> .invoice-name-date {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -630,6 +646,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-name-date {
|
||||
> .invoice-name {
|
||||
font-size: var(--text-md);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> .invoice-date {
|
||||
@extend .nowrap;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-total-date,
|
||||
> .invoice-total-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -650,6 +681,18 @@
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
|
||||
> .invoice-total-status {
|
||||
> .invoice-total {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
> .invoice-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .item-details-container {
|
||||
@@ -1020,21 +1063,31 @@
|
||||
justify-content: flex-end;
|
||||
padding-right: var(--padding-sm);
|
||||
|
||||
> .customer-name {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
> .customer-section {
|
||||
margin-bottom: auto;
|
||||
|
||||
> .customer-email {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
> .customer-name {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
> .customer-code {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
> .customer-email {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
> .cashier {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 500;
|
||||
color: var(--gray-600);
|
||||
margin-top: auto;
|
||||
margin-top: var(--margin-md);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,7 +1095,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
|
||||
> .paid-amount {
|
||||
font-size: var(--text-2xl);
|
||||
|
||||
@@ -117,14 +117,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
|
||||
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
|
||||
if (
|
||||
frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
|
||||
!doc.valid_till ||
|
||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0
|
||||
frappe.model.can_create("Sales Order") &&
|
||||
(frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation ||
|
||||
!doc.valid_till ||
|
||||
frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0)
|
||||
) {
|
||||
this.frm.add_custom_button(__("Sales Order"), () => this.make_sales_order(), __("Create"));
|
||||
}
|
||||
|
||||
if (doc.status !== "Ordered") {
|
||||
if (doc.status !== "Ordered" && this.frm.has_perm("write")) {
|
||||
this.frm.add_custom_button(__("Set as Lost"), () => {
|
||||
this.frm.trigger("set_as_lost_dialog");
|
||||
});
|
||||
@@ -133,7 +134,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
}
|
||||
|
||||
if (this.frm.doc.docstatus === 0) {
|
||||
if (this.frm.doc.docstatus === 0 && frappe.model.can_read("Opportunity")) {
|
||||
this.frm.add_custom_button(
|
||||
__("Opportunity"),
|
||||
function () {
|
||||
|
||||
@@ -1741,8 +1741,8 @@ def create_pick_list(source_name, target_doc=None):
|
||||
"doctype": "Pick List Item",
|
||||
"field_map": {
|
||||
"parent": "sales_order",
|
||||
"name": "sales_order_item",
|
||||
"parent_detail_docname": "product_bundle_item",
|
||||
"parent_detail_docname": "sales_order_item",
|
||||
"name": "product_bundle_item",
|
||||
},
|
||||
"field_no_map": ["picked_qty"],
|
||||
"postprocess": update_packed_item_qty,
|
||||
|
||||
@@ -338,13 +338,17 @@ def create_opening_voucher(pos_profile, company, balance_details):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_past_order_list(search_term, status, limit=20):
|
||||
fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"]
|
||||
fields = ["name", "grand_total", "currency", "customer", "customer_name", "posting_time", "posting_date"]
|
||||
invoice_list = []
|
||||
|
||||
if search_term and status:
|
||||
invoices_by_customer = frappe.db.get_list(
|
||||
"POS Invoice",
|
||||
filters={"customer": ["like", f"%{search_term}%"], "status": status},
|
||||
filters={"status": status},
|
||||
or_filters={
|
||||
"customer_name": ["like", f"%{search_term}%"],
|
||||
"customer": ["like", f"%{search_term}%"],
|
||||
},
|
||||
fields=fields,
|
||||
page_length=limit,
|
||||
)
|
||||
|
||||
@@ -342,7 +342,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (customer) {
|
||||
return new Promise((resolve) => {
|
||||
frappe.db
|
||||
.get_value("Customer", customer, ["email_id", "mobile_no", "image", "loyalty_program"])
|
||||
.get_value("Customer", customer, [
|
||||
"email_id",
|
||||
"customer_name",
|
||||
"mobile_no",
|
||||
"image",
|
||||
"loyalty_program",
|
||||
])
|
||||
.then(({ message }) => {
|
||||
const { loyalty_program } = message;
|
||||
// if loyalty program then fetch loyalty points too
|
||||
@@ -439,7 +445,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
update_customer_section() {
|
||||
const me = this;
|
||||
const { customer, email_id = "", mobile_no = "", image } = this.customer_info || {};
|
||||
const { customer, customer_name, email_id = "", mobile_no = "", image } = this.customer_info || {};
|
||||
|
||||
if (customer) {
|
||||
this.$customer_section.html(
|
||||
@@ -447,7 +453,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer}</div>
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
${get_customer_description()}
|
||||
</div>
|
||||
<div class="reset-customer-btn" data-customer="${escape(customer)}">
|
||||
@@ -867,7 +873,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
toggle_customer_info(show) {
|
||||
if (show) {
|
||||
const { customer } = this.customer_info || {};
|
||||
const { customer, customer_name } = this.customer_info || {};
|
||||
|
||||
this.$cart_container.css("display", "none");
|
||||
this.$customer_section.css({
|
||||
@@ -886,8 +892,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer}</div>
|
||||
<div class="customer-desc"></div>
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
<div class="customer-desc">${customer}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-fields-container">
|
||||
@@ -896,7 +902,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="loyalty_program-field"></div>
|
||||
<div class="loyalty_points-field"></div>
|
||||
</div>
|
||||
<div class="transactions-label">${__("Recent Transactions")}</div>`
|
||||
<div class="transactions-section">
|
||||
<div class="recent-transactions">${__("Recent Transactions")}</div>
|
||||
<div class="last-transaction"></div>
|
||||
</div>`
|
||||
);
|
||||
// transactions need to be in diff div from sticky elem for scrolling
|
||||
this.$customer_section.append(`<div class="customer-transactions"></div>`);
|
||||
@@ -1005,7 +1014,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
const elapsed_time = moment(res[0].posting_date + " " + res[0].posting_time).fromNow();
|
||||
this.$customer_section
|
||||
.find(".customer-desc")
|
||||
.find(".last-transaction")
|
||||
.html(`${__("Last transacted")} ${__(elapsed_time)}`);
|
||||
|
||||
res.forEach((invoice) => {
|
||||
@@ -1027,7 +1036,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
<div class="invoice-total">
|
||||
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
|
||||
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
|
||||
</div>
|
||||
<div class="invoice-status">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
|
||||
|
||||
@@ -106,7 +106,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${frappe.ellipsis(invoice.customer, 20)}
|
||||
${frappe.ellipsis(invoice.customer_name, 20)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
|
||||
@@ -73,14 +73,18 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
get_upper_section_html(doc) {
|
||||
const { status } = doc;
|
||||
let indicator_color = "";
|
||||
const is_customer_naming_by_customer_name = frappe.sys_defaults.cust_master_name !== "Customer Name";
|
||||
|
||||
["Paid", "Consolidated"].includes(status) && (indicator_color = "green");
|
||||
status === "Draft" && (indicator_color = "red");
|
||||
status === "Return" && (indicator_color = "grey");
|
||||
|
||||
return `<div class="left-section">
|
||||
<div class="customer-name">${doc.customer}</div>
|
||||
<div class="customer-email">${this.customer_email}</div>
|
||||
<div class="customer-section">
|
||||
<div class="customer-name">${doc.customer_name}</div>
|
||||
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
|
||||
<div class="customer-email">${this.customer_email}</div>
|
||||
</div>
|
||||
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
|
||||
</div>
|
||||
<div class="right-section">
|
||||
|
||||
@@ -395,7 +395,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"General Ledger",
|
||||
self.meta.get_label("income"),
|
||||
_(self.meta.get_label("income")),
|
||||
filters={
|
||||
"from_date": self.future_from_date,
|
||||
"to_date": self.future_to_date,
|
||||
@@ -427,7 +427,7 @@ class EmailDigest(Document):
|
||||
filters = {"currency": self.currency}
|
||||
label = get_link_to_report(
|
||||
"Profit and Loss Statement",
|
||||
label=self.meta.get_label(root_type + "_year_to_date"),
|
||||
label=_(self.meta.get_label(root_type + "_year_to_date")),
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
@@ -435,7 +435,7 @@ class EmailDigest(Document):
|
||||
filters = {"currency": self.currency}
|
||||
label = get_link_to_report(
|
||||
"Profit and Loss Statement",
|
||||
label=self.meta.get_label(root_type + "_year_to_date"),
|
||||
label=_(self.meta.get_label(root_type + "_year_to_date")),
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
@@ -466,7 +466,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"General Ledger",
|
||||
self.meta.get_label("expenses_booked"),
|
||||
_(self.meta.get_label("expenses_booked")),
|
||||
filters={
|
||||
"company": self.company,
|
||||
"from_date": self.future_from_date,
|
||||
@@ -500,7 +500,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Sales Order",
|
||||
label=self.meta.get_label("sales_orders_to_bill"),
|
||||
label=_(self.meta.get_label("sales_orders_to_bill")),
|
||||
report_type="Report Builder",
|
||||
doctype="Sales Order",
|
||||
filters={
|
||||
@@ -526,7 +526,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Sales Order",
|
||||
label=self.meta.get_label("sales_orders_to_deliver"),
|
||||
label=_(self.meta.get_label("sales_orders_to_deliver")),
|
||||
report_type="Report Builder",
|
||||
doctype="Sales Order",
|
||||
filters={
|
||||
@@ -552,7 +552,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=self.meta.get_label("purchase_orders_to_receive"),
|
||||
label=_(self.meta.get_label("purchase_orders_to_receive")),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters={
|
||||
@@ -578,7 +578,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Purchase Order",
|
||||
label=self.meta.get_label("purchase_orders_to_bill"),
|
||||
label=_(self.meta.get_label("purchase_orders_to_bill")),
|
||||
report_type="Report Builder",
|
||||
doctype="Purchase Order",
|
||||
filters={
|
||||
@@ -630,7 +630,7 @@ class EmailDigest(Document):
|
||||
"company": self.company,
|
||||
}
|
||||
label = get_link_to_report(
|
||||
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
|
||||
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
|
||||
)
|
||||
else:
|
||||
filters = {
|
||||
@@ -640,7 +640,7 @@ class EmailDigest(Document):
|
||||
"company": self.company,
|
||||
}
|
||||
label = get_link_to_report(
|
||||
"Account Balance", label=self.meta.get_label(fieldname), filters=filters
|
||||
"Account Balance", label=_(self.meta.get_label(fieldname)), filters=filters
|
||||
)
|
||||
|
||||
return {"label": label, "value": balance, "last_value": prev_balance}
|
||||
@@ -648,17 +648,17 @@ class EmailDigest(Document):
|
||||
if account_type == "Payable":
|
||||
label = get_link_to_report(
|
||||
"Accounts Payable",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
filters={"report_date": self.future_to_date, "company": self.company},
|
||||
)
|
||||
elif account_type == "Receivable":
|
||||
label = get_link_to_report(
|
||||
"Accounts Receivable",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
filters={"report_date": self.future_to_date, "company": self.company},
|
||||
)
|
||||
else:
|
||||
label = self.meta.get_label(fieldname)
|
||||
label = _(self.meta.get_label(fieldname))
|
||||
|
||||
return {"label": label, "value": balance, "last_value": prev_balance, "count": count}
|
||||
|
||||
@@ -748,7 +748,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
"Quotation",
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
report_type="Report Builder",
|
||||
doctype="Quotation",
|
||||
filters={
|
||||
@@ -779,7 +779,7 @@ class EmailDigest(Document):
|
||||
|
||||
label = get_link_to_report(
|
||||
doc_type,
|
||||
label=self.meta.get_label(fieldname),
|
||||
label=_(self.meta.get_label(fieldname)),
|
||||
report_type="Report Builder",
|
||||
filters=filters,
|
||||
doctype=doc_type,
|
||||
|
||||
@@ -51,7 +51,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
let stock_uom = unescape(element.attr("data-stock-uom"));
|
||||
|
||||
if (disable_quick_entry) {
|
||||
open_stock_entry(item, warehouse, entry_type);
|
||||
open_stock_entry(item, warehouse, entry_type, stock_uom);
|
||||
} else {
|
||||
if (action === "Add") {
|
||||
let rate = unescape($(this).attr("data-rate"));
|
||||
@@ -66,7 +66,7 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
}
|
||||
}
|
||||
|
||||
function open_stock_entry(item, warehouse, entry_type) {
|
||||
function open_stock_entry(item, warehouse, entry_type, stock_uom) {
|
||||
frappe.model.with_doctype("Stock Entry", function () {
|
||||
var doc = frappe.model.get_new_doc("Stock Entry");
|
||||
if (entry_type) {
|
||||
@@ -75,6 +75,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
|
||||
var row = frappe.model.add_child(doc, "items");
|
||||
row.item_code = item;
|
||||
row.uom = stock_uom;
|
||||
row.stock_uom = stock_uom;
|
||||
row.conversion_factor = 1;
|
||||
|
||||
if (entry_type === "Material Transfer") {
|
||||
row.s_warehouse = warehouse;
|
||||
|
||||
@@ -264,7 +264,7 @@ def update_qty(bin_name, args):
|
||||
actual_qty = bin_details.actual_qty or 0.0
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args, allow_force_reposting=False):
|
||||
if future_sle_exists(args):
|
||||
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
|
||||
|
||||
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
||||
|
||||
@@ -188,6 +188,55 @@ erpnext.stock.DeliveryNoteController = class DeliveryNoteController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!doc.is_return &&
|
||||
doc.status != "Closed" &&
|
||||
this.frm.has_perm("write") &&
|
||||
frappe.model.can_read("Pick List") &&
|
||||
this.frm.doc.docstatus === 0
|
||||
) {
|
||||
this.frm.add_custom_button(
|
||||
__("Pick List"),
|
||||
function () {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer"),
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.stock.doctype.pick_list.pick_list.create_dn_for_pick_lists",
|
||||
source_doctype: "Pick List",
|
||||
target: me.frm,
|
||||
setters: [
|
||||
{
|
||||
fieldname: "customer",
|
||||
default: me.frm.doc.customer,
|
||||
label: __("Customer"),
|
||||
fieldtype: "Link",
|
||||
options: "Customer",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "sales_order",
|
||||
label: __("Sales Order"),
|
||||
fieldtype: "Link",
|
||||
options: "Sales Order",
|
||||
link_filters: `[["Sales Order","customer","=","${me.frm.doc.customer}"],["Sales Order","docstatus","=","1"],["Sales Order","delivery_status","not in",["Closed","Fully Delivered"]]]`,
|
||||
},
|
||||
],
|
||||
get_query_filters: {
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
get_query_method: "erpnext.stock.doctype.pick_list.pick_list.get_pick_list_query",
|
||||
size: "extra-large",
|
||||
});
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
}
|
||||
|
||||
if (!doc.is_return && doc.status != "Closed") {
|
||||
if (doc.docstatus == 1 && frappe.model.can_create("Shipment")) {
|
||||
this.frm.add_custom_button(
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"ignore_pricing_rule",
|
||||
"items_section",
|
||||
"scan_barcode",
|
||||
"pick_list",
|
||||
"col_break_warehouse",
|
||||
"set_warehouse",
|
||||
"set_target_warehouse",
|
||||
@@ -1218,15 +1217,6 @@
|
||||
"options": "Sales Team",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "pick_list",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Pick List",
|
||||
"options": "Pick List",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "customer.is_internal_customer",
|
||||
|
||||
@@ -174,6 +174,19 @@ class DeliveryNote(SellingController):
|
||||
"overflow_type": "delivery",
|
||||
"no_allowance": 1,
|
||||
},
|
||||
{
|
||||
"source_dt": "Delivery Note Item",
|
||||
"target_dt": "Pick List Item",
|
||||
"join_field": "pick_list_item",
|
||||
"target_field": "delivered_qty",
|
||||
"target_parent_dt": "Pick List",
|
||||
"target_parent_field": "per_delivered",
|
||||
"target_ref_field": "picked_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "against_pick_list",
|
||||
"status_field": "delivery_status",
|
||||
"keyword": "Delivered",
|
||||
},
|
||||
]
|
||||
if cint(self.is_return):
|
||||
self.status_updater.extend(
|
||||
@@ -326,18 +339,15 @@ class DeliveryNote(SellingController):
|
||||
def set_serial_and_batch_bundle_from_pick_list(self):
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
if not self.pick_list:
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if item.use_serial_batch_fields:
|
||||
if item.use_serial_batch_fields or not item.against_pick_list:
|
||||
continue
|
||||
|
||||
if item.pick_list_item and not item.serial_and_batch_bundle:
|
||||
filters = {
|
||||
"item_code": item.item_code,
|
||||
"voucher_type": "Pick List",
|
||||
"voucher_no": self.pick_list,
|
||||
"voucher_no": item.against_pick_list,
|
||||
"voucher_detail_no": item.pick_list_item,
|
||||
}
|
||||
|
||||
@@ -586,7 +596,9 @@ class DeliveryNote(SellingController):
|
||||
def update_pick_list_status(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status
|
||||
|
||||
update_pick_list_status(self.pick_list)
|
||||
pick_lists = {row.against_pick_list for row in self.items if row.against_pick_list}
|
||||
for pick_list in pick_lists:
|
||||
update_pick_list_status(pick_list)
|
||||
|
||||
def check_next_docstatus(self):
|
||||
submit_rv = frappe.db.sql(
|
||||
@@ -1269,6 +1281,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse"],
|
||||
"field_map": {"shipping_address_name": "shipping_address"},
|
||||
},
|
||||
doctype + " Item": {
|
||||
"doctype": target_doctype + " Item",
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"against_sales_invoice",
|
||||
"si_detail",
|
||||
"dn_detail",
|
||||
"against_pick_list",
|
||||
"pick_list_item",
|
||||
"section_break_40",
|
||||
"pick_serial_and_batch",
|
||||
@@ -935,13 +936,23 @@
|
||||
{
|
||||
"fieldname": "column_break_fguf",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "against_pick_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Against Pick List",
|
||||
"no_copy": 1,
|
||||
"options": "Pick List",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-05 14:28:33.322181",
|
||||
"modified": "2025-05-31 18:51:32.651562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
||||
@@ -16,6 +16,7 @@ class DeliveryNoteItem(Document):
|
||||
|
||||
actual_batch_qty: DF.Float
|
||||
actual_qty: DF.Float
|
||||
against_pick_list: DF.Link | None
|
||||
against_sales_invoice: DF.Link | None
|
||||
against_sales_order: DF.Link | None
|
||||
allow_zero_valuation_rate: DF.Check
|
||||
|
||||
@@ -75,7 +75,9 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
|
||||
set_parent_fields(frm) {
|
||||
if (frm.doc.apply_to_all_doctypes) {
|
||||
frm.set_df_property("fetch_from_parent", "options", frm.doc.reference_document);
|
||||
let options = ["\n", frm.doc.reference_document];
|
||||
|
||||
frm.set_df_property("fetch_from_parent", "options", options);
|
||||
} else if (frm.doc.document_type && frm.doc.istable) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.inventory_dimension.inventory_dimension.get_parent_fields",
|
||||
@@ -85,7 +87,7 @@ frappe.ui.form.on("Inventory Dimension", {
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message && r.message.length) {
|
||||
frm.set_df_property("fetch_from_parent", "options", [""].concat(r.message));
|
||||
frm.set_df_property("fetch_from_parent", "options", ["\n"].concat(r.message));
|
||||
} else {
|
||||
frm.set_df_property("fetch_from_parent", "hidden", 1);
|
||||
}
|
||||
|
||||
@@ -143,7 +143,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.apply_to_all_doctypes",
|
||||
"description": "Set fieldname from which you want to fetch the data from the parent form.",
|
||||
"fieldname": "fetch_from_parent",
|
||||
"fieldtype": "Select",
|
||||
@@ -189,7 +188,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-07-08 08:58:50.228211",
|
||||
"modified": "2025-07-07 15:51:29.329064",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Inventory Dimension",
|
||||
|
||||
@@ -64,16 +64,11 @@ class InventoryDimension(Document):
|
||||
self.reset_value()
|
||||
self.set_source_and_target_fieldname()
|
||||
self.set_type_of_transaction()
|
||||
self.set_fetch_value_from()
|
||||
|
||||
def set_type_of_transaction(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.type_of_transaction = "Both"
|
||||
|
||||
def set_fetch_value_from(self):
|
||||
if self.apply_to_all_doctypes:
|
||||
self.fetch_from_parent = self.reference_document
|
||||
|
||||
def do_not_update_document(self):
|
||||
if self.is_new() or not self.has_stock_ledger():
|
||||
return
|
||||
|
||||
@@ -155,6 +155,8 @@ class TestInventoryDimension(FrappeTestCase):
|
||||
reference_document="Rack", dimension_name="Rack", apply_to_all_doctypes=1
|
||||
)
|
||||
|
||||
inv_dimension.db_set("fetch_from_parent", "Rack")
|
||||
|
||||
self.assertEqual(inv_dimension.type_of_transaction, "Both")
|
||||
self.assertEqual(inv_dimension.fetch_from_parent, "Rack")
|
||||
|
||||
|
||||
@@ -634,7 +634,7 @@ class Item(Document):
|
||||
|
||||
if new_properties != [cstr(self.get(field)) for field in field_list]:
|
||||
msg = _("To merge, following properties must be same for both items")
|
||||
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
|
||||
msg += ": \n" + ", ".join([_(self.meta.get_label(fld)) for fld in field_list])
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
@@ -981,7 +981,7 @@ class Item(Document):
|
||||
return
|
||||
|
||||
if linked_doc := self._get_linked_submitted_documents(changed_fields):
|
||||
changed_field_labels = [frappe.bold(self.meta.get_label(f)) for f in changed_fields]
|
||||
changed_field_labels = [frappe.bold(_(self.meta.get_label(f))) for f in changed_fields]
|
||||
msg = _(
|
||||
"As there are existing submitted transactions against item {0}, you can not change the value of {1}."
|
||||
).format(self.name, ", ".join(changed_field_labels))
|
||||
|
||||
@@ -332,5 +332,6 @@ def get_pr_items(purchase_receipt):
|
||||
(pr_item.parent == purchase_receipt.receipt_document)
|
||||
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
|
||||
)
|
||||
.orderby(pr_item.idx)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -42,6 +42,15 @@ frappe.ui.form.on("Material Request", {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("price_list", () => {
|
||||
return {
|
||||
filters: {
|
||||
buying: 1,
|
||||
enabled: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
@@ -70,6 +79,7 @@ frappe.ui.form.on("Material Request", {
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
frm.doc.price_list = frappe.defaults.get_default("buying_price_list");
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
@@ -245,7 +255,9 @@ frappe.ui.form.on("Material Request", {
|
||||
from_warehouse: item.from_warehouse,
|
||||
warehouse: item.warehouse,
|
||||
doctype: frm.doc.doctype,
|
||||
buying_price_list: frappe.defaults.get_default("buying_price_list"),
|
||||
buying_price_list: frm.doc.price_list
|
||||
? frm.doc.price_list
|
||||
: frappe.defaults.get_default("buying_price_list"),
|
||||
currency: frappe.defaults.get_default("Currency"),
|
||||
name: frm.doc.name,
|
||||
qty: item.qty || 1,
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"column_break_2",
|
||||
"transaction_date",
|
||||
"schedule_date",
|
||||
"price_list",
|
||||
"amended_from",
|
||||
"warehouse_section",
|
||||
"scan_barcode",
|
||||
@@ -351,13 +352,19 @@
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-ticket",
|
||||
"idx": 70,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-21 18:36:04.827917",
|
||||
"modified": "2025-07-07 13:15:28.615984",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request",
|
||||
@@ -425,10 +432,11 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "status,transaction_date",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -45,6 +46,7 @@ class MaterialRequest(BuyingController):
|
||||
naming_series: DF.Literal["MAT-MR-.YYYY.-"]
|
||||
per_ordered: DF.Percent
|
||||
per_received: DF.Percent
|
||||
price_list: DF.Link | None
|
||||
scan_barcode: DF.Data | None
|
||||
schedule_date: DF.Date | None
|
||||
select_print_heading: DF.Link | None
|
||||
@@ -151,6 +153,9 @@ class MaterialRequest(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
if not self.price_list:
|
||||
self.price_list = frappe.defaults.get_defaults().buying_price_list
|
||||
|
||||
def before_update_after_submit(self):
|
||||
self.validate_schedule_date()
|
||||
|
||||
@@ -764,10 +769,11 @@ def raise_work_orders(material_request):
|
||||
"material_request_item": d.name,
|
||||
"planned_start_date": mr.transaction_date,
|
||||
"company": mr.company,
|
||||
"project": d.project,
|
||||
}
|
||||
)
|
||||
|
||||
wo_order.set_work_order_operations()
|
||||
wo_order.get_items_and_operations_from_bom()
|
||||
wo_order.flags.ignore_validate = True
|
||||
wo_order.flags.ignore_mandatory = True
|
||||
wo_order.save()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
|
||||
@@ -80,6 +81,10 @@ def make_packing_list(doc):
|
||||
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
|
||||
update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
|
||||
update_packed_item_price_data(pi_row, item_data, doc)
|
||||
|
||||
if item_row.get("against_pick_list"):
|
||||
update_packed_item_with_pick_list_info(item_row, pi_row)
|
||||
|
||||
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
|
||||
|
||||
if set_price_from_children: # create/update bundle item wise price dict
|
||||
@@ -228,6 +233,28 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
|
||||
pi_row.use_serial_batch_fields = frappe.db.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||
|
||||
|
||||
def update_packed_item_with_pick_list_info(main_item_row, pi_row):
|
||||
pl_row = frappe.db.get_value(
|
||||
"Pick List Item",
|
||||
{
|
||||
"item_code": pi_row.item_code,
|
||||
"sales_order": main_item_row.get("against_sales_order"),
|
||||
"sales_order_item": main_item_row.get("so_detail"),
|
||||
"parent": main_item_row.against_pick_list,
|
||||
},
|
||||
["warehouse", "batch_no", "serial_no"],
|
||||
as_dict=True,
|
||||
order_by="qty desc",
|
||||
)
|
||||
|
||||
if not pl_row:
|
||||
return
|
||||
|
||||
pi_row.warehouse = pl_row.warehouse
|
||||
pi_row.batch_no = pl_row.batch_no
|
||||
pi_row.serial_no = pl_row.serial_no
|
||||
|
||||
|
||||
def update_packed_item_price_data(pi_row, item_data, doc):
|
||||
"Set price as per price list or from the Item master."
|
||||
if pi_row.rate:
|
||||
@@ -313,12 +340,20 @@ def on_doctype_update():
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_from_product_bundle(row):
|
||||
def get_items_from_product_bundle(row, price_list):
|
||||
row, items = json.loads(row), []
|
||||
|
||||
bundled_items = get_product_bundle_items(row["item_code"])
|
||||
for item in bundled_items:
|
||||
row.update({"item_code": item.item_code, "qty": flt(row["quantity"]) * flt(item.qty)})
|
||||
row.update(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"qty": flt(row["quantity"]) * flt(item.qty),
|
||||
"conversion_rate": 1,
|
||||
"price_list": price_list,
|
||||
"currency": frappe.defaults.get_defaults().currency,
|
||||
}
|
||||
)
|
||||
items.append(get_item_details(row))
|
||||
|
||||
return items
|
||||
|
||||
@@ -98,34 +98,28 @@ frappe.ui.form.on("Pick List", {
|
||||
refresh: (frm) => {
|
||||
frm.trigger("add_get_items_button");
|
||||
if (frm.doc.docstatus === 1) {
|
||||
frappe
|
||||
.xcall("erpnext.stock.doctype.pick_list.pick_list.target_document_exists", {
|
||||
pick_list_name: frm.doc.name,
|
||||
purpose: frm.doc.purpose,
|
||||
})
|
||||
.then((target_document_exists) => {
|
||||
frm.set_df_property("locations", "allow_on_submit", target_document_exists ? 0 : 1);
|
||||
const status_completed = frm.doc.status === "Completed";
|
||||
frm.set_df_property("locations", "allow_on_submit", status_completed ? 0 : 1);
|
||||
|
||||
if (target_document_exists) return;
|
||||
if (!status_completed) {
|
||||
frm.add_custom_button(__("Update Current Stock"), () =>
|
||||
frm.trigger("update_pick_list_stock")
|
||||
);
|
||||
|
||||
frm.add_custom_button(__("Update Current Stock"), () =>
|
||||
frm.trigger("update_pick_list_stock")
|
||||
if (frm.doc.purpose === "Delivery") {
|
||||
frm.add_custom_button(
|
||||
__("Create Delivery Note"),
|
||||
() => frm.trigger("create_delivery_note"),
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (frm.doc.purpose === "Delivery") {
|
||||
frm.add_custom_button(
|
||||
__("Delivery Note"),
|
||||
() => frm.trigger("create_delivery_note"),
|
||||
__("Create")
|
||||
);
|
||||
} else {
|
||||
frm.add_custom_button(
|
||||
__("Stock Entry"),
|
||||
() => frm.trigger("create_stock_entry"),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.add_custom_button(
|
||||
__("Create Stock Entry"),
|
||||
() => frm.trigger("create_stock_entry"),
|
||||
__("Create")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.purpose === "Delivery" && frm.doc.status === "Open") {
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_unreserved_stock) {
|
||||
|
||||
@@ -30,7 +30,11 @@
|
||||
"amended_from",
|
||||
"print_settings_section",
|
||||
"group_same_items",
|
||||
"status"
|
||||
"status_section",
|
||||
"status",
|
||||
"column_break_qyam",
|
||||
"delivery_status",
|
||||
"per_delivered"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -181,7 +185,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nOpen\nCompleted\nCancelled",
|
||||
"options": "Draft\nOpen\nPartly Delivered\nCompleted\nCancelled",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"report_hide": 1,
|
||||
@@ -208,11 +212,42 @@
|
||||
"fieldname": "ignore_pricing_rule",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "status_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "delivery_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Delivery Status",
|
||||
"no_copy": 1,
|
||||
"options": "Not Delivered\nFully Delivered\nPartly Delivered",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal && doc.purpose === \"Delivery\"",
|
||||
"description": "% of materials delivered against this Pick List",
|
||||
"fieldname": "per_delivered",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Delivered",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qyam",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-14 13:20:42.168827",
|
||||
"modified": "2025-05-31 19:18:30.860044",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -280,6 +315,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -7,8 +7,7 @@ from itertools import groupby
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc
|
||||
from frappe.model.mapper import get_mapped_doc, map_child_doc
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import GROUP_CONCAT
|
||||
from frappe.query_builder.functions import Coalesce, Locate, Replace, Sum
|
||||
@@ -28,11 +27,12 @@ from erpnext.stock.serial_batch_bundle import (
|
||||
get_batches_from_bundle,
|
||||
get_serial_nos_from_bundle,
|
||||
)
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
# TODO: Prioritize SO or WO group warehouse
|
||||
|
||||
|
||||
class PickList(Document):
|
||||
class PickList(TransactionBase):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
@@ -48,6 +48,7 @@ class PickList(Document):
|
||||
consider_rejected_warehouses: DF.Check
|
||||
customer: DF.Link | None
|
||||
customer_name: DF.Data | None
|
||||
delivery_status: DF.Literal["Not Delivered", "Fully Delivered", "Partly Delivered"]
|
||||
for_qty: DF.Float
|
||||
group_same_items: DF.Check
|
||||
ignore_pricing_rule: DF.Check
|
||||
@@ -55,12 +56,13 @@ class PickList(Document):
|
||||
material_request: DF.Link | None
|
||||
naming_series: DF.Literal["STO-PICK-.YYYY.-"]
|
||||
parent_warehouse: DF.Link | None
|
||||
per_delivered: DF.Percent
|
||||
pick_manually: DF.Check
|
||||
prompt_qty: DF.Check
|
||||
purpose: DF.Literal["Material Transfer for Manufacture", "Material Transfer", "Delivery"]
|
||||
scan_barcode: DF.Data | None
|
||||
scan_mode: DF.Check
|
||||
status: DF.Literal["Draft", "Open", "Completed", "Cancelled"]
|
||||
status: DF.Literal["Draft", "Open", "Partly Delivered", "Completed", "Cancelled"]
|
||||
work_order: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -77,6 +79,7 @@ class PickList(Document):
|
||||
self.validate_for_qty()
|
||||
self.validate_stock_qty()
|
||||
self.check_serial_no_status()
|
||||
self.validate_with_previous_doc()
|
||||
|
||||
def before_save(self):
|
||||
self.update_status()
|
||||
@@ -150,6 +153,18 @@ class PickList(Document):
|
||||
title=_("Incorrect Warehouse"),
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
super().validate_with_previous_doc(
|
||||
{
|
||||
"Sales Order": {
|
||||
"ref_dn_field": "sales_order",
|
||||
"compare_fields": [
|
||||
["company", "="],
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
def validate_sales_order_percentage(self):
|
||||
# set percentage picked in SO
|
||||
for location in self.get("locations"):
|
||||
@@ -326,19 +341,19 @@ class PickList(Document):
|
||||
doc.submit()
|
||||
|
||||
def update_status(self, status=None, update_modified=True):
|
||||
if not status:
|
||||
if self.docstatus == 0:
|
||||
status = "Draft"
|
||||
elif self.docstatus == 1:
|
||||
if target_document_exists(self.name, self.purpose):
|
||||
status = "Completed"
|
||||
else:
|
||||
status = "Open"
|
||||
elif self.docstatus == 2:
|
||||
status = "Cancelled"
|
||||
|
||||
if status:
|
||||
self.db_set("status", status)
|
||||
self.db_set("status", status, update_modified=update_modified)
|
||||
else:
|
||||
self.set_status(update=True)
|
||||
|
||||
def stock_entry_exists(self):
|
||||
if self.docstatus != 1:
|
||||
return False
|
||||
|
||||
if self.purpose == "Delivery":
|
||||
return False
|
||||
|
||||
return stock_entry_exists(self.name)
|
||||
|
||||
def update_reference_qty(self):
|
||||
packed_items = []
|
||||
@@ -346,7 +361,7 @@ class PickList(Document):
|
||||
|
||||
for item in self.locations:
|
||||
if item.product_bundle_item:
|
||||
packed_items.append(item.sales_order_item)
|
||||
packed_items.append(item.product_bundle_item)
|
||||
elif item.sales_order_item:
|
||||
so_items.append(item.sales_order_item)
|
||||
|
||||
@@ -357,12 +372,12 @@ class PickList(Document):
|
||||
self.update_sales_order_item_qty(so_items)
|
||||
|
||||
def update_packed_items_qty(self, packed_items):
|
||||
picked_items = get_picked_items_qty(packed_items)
|
||||
picked_items = get_picked_items_qty(packed_items, contains_packed_items=True)
|
||||
self.validate_picked_qty(picked_items)
|
||||
|
||||
picked_qty = frappe._dict()
|
||||
for d in picked_items:
|
||||
picked_qty[d.sales_order_item] = d.picked_qty
|
||||
picked_qty[d.product_bundle_item] = d.picked_qty
|
||||
|
||||
for packed_item in packed_items:
|
||||
frappe.db.set_value(
|
||||
@@ -575,7 +590,6 @@ class PickList(Document):
|
||||
# maintain count of each item (useful to limit get query)
|
||||
self.item_count_map.setdefault(item_code, 0)
|
||||
self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
|
||||
|
||||
return item_map.values()
|
||||
|
||||
def validate_for_qty(self):
|
||||
@@ -739,9 +753,10 @@ class PickList(Document):
|
||||
for item in self.locations:
|
||||
if not item.product_bundle_item:
|
||||
continue
|
||||
product_bundles[item.product_bundle_item] = frappe.db.get_value(
|
||||
|
||||
product_bundles[item.sales_order_item] = frappe.db.get_value(
|
||||
"Sales Order Item",
|
||||
item.product_bundle_item,
|
||||
item.sales_order_item,
|
||||
"item_code",
|
||||
)
|
||||
return product_bundles
|
||||
@@ -757,17 +772,16 @@ class PickList(Document):
|
||||
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
|
||||
"""Compute how many full bundles can be created from picked items."""
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
|
||||
|
||||
possible_bundles = []
|
||||
possible_bundles = {}
|
||||
for item in self.locations:
|
||||
if item.product_bundle_item != bundle_row:
|
||||
if item.sales_order_item != bundle_row:
|
||||
continue
|
||||
|
||||
if qty_in_bundle := bundle_items.get(item.item_code):
|
||||
possible_bundles.append(item.picked_qty / qty_in_bundle)
|
||||
else:
|
||||
possible_bundles.append(0)
|
||||
return int(flt(min(possible_bundles), precision or 6))
|
||||
possible_bundles.setdefault(item.product_bundle_item, 0)
|
||||
possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle
|
||||
|
||||
return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0
|
||||
|
||||
def has_unreserved_stock(self):
|
||||
if self.purpose == "Delivery":
|
||||
@@ -800,24 +814,35 @@ def update_pick_list_status(pick_list):
|
||||
doc.run_method("update_status")
|
||||
|
||||
|
||||
def get_picked_items_qty(items) -> list[dict]:
|
||||
def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
return (
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(pi_item)
|
||||
.select(
|
||||
pi_item.sales_order_item,
|
||||
pi_item.product_bundle_item,
|
||||
pi_item.item_code,
|
||||
pi_item.sales_order,
|
||||
Sum(pi_item.stock_qty).as_("stock_qty"),
|
||||
Sum(pi_item.picked_qty).as_("picked_qty"),
|
||||
)
|
||||
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
|
||||
.groupby(
|
||||
.where(pi_item.docstatus == 1)
|
||||
.for_update()
|
||||
)
|
||||
|
||||
if contains_packed_items:
|
||||
query = query.groupby(
|
||||
pi_item.product_bundle_item,
|
||||
pi_item.sales_order,
|
||||
).where(pi_item.product_bundle_item.isin(items))
|
||||
else:
|
||||
query = query.groupby(
|
||||
pi_item.sales_order_item,
|
||||
pi_item.sales_order,
|
||||
)
|
||||
.for_update()
|
||||
).run(as_dict=True)
|
||||
).where(pi_item.sales_order_item.isin(items))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def validate_item_locations(pick_list):
|
||||
@@ -1188,13 +1213,17 @@ def create_delivery_note(source_name, target_doc=None):
|
||||
|
||||
if not all(item.sales_order for item in pick_list.locations):
|
||||
delivery_note = create_dn_wo_so(pick_list)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.save()
|
||||
|
||||
frappe.msgprint(_("Delivery Note(s) created for the Pick List"))
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_wo_so(pick_list):
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
if not delivery_note:
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
@@ -1206,14 +1235,61 @@ def create_dn_wo_so(pick_list):
|
||||
},
|
||||
}
|
||||
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
|
||||
delivery_note.insert(ignore_mandatory=True)
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
"""Get Items from Multiple Pick Lists and create a Delivery Note for filtered customer"""
|
||||
pick_list = frappe.get_doc("Pick List", source_name)
|
||||
validate_item_locations(pick_list)
|
||||
|
||||
sales_order_arg = kwargs.get("sales_order") if kwargs else None
|
||||
customer_arg = kwargs.get("customer") if kwargs else None
|
||||
|
||||
if sales_order_arg:
|
||||
sales_orders = {sales_order_arg}
|
||||
else:
|
||||
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
|
||||
|
||||
if customer_arg:
|
||||
sales_orders = frappe.get_all(
|
||||
"Sales Order",
|
||||
filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
|
||||
|
||||
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
|
||||
if isinstance(delivery_note, str):
|
||||
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
|
||||
|
||||
delivery_note = create_dn_wo_so(pick_list, delivery_note)
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_with_so(sales_dict, pick_list):
|
||||
"""Create Delivery Note for each customer (based on SO) in a Pick List."""
|
||||
delivery_note = None
|
||||
|
||||
for customer in sales_dict:
|
||||
delivery_note = create_dn_from_so(pick_list, sales_dict[customer], None)
|
||||
if delivery_note:
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
# updates packed_items on save
|
||||
# save as multiple customers are possible
|
||||
delivery_note.save()
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
if not sales_order_list:
|
||||
return delivery_note
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
@@ -1224,20 +1300,17 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier != 1,
|
||||
}
|
||||
|
||||
for customer in sales_dict:
|
||||
for so in sales_dict[customer]:
|
||||
delivery_note = None
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
delivery_note = create_delivery_note_from_sales_order(so, delivery_note, kwargs=kwargs)
|
||||
break
|
||||
if delivery_note:
|
||||
# map all items of all sales orders of that customer
|
||||
for so in sales_dict[customer]:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.insert()
|
||||
update_packed_item_details(pick_list, delivery_note)
|
||||
delivery_note.save()
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
|
||||
delivery_note = create_delivery_note_from_sales_order(
|
||||
next(iter(sales_order_list)), delivery_note, kwargs=kwargs
|
||||
)
|
||||
|
||||
if not delivery_note:
|
||||
return
|
||||
|
||||
for so in sales_order_list:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
|
||||
return delivery_note
|
||||
|
||||
@@ -1257,24 +1330,29 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
dn_item = map_child_doc(source_doc, delivery_note, item_mapper)
|
||||
|
||||
if dn_item:
|
||||
dn_item.against_pick_list = pick_list.name
|
||||
dn_item.pick_list_item = location.name
|
||||
dn_item.warehouse = location.warehouse
|
||||
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
|
||||
dn_item.qty = flt(location.picked_qty - location.delivered_qty) / (
|
||||
flt(dn_item.conversion_factor) or 1
|
||||
)
|
||||
dn_item.batch_no = location.batch_no
|
||||
dn_item.serial_no = location.serial_no
|
||||
dn_item.use_serial_batch_fields = location.use_serial_batch_fields
|
||||
|
||||
update_delivery_note_item(source_doc, dn_item, delivery_note)
|
||||
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper)
|
||||
add_product_bundles_to_delivery_note(pick_list, delivery_note, item_mapper, sales_order)
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
|
||||
delivery_note.pick_list = pick_list.name
|
||||
delivery_note.company = pick_list.company
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
if sales_order:
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, item_mapper) -> None:
|
||||
def add_product_bundles_to_delivery_note(
|
||||
pick_list: "PickList", delivery_note, item_mapper, sales_order=None
|
||||
) -> None:
|
||||
"""Add product bundles found in pick list to delivery note.
|
||||
|
||||
When mapping pick list items, the bundle item itself isn't part of the
|
||||
@@ -1284,38 +1362,17 @@ def add_product_bundles_to_delivery_note(pick_list: "PickList", delivery_note, i
|
||||
|
||||
for so_row, item_code in product_bundles.items():
|
||||
sales_order_item = frappe.get_doc("Sales Order Item", so_row)
|
||||
if sales_order and sales_order_item.parent != sales_order:
|
||||
continue
|
||||
|
||||
dn_bundle_item = map_child_doc(sales_order_item, delivery_note, item_mapper)
|
||||
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
|
||||
so_row, product_bundle_qty_map[item_code]
|
||||
)
|
||||
dn_bundle_item.against_pick_list = pick_list.name
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
|
||||
|
||||
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
|
||||
"""Update stock details on packed items table of delivery note."""
|
||||
|
||||
def _find_so_row(packed_item):
|
||||
for item in delivery_note.items:
|
||||
if packed_item.parent_detail_docname == item.name:
|
||||
return item.so_detail
|
||||
|
||||
def _find_pick_list_location(bundle_row, packed_item):
|
||||
if not bundle_row:
|
||||
return
|
||||
for loc in pick_list.locations:
|
||||
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
|
||||
return loc
|
||||
|
||||
for packed_item in delivery_note.packed_items:
|
||||
so_row = _find_so_row(packed_item)
|
||||
location = _find_pick_list_location(so_row, packed_item)
|
||||
if not location:
|
||||
continue
|
||||
packed_item.warehouse = location.warehouse
|
||||
packed_item.batch_no = location.batch_no
|
||||
packed_item.serial_no = location.serial_no
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_entry(pick_list):
|
||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
||||
@@ -1362,14 +1419,6 @@ def get_pending_work_orders(doctype, txt, searchfield, start, page_length, filte
|
||||
).run(as_dict=as_dict)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def target_document_exists(pick_list_name, purpose):
|
||||
if purpose == "Delivery":
|
||||
return frappe.db.exists("Delivery Note", {"pick_list": pick_list_name, "docstatus": 1})
|
||||
|
||||
return stock_entry_exists(pick_list_name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, uom=None):
|
||||
details = frappe.db.get_value("Item", item_code, ["stock_uom", "name"], as_dict=1)
|
||||
@@ -1490,3 +1539,50 @@ def get_rejected_warehouses():
|
||||
)
|
||||
|
||||
return frappe.local.rejected_warehouses
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pick_list_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.has_permission("Pick List", throw=True)
|
||||
|
||||
if not filters.get("company"):
|
||||
frappe.throw(_("Please select a Company"))
|
||||
|
||||
PICK_LIST = frappe.qb.DocType("Pick List")
|
||||
PICK_LIST_ITEM = frappe.qb.DocType("Pick List Item")
|
||||
SALES_ORDER = frappe.qb.DocType("Sales Order")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PICK_LIST)
|
||||
.join(PICK_LIST_ITEM)
|
||||
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
|
||||
.join(SALES_ORDER)
|
||||
.on(PICK_LIST_ITEM.sales_order == SALES_ORDER.name)
|
||||
.select(
|
||||
PICK_LIST.name,
|
||||
SALES_ORDER.customer,
|
||||
Replace(GROUP_CONCAT(PICK_LIST_ITEM.sales_order).distinct(), ",", "<br>").as_("sales_order"),
|
||||
)
|
||||
.where(PICK_LIST.docstatus == 1)
|
||||
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
|
||||
.where(PICK_LIST.company == filters.get("company"))
|
||||
.where(SALES_ORDER.customer == filters.get("customer"))
|
||||
.groupby(PICK_LIST.name)
|
||||
)
|
||||
|
||||
if filters.get("sales_order"):
|
||||
query = query.where(PICK_LIST_ITEM.sales_order == filters.get("sales_order"))
|
||||
|
||||
if txt:
|
||||
meta = frappe.get_meta("Pick List")
|
||||
search_fields = meta.get_search_fields()
|
||||
|
||||
txt = f"%{txt}%"
|
||||
txt_condition = PICK_LIST[search_fields[-1]].like(txt)
|
||||
|
||||
for field in search_fields[:-1]:
|
||||
txt_condition |= PICK_LIST[field].like(txt)
|
||||
|
||||
query = query.where(txt_condition)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user