mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-15 19:28:50 +00:00
Compare commits
89 Commits
skip_enque
...
pot_develo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a79c3d56e0 | ||
|
|
6d47b8a24e | ||
|
|
cefb8f69e5 | ||
|
|
1fc099dacf | ||
|
|
c46c329ed8 | ||
|
|
bb279e368c | ||
|
|
aec630c74f | ||
|
|
3cde81be65 | ||
|
|
2fadfd7cfc | ||
|
|
60ba25caf2 | ||
|
|
f3f6bc8042 | ||
|
|
b171784d85 | ||
|
|
c80356c7c9 | ||
|
|
c9039d767b | ||
|
|
4f396d3049 | ||
|
|
4d090bd3b8 | ||
|
|
1ff8a6f24f | ||
|
|
59222813af | ||
|
|
a84b9434a9 | ||
|
|
a1e8caa5c1 | ||
|
|
ec55831440 | ||
|
|
a06c3e9f9f | ||
|
|
80d3dd2846 | ||
|
|
a3d4aff29c | ||
|
|
3e0c798cca | ||
|
|
f504f2caf3 | ||
|
|
ad42389352 | ||
|
|
2a08072443 | ||
|
|
6d9074d585 | ||
|
|
5f15297f28 | ||
|
|
647bba0f00 | ||
|
|
898affbee9 | ||
|
|
4ed7e3eacd | ||
|
|
12029557fe | ||
|
|
b62a6b6ab0 | ||
|
|
a9a9b1297e | ||
|
|
437bdc2d8c | ||
|
|
62d2fe50d2 | ||
|
|
c417e7d800 | ||
|
|
ec83d54826 | ||
|
|
4cda88690f | ||
|
|
d15cdd9190 | ||
|
|
cd79dcccb6 | ||
|
|
f4a945aee4 | ||
|
|
65ae9616ba | ||
|
|
9f3a310300 | ||
|
|
8d405684ce | ||
|
|
d7a4c0dfde | ||
|
|
7690a40ebb | ||
|
|
effc9d1492 | ||
|
|
e87b44f758 | ||
|
|
c9623b6fd6 | ||
|
|
83d48ca8d7 | ||
|
|
445d2acf50 | ||
|
|
ce3b1f09f5 | ||
|
|
4d0c2d8e82 | ||
|
|
98a12dd66f | ||
|
|
98c7f44280 | ||
|
|
f24f51b7de | ||
|
|
e786a93c2e | ||
|
|
f3b4237ab6 | ||
|
|
849f478894 | ||
|
|
daf4ae2a6b | ||
|
|
f8a1a7f515 | ||
|
|
948297692e | ||
|
|
79a0473705 | ||
|
|
72614bb8ff | ||
|
|
77aac6f571 | ||
|
|
4a02d1497c | ||
|
|
5ac5b7f258 | ||
|
|
a419812864 | ||
|
|
0ffc5cf012 | ||
|
|
7075c50b85 | ||
|
|
dedae4fecf | ||
|
|
0da19a40a7 | ||
|
|
7e301f02df | ||
|
|
b9a8dd468b | ||
|
|
a97a851333 | ||
|
|
0344ff2aac | ||
|
|
767f2157e6 | ||
|
|
adf13a19c4 | ||
|
|
2cefe2a20e | ||
|
|
d2b34ea05d | ||
|
|
cde5b934e5 | ||
|
|
c4570ccfad | ||
|
|
711b72b26c | ||
|
|
c096133897 | ||
|
|
287eff533a | ||
|
|
1951f71eeb |
@@ -9,7 +9,6 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import cint, flt
|
||||
from pypika.terms import Parameter
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
|
||||
@@ -509,6 +508,18 @@ def check_matching(
|
||||
to_reference_date,
|
||||
):
|
||||
exact_match = True if "exact_match" in document_types else False
|
||||
|
||||
common_filters = frappe._dict(
|
||||
{
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||
"reference_no": transaction.reference_number,
|
||||
"party_type": transaction.party_type,
|
||||
"party": transaction.party,
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
)
|
||||
|
||||
queries = get_queries(
|
||||
bank_account,
|
||||
company,
|
||||
@@ -520,20 +531,12 @@ def check_matching(
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
)
|
||||
|
||||
filters = {
|
||||
"amount": transaction.unallocated_amount,
|
||||
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
|
||||
"reference_no": transaction.reference_number,
|
||||
"party_type": transaction.party_type,
|
||||
"party": transaction.party,
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
|
||||
matching_vouchers = []
|
||||
for query in queries:
|
||||
matching_vouchers.extend(frappe.db.sql(query, filters, as_dict=True))
|
||||
matching_vouchers.extend(query.run(as_dict=True))
|
||||
|
||||
return (
|
||||
sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
|
||||
@@ -551,6 +554,7 @@ def get_queries(
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
exact_match,
|
||||
common_filters,
|
||||
):
|
||||
# get queries to get matching vouchers
|
||||
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
|
||||
@@ -571,6 +575,7 @@ def get_queries(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
)
|
||||
or []
|
||||
)
|
||||
@@ -590,6 +595,7 @@ def get_matching_queries(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
):
|
||||
queries = []
|
||||
currency = get_account_currency(bank_account)
|
||||
@@ -604,6 +610,7 @@ def get_matching_queries(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
)
|
||||
queries.append(query)
|
||||
|
||||
@@ -616,16 +623,17 @@ def get_matching_queries(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
|
||||
query = get_si_matching_query(exact_match, currency)
|
||||
query = get_si_matching_query(exact_match, currency, common_filters)
|
||||
queries.append(query)
|
||||
|
||||
if transaction.withdrawal > 0.0:
|
||||
if "purchase_invoice" in document_types:
|
||||
query = get_pi_matching_query(exact_match, currency)
|
||||
query = get_pi_matching_query(exact_match, currency, common_filters)
|
||||
queries.append(query)
|
||||
|
||||
if "bank_transaction" in document_types:
|
||||
@@ -680,7 +688,7 @@ def get_bt_matching_query(exact_match, transaction):
|
||||
.where(amount_condition)
|
||||
.where(bt.docstatus == 1)
|
||||
)
|
||||
return str(query)
|
||||
return query
|
||||
|
||||
|
||||
def get_pe_matching_query(
|
||||
@@ -692,6 +700,7 @@ def get_pe_matching_query(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
):
|
||||
# get matching payment entries query
|
||||
to_from = "to" if transaction.deposit > 0.0 else "from"
|
||||
@@ -734,7 +743,7 @@ def get_pe_matching_query(
|
||||
.where(pe.docstatus == 1)
|
||||
.where(pe.payment_type.isin([payment_type, "Internal Transfer"]))
|
||||
.where(pe.clearance_date.isnull())
|
||||
.where(getattr(pe, account_from_to) == Parameter("%(bank_account)s"))
|
||||
.where(getattr(pe, account_from_to) == common_filters.bank_account)
|
||||
.where(amount_condition)
|
||||
.where(filter_by_date)
|
||||
.orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
|
||||
@@ -743,7 +752,7 @@ def get_pe_matching_query(
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return str(query)
|
||||
return query
|
||||
|
||||
|
||||
def get_je_matching_query(
|
||||
@@ -754,6 +763,7 @@ def get_je_matching_query(
|
||||
filter_by_reference_date,
|
||||
from_reference_date,
|
||||
to_reference_date,
|
||||
common_filters,
|
||||
):
|
||||
# get matching journal entry query
|
||||
# We have mapping at the bank level
|
||||
@@ -793,7 +803,7 @@ def get_je_matching_query(
|
||||
.where(je.docstatus == 1)
|
||||
.where(je.voucher_type != "Opening Entry")
|
||||
.where(je.clearance_date.isnull())
|
||||
.where(jea.account == Parameter("%(bank_account)s"))
|
||||
.where(jea.account == common_filters.bank_account)
|
||||
.where(amount_equality if exact_match else getattr(jea, amount_field) > 0.0)
|
||||
.where(je.docstatus == 1)
|
||||
.where(filter_by_date)
|
||||
@@ -803,19 +813,19 @@ def get_je_matching_query(
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return str(query)
|
||||
return query
|
||||
|
||||
|
||||
def get_si_matching_query(exact_match, currency):
|
||||
def get_si_matching_query(exact_match, currency, common_filters):
|
||||
# get matching sales invoice query
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
sip = frappe.qb.DocType("Sales Invoice Payment")
|
||||
|
||||
amount_equality = sip.amount == Parameter("%(amount)s")
|
||||
amount_equality = sip.amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else sip.amount > 0.0
|
||||
|
||||
party_condition = si.customer == Parameter("%(party)s")
|
||||
party_condition = si.customer == common_filters.party
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
@@ -836,23 +846,23 @@ def get_si_matching_query(exact_match, currency):
|
||||
)
|
||||
.where(si.docstatus == 1)
|
||||
.where(sip.clearance_date.isnull())
|
||||
.where(sip.account == Parameter("%(bank_account)s"))
|
||||
.where(sip.account == common_filters.bank_account)
|
||||
.where(amount_condition)
|
||||
.where(si.currency == currency)
|
||||
)
|
||||
|
||||
return str(query)
|
||||
return query
|
||||
|
||||
|
||||
def get_pi_matching_query(exact_match, currency):
|
||||
def get_pi_matching_query(exact_match, currency, common_filters):
|
||||
# get matching purchase invoice query when they are also used as payment entries (is_paid)
|
||||
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
amount_equality = purchase_invoice.paid_amount == Parameter("%(amount)s")
|
||||
amount_equality = purchase_invoice.paid_amount == common_filters.amount
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else purchase_invoice.paid_amount > 0.0
|
||||
|
||||
party_condition = purchase_invoice.supplier == Parameter("%(party)s")
|
||||
party_condition = purchase_invoice.supplier == common_filters.party
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
query = (
|
||||
@@ -872,9 +882,9 @@ def get_pi_matching_query(exact_match, currency):
|
||||
.where(purchase_invoice.docstatus == 1)
|
||||
.where(purchase_invoice.is_paid == 1)
|
||||
.where(purchase_invoice.clearance_date.isnull())
|
||||
.where(purchase_invoice.cash_bank_account == Parameter("%(bank_account)s"))
|
||||
.where(purchase_invoice.cash_bank_account == common_filters.bank_account)
|
||||
.where(amount_condition)
|
||||
.where(purchase_invoice.currency == currency)
|
||||
)
|
||||
|
||||
return str(query)
|
||||
return query
|
||||
|
||||
@@ -149,6 +149,9 @@ def start_import(
|
||||
import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
# Importer expects 'Data Import' class, which has 'payload_count' attribute
|
||||
if not data_import.get("payload_count"):
|
||||
data_import.payload_count = len(data) - 1
|
||||
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
|
||||
@@ -56,17 +56,17 @@ class BankTransaction(Document):
|
||||
Bank Transaction should be on the same currency as the Bank Account.
|
||||
"""
|
||||
if self.currency and self.bank_account:
|
||||
account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
|
||||
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||
if account := frappe.get_cached_value("Bank Account", self.bank_account, "account"):
|
||||
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||
|
||||
if self.currency != account_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||
).format(
|
||||
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
|
||||
if self.currency != account_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||
).format(
|
||||
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def set_status(self):
|
||||
if self.docstatus == 2:
|
||||
|
||||
@@ -628,21 +628,21 @@ def get_account_details(
|
||||
if account_balance and (
|
||||
account_balance[0].balance or account_balance[0].balance_in_account_currency
|
||||
):
|
||||
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
company, posting_date, account_balance
|
||||
)
|
||||
row = account_with_new_balance[0]
|
||||
account_details.update(
|
||||
{
|
||||
"balance_in_base_currency": row["balance_in_base_currency"],
|
||||
"balance_in_account_currency": row["balance_in_account_currency"],
|
||||
"current_exchange_rate": row["current_exchange_rate"],
|
||||
"new_exchange_rate": row["new_exchange_rate"],
|
||||
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
|
||||
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
|
||||
"zero_balance": row["zero_balance"],
|
||||
"gain_loss": row["gain_loss"],
|
||||
}
|
||||
)
|
||||
):
|
||||
row = account_with_new_balance[0]
|
||||
account_details.update(
|
||||
{
|
||||
"balance_in_base_currency": row["balance_in_base_currency"],
|
||||
"balance_in_account_currency": row["balance_in_account_currency"],
|
||||
"current_exchange_rate": row["current_exchange_rate"],
|
||||
"new_exchange_rate": row["new_exchange_rate"],
|
||||
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
|
||||
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
|
||||
"zero_balance": row["zero_balance"],
|
||||
"gain_loss": row["gain_loss"],
|
||||
}
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@@ -395,10 +395,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Customer") {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.customer_query",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ from erpnext.accounts.general_ledger import (
|
||||
make_reverse_gl_entries,
|
||||
process_gl_map,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.party import get_party_account, set_contact_details
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
@@ -444,6 +444,8 @@ class PaymentEntry(AccountsController):
|
||||
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
|
||||
|
||||
if self.party:
|
||||
if not self.contact_person:
|
||||
set_contact_details(self, party=frappe._dict({"name": self.party}), party_type=self.party_type)
|
||||
if not self.party_balance:
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
@@ -609,9 +611,9 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def get_valid_reference_doctypes(self):
|
||||
if self.party_type == "Customer":
|
||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
|
||||
elif self.party_type == "Supplier":
|
||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
|
||||
elif self.party_type == "Shareholder":
|
||||
return ("Journal Entry",)
|
||||
elif self.party_type == "Employee":
|
||||
@@ -1277,6 +1279,7 @@ class PaymentEntry(AccountsController):
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Payment Entry",
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
|
||||
@@ -1299,7 +1302,9 @@ class PaymentEntry(AccountsController):
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit"
|
||||
dr_or_cr = (
|
||||
"credit" if invoice.reference_doctype in ["Sales Invoice", "Payment Entry"] else "debit"
|
||||
)
|
||||
args_dict["account"] = invoice.account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
@@ -1749,7 +1754,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
party_account,
|
||||
[party_account],
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
|
||||
@@ -1514,6 +1514,168 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
for field in ["account", "debit", "credit"]:
|
||||
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])
|
||||
|
||||
def test_reverse_payment_reconciliation(self):
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
pe.submit()
|
||||
|
||||
reverse_pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Pay",
|
||||
paid_from="_Test Cash - _TC",
|
||||
paid_to="Debtors - _TC",
|
||||
)
|
||||
reverse_pe.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = "_Test Company"
|
||||
pr.party_type = "Customer"
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = "Debtors - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
self.assertEqual(reverse_pe.name, pr.invoices[0].invoice_number)
|
||||
self.assertEqual(pe.name, pr.payments[0].reference_name)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
def test_advance_reverse_payment_reconciliation(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
company = "_Test Company"
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
advance_account = create_account(
|
||||
parent_account="Current Assets - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": advance_account,
|
||||
},
|
||||
)
|
||||
# Reverse Payment(essentially an Invoice)
|
||||
reverse_pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Pay",
|
||||
paid_from="_Test Cash - _TC",
|
||||
paid_to=advance_account,
|
||||
)
|
||||
reverse_pe.save() # use save() to trigger set_liability_account()
|
||||
reverse_pe.submit()
|
||||
|
||||
# Advance Payment
|
||||
pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Receive",
|
||||
paid_from=advance_account,
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
pe.save() # use save() to trigger set_liability_account()
|
||||
pe.submit()
|
||||
|
||||
# Partially reconcile advance against invoice
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = "Debtors - _TC"
|
||||
pr.default_advance_account = advance_account
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = 400
|
||||
pr.reconcile()
|
||||
|
||||
# assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": "Debtors - _TC", "debit": 0.0, "credit": 400.0},
|
||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -1000.0,
|
||||
},
|
||||
{
|
||||
"account": "Debtors - _TC",
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -400.0,
|
||||
},
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": 400.0,
|
||||
},
|
||||
]
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
# Unreconcile
|
||||
unrecon = (
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
"allocations": [{"reference_doctype": reverse_pe.doctype, "reference_name": reverse_pe.name}],
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
# assert General and Payment Ledger entries post unreconciliation
|
||||
self.expected_gle = [
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -1000.0,
|
||||
},
|
||||
]
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -340,10 +340,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
self.build_qb_filter_conditions(get_invoices=True)
|
||||
|
||||
accounts = [self.receivable_payable_account]
|
||||
|
||||
if self.default_advance_account:
|
||||
accounts.append(self.default_advance_account)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
accounts,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
|
||||
@@ -60,6 +60,8 @@ product_discount_fields = [
|
||||
"free_item_rate",
|
||||
"same_item",
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over",
|
||||
"apply_multiple_pricing_rules",
|
||||
]
|
||||
|
||||
|
||||
@@ -107,6 +107,28 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_pricing_rule_for_product_discount_slabs(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.set("price_discount_slabs", [])
|
||||
ps.set(
|
||||
"product_discount_slabs",
|
||||
[
|
||||
{
|
||||
"rule_description": "12+1",
|
||||
"min_qty": 12,
|
||||
"free_item": "_Test Item 2",
|
||||
"free_qty": 1,
|
||||
"is_recursive": 1,
|
||||
"recurse_for": 12,
|
||||
}
|
||||
],
|
||||
)
|
||||
ps.save()
|
||||
pr = frappe.get_doc("Pricing Rule", {"promotional_scheme_id": ps.product_discount_slabs[0].name})
|
||||
self.assertSequenceEqual(
|
||||
[pr.min_qty, pr.free_item, pr.free_qty, pr.recurse_for], [12, "_Test Item 2", 1, 12]
|
||||
)
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
"threshold_percentage",
|
||||
"column_break_15",
|
||||
"priority",
|
||||
"is_recursive"
|
||||
"is_recursive",
|
||||
"recurse_for",
|
||||
"apply_recursion_over"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -161,17 +163,36 @@
|
||||
"fieldname": "is_recursive",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_recursive",
|
||||
"description": "Give free item for every N quantity",
|
||||
"fieldname": "recurse_for",
|
||||
"fieldtype": "Float",
|
||||
"label": "Recurse Every (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_recursive",
|
||||
"description": "Qty for which recursion isn't applicable.",
|
||||
"fieldname": "apply_recursion_over",
|
||||
"fieldtype": "Float",
|
||||
"label": "Apply Recursion Over (As Per Transaction UOM)",
|
||||
"mandatory_depends_on": "is_recursive"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-06 21:58:18.162346",
|
||||
"modified": "2024-03-12 12:53:58.199108",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme Product Discount",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
apply_multiple_pricing_rules: DF.Check
|
||||
apply_recursion_over: DF.Float
|
||||
disable: DF.Check
|
||||
free_item: DF.Link | None
|
||||
free_item_rate: DF.Currency
|
||||
@@ -51,6 +52,7 @@ class PromotionalSchemeProductDiscount(Document):
|
||||
"19",
|
||||
"20",
|
||||
]
|
||||
recurse_for: DF.Float
|
||||
rule_description: DF.SmallText
|
||||
same_item: DF.Check
|
||||
threshold_percentage: DF.Percent
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.provide("erpnext.accounts");
|
||||
|
||||
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
|
||||
|
||||
erpnext.accounts.payment_triggers.setup("Purchase Invoice");
|
||||
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
|
||||
erpnext.accounts.taxes.setup_tax_validations("Purchase Invoice");
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"is_paid",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"update_outstanding_for_self",
|
||||
"update_billed_amount_in_purchase_order",
|
||||
"update_billed_amount_in_purchase_receipt",
|
||||
"apply_tds",
|
||||
@@ -1623,13 +1624,21 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Group",
|
||||
"options": "Supplier Group"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"description": "Debit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||
"fieldname": "update_outstanding_for_self",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Outstanding for Self"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-25 11:20:28.366808",
|
||||
"modified": "2024-03-11 14:46:30.298184",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -217,6 +217,7 @@ class PurchaseInvoice(BuyingController):
|
||||
unrealized_profit_loss_account: DF.Link | None
|
||||
update_billed_amount_in_purchase_order: DF.Check
|
||||
update_billed_amount_in_purchase_receipt: DF.Check
|
||||
update_outstanding_for_self: DF.Check
|
||||
update_stock: DF.Check
|
||||
use_company_roundoff_cost_center: DF.Check
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
@@ -829,6 +830,10 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -842,7 +847,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
|
||||
erpnext.accounts.taxes.setup_tax_validations("Purchase Taxes and Charges Template");
|
||||
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.provide("erpnext.accounts");
|
||||
|
||||
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_validations("Sales Invoice");
|
||||
erpnext.accounts.payment_triggers.setup("Sales Invoice");
|
||||
erpnext.accounts.pos.setup("Sales Invoice");
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"is_consolidated",
|
||||
"is_return",
|
||||
"return_against",
|
||||
"update_outstanding_for_self",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"update_billed_amount_in_delivery_note",
|
||||
"is_debit_note",
|
||||
@@ -2171,6 +2172,15 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't Create Loyalty Points",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"description": "Credit Note will update it's own outstanding amount, even if \"Return Against\" is specified.",
|
||||
"fieldname": "update_outstanding_for_self",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Outstanding for Self",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2183,7 +2193,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-03-01 09:21:54.201289",
|
||||
"modified": "2024-03-15 16:44:17.778370",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2238,4 +2248,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ class SalesInvoice(SellingController):
|
||||
unrealized_profit_loss_account: DF.Link | None
|
||||
update_billed_amount_in_delivery_note: DF.Check
|
||||
update_billed_amount_in_sales_order: DF.Check
|
||||
update_outstanding_for_self: DF.Check
|
||||
update_stock: DF.Check
|
||||
use_company_roundoff_cost_center: DF.Check
|
||||
write_off_account: DF.Link | None
|
||||
@@ -1219,6 +1220,10 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -1232,7 +1237,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
|
||||
@@ -1588,6 +1588,12 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
||||
|
||||
def test_zero_qty_return_invoice_with_stock_effect(self):
|
||||
cr_note = create_sales_invoice(qty=-1, rate=300, is_return=1, do_not_submit=True)
|
||||
cr_note.update_stock = True
|
||||
cr_note.items[0].qty = 0
|
||||
self.assertRaises(frappe.ValidationError, cr_note.save)
|
||||
|
||||
def test_return_invoice_with_account_mismatch(self):
|
||||
debtors2 = create_account(
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
@@ -3945,7 +3951,6 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter
|
||||
)
|
||||
|
||||
supplier.append("companies", {"company": allowed_to_interact_with})
|
||||
|
||||
supplier.insert()
|
||||
supplier_name = supplier.name
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
|
||||
erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
|
||||
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
|
||||
|
||||
@@ -408,11 +408,11 @@ class Subscription(Document):
|
||||
# Earlier subscription didn't had any company field
|
||||
company = self.get("company") or get_default_company()
|
||||
if not company:
|
||||
# fmt: off
|
||||
frappe.throw(
|
||||
_("Company is mandatory was generating invoice. Please set default company in Global Defaults.")
|
||||
_(
|
||||
"Company is mandatory for generating an invoice. Please set a default company in Global Defaults."
|
||||
)
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
invoice = frappe.new_doc(self.invoice_document_type)
|
||||
invoice.company = company
|
||||
|
||||
@@ -690,7 +690,12 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
filters = {
|
||||
"is_return": 1,
|
||||
"docstatus": 1,
|
||||
"company": self.filters.company,
|
||||
"update_outstanding_for_self": 0,
|
||||
}
|
||||
or_filters = {}
|
||||
for party_type in self.party_type:
|
||||
party_field = scrub(party_type)
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
def create_credit_note(self, docname):
|
||||
def create_credit_note(self, docname, do_not_submit=False):
|
||||
credit_note = create_sales_invoice(
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
@@ -72,6 +72,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
cost_center=self.cost_center,
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
@@ -149,7 +150,9 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
self.create_credit_note(si.name)
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = False
|
||||
cr_note.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
|
||||
@@ -167,6 +170,82 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_cr_note_flag_to_update_self(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"show_remarks": True,
|
||||
}
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.set_posting_time = True
|
||||
si.posting_date = add_days(today(), -1)
|
||||
si.save().submit()
|
||||
|
||||
report = execute(filters)
|
||||
|
||||
expected_data = [100, 100, "No Remarks"]
|
||||
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data, [row.invoice_grand_total, row.invoiced, row.remarks])
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after payment
|
||||
self.create_payment_entry(si.name)
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_payment = [100, 100, 40, 60]
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(
|
||||
expected_data_after_payment,
|
||||
[row.invoice_grand_total, row.invoiced, row.paid, row.outstanding],
|
||||
)
|
||||
|
||||
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
|
||||
cr_note = self.create_credit_note(si.name, do_not_submit=True)
|
||||
cr_note.update_outstanding_for_self = True
|
||||
cr_note.save().submit()
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_after_credit_note = [
|
||||
[100.0, 100.0, 40.0, 0.0, 60.0, si.name],
|
||||
[0, 0, 100.0, 0.0, -100.0, cr_note.name],
|
||||
]
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
si_row = [
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.voucher_no,
|
||||
]
|
||||
for row in report[1]
|
||||
if row.voucher_no == si.name
|
||||
][0]
|
||||
|
||||
cr_note_row = [
|
||||
[
|
||||
row.invoice_grand_total,
|
||||
row.invoiced,
|
||||
row.paid,
|
||||
row.credit_note,
|
||||
row.outstanding,
|
||||
row.voucher_no,
|
||||
]
|
||||
for row in report[1]
|
||||
if row.voucher_no == cr_note.name
|
||||
][0]
|
||||
self.assertEqual(expected_data_after_credit_note[0], si_row)
|
||||
self.assertEqual(expected_data_after_credit_note[1], cr_note_row)
|
||||
|
||||
def test_payment_againt_po_in_receivable_report(self):
|
||||
"""
|
||||
Payments made against Purchase Order will show up as outstanding amount
|
||||
|
||||
@@ -1027,7 +1027,7 @@ def get_outstanding_invoices(
|
||||
|
||||
if account:
|
||||
root_type, account_type = frappe.get_cached_value(
|
||||
"Account", account, ["root_type", "account_type"]
|
||||
"Account", account[0], ["root_type", "account_type"]
|
||||
)
|
||||
party_account_type = "Receivable" if root_type == "Asset" else "Payable"
|
||||
party_account_type = account_type or party_account_type
|
||||
@@ -1038,7 +1038,7 @@ def get_outstanding_invoices(
|
||||
|
||||
common_filter = common_filter or []
|
||||
common_filter.append(ple.account_type == party_account_type)
|
||||
common_filter.append(ple.account == account)
|
||||
common_filter.append(ple.account.isin(account))
|
||||
common_filter.append(ple.party_type == party_type)
|
||||
common_filter.append(ple.party == party)
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ class Asset(AccountsController):
|
||||
def on_submit(self):
|
||||
self.validate_in_use_date()
|
||||
self.make_asset_movement()
|
||||
self.reload()
|
||||
if not self.booked_fixed_asset and self.validate_make_gl_entry():
|
||||
self.make_gl_entries()
|
||||
if self.calculate_depreciation and not self.split_from:
|
||||
@@ -163,6 +164,7 @@ class Asset(AccountsController):
|
||||
self.validate_cancellation()
|
||||
self.cancel_movement_entries()
|
||||
self.cancel_capitalization()
|
||||
self.reload()
|
||||
self.delete_depreciation_entries()
|
||||
cancel_asset_depr_schedules(self)
|
||||
self.set_status()
|
||||
@@ -698,7 +700,9 @@ class Asset(AccountsController):
|
||||
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||
|
||||
if (
|
||||
purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()
|
||||
purchase_document
|
||||
and self.purchase_receipt_amount
|
||||
and getdate(self.available_for_use_date) <= getdate()
|
||||
):
|
||||
|
||||
gl_entries.append(
|
||||
|
||||
@@ -242,9 +242,7 @@ def make_depreciation_entry(
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
depreciation_posting_error = e
|
||||
|
||||
asset.set_status()
|
||||
@@ -523,6 +521,7 @@ def depreciate_asset(asset_doc, date, notes):
|
||||
|
||||
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
|
||||
|
||||
asset_doc.reload()
|
||||
cancel_depreciation_entries(asset_doc, date)
|
||||
|
||||
|
||||
|
||||
@@ -145,6 +145,10 @@ class AssetCapitalization(StockController):
|
||||
self.make_gl_entries()
|
||||
self.restore_consumed_asset_items()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
super(AssetCapitalization, self).on_trash()
|
||||
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
|
||||
@@ -327,7 +327,7 @@ class AssetDepreciationSchedule(Document):
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# if asset is being sold or scrapped
|
||||
if date_of_disposal:
|
||||
if date_of_disposal and getdate(schedule_date) >= getdate(date_of_disposal):
|
||||
from_date = add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(asset_doc.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
frappe.provide("erpnext.buying");
|
||||
frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
|
||||
erpnext.accounts.taxes.setup_tax_validations("Purchase Order");
|
||||
erpnext.buying.setup_buying_controller();
|
||||
@@ -507,7 +509,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
target: me.frm,
|
||||
setters: {
|
||||
schedule_date: undefined,
|
||||
status: undefined,
|
||||
},
|
||||
get_query_filters: {
|
||||
material_request_type: "Purchase",
|
||||
|
||||
@@ -485,7 +485,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2023-10-19 16:55:15.148325",
|
||||
"modified": "2024-03-13 11:14:06.516519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
@@ -544,7 +544,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "supplier_name, supplier_group",
|
||||
"search_fields": "supplier_group",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
|
||||
@@ -154,44 +154,6 @@ class TestSupplier(FrappeTestCase):
|
||||
# Rollback
|
||||
address.delete()
|
||||
|
||||
def test_serach_fields_for_supplier(self):
|
||||
from erpnext.controllers.queries import supplier_query
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "supp_master_name", "Naming Series")
|
||||
|
||||
supplier_name = create_supplier(supplier_name="Test Supplier 1").name
|
||||
|
||||
make_property_setter(
|
||||
"Supplier", None, "search_fields", "supplier_group", "Data", for_doctype="Doctype"
|
||||
)
|
||||
|
||||
data = supplier_query(
|
||||
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, supplier_name)
|
||||
self.assertEqual(data[0].supplier_group, "Services")
|
||||
self.assertTrue("supplier_type" not in data[0])
|
||||
|
||||
make_property_setter(
|
||||
"Supplier",
|
||||
None,
|
||||
"search_fields",
|
||||
"supplier_group, supplier_type",
|
||||
"Data",
|
||||
for_doctype="Doctype",
|
||||
)
|
||||
data = supplier_query(
|
||||
"Supplier", supplier_name, "name", 0, 20, filters={"name": supplier_name}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, supplier_name)
|
||||
self.assertEqual(data[0].supplier_group, "Services")
|
||||
self.assertEqual(data[0].supplier_type, "Company")
|
||||
self.assertTrue("supplier_type" in data[0])
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "supp_master_name", "Supplier Name")
|
||||
|
||||
|
||||
def create_supplier(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -89,6 +89,7 @@ force_item_fields = (
|
||||
"weight_per_unit",
|
||||
"weight_uom",
|
||||
"total_weight",
|
||||
"valuation_rate",
|
||||
)
|
||||
|
||||
|
||||
@@ -168,6 +169,13 @@ class AccountsController(TransactionBase):
|
||||
if not self.get("is_return") and not self.get("is_debit_note"):
|
||||
self.validate_qty_is_not_zero()
|
||||
|
||||
if (
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
and self.get("is_return")
|
||||
and self.get("update_stock")
|
||||
):
|
||||
self.validate_zero_qty_for_return_invoices_with_stock()
|
||||
|
||||
if self.get("_action") and self._action != "update_after_submit":
|
||||
self.set_missing_values(for_validate=True)
|
||||
|
||||
@@ -218,17 +226,18 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
if self.get("is_return") and self.get("return_against") and not self.get("is_pos"):
|
||||
# if self.get("is_return") and self.get("return_against"):
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0} will be treated as a standalone {0}. Post creation use {1} tool to reconcile against {2}."
|
||||
).format(
|
||||
document_type,
|
||||
get_link_to_form("Payment Reconciliation"),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
if self.get("update_outstanding_for_self"):
|
||||
document_type = "Credit Note" if self.doctype == "Sales Invoice" else "Debit Note"
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"We can see {0} is made against {1}. If you want {1}'s outstanding to be updated, uncheck '{2}' checkbox. <br><br> Or you can use {3} tool to reconcile against {1} later."
|
||||
).format(
|
||||
frappe.bold(document_type),
|
||||
get_link_to_form(self.doctype, self.get("return_against")),
|
||||
frappe.bold("Update Outstanding for Self"),
|
||||
get_link_to_form("Payment Reconciliation"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
@@ -601,23 +610,31 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_due_date(self):
|
||||
if self.get("is_pos"):
|
||||
if self.get("is_pos") or self.doctype not in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return
|
||||
|
||||
from erpnext.accounts.party import validate_due_date
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
posting_date = (
|
||||
self.posting_date if self.doctype == "Sales Invoice" else (self.bill_date or self.posting_date)
|
||||
)
|
||||
|
||||
# skip due date validation for records via Data Import
|
||||
if frappe.flags.in_import and getdate(self.due_date) < getdate(posting_date):
|
||||
self.due_date = posting_date
|
||||
|
||||
elif self.doctype == "Sales Invoice":
|
||||
if not self.due_date:
|
||||
frappe.throw(_("Due Date is mandatory"))
|
||||
|
||||
validate_due_date(
|
||||
self.posting_date,
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.payment_terms_template,
|
||||
)
|
||||
elif self.doctype == "Purchase Invoice":
|
||||
validate_due_date(
|
||||
self.bill_date or self.posting_date,
|
||||
posting_date,
|
||||
self.due_date,
|
||||
self.bill_date,
|
||||
self.payment_terms_template,
|
||||
@@ -1043,6 +1060,18 @@ class AccountsController(TransactionBase):
|
||||
else:
|
||||
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
|
||||
|
||||
def validate_zero_qty_for_return_invoices_with_stock(self):
|
||||
rows = []
|
||||
for item in self.items:
|
||||
if not flt(item.qty):
|
||||
rows.append(item)
|
||||
if rows:
|
||||
frappe.throw(
|
||||
_(
|
||||
"For Return Invoices with Stock effect, '0' qty Items are not allowed. Following rows are affected: {0}"
|
||||
).format(frappe.bold(comma_and(["#" + str(x.idx) for x in rows])))
|
||||
)
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
for item in self.items:
|
||||
if self.doctype == "Purchase Receipt" and item.rejected_qty:
|
||||
@@ -2707,14 +2736,20 @@ def get_advance_journal_entries(
|
||||
else:
|
||||
q = q.where(journal_acc.debit_in_account_currency > 0)
|
||||
|
||||
reference_or_condition = []
|
||||
|
||||
if include_unallocated:
|
||||
q = q.where((journal_acc.reference_name.isnull()) | (journal_acc.reference_name == ""))
|
||||
reference_or_condition.append(journal_acc.reference_name.isnull())
|
||||
reference_or_condition.append(journal_acc.reference_name == "")
|
||||
|
||||
if order_list:
|
||||
q = q.where(
|
||||
reference_or_condition.append(
|
||||
(journal_acc.reference_type == order_doctype) & ((journal_acc.reference_name).isin(order_list))
|
||||
)
|
||||
|
||||
if reference_or_condition:
|
||||
q = q.where(Criterion.any(reference_or_condition))
|
||||
|
||||
q = q.orderby(journal_entry.posting_date)
|
||||
|
||||
journal_entries = q.run(as_dict=True)
|
||||
|
||||
@@ -513,6 +513,14 @@ class BuyingController(SubcontractingController):
|
||||
(not cint(self.is_return) and self.docstatus == 1)
|
||||
or (cint(self.is_return) and self.docstatus == 2)
|
||||
):
|
||||
serial_and_batch_bundle = d.get("serial_and_batch_bundle")
|
||||
if self.is_internal_transfer() and self.is_return and self.docstatus == 2:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": d.name, "warehouse": d.from_warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
@@ -521,19 +529,24 @@ class BuyingController(SubcontractingController):
|
||||
"outgoing_rate": d.rate,
|
||||
"recalculate_rate": 1,
|
||||
"dependant_sle_voucher_detail_no": d.name,
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
)
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
type_of_transaction = "Inward"
|
||||
if self.docstatus == 2:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": flt(pr_qty),
|
||||
"serial_and_batch_bundle": (
|
||||
d.serial_and_batch_bundle
|
||||
if not self.is_internal_transfer()
|
||||
else self.get_package_for_target_warehouse(d)
|
||||
if not self.is_internal_transfer() or self.is_return
|
||||
else self.get_package_for_target_warehouse(d, type_of_transaction=type_of_transaction)
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -570,7 +583,17 @@ class BuyingController(SubcontractingController):
|
||||
or (cint(self.is_return) and self.docstatus == 1)
|
||||
):
|
||||
from_warehouse_sle = self.get_sl_entries(
|
||||
d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
|
||||
d,
|
||||
{
|
||||
"actual_qty": -1 * pr_qty,
|
||||
"warehouse": d.from_warehouse,
|
||||
"recalculate_rate": 1,
|
||||
"serial_and_batch_bundle": (
|
||||
self.get_package_for_target_warehouse(d, d.from_warehouse, "Inward")
|
||||
if self.is_internal_transfer() and self.is_return
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
@@ -597,13 +620,15 @@ class BuyingController(SubcontractingController):
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
|
||||
def get_package_for_target_warehouse(self, item) -> str:
|
||||
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
|
||||
if not item.serial_and_batch_bundle:
|
||||
return ""
|
||||
|
||||
if not warehouse:
|
||||
warehouse = item.warehouse
|
||||
|
||||
return self.make_package_for_transfer(
|
||||
item.serial_and_batch_bundle,
|
||||
item.warehouse,
|
||||
item.serial_and_batch_bundle, warehouse, type_of_transaction=type_of_transaction
|
||||
)
|
||||
|
||||
def update_ordered_and_reserved_qty(self):
|
||||
|
||||
@@ -85,79 +85,6 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
)
|
||||
|
||||
# searches for customer
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Customer"
|
||||
conditions = []
|
||||
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
|
||||
|
||||
fields = ["name"]
|
||||
if cust_master_name != "Customer Name":
|
||||
fields.append("customer_name")
|
||||
|
||||
fields = get_fields(doctype, fields)
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabCustomer`
|
||||
where docstatus < 2
|
||||
and ({scond}) and disabled=0
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
|
||||
idx desc,
|
||||
name, customer_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{
|
||||
"fields": ", ".join(fields),
|
||||
"scond": searchfields,
|
||||
"mcond": get_match_cond(doctype),
|
||||
"fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
|
||||
}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
as_dict=as_dict,
|
||||
)
|
||||
|
||||
|
||||
# searches for supplier
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Supplier"
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
|
||||
fields = ["name"]
|
||||
if supp_master_name != "Supplier Name":
|
||||
fields.append("supplier_name")
|
||||
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {field} from `tabSupplier`
|
||||
where docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
|
||||
{mcond}
|
||||
order by
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
|
||||
idx desc,
|
||||
name, supplier_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||
),
|
||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||
as_dict=as_dict,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@@ -670,7 +597,7 @@ def get_filtered_dimensions(
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
|
||||
meta = frappe.get_meta(doctype)
|
||||
if meta.is_tree:
|
||||
if meta.is_tree and meta.has_field("is_group"):
|
||||
query_filters.append(["is_group", "=", 0])
|
||||
|
||||
if meta.has_field("disabled"):
|
||||
|
||||
@@ -423,6 +423,15 @@ def make_return_doc(
|
||||
]:
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
warehouse = source_doc.warehouse if qty_field == "stock_qty" else source_doc.rejected_warehouse
|
||||
if source_parent.doctype in [
|
||||
"Sales Invoice",
|
||||
"POS Invoice",
|
||||
"Delivery Note",
|
||||
] and source_parent.get("is_internal_customer"):
|
||||
type_of_transaction = "Outward"
|
||||
warehouse = source_doc.target_warehouse
|
||||
|
||||
cls_obj = SerialBatchCreation(
|
||||
{
|
||||
"type_of_transaction": type_of_transaction,
|
||||
@@ -432,7 +441,7 @@ def make_return_doc(
|
||||
"returned_serial_nos": returned_serial_nos,
|
||||
"voucher_type": source_parent.doctype,
|
||||
"do_not_submit": True,
|
||||
"warehouse": source_doc.warehouse,
|
||||
"warehouse": warehouse,
|
||||
"has_serial_no": item_details.has_serial_no,
|
||||
"has_batch_no": item_details.has_batch_no,
|
||||
}
|
||||
@@ -575,9 +584,56 @@ def make_return_doc(
|
||||
if not item_details.has_batch_no and not item_details.has_serial_no:
|
||||
return
|
||||
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if target_doc.get(qty_field):
|
||||
if not target_doc.get("use_serial_batch_fields"):
|
||||
for qty_field in ["stock_qty", "rejected_qty"]:
|
||||
if not target_doc.get(qty_field):
|
||||
continue
|
||||
|
||||
update_serial_batch_no(source_doc, target_doc, source_parent, item_details, qty_field)
|
||||
elif target_doc.get("use_serial_batch_fields"):
|
||||
update_non_bundled_serial_nos(source_doc, target_doc, source_parent)
|
||||
|
||||
def update_non_bundled_serial_nos(source_doc, target_doc, source_parent):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
if source_doc.serial_no:
|
||||
returned_serial_nos = get_returned_non_bundled_serial_nos(source_doc, source_parent)
|
||||
serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
|
||||
if serial_nos:
|
||||
target_doc.serial_no = "\n".join(serial_nos)
|
||||
|
||||
if source_doc.get("rejected_serial_no"):
|
||||
returned_serial_nos = get_returned_non_bundled_serial_nos(
|
||||
source_doc, source_parent, serial_no_field="rejected_serial_no"
|
||||
)
|
||||
rejected_serial_nos = list(
|
||||
set(get_serial_nos(source_doc.rejected_serial_no)) - set(returned_serial_nos)
|
||||
)
|
||||
if rejected_serial_nos:
|
||||
target_doc.rejected_serial_no = "\n".join(rejected_serial_nos)
|
||||
|
||||
def get_returned_non_bundled_serial_nos(child_doc, parent_doc, serial_no_field="serial_no"):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||
if child_doc.doctype == "Delivery Note Item":
|
||||
return_ref_field = "dn_detail"
|
||||
|
||||
serial_nos = []
|
||||
|
||||
fields = [f"`{'tab' + child_doc.doctype}`.`{serial_no_field}`"]
|
||||
|
||||
filters = [
|
||||
[parent_doc.doctype, "return_against", "=", parent_doc.name],
|
||||
[parent_doc.doctype, "is_return", "=", 1],
|
||||
[child_doc.doctype, return_ref_field, "=", child_doc.name],
|
||||
[parent_doc.doctype, "docstatus", "=", 1],
|
||||
]
|
||||
|
||||
for row in frappe.get_all(parent_doc.doctype, fields=fields, filters=filters):
|
||||
serial_nos.extend(get_serial_nos(row.get(serial_no_field)))
|
||||
|
||||
return serial_nos
|
||||
|
||||
def update_terms(source_doc, target_doc, source_parent):
|
||||
target_doc.payment_amount = -source_doc.payment_amount
|
||||
|
||||
@@ -439,8 +439,10 @@ class SellingController(StockController):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty"))
|
||||
|
||||
if not d.incoming_rate or (
|
||||
get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return")
|
||||
if (
|
||||
not d.incoming_rate
|
||||
or self.is_internal_transfer()
|
||||
or (get_valuation_method(d.item_code) == "Moving Average" and self.get("is_return"))
|
||||
):
|
||||
d.incoming_rate = get_incoming_rate(
|
||||
{
|
||||
@@ -455,6 +457,8 @@ class SellingController(StockController):
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": d.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"batch_no": d.batch_no,
|
||||
"serial_no": d.serial_no,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
@@ -527,13 +531,26 @@ class SellingController(StockController):
|
||||
self.make_sl_entries(sl_entries)
|
||||
|
||||
def get_sle_for_source_warehouse(self, item_row):
|
||||
serial_and_batch_bundle = item_row.serial_and_batch_bundle
|
||||
if serial_and_batch_bundle and self.is_internal_transfer() and self.is_return:
|
||||
if self.docstatus == 1:
|
||||
serial_and_batch_bundle = self.make_package_for_transfer(
|
||||
serial_and_batch_bundle, item_row.warehouse, type_of_transaction="Inward"
|
||||
)
|
||||
else:
|
||||
serial_and_batch_bundle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_detail_no": item_row.name, "warehouse": item_row.warehouse},
|
||||
"serial_and_batch_bundle",
|
||||
)
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
item_row,
|
||||
{
|
||||
"actual_qty": -1 * flt(item_row.qty),
|
||||
"incoming_rate": item_row.incoming_rate,
|
||||
"recalculate_rate": cint(self.is_return),
|
||||
"serial_and_batch_bundle": item_row.serial_and_batch_bundle,
|
||||
"serial_and_batch_bundle": serial_and_batch_bundle,
|
||||
},
|
||||
)
|
||||
if item_row.target_warehouse and not cint(self.is_return):
|
||||
@@ -554,9 +571,15 @@ class SellingController(StockController):
|
||||
if item_row.warehouse:
|
||||
sle.dependant_sle_voucher_detail_no = item_row.name
|
||||
|
||||
if item_row.serial_and_batch_bundle:
|
||||
if item_row.serial_and_batch_bundle and not cint(self.is_return):
|
||||
type_of_transaction = "Inward"
|
||||
if cint(self.is_return):
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
sle["serial_and_batch_bundle"] = self.make_package_for_transfer(
|
||||
item_row.serial_and_batch_bundle, item_row.target_warehouse
|
||||
item_row.serial_and_batch_bundle,
|
||||
item_row.target_warehouse,
|
||||
type_of_transaction=type_of_transaction,
|
||||
)
|
||||
|
||||
return sle
|
||||
|
||||
@@ -236,6 +236,14 @@ class StockController(AccountsController):
|
||||
qty = row.get("rejected_qty")
|
||||
warehouse = row.get("rejected_warehouse")
|
||||
|
||||
if (
|
||||
self.is_internal_transfer()
|
||||
and self.doctype in ["Sales Invoice", "Delivery Note"]
|
||||
and self.is_return
|
||||
):
|
||||
warehouse = row.get("target_warehouse") or row.get("warehouse")
|
||||
type_of_transaction = "Outward"
|
||||
|
||||
bundle_details.update(
|
||||
{
|
||||
"qty": qty,
|
||||
@@ -579,7 +587,7 @@ class StockController(AccountsController):
|
||||
bundle_doc.warehouse = warehouse
|
||||
bundle_doc.type_of_transaction = type_of_transaction
|
||||
bundle_doc.voucher_type = self.doctype
|
||||
bundle_doc.voucher_no = self.name
|
||||
bundle_doc.voucher_no = "" if self.is_new() or self.docstatus == 2 else self.name
|
||||
bundle_doc.is_cancelled = 0
|
||||
|
||||
for row in bundle_doc.entries:
|
||||
@@ -595,6 +603,7 @@ class StockController(AccountsController):
|
||||
|
||||
bundle_doc.calculate_qty_and_amount()
|
||||
bundle_doc.flags.ignore_permissions = True
|
||||
bundle_doc.flags.ignore_validate = True
|
||||
bundle_doc.save(ignore_permissions=True)
|
||||
|
||||
return bundle_doc.name
|
||||
|
||||
@@ -31,18 +31,6 @@ class TestQueries(unittest.TestCase):
|
||||
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
|
||||
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
|
||||
|
||||
def test_customer_query(self):
|
||||
query = add_default_params(queries.customer_query, "Customer")
|
||||
|
||||
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
|
||||
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
|
||||
|
||||
def test_supplier_query(self):
|
||||
query = add_default_params(queries.supplier_query, "Supplier")
|
||||
|
||||
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
|
||||
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
|
||||
|
||||
def test_item_query(self):
|
||||
query = add_default_params(queries.item_query, "Item")
|
||||
|
||||
|
||||
@@ -17,10 +17,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.frm.set_query("customer", function (doc, cdt, cdn) {
|
||||
return { query: "erpnext.controllers.queries.customer_query" };
|
||||
});
|
||||
|
||||
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" };
|
||||
});
|
||||
|
||||
@@ -282,9 +282,6 @@ has_website_permission = {
|
||||
|
||||
before_tests = "erpnext.setup.utils.before_tests"
|
||||
|
||||
standard_queries = {
|
||||
"Customer": "erpnext.controllers.queries.customer_query",
|
||||
}
|
||||
|
||||
period_closing_doctypes = [
|
||||
"Sales Invoice",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,28 +27,6 @@ test_dependencies = ["Item", "Quality Inspection Template"]
|
||||
|
||||
|
||||
class TestBOM(FrappeTestCase):
|
||||
@timeout
|
||||
def test_bom_qty(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# No error.
|
||||
bom = frappe.new_doc("BOM")
|
||||
item = make_item(properties={"is_stock_item": 1})
|
||||
bom.item = fg_item.item_code
|
||||
bom.quantity = 1
|
||||
bom.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": 0,
|
||||
"uom": bom_item.stock_uom,
|
||||
"stock_uom": bom_item.stock_uom,
|
||||
"rate": 100.0,
|
||||
},
|
||||
)
|
||||
bom.save()
|
||||
self.assertEqual(bom.items[0].qty, 0)
|
||||
|
||||
@timeout
|
||||
def test_get_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
@@ -264,7 +264,7 @@ class JobCard(Document):
|
||||
if not self.has_overlap(production_capacity, time_logs):
|
||||
return {}
|
||||
|
||||
if self.workstation_type and time_logs:
|
||||
if not self.workstation and self.workstation_type and time_logs:
|
||||
if workstation_time := self.get_workstation_based_on_available_slot(time_logs):
|
||||
self.workstation = workstation_time.get("workstation")
|
||||
return workstation_time
|
||||
@@ -420,7 +420,7 @@ class JobCard(Document):
|
||||
if not workstation_doc.working_hours or cint(
|
||||
frappe.db.get_single_value("Manufacturing Settings", "allow_overtime")
|
||||
):
|
||||
if get_datetime(row.planned_end_time) < get_datetime(row.planned_start_time):
|
||||
if get_datetime(row.planned_end_time) <= get_datetime(row.planned_start_time):
|
||||
row.planned_end_time = add_to_date(row.planned_start_time, minutes=row.time_in_mins)
|
||||
row.remaining_time_in_mins = 0.0
|
||||
else:
|
||||
|
||||
@@ -52,10 +52,10 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">Add</button>
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-add" data-item-code="{{ escape(row.item_code) }}">{{ __("Add") }}</button>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">Move</button>
|
||||
<button style="margin-left: 7px;" class="btn btn-default btn-xs btn-move" data-item-code="{{ escape(row.item_code) }}">{{ __("Move") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{% }); %}
|
||||
{% }); %}
|
||||
|
||||
@@ -330,6 +330,13 @@ class WorkOrder(Document):
|
||||
else:
|
||||
status = "Cancelled"
|
||||
|
||||
if (
|
||||
self.skip_transfer
|
||||
and self.produced_qty
|
||||
and self.qty > (flt(self.produced_qty) + flt(self.process_loss_qty))
|
||||
):
|
||||
status = "In Process"
|
||||
|
||||
return status
|
||||
|
||||
def update_work_order_qty(self):
|
||||
@@ -784,7 +791,7 @@ class WorkOrder(Document):
|
||||
)
|
||||
|
||||
def update_completed_qty_in_material_request(self):
|
||||
if self.material_request:
|
||||
if self.material_request and self.material_request_item:
|
||||
frappe.get_doc("Material Request", self.material_request).update_completed_qty(
|
||||
[self.material_request_item]
|
||||
)
|
||||
|
||||
@@ -356,8 +356,10 @@ erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
erpnext.patches.v15_0.create_advance_payment_status
|
||||
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
|
||||
erpnext.patches.v14_0.update_flag_for_return_invoices
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
erpnext.patches.v14_0.set_maintain_stock_for_bom_item
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.delete_orphaned_asset_movement_item_records
|
||||
erpnext.patches.v15_0.remove_cancelled_asset_capitalization_from_asset
|
||||
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal file
62
erpnext/patches/v14_0/update_flag_for_return_invoices.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from frappe import qb
|
||||
|
||||
|
||||
def execute():
|
||||
# Set "update_outstanding_for_self" flag in Credit/Debit Notes
|
||||
# Fetch Credit/Debit notes that does have 'return_against' but still post ledger entries against themselves.
|
||||
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
# Use hardcoded 'creation' date to isolate Credit/Debit notes created post v14 backport
|
||||
# https://github.com/frappe/erpnext/pull/39497
|
||||
creation_date = "2024-01-25"
|
||||
|
||||
si = qb.DocType("Sales Invoice")
|
||||
if cr_notes := (
|
||||
qb.from_(si)
|
||||
.select(si.name)
|
||||
.where(
|
||||
(si.creation.gte(creation_date))
|
||||
& (si.docstatus == 1)
|
||||
& (si.is_return == True)
|
||||
& (si.return_against.notnull())
|
||||
)
|
||||
.run()
|
||||
):
|
||||
cr_notes = [x[0] for x in cr_notes]
|
||||
if docs_that_require_update := (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_no)
|
||||
.distinct()
|
||||
.where((gle.voucher_no.isin(cr_notes)) & (gle.voucher_no == gle.against_voucher))
|
||||
.run()
|
||||
):
|
||||
docs_that_require_update = [x[0] for x in docs_that_require_update]
|
||||
qb.update(si).set(si.update_outstanding_for_self, True).where(
|
||||
si.name.isin(docs_that_require_update)
|
||||
).run()
|
||||
|
||||
pi = qb.DocType("Purchase Invoice")
|
||||
if dr_notes := (
|
||||
qb.from_(pi)
|
||||
.select(pi.name)
|
||||
.where(
|
||||
(pi.creation.gte(creation_date))
|
||||
& (pi.docstatus == 1)
|
||||
& (pi.is_return == True)
|
||||
& (pi.return_against.notnull())
|
||||
)
|
||||
.run()
|
||||
):
|
||||
dr_notes = [x[0] for x in dr_notes]
|
||||
if docs_that_require_update := (
|
||||
qb.from_(gle)
|
||||
.select(gle.voucher_no)
|
||||
.distinct()
|
||||
.where((gle.voucher_no.isin(dr_notes)) & (gle.voucher_no == gle.against_voucher))
|
||||
.run()
|
||||
):
|
||||
docs_that_require_update = [x[0] for x in docs_that_require_update]
|
||||
qb.update(pi).set(pi.update_outstanding_for_self, True).where(
|
||||
pi.name.isin(docs_that_require_update)
|
||||
).run()
|
||||
@@ -0,0 +1,11 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
cancelled_asset_capitalizations = frappe.get_all(
|
||||
"Asset Capitalization",
|
||||
filters={"docstatus": 2},
|
||||
fields=["name", "target_asset"],
|
||||
)
|
||||
for asset_capitalization in cancelled_asset_capitalizations:
|
||||
frappe.db.set_value("Asset", asset_capitalization.target_asset, "capitalized_in", None)
|
||||
@@ -27,8 +27,6 @@ frappe.ui.form.on("Project", {
|
||||
};
|
||||
};
|
||||
|
||||
frm.set_query("customer", "erpnext.controllers.queries.customer_query");
|
||||
|
||||
frm.set_query("user", "users", function () {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.project.project.get_users_for_project",
|
||||
|
||||
@@ -74,11 +74,6 @@ erpnext.buying = {
|
||||
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.supplier) {
|
||||
this.frm.set_query("supplier", function() {
|
||||
return{ query: "erpnext.controllers.queries.supplier_query" }});
|
||||
}
|
||||
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
if (me.frm.doc.is_subcontracted) {
|
||||
var filters = {'supplier': me.frm.doc.supplier};
|
||||
|
||||
@@ -11,6 +11,18 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con
|
||||
}
|
||||
}
|
||||
|
||||
barcode(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.barcode) {
|
||||
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
"item_code": r.message.item_code,
|
||||
"qty": 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup_warehouse_query() {
|
||||
var me = this;
|
||||
erpnext.queries.setup_queries(this.frm, "Warehouse", function() {
|
||||
|
||||
@@ -411,6 +411,19 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
barcode_scanner.process_scan();
|
||||
}
|
||||
|
||||
barcode(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
if (row.barcode) {
|
||||
erpnext.stock.utils.set_item_details_using_barcode(this.frm, row, (r) => {
|
||||
debugger
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
"item_code": r.message.item_code,
|
||||
"qty": 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validate_has_items () {
|
||||
let table = this.frm.doc.items;
|
||||
this.frm.has_items = (table && table.length
|
||||
@@ -1813,8 +1826,8 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
let items = [];
|
||||
|
||||
me.frm.doc.items.forEach(d => {
|
||||
// if same item was added a free item through a different pricing rule, keep it
|
||||
if(d.item_code != item.remove_free_item || !d.is_free_item || removed_pricing_rule?.includes(d.pricing_rules)) {
|
||||
// if same item was added as free item through a different pricing rule, keep it
|
||||
if(d.item_code != item.remove_free_item || !d.is_free_item || !removed_pricing_rule?.includes(d.pricing_rules)) {
|
||||
items.push(d);
|
||||
}
|
||||
});
|
||||
@@ -2216,7 +2229,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
});
|
||||
|
||||
this.frm.doc.items.forEach(item => {
|
||||
if (!item.quality_inspection) {
|
||||
if (this.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
"docname": item.name,
|
||||
@@ -2240,6 +2253,16 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
}
|
||||
|
||||
has_inspection_required(item) {
|
||||
if (this.frm.doc.doctype === "Stock Entry" && this.frm.doc.purpose == "Manufacture" ) {
|
||||
if (item.is_finished_item && !item.quality_inspection) {
|
||||
return true;
|
||||
}
|
||||
} else if (!item.quality_inspection) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
get_method_for_payment() {
|
||||
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
|
||||
if(cur_frm.doc.__onload && cur_frm.doc.__onload.make_payment_via_journal_entry){
|
||||
|
||||
@@ -12,14 +12,6 @@ $.extend(erpnext.queries, {
|
||||
return { query: "erpnext.controllers.queries.lead_query" };
|
||||
},
|
||||
|
||||
customer: function () {
|
||||
return { query: "erpnext.controllers.queries.customer_query" };
|
||||
},
|
||||
|
||||
supplier: function () {
|
||||
return { query: "erpnext.controllers.queries.supplier_query" };
|
||||
},
|
||||
|
||||
item: function (filters) {
|
||||
var args = { query: "erpnext.controllers.queries.item_query" };
|
||||
if (filters) args["filters"] = filters;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
frappe.provide("erpnext");
|
||||
frappe.provide("erpnext.utils");
|
||||
frappe.provide("erpnext.stock.utils");
|
||||
|
||||
$.extend(erpnext, {
|
||||
get_currency: function (company) {
|
||||
@@ -1201,3 +1202,10 @@ function attach_selector_button(inner_text, append_loction, context, grid_row) {
|
||||
context.show_serial_batch_selector(grid_row.frm, grid_row.doc, "", "", true);
|
||||
});
|
||||
}
|
||||
|
||||
$.extend(erpnext.stock.utils, {
|
||||
set_item_details_using_barcode(frm, child_row, callback) {
|
||||
const barcode_scanner = new erpnext.utils.BarcodeScanner({ frm: frm });
|
||||
barcode_scanner.scan_api_call(child_row.barcode, callback);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -542,6 +542,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
frappe.throw(__("Please add atleast one Serial No / Batch No"));
|
||||
}
|
||||
|
||||
if (!warehouse) {
|
||||
frappe.throw(__("Please select a Warehouse"));
|
||||
}
|
||||
|
||||
frappe
|
||||
.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.add_serial_batch_ledgers",
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2023-12-28 13:15:36.298369",
|
||||
"modified": "2024-03-16 19:41:47.971815",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
@@ -661,7 +661,7 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "customer_name,customer_group,territory, mobile_no,primary_address",
|
||||
"search_fields": "customer_group,territory, mobile_no,primary_address",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
|
||||
@@ -370,37 +370,6 @@ class TestCustomer(FrappeTestCase):
|
||||
due_date = get_due_date("2017-01-22", "Customer", "_Test Customer")
|
||||
self.assertEqual(due_date, "2017-01-22")
|
||||
|
||||
def test_serach_fields_for_customer(self):
|
||||
from erpnext.controllers.queries import customer_query
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "cust_master_name", "Naming Series")
|
||||
|
||||
make_property_setter(
|
||||
"Customer", None, "search_fields", "customer_group", "Data", for_doctype="Doctype"
|
||||
)
|
||||
|
||||
data = customer_query(
|
||||
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, "_Test Customer")
|
||||
self.assertEqual(data[0].customer_group, "_Test Customer Group")
|
||||
self.assertTrue("territory" not in data[0])
|
||||
|
||||
make_property_setter(
|
||||
"Customer", None, "search_fields", "customer_group, territory", "Data", for_doctype="Doctype"
|
||||
)
|
||||
data = customer_query(
|
||||
"Customer", "_Test Customer", "", 0, 20, filters={"name": "_Test Customer"}, as_dict=True
|
||||
)
|
||||
|
||||
self.assertEqual(data[0].name, "_Test Customer")
|
||||
self.assertEqual(data[0].customer_group, "_Test Customer Group")
|
||||
self.assertEqual(data[0].territory, "_Test Territory")
|
||||
self.assertTrue("territory" in data[0])
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "cust_master_name", "Customer Name")
|
||||
|
||||
def test_parse_full_name(self):
|
||||
first, middle, last = parse_full_name("John")
|
||||
self.assertEqual(first, "John")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_validations("Sales Taxes and Charges Template");
|
||||
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
|
||||
erpnext.pre_sales.set_as_lost("Quotation");
|
||||
|
||||
@@ -42,6 +42,39 @@ class TestQuotation(FrappeTestCase):
|
||||
|
||||
self.assertTrue(sales_order.get("payment_schedule"))
|
||||
|
||||
def test_gross_profit(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.get_item_details import insert_item_price
|
||||
|
||||
item_doc = make_item("_Test Item for Gross Profit", {"is_stock_item": 1})
|
||||
item_code = item_doc.name
|
||||
make_stock_entry(item_code=item_code, qty=10, rate=100, target="_Test Warehouse - _TC")
|
||||
|
||||
selling_price_list = frappe.get_all("Price List", filters={"selling": 1}, limit=1)[0].name
|
||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 1)
|
||||
insert_item_price(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"price_list": selling_price_list,
|
||||
"price_list_rate": 300,
|
||||
"rate": 300,
|
||||
"conversion_factor": 1,
|
||||
"discount_amount": 0.0,
|
||||
"currency": frappe.db.get_value("Price List", selling_price_list, "currency"),
|
||||
"uom": item_doc.stock_uom,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
quotation = make_quotation(
|
||||
item_code=item_code, qty=1, rate=300, selling_price_list=selling_price_list
|
||||
)
|
||||
self.assertEqual(quotation.items[0].valuation_rate, 100)
|
||||
self.assertEqual(quotation.items[0].gross_profit, 200)
|
||||
frappe.db.set_single_value("Stock Settings", "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
def test_maintain_rate_in_sales_cycle_is_enforced(self):
|
||||
from erpnext.selling.doctype.quotation.quotation import make_sales_order
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_filters("Sales Taxes and Charges");
|
||||
erpnext.accounts.taxes.setup_tax_validations("Sales Order");
|
||||
erpnext.sales_common.setup_selling_controller();
|
||||
@@ -737,14 +739,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
|
||||
status: ["!=", "Lost"],
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
d.$parent.append(`
|
||||
<span class='small text-muted'>
|
||||
${__("Note: Please create Sales Orders from individual Quotations to select from among Alternative Items.")}
|
||||
</span>
|
||||
`);
|
||||
}, 200);
|
||||
},
|
||||
__("Get Items From")
|
||||
);
|
||||
|
||||
@@ -2101,6 +2101,46 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertFalse(row.warehouse == rejected_warehouse)
|
||||
self.assertTrue(row.warehouse == warehouse)
|
||||
|
||||
def test_pick_list_for_batch(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Batch Item for Pick LIST",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BATCH-SDDTBIFRM-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
se = make_stock_entry(item_code=batch_item, qty=10, target=warehouse, use_serial_batch_fields=1)
|
||||
so = make_sales_order(item_code=batch_item, qty=10, warehouse=warehouse)
|
||||
pick_list = create_pick_list(so.name)
|
||||
|
||||
pick_list.save()
|
||||
batch_no = frappe.get_all(
|
||||
"Serial and Batch Entry",
|
||||
filters={"parent": se.items[0].serial_and_batch_bundle},
|
||||
fields=["batch_no"],
|
||||
)[0].batch_no
|
||||
|
||||
for row in pick_list.locations:
|
||||
self.assertEqual(row.qty, 10.0)
|
||||
self.assertTrue(row.warehouse == warehouse)
|
||||
self.assertTrue(row.batch_no == batch_no)
|
||||
|
||||
pick_list.submit()
|
||||
|
||||
dn = create_delivery_note(pick_list.name)
|
||||
for row in dn.items:
|
||||
self.assertEqual(row.qty, 10.0)
|
||||
self.assertTrue(row.warehouse == warehouse)
|
||||
self.assertTrue(row.batch_no == batch_no)
|
||||
|
||||
dn.submit()
|
||||
dn.reload()
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
@@ -2166,13 +2206,14 @@ def make_sales_order(**args):
|
||||
return so
|
||||
|
||||
|
||||
def create_dn_against_so(so, delivered_qty=0):
|
||||
def create_dn_against_so(so, delivered_qty=0, do_not_submit=False):
|
||||
frappe.db.set_single_value("Stock Settings", "allow_negative_stock", 1)
|
||||
|
||||
dn = make_delivery_note(so)
|
||||
dn.get("items")[0].qty = delivered_qty or 5
|
||||
dn.insert()
|
||||
dn.submit()
|
||||
if not do_not_submit:
|
||||
dn.submit()
|
||||
return dn
|
||||
|
||||
|
||||
|
||||
@@ -295,10 +295,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-field"></div>
|
||||
`);
|
||||
const me = this;
|
||||
const query = { query: "erpnext.controllers.queries.customer_query" };
|
||||
const allowed_customer_group = this.allowed_customer_groups || [];
|
||||
let filters = {};
|
||||
if (allowed_customer_group.length) {
|
||||
query.filters = {
|
||||
filters = {
|
||||
customer_group: ["in", allowed_customer_group],
|
||||
};
|
||||
}
|
||||
@@ -308,7 +308,11 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
fieldtype: "Link",
|
||||
options: "Customer",
|
||||
placeholder: __("Search by customer name, phone, email."),
|
||||
get_query: () => query,
|
||||
get_query: function () {
|
||||
return {
|
||||
filters: filters,
|
||||
};
|
||||
},
|
||||
onchange: function () {
|
||||
if (this.value) {
|
||||
const frm = me.events.get_frm();
|
||||
|
||||
@@ -197,6 +197,8 @@ def prepare_data(
|
||||
):
|
||||
details[p_key] += r.get(qty_or_amount_field, 0)
|
||||
details[variance_key] = details.get(p_key) - details.get(target_key)
|
||||
else:
|
||||
details[variance_key] = details.get(p_key) - details.get(target_key)
|
||||
|
||||
details["total_achieved"] += details.get(p_key)
|
||||
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
|
||||
@@ -209,31 +211,32 @@ def get_actual_data(filters, sales_users_or_territory_data, date_field, sales_fi
|
||||
|
||||
parent_doc = frappe.qb.DocType(filters.get("doctype"))
|
||||
child_doc = frappe.qb.DocType(filters.get("doctype") + " Item")
|
||||
sales_team = frappe.qb.DocType("Sales Team")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(parent_doc)
|
||||
.inner_join(child_doc)
|
||||
.on(child_doc.parent == parent_doc.name)
|
||||
.inner_join(sales_team)
|
||||
.on(sales_team.parent == parent_doc.name)
|
||||
.select(
|
||||
child_doc.item_group,
|
||||
(child_doc.stock_qty * sales_team.allocated_percentage / 100).as_("stock_qty"),
|
||||
(child_doc.base_net_amount * sales_team.allocated_percentage / 100).as_("base_net_amount"),
|
||||
sales_team.sales_person,
|
||||
parent_doc[date_field],
|
||||
)
|
||||
.where(
|
||||
(parent_doc.docstatus == 1)
|
||||
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
|
||||
)
|
||||
)
|
||||
query = frappe.qb.from_(parent_doc).inner_join(child_doc).on(child_doc.parent == parent_doc.name)
|
||||
|
||||
if sales_field == "sales_person":
|
||||
query = query.where(sales_team.sales_person.isin(sales_users_or_territory_data))
|
||||
sales_team = frappe.qb.DocType("Sales Team")
|
||||
stock_qty = child_doc.stock_qty * sales_team.allocated_percentage / 100
|
||||
net_amount = child_doc.base_net_amount * sales_team.allocated_percentage / 100
|
||||
sales_field_col = sales_team[sales_field]
|
||||
|
||||
query = query.inner_join(sales_team).on(sales_team.parent == parent_doc.name)
|
||||
else:
|
||||
query = query.where(parent_doc[sales_field].isin(sales_users_or_territory_data))
|
||||
stock_qty = child_doc.stock_qty
|
||||
net_amount = child_doc.base_net_amount
|
||||
sales_field_col = parent_doc[sales_field]
|
||||
|
||||
query = query.select(
|
||||
child_doc.item_group,
|
||||
parent_doc[date_field],
|
||||
(stock_qty).as_("stock_qty"),
|
||||
(net_amount).as_("base_net_amount"),
|
||||
sales_field_col,
|
||||
).where(
|
||||
(parent_doc.docstatus == 1)
|
||||
& (parent_doc[date_field].between(fiscal_year.year_start_date, fiscal_year.year_end_date))
|
||||
& (sales_field_col.isin(sales_users_or_territory_data))
|
||||
)
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.selling.report.sales_partner_target_variance_based_on_item_group.sales_partner_target_variance_based_on_item_group import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.selling.report.sales_person_target_variance_based_on_item_group.test_sales_person_target_variance_based_on_item_group import (
|
||||
create_sales_target_doc,
|
||||
create_target_distribution,
|
||||
)
|
||||
|
||||
|
||||
class TestSalesPartnerTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_achieved_target_and_variance_for_partner(self):
|
||||
# Create a Target Distribution
|
||||
distribution = create_target_distribution(self.fiscal_year)
|
||||
|
||||
# Create Sales Partner with targets for the current fiscal year
|
||||
sales_partner = create_sales_target_doc(
|
||||
"Sales Partner", "partner_name", "Sales Partner 1", self.fiscal_year, distribution.name
|
||||
)
|
||||
|
||||
# Create a Sales Invoice for the Partner
|
||||
si = create_sales_invoice(
|
||||
rate=1000,
|
||||
qty=20,
|
||||
do_not_submit=True,
|
||||
)
|
||||
si.sales_partner = sales_partner
|
||||
si.commission_rate = 5
|
||||
si.submit()
|
||||
|
||||
# Check Achieved Target and Variance for the Sales Partner
|
||||
result = execute(
|
||||
frappe._dict(
|
||||
{
|
||||
"fiscal_year": self.fiscal_year,
|
||||
"doctype": "Sales Invoice",
|
||||
"period": "Yearly",
|
||||
"target_on": "Quantity",
|
||||
}
|
||||
)
|
||||
)[1]
|
||||
row = frappe._dict(result[0])
|
||||
self.assertSequenceEqual(
|
||||
[flt(value, 2) for value in (row.total_target, row.total_achieved, row.total_variance)],
|
||||
[50, 20, -30],
|
||||
)
|
||||
@@ -18,17 +18,17 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
|
||||
def test_achieved_target_and_variance(self):
|
||||
# Create a Target Distribution
|
||||
distribution = frappe.new_doc("Monthly Distribution")
|
||||
distribution.distribution_id = "Target Report Distribution"
|
||||
distribution.fiscal_year = self.fiscal_year
|
||||
distribution.get_months()
|
||||
distribution.insert()
|
||||
distribution = create_target_distribution(self.fiscal_year)
|
||||
|
||||
# Create sales people with targets
|
||||
person_1 = create_sales_person_with_target("Sales Person 1", self.fiscal_year, distribution.name)
|
||||
person_2 = create_sales_person_with_target("Sales Person 2", self.fiscal_year, distribution.name)
|
||||
# Create sales people with targets for the current fiscal year
|
||||
person_1 = create_sales_target_doc(
|
||||
"Sales Person", "sales_person_name", "Sales Person 1", self.fiscal_year, distribution.name
|
||||
)
|
||||
person_2 = create_sales_target_doc(
|
||||
"Sales Person", "sales_person_name", "Sales Person 2", self.fiscal_year, distribution.name
|
||||
)
|
||||
|
||||
# Create a Sales Order with 50-50 contribution
|
||||
# Create a Sales Order with 50-50 contribution between both Sales people
|
||||
so = make_sales_order(
|
||||
rate=1000,
|
||||
qty=20,
|
||||
@@ -69,10 +69,20 @@ class TestSalesPersonTargetVarianceBasedOnItemGroup(FrappeTestCase):
|
||||
)
|
||||
|
||||
|
||||
def create_sales_person_with_target(sales_person_name, fiscal_year, distribution_id):
|
||||
sales_person = frappe.new_doc("Sales Person")
|
||||
sales_person.sales_person_name = sales_person_name
|
||||
sales_person.append(
|
||||
def create_target_distribution(fiscal_year):
|
||||
distribution = frappe.new_doc("Monthly Distribution")
|
||||
distribution.distribution_id = "Target Report Distribution"
|
||||
distribution.fiscal_year = fiscal_year
|
||||
distribution.get_months()
|
||||
return distribution.insert()
|
||||
|
||||
|
||||
def create_sales_target_doc(
|
||||
sales_field_dt, sales_field_name, sales_field_value, fiscal_year, distribution_id
|
||||
):
|
||||
sales_target_doc = frappe.new_doc(sales_field_dt)
|
||||
sales_target_doc.set(sales_field_name, sales_field_value)
|
||||
sales_target_doc.append(
|
||||
"targets",
|
||||
{
|
||||
"fiscal_year": fiscal_year,
|
||||
@@ -81,4 +91,6 @@ def create_sales_person_with_target(sales_person_name, fiscal_year, distribution
|
||||
"distribution_id": distribution_id,
|
||||
},
|
||||
)
|
||||
return sales_person.insert()
|
||||
if sales_field_dt == "Sales Partner":
|
||||
sales_target_doc.commission_rate = 5
|
||||
return sales_target_doc.insert()
|
||||
|
||||
@@ -54,25 +54,30 @@ class SalesPartner(WebsiteGenerator):
|
||||
self.partner_website = "http://" + self.partner_website
|
||||
|
||||
def get_context(self, context):
|
||||
address = frappe.db.get_value(
|
||||
"Address", {"sales_partner": self.name, "is_primary_address": 1}, "*", as_dict=True
|
||||
address_names = frappe.db.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": "Sales Partner", "link_name": self.name, "parenttype": "Address"},
|
||||
pluck=["parent"],
|
||||
)
|
||||
if address:
|
||||
city_state = ", ".join(filter(None, [address.city, address.state]))
|
||||
address_rows = [
|
||||
address.address_line1,
|
||||
address.address_line2,
|
||||
city_state,
|
||||
address.pincode,
|
||||
address.country,
|
||||
]
|
||||
|
||||
context.update(
|
||||
addresses = []
|
||||
for address_name in address_names:
|
||||
address_doc = frappe.get_doc("Address", address_name)
|
||||
city_state = ", ".join([item for item in [address_doc.city, address_doc.state] if item])
|
||||
address_rows = [
|
||||
address_doc.address_line1,
|
||||
address_doc.address_line2,
|
||||
city_state,
|
||||
address_doc.pincode,
|
||||
address_doc.country,
|
||||
]
|
||||
addresses.append(
|
||||
{
|
||||
"email": address.email_id,
|
||||
"email": address_doc.email_id,
|
||||
"partner_address": filter_strip_join(address_rows, "\n<br>"),
|
||||
"phone": filter_strip_join(cstr(address.phone).split(","), "\n<br>"),
|
||||
"phone": filter_strip_join(cstr(address_doc.phone).split(","), "\n<br>"),
|
||||
}
|
||||
)
|
||||
|
||||
context["addresses"] = addresses
|
||||
return context
|
||||
|
||||
@@ -123,7 +123,9 @@ class ClosingStockBalance(Document):
|
||||
)
|
||||
)
|
||||
|
||||
create_json_gz_file({"columns": columns, "data": data}, self.doctype, self.name)
|
||||
create_json_gz_file(
|
||||
{"columns": columns, "data": data}, self.doctype, self.name, "closing-stock-balance"
|
||||
)
|
||||
|
||||
def get_prepared_data(self):
|
||||
if attachments := get_attachments(self.doctype, self.name):
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
cur_frm.add_fetch("customer", "tax_id", "tax_id");
|
||||
|
||||
cur_frm.cscript.tax_table = "Sales Taxes and Charges";
|
||||
|
||||
frappe.provide("erpnext.stock");
|
||||
frappe.provide("erpnext.stock.delivery_note");
|
||||
frappe.provide("erpnext.accounts.dimensions");
|
||||
|
||||
@@ -251,6 +251,7 @@ class DeliveryNote(SellingController):
|
||||
def validate(self):
|
||||
self.validate_posting_time()
|
||||
super(DeliveryNote, self).validate()
|
||||
self.validate_references()
|
||||
self.set_status()
|
||||
self.so_required()
|
||||
self.validate_proj_cust()
|
||||
@@ -333,6 +334,7 @@ class DeliveryNote(SellingController):
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": bundle_id,
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": item.get("warehouse"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -340,6 +342,58 @@ class DeliveryNote(SellingController):
|
||||
|
||||
item.serial_and_batch_bundle = cls_obj.serial_and_batch_bundle
|
||||
|
||||
def validate_references(self):
|
||||
self.validate_sales_order_references()
|
||||
self.validate_sales_invoice_references()
|
||||
|
||||
def validate_sales_order_references(self):
|
||||
err_msg = ""
|
||||
for item in self.items:
|
||||
if (item.against_sales_order and not item.so_detail) or (
|
||||
not item.against_sales_order and item.so_detail
|
||||
):
|
||||
if not item.against_sales_order:
|
||||
err_msg += (
|
||||
_("'Sales Order' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("against_sales_order")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
else:
|
||||
err_msg += (
|
||||
_("'Sales Order Item' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("so_detail")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
|
||||
if err_msg:
|
||||
frappe.throw(err_msg, title=_("References to Sales Orders are Incomplete"))
|
||||
|
||||
def validate_sales_invoice_references(self):
|
||||
err_msg = ""
|
||||
for item in self.items:
|
||||
if (item.against_sales_invoice and not item.si_detail) or (
|
||||
not item.against_sales_invoice and item.si_detail
|
||||
):
|
||||
if not item.against_sales_invoice:
|
||||
err_msg += (
|
||||
_("'Sales Invoice' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("against_sales_invoice")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
else:
|
||||
err_msg += (
|
||||
_("'Sales Invoice Item' reference ({1}) is missing in row {0}").format(
|
||||
frappe.bold(item.idx), frappe.bold("si_detail")
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
|
||||
if err_msg:
|
||||
frappe.throw(err_msg, title=_("References to Sales Invoices are Incomplete"))
|
||||
|
||||
def validate_proj_cust(self):
|
||||
"""check for does customer belong to same project as entered.."""
|
||||
if self.project and self.customer:
|
||||
|
||||
@@ -824,6 +824,15 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.cancel()
|
||||
self.assertEqual(dn.status, "Cancelled")
|
||||
|
||||
def test_sales_order_reference_validation(self):
|
||||
so = make_sales_order(po_no="12345")
|
||||
dn = create_dn_against_so(so.name, delivered_qty=2, do_not_submit=True)
|
||||
dn.items[0].against_sales_order = None
|
||||
self.assertRaises(frappe.ValidationError, dn.save)
|
||||
dn.reload()
|
||||
dn.items[0].so_detail = None
|
||||
self.assertRaises(frappe.ValidationError, dn.save)
|
||||
|
||||
def test_dn_billing_status_case1(self):
|
||||
# SO -> DN -> SI
|
||||
so = make_sales_order(po_no="12345")
|
||||
|
||||
@@ -406,14 +406,6 @@ $.extend(erpnext.item, {
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict.customer_items.grid.get_field("customer_name").get_query = function (doc, cdt, cdn) {
|
||||
return { query: "erpnext.controllers.queries.customer_query" };
|
||||
};
|
||||
|
||||
frm.fields_dict.supplier_items.grid.get_field("supplier").get_query = function (doc, cdt, cdn) {
|
||||
return { query: "erpnext.controllers.queries.supplier_query" };
|
||||
};
|
||||
|
||||
frm.fields_dict["item_defaults"].grid.get_field("default_warehouse").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
frappe.provide("erpnext.stock");
|
||||
|
||||
cur_frm.cscript.tax_table = "Purchase Taxes and Charges";
|
||||
|
||||
erpnext.accounts.taxes.setup_tax_filters("Purchase Taxes and Charges");
|
||||
erpnext.accounts.taxes.setup_tax_validations("Purchase Receipt");
|
||||
erpnext.buying.setup_buying_controller();
|
||||
|
||||
@@ -2559,6 +2559,280 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
self.assertEqual(row.serial_no, "\n".join(serial_nos[:2]))
|
||||
self.assertEqual(row.rejected_serial_no, serial_nos[2])
|
||||
|
||||
def test_internal_transfer_with_serial_batch_items_and_their_valuation(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
batch_item_doc = make_item(
|
||||
"_Test Batch Item For Stock Transfer",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "BT-BIFST-.####"},
|
||||
)
|
||||
|
||||
serial_item_doc = make_item(
|
||||
"_Test Serial No Item For Stock Transfer",
|
||||
{"has_serial_no": 1, "serial_no_series": "BT-BIFST-.####"},
|
||||
)
|
||||
|
||||
inward_entry = make_purchase_receipt(
|
||||
item_code=batch_item_doc.name,
|
||||
qty=10,
|
||||
rate=150,
|
||||
warehouse="Stores - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
use_serial_batch_fields=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inward_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 250,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"use_serial_batch_fields": 1,
|
||||
},
|
||||
)
|
||||
|
||||
inward_entry.submit()
|
||||
inward_entry.reload()
|
||||
|
||||
for row in inward_entry.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_dn = create_delivery_note(
|
||||
item_code=inward_entry.items[0].item_code,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=500,
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse="Work In Progress - TCP1",
|
||||
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
|
||||
use_serial_batch_fields=1,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inter_transfer_dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 350,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"target_warehouse": "Work In Progress - TCP1",
|
||||
"serial_no": "\n".join(
|
||||
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
|
||||
),
|
||||
"use_serial_batch_fields": 1,
|
||||
},
|
||||
)
|
||||
|
||||
inter_transfer_dn.submit()
|
||||
inter_transfer_dn.reload()
|
||||
for row in inter_transfer_dn.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
|
||||
for row in inter_transfer_pr.items:
|
||||
row.from_warehouse = "Work In Progress - TCP1"
|
||||
row.warehouse = "Stores - TCP1"
|
||||
inter_transfer_pr.submit()
|
||||
|
||||
for row in inter_transfer_pr.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
|
||||
|
||||
inter_transfer_pr_return.submit()
|
||||
inter_transfer_pr_return.reload()
|
||||
for row in inter_transfer_pr_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
if row.item_code == serial_item_doc.name:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
for sn in serial_nos:
|
||||
serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
|
||||
self.assertTrue(serial_no_details.status == "Active")
|
||||
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
|
||||
|
||||
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
|
||||
inter_transfer_dn_return.posting_date = today()
|
||||
inter_transfer_dn_return.posting_time = nowtime()
|
||||
for row in inter_transfer_dn_return.items:
|
||||
row.target_warehouse = "Work In Progress - TCP1"
|
||||
|
||||
inter_transfer_dn_return.submit()
|
||||
inter_transfer_dn_return.reload()
|
||||
|
||||
for row in inter_transfer_dn_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
def test_internal_transfer_with_serial_batch_items_without_user_serial_batch_fields(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 0)
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
batch_item_doc = make_item(
|
||||
"_Test Batch Item For Stock Transfer USE SERIAL BATCH FIELDS",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "USBF-BT-BIFST-.####"},
|
||||
)
|
||||
|
||||
serial_item_doc = make_item(
|
||||
"_Test Serial No Item For Stock Transfer USE SERIAL BATCH FIELDS",
|
||||
{"has_serial_no": 1, "serial_no_series": "USBF-BT-BIFST-.####"},
|
||||
)
|
||||
|
||||
inward_entry = make_purchase_receipt(
|
||||
item_code=batch_item_doc.name,
|
||||
qty=10,
|
||||
rate=150,
|
||||
warehouse="Stores - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
use_serial_batch_fields=0,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inward_entry.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 250,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"use_serial_batch_fields": 0,
|
||||
},
|
||||
)
|
||||
|
||||
inward_entry.submit()
|
||||
inward_entry.reload()
|
||||
|
||||
for row in inward_entry.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_dn = create_delivery_note(
|
||||
item_code=inward_entry.items[0].item_code,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=10,
|
||||
rate=500,
|
||||
warehouse="Stores - TCP1",
|
||||
target_warehouse="Work In Progress - TCP1",
|
||||
batch_no=get_batch_from_bundle(inward_entry.items[0].serial_and_batch_bundle),
|
||||
use_serial_batch_fields=0,
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
inter_transfer_dn.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": serial_item_doc.name,
|
||||
"qty": 15,
|
||||
"rate": 350,
|
||||
"item_name": serial_item_doc.item_name,
|
||||
"conversion_factor": 1.0,
|
||||
"uom": serial_item_doc.stock_uom,
|
||||
"stock_uom": serial_item_doc.stock_uom,
|
||||
"warehouse": "Stores - TCP1",
|
||||
"target_warehouse": "Work In Progress - TCP1",
|
||||
"serial_no": "\n".join(
|
||||
get_serial_nos_from_bundle(inward_entry.items[1].serial_and_batch_bundle)
|
||||
),
|
||||
"use_serial_batch_fields": 0,
|
||||
},
|
||||
)
|
||||
|
||||
inter_transfer_dn.submit()
|
||||
inter_transfer_dn.reload()
|
||||
for row in inter_transfer_dn.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr = make_inter_company_purchase_receipt(inter_transfer_dn.name)
|
||||
for row in inter_transfer_pr.items:
|
||||
row.from_warehouse = "Work In Progress - TCP1"
|
||||
row.warehouse = "Stores - TCP1"
|
||||
inter_transfer_pr.submit()
|
||||
|
||||
for row in inter_transfer_pr.items:
|
||||
if row.item_code == batch_item_doc.name:
|
||||
self.assertEqual(row.rate, 150.0)
|
||||
else:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
inter_transfer_pr_return = make_return_doc("Purchase Receipt", inter_transfer_pr.name)
|
||||
|
||||
inter_transfer_pr_return.submit()
|
||||
inter_transfer_pr_return.reload()
|
||||
for row in inter_transfer_pr_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
if row.item_code == serial_item_doc.name:
|
||||
self.assertEqual(row.rate, 250.0)
|
||||
serial_nos = get_serial_nos_from_bundle(row.serial_and_batch_bundle)
|
||||
for sn in serial_nos:
|
||||
serial_no_details = frappe.db.get_value("Serial No", sn, ["status", "warehouse"], as_dict=1)
|
||||
self.assertTrue(serial_no_details.status == "Active")
|
||||
self.assertEqual(serial_no_details.warehouse, "Work In Progress - TCP1")
|
||||
|
||||
inter_transfer_dn_return = make_return_doc("Delivery Note", inter_transfer_dn.name)
|
||||
inter_transfer_dn_return.posting_date = today()
|
||||
inter_transfer_dn_return.posting_time = nowtime()
|
||||
for row in inter_transfer_dn_return.items:
|
||||
row.target_warehouse = "Work In Progress - TCP1"
|
||||
|
||||
inter_transfer_dn_return.submit()
|
||||
inter_transfer_dn_return.reload()
|
||||
|
||||
for row in inter_transfer_dn_return.items:
|
||||
self.assertTrue(row.serial_and_batch_bundle)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "use_serial_batch_fields", 1)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Voucher No",
|
||||
"no_copy": 1,
|
||||
"options": "voucher_type",
|
||||
@@ -250,7 +251,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-07 17:56:55.528563",
|
||||
"modified": "2024-03-15 15:22:24.003486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial and Batch Bundle",
|
||||
|
||||
@@ -801,6 +801,7 @@ class SerialandBatchBundle(Document):
|
||||
self.set_purchase_document_no()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_batch_inventory()
|
||||
self.validate_serial_nos_inventory()
|
||||
|
||||
def set_purchase_document_no(self):
|
||||
|
||||
@@ -2069,6 +2069,7 @@ class StockEntry(StockController):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
precision = frappe.get_precision("Stock Entry Detail", "qty")
|
||||
for key, row in available_materials.items():
|
||||
remaining_qty_to_produce = flt(wo_data.trans_qty) - flt(wo_data.produced_qty)
|
||||
if remaining_qty_to_produce <= 0 and not self.is_return:
|
||||
@@ -2091,7 +2092,8 @@ class StockEntry(StockController):
|
||||
serial_nos = row.serial_nos[0 : cint(qty)]
|
||||
row.serial_nos = serial_nos
|
||||
|
||||
self.update_item_in_stock_entry_detail(row, item, qty)
|
||||
if flt(qty, precision) != 0.0:
|
||||
self.update_item_in_stock_entry_detail(row, item, qty)
|
||||
|
||||
def update_batches_to_be_consume(self, batches, row, qty):
|
||||
qty_to_be_consumed = qty
|
||||
@@ -2609,6 +2611,7 @@ def move_sample_to_retention_warehouse(company, items):
|
||||
"type_of_transaction": "Outward",
|
||||
"serial_and_batch_bundle": item.get("serial_and_batch_bundle"),
|
||||
"item_code": item.get("item_code"),
|
||||
"warehouse": item.get("t_warehouse"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1008,6 +1008,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
"type_of_transaction": "Inward",
|
||||
"serial_and_batch_bundle": s2.items[0].serial_and_batch_bundle,
|
||||
"item_code": "_Test Serialized Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_queue",
|
||||
"fieldtype": "Text",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "FIFO Stock Queue (qty, rate)",
|
||||
"oldfieldname": "fcfs_stack",
|
||||
"oldfieldtype": "Text",
|
||||
@@ -360,7 +360,7 @@
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-02-07 09:18:13.999231",
|
||||
"modified": "2024-03-13 09:56:13.021696",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Entry",
|
||||
|
||||
@@ -58,7 +58,7 @@ class StockLedgerEntry(Document):
|
||||
recalculate_rate: DF.Check
|
||||
serial_and_batch_bundle: DF.Link | None
|
||||
serial_no: DF.LongText | None
|
||||
stock_queue: DF.Text | None
|
||||
stock_queue: DF.LongText | None
|
||||
stock_uom: DF.Link | None
|
||||
stock_value: DF.Currency
|
||||
stock_value_difference: DF.Currency
|
||||
|
||||
@@ -138,12 +138,12 @@ class StockReconciliation(StockController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": row.name,
|
||||
"qty": row.qty,
|
||||
"qty": row.current_qty,
|
||||
"type_of_transaction": "Outward",
|
||||
"company": self.company,
|
||||
"is_rejected": 0,
|
||||
"serial_nos": get_serial_nos(row.current_serial_no) if row.current_serial_no else None,
|
||||
"batches": frappe._dict({row.batch_no: row.qty}) if row.batch_no else None,
|
||||
"batches": frappe._dict({row.batch_no: row.current_qty}) if row.batch_no else None,
|
||||
"batch_no": row.batch_no,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
|
||||
@@ -599,6 +599,7 @@ class BatchNoValuation(DeprecatedBatchNoValuation):
|
||||
elif self.sle.voucher_no:
|
||||
query = query.where(parent.voucher_no != self.sle.voucher_no)
|
||||
|
||||
query = query.where(parent.voucher_type != "Pick List")
|
||||
if timestamp_condition:
|
||||
query = query.where(timestamp_condition)
|
||||
|
||||
@@ -819,6 +820,10 @@ class SerialBatchCreation:
|
||||
self.remove_returned_serial_nos(new_package)
|
||||
|
||||
new_package.docstatus = 0
|
||||
new_package.warehouse = self.warehouse
|
||||
new_package.voucher_no = ""
|
||||
new_package.posting_date = today()
|
||||
new_package.posting_time = nowtime()
|
||||
new_package.type_of_transaction = self.type_of_transaction
|
||||
new_package.returned_against = self.get("returned_against")
|
||||
new_package.save()
|
||||
|
||||
@@ -8,18 +8,20 @@
|
||||
<div class="partner-content" itemscope itemtype="http://schema.org/Organization">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{% if logo -%}
|
||||
{% if logo %}
|
||||
<img itemprop="brand" src="{{ logo }}" class="partner-logo"
|
||||
alt="{{ partner_name }}" title="{{ partner_name }}" />
|
||||
<br><br>
|
||||
{%- endif %}
|
||||
<address>
|
||||
{% if partner_website -%}<p><a href="{{ partner_website }}"
|
||||
target="_blank">{{ partner_website }}</a></p>{%- endif %}
|
||||
{% if partner_address -%}<p itemprop="address">{{ partner_address }}</p>{%- endif %}
|
||||
{% if phone -%}<p itemprop="telephone">{{ phone }}</p>{%- endif %}
|
||||
{% if email -%}<p itemprop="email"><span class="fa fa-envelope"></span> {{ email }}</p>{%- endif %}
|
||||
</address>
|
||||
{% endif %}
|
||||
{% if addresses %}
|
||||
{% for address in addresses %}
|
||||
<address>
|
||||
{% if address.partner_address %}<p itemprop="address">{{ address.partner_address }}</p>{% endif %}
|
||||
{% if address.phone %}<p itemprop="telephone">{{ address.phone }}</p>{% endif %}
|
||||
{% if address.email %}<p itemprop="email"><span class="fa fa-envelope"></span> {{ address.email }}</p>{% endif %}
|
||||
</address>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<p>{{ description }}</p>
|
||||
|
||||
Reference in New Issue
Block a user